feat(angular): add generator to convert targets to use the esbuild-based application executor (#21333)
This commit is contained in:
parent
c46c28694f
commit
7a77b0d6a7
@ -6789,6 +6789,14 @@
|
|||||||
"isExternal": false,
|
"isExternal": false,
|
||||||
"disableCollapsible": false
|
"disableCollapsible": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "convert-to-application-executor",
|
||||||
|
"path": "/nx-api/angular/generators/convert-to-application-executor",
|
||||||
|
"name": "convert-to-application-executor",
|
||||||
|
"children": [],
|
||||||
|
"isExternal": false,
|
||||||
|
"disableCollapsible": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "directive",
|
"id": "directive",
|
||||||
"path": "/nx-api/angular/generators/directive",
|
"path": "/nx-api/angular/generators/directive",
|
||||||
|
|||||||
@ -177,6 +177,15 @@
|
|||||||
"path": "/nx-api/angular/generators/component-test",
|
"path": "/nx-api/angular/generators/component-test",
|
||||||
"type": "generator"
|
"type": "generator"
|
||||||
},
|
},
|
||||||
|
"/nx-api/angular/generators/convert-to-application-executor": {
|
||||||
|
"description": "Converts projects to use the `@nx/angular:application` executor or the `@angular-devkit/build-angular:application` builder. _Note: this is only supported in Angular versions >= 17.0.0_.",
|
||||||
|
"file": "generated/packages/angular/generators/convert-to-application-executor.json",
|
||||||
|
"hidden": false,
|
||||||
|
"name": "convert-to-application-executor",
|
||||||
|
"originalFilePath": "/packages/angular/src/generators/convert-to-application-executor/schema.json",
|
||||||
|
"path": "/nx-api/angular/generators/convert-to-application-executor",
|
||||||
|
"type": "generator"
|
||||||
|
},
|
||||||
"/nx-api/angular/generators/directive": {
|
"/nx-api/angular/generators/directive": {
|
||||||
"description": "Generate an Angular directive.",
|
"description": "Generate an Angular directive.",
|
||||||
"file": "generated/packages/angular/generators/directive.json",
|
"file": "generated/packages/angular/generators/directive.json",
|
||||||
|
|||||||
@ -172,6 +172,15 @@
|
|||||||
"path": "angular/generators/component-test",
|
"path": "angular/generators/component-test",
|
||||||
"type": "generator"
|
"type": "generator"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Converts projects to use the `@nx/angular:application` executor or the `@angular-devkit/build-angular:application` builder. _Note: this is only supported in Angular versions >= 17.0.0_.",
|
||||||
|
"file": "generated/packages/angular/generators/convert-to-application-executor.json",
|
||||||
|
"hidden": false,
|
||||||
|
"name": "convert-to-application-executor",
|
||||||
|
"originalFilePath": "/packages/angular/src/generators/convert-to-application-executor/schema.json",
|
||||||
|
"path": "angular/generators/convert-to-application-executor",
|
||||||
|
"type": "generator"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Generate an Angular directive.",
|
"description": "Generate an Angular directive.",
|
||||||
"file": "generated/packages/angular/generators/directive.json",
|
"file": "generated/packages/angular/generators/directive.json",
|
||||||
|
|||||||
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "convert-to-application-executor",
|
||||||
|
"factory": "./src/generators/convert-to-application-executor/convert-to-application-executor",
|
||||||
|
"schema": {
|
||||||
|
"$schema": "http://json-schema.org/schema",
|
||||||
|
"$id": "NxAngularConvertToApplicationExecutorGenerator",
|
||||||
|
"cli": "nx",
|
||||||
|
"title": "Converts projects to use the `@nx/angular:application` executor or the `@angular-devkit/build-angular:application` builder. _Note: this is only supported in Angular versions >= 17.0.0_.",
|
||||||
|
"description": "Converts a project or all projects using one of the `@angular-devkit/build-angular:browser`, `@angular-devkit/build-angular:browser-esbuild`, `@nx/angular:browser` and `@nx/angular:browser-esbuild` executors to use the `@nx/angular:application` executor or the `@angular-devkit/build-angular:application` builder. If the converted target is using one of the `@nx/angular` executors, the `@nx/angular:application` executor will be used. Otherwise, the `@angular-devkit/build-angular:application` builder will be used.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Name of the Angular application project to convert. It has to contain a target using one of the `@angular-devkit/build-angular:browser`, `@angular-devkit/build-angular:browser-esbuild`, `@nx/angular:browser` and `@nx/angular:browser-esbuild` executors. If not specified, all projects with such targets will be converted.",
|
||||||
|
"$default": { "$source": "argv", "index": 0 },
|
||||||
|
"x-priority": "important"
|
||||||
|
},
|
||||||
|
"skipFormat": {
|
||||||
|
"description": "Skip formatting files.",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"x-priority": "internal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"presets": []
|
||||||
|
},
|
||||||
|
"description": "Converts projects to use the `@nx/angular:application` executor or the `@angular-devkit/build-angular:application` builder. _Note: this is only supported in Angular versions >= 17.0.0_.",
|
||||||
|
"implementation": "/packages/angular/src/generators/convert-to-application-executor/convert-to-application-executor.ts",
|
||||||
|
"aliases": [],
|
||||||
|
"hidden": false,
|
||||||
|
"path": "/packages/angular/src/generators/convert-to-application-executor/schema.json",
|
||||||
|
"type": "generator"
|
||||||
|
}
|
||||||
@ -343,6 +343,7 @@
|
|||||||
- [component-cypress-spec](/nx-api/angular/generators/component-cypress-spec)
|
- [component-cypress-spec](/nx-api/angular/generators/component-cypress-spec)
|
||||||
- [component-story](/nx-api/angular/generators/component-story)
|
- [component-story](/nx-api/angular/generators/component-story)
|
||||||
- [component-test](/nx-api/angular/generators/component-test)
|
- [component-test](/nx-api/angular/generators/component-test)
|
||||||
|
- [convert-to-application-executor](/nx-api/angular/generators/convert-to-application-executor)
|
||||||
- [directive](/nx-api/angular/generators/directive)
|
- [directive](/nx-api/angular/generators/directive)
|
||||||
- [federate-module](/nx-api/angular/generators/federate-module)
|
- [federate-module](/nx-api/angular/generators/federate-module)
|
||||||
- [init](/nx-api/angular/generators/init)
|
- [init](/nx-api/angular/generators/init)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { names } from '@nx/devkit';
|
import { names } from '@nx/devkit';
|
||||||
import {
|
import {
|
||||||
|
checkFilesDoNotExist,
|
||||||
checkFilesExist,
|
checkFilesExist,
|
||||||
cleanupProject,
|
cleanupProject,
|
||||||
getSize,
|
getSize,
|
||||||
@ -8,6 +9,7 @@ import {
|
|||||||
newProject,
|
newProject,
|
||||||
readFile,
|
readFile,
|
||||||
removeFile,
|
removeFile,
|
||||||
|
rmDist,
|
||||||
runCLI,
|
runCLI,
|
||||||
runCommandUntil,
|
runCommandUntil,
|
||||||
runE2ETests,
|
runE2ETests,
|
||||||
@ -552,4 +554,52 @@ describe('Angular Projects', () => {
|
|||||||
`Successfully ran target test for project ${libName}`
|
`Successfully ran target test for project ${libName}`
|
||||||
);
|
);
|
||||||
}, 500_000);
|
}, 500_000);
|
||||||
|
|
||||||
|
it('should support generating applications with SSR and converting targets with webpack-based executors to use the application executor', async () => {
|
||||||
|
const esbuildApp = uniq('esbuild-app');
|
||||||
|
const webpackApp = uniq('webpack-app');
|
||||||
|
|
||||||
|
runCLI(
|
||||||
|
`generate @nx/angular:app ${esbuildApp} --bundler=esbuild --ssr --project-name-and-root-format=as-provided --no-interactive`
|
||||||
|
);
|
||||||
|
|
||||||
|
// check build produces both the browser and server bundles
|
||||||
|
runCLI(`build ${esbuildApp} --output-hashing none`);
|
||||||
|
checkFilesExist(
|
||||||
|
`dist/${esbuildApp}/browser/main.js`,
|
||||||
|
`dist/${esbuildApp}/server/server.mjs`
|
||||||
|
);
|
||||||
|
|
||||||
|
runCLI(
|
||||||
|
`generate @nx/angular:app ${webpackApp} --bundler=webpack --ssr --project-name-and-root-format=as-provided --no-interactive`
|
||||||
|
);
|
||||||
|
|
||||||
|
// check build only produces the browser bundle
|
||||||
|
runCLI(`build ${webpackApp} --output-hashing none`);
|
||||||
|
checkFilesExist(`dist/${webpackApp}/browser/main.js`);
|
||||||
|
checkFilesDoNotExist(`dist/${webpackApp}/server/main.js`);
|
||||||
|
|
||||||
|
// check server produces the server bundle
|
||||||
|
runCLI(`server ${webpackApp} --output-hashing none`);
|
||||||
|
checkFilesExist(`dist/${webpackApp}/server/main.js`);
|
||||||
|
|
||||||
|
rmDist();
|
||||||
|
|
||||||
|
// convert target with webpack-based executors to use the application executor
|
||||||
|
runCLI(
|
||||||
|
`generate @nx/angular:convert-to-application-executor ${webpackApp}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// check build now produces both the browser and server bundles
|
||||||
|
runCLI(`build ${webpackApp} --output-hashing none`);
|
||||||
|
checkFilesExist(
|
||||||
|
`dist/${webpackApp}/browser/main.js`,
|
||||||
|
`dist/${webpackApp}/server/server.mjs`
|
||||||
|
);
|
||||||
|
|
||||||
|
// check server target is no longer available
|
||||||
|
expect(() =>
|
||||||
|
runCLI(`server ${webpackApp} --output-hashing none`)
|
||||||
|
).toThrow();
|
||||||
|
}, 500_000);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -39,6 +39,11 @@
|
|||||||
"schema": "./src/generators/component-test/schema.json",
|
"schema": "./src/generators/component-test/schema.json",
|
||||||
"description": "Creates a cypress component test file for a component."
|
"description": "Creates a cypress component test file for a component."
|
||||||
},
|
},
|
||||||
|
"convert-to-application-executor": {
|
||||||
|
"factory": "./src/generators/convert-to-application-executor/convert-to-application-executor",
|
||||||
|
"schema": "./src/generators/convert-to-application-executor/schema.json",
|
||||||
|
"description": "Converts projects to use the `@nx/angular:application` executor or the `@angular-devkit/build-angular:application` builder. _Note: this is only supported in Angular versions >= 17.0.0_."
|
||||||
|
},
|
||||||
"directive": {
|
"directive": {
|
||||||
"factory": "./src/generators/directive/directive",
|
"factory": "./src/generators/directive/directive",
|
||||||
"schema": "./src/generators/directive/schema.json",
|
"schema": "./src/generators/directive/schema.json",
|
||||||
|
|||||||
@ -0,0 +1,315 @@
|
|||||||
|
import {
|
||||||
|
addProjectConfiguration,
|
||||||
|
logger,
|
||||||
|
readProjectConfiguration,
|
||||||
|
updateJson,
|
||||||
|
type Tree,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||||
|
import { convertToApplicationExecutor } from './convert-to-application-executor';
|
||||||
|
|
||||||
|
describe('convert-to-application-executor generator', () => {
|
||||||
|
let tree: Tree;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tree = createTreeWithEmptyWorkspace();
|
||||||
|
jest.spyOn(logger, 'info').mockImplementation(() => {});
|
||||||
|
jest.spyOn(logger, 'warn').mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
executor | expected
|
||||||
|
${'@angular-devkit/build-angular:browser'} | ${'@angular-devkit/build-angular:application'}
|
||||||
|
${'@angular-devkit/build-angular:browser-esbuild'} | ${'@angular-devkit/build-angular:application'}
|
||||||
|
${'@nx/angular:webpack-browser'} | ${'@nx/angular:application'}
|
||||||
|
${'@nx/angular:browser-esbuild'} | ${'@nx/angular:application'}
|
||||||
|
`(
|
||||||
|
'should replace "$executor" with "$expected"',
|
||||||
|
async ({ executor, expected }) => {
|
||||||
|
addProjectConfiguration(tree, 'app1', {
|
||||||
|
root: 'app1',
|
||||||
|
projectType: 'application',
|
||||||
|
targets: { build: { executor } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await convertToApplicationExecutor(tree, {});
|
||||||
|
|
||||||
|
const project = readProjectConfiguration(tree, 'app1');
|
||||||
|
expect(project.targets.build.executor).toBe(expected);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should not convert the target when using a custom webpack config', async () => {
|
||||||
|
addProjectConfiguration(tree, 'app1', {
|
||||||
|
root: 'app1',
|
||||||
|
projectType: 'application',
|
||||||
|
targets: {
|
||||||
|
build: {
|
||||||
|
executor: '@nx/angular:webpack-browser',
|
||||||
|
options: {
|
||||||
|
customWebpackConfig: {
|
||||||
|
path: 'app1/webpack.config.js',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await convertToApplicationExecutor(tree, {});
|
||||||
|
|
||||||
|
const project = readProjectConfiguration(tree, 'app1');
|
||||||
|
expect(project.targets.build.executor).toBe('@nx/angular:webpack-browser');
|
||||||
|
expect(project.targets.build.options.customWebpackConfig).toStrictEqual({
|
||||||
|
path: 'app1/webpack.config.js',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should rename "main" to "browser"', async () => {
|
||||||
|
addProjectConfiguration(tree, 'app1', {
|
||||||
|
root: 'app1',
|
||||||
|
projectType: 'application',
|
||||||
|
targets: {
|
||||||
|
build: {
|
||||||
|
executor: '@angular-devkit/build-angular:browser',
|
||||||
|
options: {
|
||||||
|
main: 'app1/main.ts',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await convertToApplicationExecutor(tree, {});
|
||||||
|
|
||||||
|
const project = readProjectConfiguration(tree, 'app1');
|
||||||
|
expect(project.targets.build.options.browser).toBe('app1/main.ts');
|
||||||
|
expect(project.targets.build.options.main).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should rename "ngswConfigPath" to "serviceWorker"', async () => {
|
||||||
|
addProjectConfiguration(tree, 'app1', {
|
||||||
|
root: 'app1',
|
||||||
|
projectType: 'application',
|
||||||
|
targets: {
|
||||||
|
build: {
|
||||||
|
executor: '@angular-devkit/build-angular:browser',
|
||||||
|
options: {
|
||||||
|
ngswConfigPath: 'app1/ngsw-config.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await convertToApplicationExecutor(tree, {});
|
||||||
|
|
||||||
|
const project = readProjectConfiguration(tree, 'app1');
|
||||||
|
expect(project.targets.build.options.serviceWorker).toBe(
|
||||||
|
'app1/ngsw-config.json'
|
||||||
|
);
|
||||||
|
expect(project.targets.build.options.ngswConfigPath).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert a string value for "polyfills" to an array', async () => {
|
||||||
|
addProjectConfiguration(tree, 'app1', {
|
||||||
|
root: 'app1',
|
||||||
|
projectType: 'application',
|
||||||
|
targets: {
|
||||||
|
build: {
|
||||||
|
executor: '@angular-devkit/build-angular:browser',
|
||||||
|
options: {
|
||||||
|
polyfills: 'zone.js',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await convertToApplicationExecutor(tree, {});
|
||||||
|
|
||||||
|
const project = readProjectConfiguration(tree, 'app1');
|
||||||
|
expect(project.targets.build.options.polyfills).toStrictEqual(['zone.js']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update "outputs"', async () => {
|
||||||
|
addProjectConfiguration(tree, 'app1', {
|
||||||
|
root: 'app1',
|
||||||
|
projectType: 'application',
|
||||||
|
targets: {
|
||||||
|
build: {
|
||||||
|
executor: '@angular-devkit/build-angular:browser',
|
||||||
|
outputs: ['{options.outputPath}'],
|
||||||
|
options: {
|
||||||
|
outputPath: 'dist/app1',
|
||||||
|
resourcesOutputPath: 'media',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await convertToApplicationExecutor(tree, {});
|
||||||
|
|
||||||
|
const project = readProjectConfiguration(tree, 'app1');
|
||||||
|
expect(project.targets.build.outputs).toStrictEqual([
|
||||||
|
'{options.outputPath.base}',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace "outputPath" to string if "resourcesOutputPath" is set to "media"', async () => {
|
||||||
|
addProjectConfiguration(tree, 'app1', {
|
||||||
|
root: 'app1',
|
||||||
|
projectType: 'application',
|
||||||
|
targets: {
|
||||||
|
build: {
|
||||||
|
executor: '@angular-devkit/build-angular:browser',
|
||||||
|
options: {
|
||||||
|
outputPath: 'dist/app1',
|
||||||
|
resourcesOutputPath: 'media',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await convertToApplicationExecutor(tree, {});
|
||||||
|
|
||||||
|
const project = readProjectConfiguration(tree, 'app1');
|
||||||
|
const { outputPath, resourcesOutputPath } = project.targets.build.options;
|
||||||
|
expect(outputPath).toStrictEqual({ base: 'dist/app1' });
|
||||||
|
expect(resourcesOutputPath).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set "outputPath.media" if "resourcesOutputPath" is set and is not "media"', async () => {
|
||||||
|
addProjectConfiguration(tree, 'app1', {
|
||||||
|
root: 'app1',
|
||||||
|
projectType: 'application',
|
||||||
|
targets: {
|
||||||
|
build: {
|
||||||
|
executor: '@angular-devkit/build-angular:browser',
|
||||||
|
options: {
|
||||||
|
outputPath: 'dist/app1',
|
||||||
|
resourcesOutputPath: 'resources',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await convertToApplicationExecutor(tree, {});
|
||||||
|
|
||||||
|
const project = readProjectConfiguration(tree, 'app1');
|
||||||
|
const { outputPath, resourcesOutputPath } = project.targets.build.options;
|
||||||
|
expect(outputPath).toStrictEqual({ base: 'dist/app1', media: 'resources' });
|
||||||
|
expect(resourcesOutputPath).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove "browser" portion from "outputPath"', async () => {
|
||||||
|
addProjectConfiguration(tree, 'app1', {
|
||||||
|
root: 'app1',
|
||||||
|
projectType: 'application',
|
||||||
|
targets: {
|
||||||
|
build: {
|
||||||
|
executor: '@angular-devkit/build-angular:browser',
|
||||||
|
options: {
|
||||||
|
outputPath: 'dist/app1/browser',
|
||||||
|
resourcesOutputPath: 'resources',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await convertToApplicationExecutor(tree, {});
|
||||||
|
|
||||||
|
const project = readProjectConfiguration(tree, 'app1');
|
||||||
|
expect(project.targets.build.options.outputPath).toStrictEqual({
|
||||||
|
base: 'dist/app1',
|
||||||
|
media: 'resources',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove unsupported options', async () => {
|
||||||
|
addProjectConfiguration(tree, 'app1', {
|
||||||
|
root: 'app1',
|
||||||
|
projectType: 'application',
|
||||||
|
targets: {
|
||||||
|
build: {
|
||||||
|
executor: '@angular-devkit/build-angular:browser',
|
||||||
|
options: {},
|
||||||
|
configurations: {
|
||||||
|
development: {
|
||||||
|
buildOptimizer: false,
|
||||||
|
vendorChunk: true,
|
||||||
|
commonChunk: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await convertToApplicationExecutor(tree, {});
|
||||||
|
|
||||||
|
const project = readProjectConfiguration(tree, 'app1');
|
||||||
|
expect(
|
||||||
|
project.targets.build.configurations.development.buildOptimizer
|
||||||
|
).toBeUndefined();
|
||||||
|
expect(
|
||||||
|
project.targets.build.configurations.development.vendorChunk
|
||||||
|
).toBeUndefined();
|
||||||
|
expect(
|
||||||
|
project.targets.build.configurations.development.commonChunk
|
||||||
|
).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('compat', () => {
|
||||||
|
it('should not convert outputs to the object notation when angular version is lower that 17.1.0', async () => {
|
||||||
|
updateJson(tree, 'package.json', (json) => {
|
||||||
|
json.dependencies['@angular/core'] = '17.0.0';
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
addProjectConfiguration(tree, 'app1', {
|
||||||
|
root: 'app1',
|
||||||
|
projectType: 'application',
|
||||||
|
targets: {
|
||||||
|
build: {
|
||||||
|
executor: '@angular-devkit/build-angular:browser',
|
||||||
|
outputs: ['{options.outputPath}'],
|
||||||
|
options: {
|
||||||
|
outputPath: 'dist/app1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await convertToApplicationExecutor(tree, {});
|
||||||
|
|
||||||
|
const project = readProjectConfiguration(tree, 'app1');
|
||||||
|
expect(project.targets.build.outputs).toStrictEqual([
|
||||||
|
'{options.outputPath}',
|
||||||
|
]);
|
||||||
|
expect(project.targets.build.options.outputPath).toBe('dist/app1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove trailing "/browser" from output path when angular version is lower that 17.1.0', async () => {
|
||||||
|
updateJson(tree, 'package.json', (json) => {
|
||||||
|
json.dependencies['@angular/core'] = '17.0.0';
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
addProjectConfiguration(tree, 'app1', {
|
||||||
|
root: 'app1',
|
||||||
|
projectType: 'application',
|
||||||
|
targets: {
|
||||||
|
build: {
|
||||||
|
executor: '@angular-devkit/build-angular:browser',
|
||||||
|
outputs: ['{options.outputPath}'],
|
||||||
|
options: {
|
||||||
|
outputPath: 'dist/app1/browser',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await convertToApplicationExecutor(tree, {});
|
||||||
|
|
||||||
|
const project = readProjectConfiguration(tree, 'app1');
|
||||||
|
expect(project.targets.build.outputs).toStrictEqual([
|
||||||
|
'{options.outputPath}',
|
||||||
|
]);
|
||||||
|
expect(project.targets.build.options.outputPath).toBe('dist/app1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,338 @@
|
|||||||
|
import {
|
||||||
|
formatFiles,
|
||||||
|
getProjects,
|
||||||
|
installPackagesTask,
|
||||||
|
logger,
|
||||||
|
readJson,
|
||||||
|
readProjectConfiguration,
|
||||||
|
updateProjectConfiguration,
|
||||||
|
writeJson,
|
||||||
|
type TargetConfiguration,
|
||||||
|
type Tree,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
import { dirname, join } from 'node:path/posix';
|
||||||
|
import { gte, lt } from 'semver';
|
||||||
|
import { allTargetOptions } from '../../utils/targets';
|
||||||
|
import { setupSsr } from '../setup-ssr/setup-ssr';
|
||||||
|
import { validateProject } from '../utils/validations';
|
||||||
|
import { getInstalledAngularVersionInfo } from '../utils/version-utils';
|
||||||
|
import type { GeneratorOptions } from './schema';
|
||||||
|
|
||||||
|
const executorsToConvert = new Set([
|
||||||
|
'@angular-devkit/build-angular:browser',
|
||||||
|
'@angular-devkit/build-angular:browser-esbuild',
|
||||||
|
'@nx/angular:webpack-browser',
|
||||||
|
'@nx/angular:browser-esbuild',
|
||||||
|
]);
|
||||||
|
const serverTargetExecutors = new Set([
|
||||||
|
'@angular-devkit/build-angular:server',
|
||||||
|
'@nx/angular:webpack-server',
|
||||||
|
]);
|
||||||
|
const redundantExecutors = new Set([
|
||||||
|
'@angular-devkit/build-angular:server',
|
||||||
|
'@angular-devkit/build-angular:prerender',
|
||||||
|
'@angular-devkit/build-angular:app-shell',
|
||||||
|
'@angular-devkit/build-angular:ssr-dev-server',
|
||||||
|
'@nx/angular:webpack-server',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export async function convertToApplicationExecutor(
|
||||||
|
tree: Tree,
|
||||||
|
options: GeneratorOptions
|
||||||
|
) {
|
||||||
|
const { major: angularMajorVersion, version: angularVersion } =
|
||||||
|
getInstalledAngularVersionInfo(tree);
|
||||||
|
if (angularMajorVersion < 17) {
|
||||||
|
throw new Error(
|
||||||
|
`The "convert-to-application-executor" generator is only supported in Angular >= 17.0.0. You are currently using "${angularVersion}".`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let didAnySucceed = false;
|
||||||
|
if (options.project) {
|
||||||
|
validateProject(tree, options.project);
|
||||||
|
didAnySucceed = await convertProjectTargets(
|
||||||
|
tree,
|
||||||
|
options.project,
|
||||||
|
angularVersion,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const projects = getProjects(tree);
|
||||||
|
for (const [projectName] of projects) {
|
||||||
|
logger.info(`Converting project "${projectName}"...`);
|
||||||
|
const success = await convertProjectTargets(
|
||||||
|
tree,
|
||||||
|
projectName,
|
||||||
|
angularVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
logger.info(`Project "${projectName}" converted successfully.`);
|
||||||
|
} else {
|
||||||
|
logger.info(
|
||||||
|
`Project "${projectName}" could not be converted. See above for more information.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
logger.info('');
|
||||||
|
didAnySucceed = didAnySucceed || success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.skipFormat) {
|
||||||
|
await formatFiles(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
return didAnySucceed ? () => installPackagesTask(tree) : () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function convertProjectTargets(
|
||||||
|
tree: Tree,
|
||||||
|
projectName: string,
|
||||||
|
angularVersion: string,
|
||||||
|
isProvidedProject = false
|
||||||
|
): Promise<boolean> {
|
||||||
|
function warnIfProvided(message: string): void {
|
||||||
|
if (isProvidedProject) {
|
||||||
|
logger.warn(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let project = readProjectConfiguration(tree, projectName);
|
||||||
|
if (project.projectType !== 'application') {
|
||||||
|
warnIfProvided(
|
||||||
|
`The provided project "${projectName}" is not an application. Skipping conversion.`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { buildTargetName, serverTargetName } = getTargetsToConvert(
|
||||||
|
project.targets
|
||||||
|
);
|
||||||
|
if (!buildTargetName) {
|
||||||
|
warnIfProvided(
|
||||||
|
`The provided project "${projectName}" does not have any targets using on of the ` +
|
||||||
|
`'@angular-devkit/build-angular:browser', '@angular-devkit/build-angular:browser-esbuild', ` +
|
||||||
|
`'@nx/angular:browser' and '@nx/angular:browser-esbuild' executors. Skipping conversion.`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useNxExecutor =
|
||||||
|
project.targets[buildTargetName].executor.startsWith('@nx/angular:');
|
||||||
|
const newExecutor = useNxExecutor
|
||||||
|
? '@nx/angular:application'
|
||||||
|
: '@angular-devkit/build-angular:application';
|
||||||
|
|
||||||
|
const buildTarget = project.targets[buildTargetName];
|
||||||
|
buildTarget.executor = newExecutor;
|
||||||
|
|
||||||
|
if (gte(angularVersion, '17.1.0') && buildTarget.outputs) {
|
||||||
|
buildTarget.outputs = buildTarget.outputs.map((output) =>
|
||||||
|
output === '{options.outputPath}' ? '{options.outputPath.base}' : output
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [, options] of allTargetOptions(buildTarget)) {
|
||||||
|
if (options['index'] === '') {
|
||||||
|
options['index'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename and transform options
|
||||||
|
options['browser'] = options['main'];
|
||||||
|
if (serverTargetName && typeof options['browser'] === 'string') {
|
||||||
|
options['server'] = dirname(options['browser']) + '/main.server.ts';
|
||||||
|
}
|
||||||
|
options['serviceWorker'] =
|
||||||
|
options['ngswConfigPath'] ?? options['serviceWorker'];
|
||||||
|
|
||||||
|
if (typeof options['polyfills'] === 'string') {
|
||||||
|
options['polyfills'] = [options['polyfills']];
|
||||||
|
}
|
||||||
|
|
||||||
|
let outputPath = options['outputPath'];
|
||||||
|
if (lt(angularVersion, '17.1.0')) {
|
||||||
|
options['outputPath'] = outputPath?.replace(/\/browser\/?$/, '');
|
||||||
|
} else if (typeof outputPath === 'string') {
|
||||||
|
if (!/\/browser\/?$/.test(outputPath)) {
|
||||||
|
logger.warn(
|
||||||
|
`The output location of the browser build has been updated from "${outputPath}" to ` +
|
||||||
|
`"${join(outputPath, 'browser')}". ` +
|
||||||
|
'You might need to adjust your deployment pipeline or, as an alternative, ' +
|
||||||
|
'set outputPath.browser to "" in order to maintain the previous functionality.'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
outputPath = outputPath.replace(/\/browser\/?$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
options['outputPath'] = {
|
||||||
|
base: outputPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof options['resourcesOutputPath'] === 'string') {
|
||||||
|
const media = options['resourcesOutputPath'].replaceAll('/', '');
|
||||||
|
if (media && media !== 'media') {
|
||||||
|
options['outputPath'] = {
|
||||||
|
base: outputPath,
|
||||||
|
media: media,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removed options
|
||||||
|
delete options['deployUrl'];
|
||||||
|
delete options['vendorChunk'];
|
||||||
|
delete options['commonChunk'];
|
||||||
|
delete options['resourcesOutputPath'];
|
||||||
|
delete options['buildOptimizer'];
|
||||||
|
delete options['main'];
|
||||||
|
delete options['ngswConfigPath'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge browser and server tsconfig
|
||||||
|
if (serverTargetName) {
|
||||||
|
const browserTsConfigPath = buildTarget?.options?.tsConfig;
|
||||||
|
const serverTsConfigPath = project.targets['server']?.options?.tsConfig;
|
||||||
|
|
||||||
|
if (typeof browserTsConfigPath !== 'string') {
|
||||||
|
logger.warn(
|
||||||
|
`Cannot update project "${projectName}" to use the application executor ` +
|
||||||
|
`as the browser tsconfig cannot be located.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof serverTsConfigPath !== 'string') {
|
||||||
|
logger.warn(
|
||||||
|
`Cannot update project "${projectName}" to use the application executor ` +
|
||||||
|
`as the server tsconfig cannot be located.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const browserTsConfigJson = readJson(tree, browserTsConfigPath);
|
||||||
|
const serverTsConfigJson = readJson(tree, serverTsConfigPath);
|
||||||
|
|
||||||
|
const files = new Set([
|
||||||
|
...(browserTsConfigJson.files ?? []),
|
||||||
|
...(serverTsConfigJson.files ?? []),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Server file will be added later by the setup-ssr generator
|
||||||
|
files.delete('server.ts');
|
||||||
|
|
||||||
|
browserTsConfigJson.files = Array.from(files);
|
||||||
|
browserTsConfigJson.compilerOptions ?? {};
|
||||||
|
browserTsConfigJson.compilerOptions.types = Array.from(
|
||||||
|
new Set([
|
||||||
|
...(browserTsConfigJson.compilerOptions.types ?? []),
|
||||||
|
...(serverTsConfigJson.compilerOptions?.types ?? []),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete server tsconfig
|
||||||
|
tree.delete(serverTsConfigPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update project main tsconfig
|
||||||
|
const projectRootTsConfigPath = join(project.root, 'tsconfig.json');
|
||||||
|
if (tree.exists(projectRootTsConfigPath)) {
|
||||||
|
const rootTsConfigJson = readJson(tree, projectRootTsConfigPath);
|
||||||
|
rootTsConfigJson.compilerOptions ?? {};
|
||||||
|
rootTsConfigJson.compilerOptions.esModuleInterop = true;
|
||||||
|
rootTsConfigJson.compilerOptions.downlevelIteration = undefined;
|
||||||
|
rootTsConfigJson.compilerOptions.allowSyntheticDefaultImports = undefined;
|
||||||
|
writeJson(tree, projectRootTsConfigPath, rootTsConfigJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update server file
|
||||||
|
const ssrMainFile = project.targets['server']?.options?.['main'];
|
||||||
|
if (typeof ssrMainFile === 'string') {
|
||||||
|
tree.delete(ssrMainFile);
|
||||||
|
// apply changes so the setup-ssr generator can access the updated project
|
||||||
|
updateProjectConfiguration(tree, projectName, project);
|
||||||
|
await setupSsr(tree, { project: projectName, skipFormat: true });
|
||||||
|
// re-read project configuration as it might have changed
|
||||||
|
project = readProjectConfiguration(tree, projectName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all redundant targets
|
||||||
|
for (const [targetName, target] of Object.entries(project.targets)) {
|
||||||
|
if (redundantExecutors.has(target.executor)) {
|
||||||
|
delete project.targets[targetName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProjectConfiguration(tree, projectName, project);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTargetsToConvert(targets: Record<string, TargetConfiguration>): {
|
||||||
|
buildTargetName?: string;
|
||||||
|
serverTargetName?: string;
|
||||||
|
} {
|
||||||
|
let buildTargetName: string;
|
||||||
|
let serverTargetName: string;
|
||||||
|
for (const target of Object.keys(targets)) {
|
||||||
|
if (
|
||||||
|
targets[target].executor === '@nx/angular:application' ||
|
||||||
|
targets[target].executor === '@angular-devkit/build-angular:application'
|
||||||
|
) {
|
||||||
|
logger.warn(
|
||||||
|
'The project is already using the application builder. Skipping conversion.'
|
||||||
|
);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// build target
|
||||||
|
if (executorsToConvert.has(targets[target].executor)) {
|
||||||
|
for (const [, options] of allTargetOptions(targets[target])) {
|
||||||
|
if (options.deployUrl) {
|
||||||
|
logger.warn(
|
||||||
|
`The project is using the "deployUrl" option which is not available in the application builder. Skipping conversion.`
|
||||||
|
);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (options.customWebpackConfig) {
|
||||||
|
logger.warn(
|
||||||
|
`The project is using a custom webpack configuration which is not supported by the esbuild-based application executor. Skipping conversion.`
|
||||||
|
);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildTargetName) {
|
||||||
|
logger.warn(
|
||||||
|
'The project has more than one build target. Skipping conversion.'
|
||||||
|
);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
buildTargetName = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
// server target
|
||||||
|
if (serverTargetExecutors.has(targets[target].executor)) {
|
||||||
|
if (targets[target].executor === '@nx/angular:webpack-server') {
|
||||||
|
for (const [, options] of allTargetOptions(targets[target])) {
|
||||||
|
if (options.customWebpackConfig) {
|
||||||
|
logger.warn(
|
||||||
|
`The project is using a custom webpack configuration which is not supported by the esbuild-based application executor. Skipping conversion.`
|
||||||
|
);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverTargetName) {
|
||||||
|
logger.warn(
|
||||||
|
'The project has more than one server target. Skipping conversion.'
|
||||||
|
);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
serverTargetName = target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { buildTargetName, serverTargetName };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default convertToApplicationExecutor;
|
||||||
4
packages/angular/src/generators/convert-to-application-executor/schema.d.ts
vendored
Normal file
4
packages/angular/src/generators/convert-to-application-executor/schema.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface GeneratorOptions {
|
||||||
|
project?: string;
|
||||||
|
skipFormat?: boolean;
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/schema",
|
||||||
|
"$id": "NxAngularConvertToApplicationExecutorGenerator",
|
||||||
|
"cli": "nx",
|
||||||
|
"title": "Converts projects to use the `@nx/angular:application` executor or the `@angular-devkit/build-angular:application` builder. _Note: this is only supported in Angular versions >= 17.0.0_.",
|
||||||
|
"description": "Converts a project or all projects using one of the `@angular-devkit/build-angular:browser`, `@angular-devkit/build-angular:browser-esbuild`, `@nx/angular:browser` and `@nx/angular:browser-esbuild` executors to use the `@nx/angular:application` executor or the `@angular-devkit/build-angular:application` builder. If the converted target is using one of the `@nx/angular` executors, the `@nx/angular:application` executor will be used. Otherwise, the `@angular-devkit/build-angular:application` builder will be used.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Name of the Angular application project to convert. It has to contain a target using one of the `@angular-devkit/build-angular:browser`, `@angular-devkit/build-angular:browser-esbuild`, `@nx/angular:browser` and `@nx/angular:browser-esbuild` executors. If not specified, all projects with such targets will be converted.",
|
||||||
|
"$default": {
|
||||||
|
"$source": "argv",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
"x-priority": "important"
|
||||||
|
},
|
||||||
|
"skipFormat": {
|
||||||
|
"description": "Skip formatting files.",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"x-priority": "internal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
@ -0,0 +1,159 @@
|
|||||||
|
import { addProjectConfiguration, type Tree } from '@nx/devkit';
|
||||||
|
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||||
|
import { addHydration } from './add-hydration';
|
||||||
|
|
||||||
|
describe('add-hydration', () => {
|
||||||
|
let tree: Tree;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tree = createTreeWithEmptyWorkspace();
|
||||||
|
addProjectConfiguration(tree, 'app1', {
|
||||||
|
root: 'app1',
|
||||||
|
sourceRoot: 'app1/src',
|
||||||
|
projectType: 'application',
|
||||||
|
targets: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add "provideClientHydration" for standalone config', () => {
|
||||||
|
tree.write(
|
||||||
|
'app1/src/app/app.config.ts',
|
||||||
|
`import { ApplicationConfig } from '@angular/core';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { appRoutes } from './app.routes';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [provideRouter(appRoutes)],
|
||||||
|
};
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
addHydration(tree, { project: 'app1', standalone: true });
|
||||||
|
|
||||||
|
expect(tree.read('app1/src/app/app.config.ts', 'utf-8'))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"import { ApplicationConfig } from '@angular/core';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { appRoutes } from './app.routes';
|
||||||
|
import { provideClientHydration } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [provideClientHydration(),provideRouter(appRoutes)],
|
||||||
|
};
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not duplicate "provideClientHydration" for standalone config', () => {
|
||||||
|
tree.write(
|
||||||
|
'app1/src/app/app.config.ts',
|
||||||
|
`import { ApplicationConfig } from '@angular/core';
|
||||||
|
import { provideClientHydration } from '@angular/platform-browser';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { appRoutes } from './app.routes';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [provideClientHydration(), provideRouter(appRoutes)],
|
||||||
|
};
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
addHydration(tree, { project: 'app1', standalone: true });
|
||||||
|
|
||||||
|
expect(tree.read('app1/src/app/app.config.ts', 'utf-8'))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"import { ApplicationConfig } from '@angular/core';
|
||||||
|
import { provideClientHydration } from '@angular/platform-browser';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { appRoutes } from './app.routes';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [provideClientHydration(), provideRouter(appRoutes)],
|
||||||
|
};
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add "provideClientHydration" for non-standalone config', () => {
|
||||||
|
tree.write(
|
||||||
|
'app1/src/app/app.module.ts',
|
||||||
|
`import { NgModule } from '@angular/core';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import { appRoutes } from './app.routes';
|
||||||
|
import { NxWelcomeComponent } from './nx-welcome.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AppComponent, NxWelcomeComponent],
|
||||||
|
imports: [BrowserModule, RouterModule.forRoot(appRoutes)],
|
||||||
|
bootstrap: [AppComponent],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
addHydration(tree, { project: 'app1', standalone: false });
|
||||||
|
|
||||||
|
expect(tree.read('app1/src/app/app.module.ts', 'utf-8'))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"import { NgModule } from '@angular/core';
|
||||||
|
import { BrowserModule, provideClientHydration } from '@angular/platform-browser';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import { appRoutes } from './app.routes';
|
||||||
|
import { NxWelcomeComponent } from './nx-welcome.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AppComponent, NxWelcomeComponent],
|
||||||
|
imports: [BrowserModule, RouterModule.forRoot(appRoutes)],
|
||||||
|
bootstrap: [AppComponent],
|
||||||
|
providers: [provideClientHydration()],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not duplicate "provideClientHydration" for non-standalone config', () => {
|
||||||
|
tree.write(
|
||||||
|
'app1/src/app/app.module.ts',
|
||||||
|
`import { NgModule } from '@angular/core';
|
||||||
|
import { BrowserModule, provideClientHydration } from '@angular/platform-browser';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import { appRoutes } from './app.routes';
|
||||||
|
import { NxWelcomeComponent } from './nx-welcome.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AppComponent, NxWelcomeComponent],
|
||||||
|
imports: [BrowserModule, RouterModule.forRoot(appRoutes)],
|
||||||
|
bootstrap: [AppComponent],
|
||||||
|
providers: [provideClientHydration()],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
addHydration(tree, { project: 'app1', standalone: false });
|
||||||
|
|
||||||
|
expect(tree.read('app1/src/app/app.module.ts', 'utf-8'))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"import { NgModule } from '@angular/core';
|
||||||
|
import { BrowserModule, provideClientHydration } from '@angular/platform-browser';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import { appRoutes } from './app.routes';
|
||||||
|
import { NxWelcomeComponent } from './nx-welcome.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AppComponent, NxWelcomeComponent],
|
||||||
|
imports: [BrowserModule, RouterModule.forRoot(appRoutes)],
|
||||||
|
bootstrap: [AppComponent],
|
||||||
|
providers: [provideClientHydration()],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -3,23 +3,46 @@ import {
|
|||||||
readProjectConfiguration,
|
readProjectConfiguration,
|
||||||
type Tree,
|
type Tree,
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import { type Schema } from '../schema';
|
import { insertImport } from '@nx/js';
|
||||||
|
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
|
||||||
|
import type { CallExpression, SourceFile } from 'typescript';
|
||||||
import {
|
import {
|
||||||
addProviderToAppConfig,
|
addProviderToAppConfig,
|
||||||
addProviderToModule,
|
addProviderToModule,
|
||||||
} from '../../../utils/nx-devkit/ast-utils';
|
} from '../../../utils/nx-devkit/ast-utils';
|
||||||
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
|
import { type Schema } from '../schema';
|
||||||
import { SourceFile } from 'typescript';
|
|
||||||
import { insertImport } from '@nx/js';
|
|
||||||
|
|
||||||
let tsModule: typeof import('typescript');
|
let tsModule: typeof import('typescript');
|
||||||
|
let tsquery: typeof import('@phenomnomnominal/tsquery').tsquery;
|
||||||
|
|
||||||
export function addHydration(tree: Tree, options: Schema) {
|
export function addHydration(tree: Tree, options: Schema) {
|
||||||
const projectConfig = readProjectConfiguration(tree, options.project);
|
const projectConfig = readProjectConfiguration(tree, options.project);
|
||||||
|
|
||||||
if (!tsModule) {
|
if (!tsModule) {
|
||||||
tsModule = ensureTypescript();
|
tsModule = ensureTypescript();
|
||||||
|
tsquery = require('@phenomnomnominal/tsquery').tsquery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pathToClientConfigFile = options.standalone
|
||||||
|
? joinPathFragments(projectConfig.sourceRoot, 'app/app.config.ts')
|
||||||
|
: joinPathFragments(projectConfig.sourceRoot, 'app/app.module.ts');
|
||||||
|
|
||||||
|
const sourceText = tree.read(pathToClientConfigFile, 'utf-8');
|
||||||
|
let sourceFile = tsModule.createSourceFile(
|
||||||
|
pathToClientConfigFile,
|
||||||
|
sourceText,
|
||||||
|
tsModule.ScriptTarget.Latest,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const provideClientHydrationCallExpression = tsquery<CallExpression>(
|
||||||
|
sourceFile,
|
||||||
|
'ObjectLiteralExpression PropertyAssignment:has(Identifier[name=providers]) ArrayLiteralExpression CallExpression:has(Identifier[name=provideClientHydration])'
|
||||||
|
)[0];
|
||||||
|
if (provideClientHydrationCallExpression) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const addImport = (
|
const addImport = (
|
||||||
source: SourceFile,
|
source: SourceFile,
|
||||||
symbolName: string,
|
symbolName: string,
|
||||||
@ -37,18 +60,6 @@ export function addHydration(tree: Tree, options: Schema) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const pathToClientConfigFile = options.standalone
|
|
||||||
? joinPathFragments(projectConfig.sourceRoot, 'app/app.config.ts')
|
|
||||||
: joinPathFragments(projectConfig.sourceRoot, 'app/app.module.ts');
|
|
||||||
|
|
||||||
const sourceText = tree.read(pathToClientConfigFile, 'utf-8');
|
|
||||||
let sourceFile = tsModule.createSourceFile(
|
|
||||||
pathToClientConfigFile,
|
|
||||||
sourceText,
|
|
||||||
tsModule.ScriptTarget.Latest,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
sourceFile = addImport(
|
sourceFile = addImport(
|
||||||
sourceFile,
|
sourceFile,
|
||||||
'provideClientHydration',
|
'provideClientHydration',
|
||||||
|
|||||||
@ -84,7 +84,9 @@ export function updateProjectConfigForBrowserBuilder(
|
|||||||
|
|
||||||
projectConfig.targets.server = {
|
projectConfig.targets.server = {
|
||||||
dependsOn: ['build'],
|
dependsOn: ['build'],
|
||||||
executor: '@angular-devkit/build-angular:server',
|
executor: buildTarget.executor.startsWith('@angular-devkit/build-angular:')
|
||||||
|
? '@angular-devkit/build-angular:server'
|
||||||
|
: '@nx/angular:webpack-server',
|
||||||
options: {
|
options: {
|
||||||
outputPath: joinPathFragments(baseOutputPath, 'server'),
|
outputPath: joinPathFragments(baseOutputPath, 'server'),
|
||||||
main: joinPathFragments(projectConfig.root, schema.serverFileName),
|
main: joinPathFragments(projectConfig.root, schema.serverFileName),
|
||||||
|
|||||||
@ -27,7 +27,8 @@ export async function setupSsr(tree: Tree, schema: Schema) {
|
|||||||
|
|
||||||
const { targets } = readProjectConfiguration(tree, options.project);
|
const { targets } = readProjectConfiguration(tree, options.project);
|
||||||
const isUsingApplicationBuilder =
|
const isUsingApplicationBuilder =
|
||||||
targets.build.executor === '@angular-devkit/build-angular:application';
|
targets.build.executor === '@angular-devkit/build-angular:application' ||
|
||||||
|
targets.build.executor === '@nx/angular:application';
|
||||||
|
|
||||||
addDependencies(tree, isUsingApplicationBuilder);
|
addDependencies(tree, isUsingApplicationBuilder);
|
||||||
generateSSRFiles(tree, options, isUsingApplicationBuilder);
|
generateSSRFiles(tree, options, isUsingApplicationBuilder);
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import {
|
|||||||
readJson,
|
readJson,
|
||||||
removeDependenciesFromPackageJson,
|
removeDependenciesFromPackageJson,
|
||||||
visitNotIgnoredFiles,
|
visitNotIgnoredFiles,
|
||||||
type TargetConfiguration,
|
|
||||||
type Tree,
|
type Tree,
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import { dirname, relative } from 'path';
|
import { dirname, relative } from 'path';
|
||||||
@ -13,6 +12,7 @@ import {
|
|||||||
getInstalledPackageVersionInfo,
|
getInstalledPackageVersionInfo,
|
||||||
versions,
|
versions,
|
||||||
} from '../../generators/utils/version-utils';
|
} from '../../generators/utils/version-utils';
|
||||||
|
import { allTargetOptions } from '../../utils/targets';
|
||||||
import { getProjectsFilteredByDependencies } from '../utils/projects';
|
import { getProjectsFilteredByDependencies } from '../utils/projects';
|
||||||
|
|
||||||
const UNIVERSAL_PACKAGES = [
|
const UNIVERSAL_PACKAGES = [
|
||||||
@ -141,24 +141,6 @@ export default async function (tree: Tree) {
|
|||||||
await formatFiles(tree);
|
await formatFiles(tree);
|
||||||
}
|
}
|
||||||
|
|
||||||
function* allTargetOptions<T>(
|
|
||||||
target: TargetConfiguration<T>
|
|
||||||
): Iterable<[string | undefined, T]> {
|
|
||||||
if (target.options) {
|
|
||||||
yield [undefined, target.options];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!target.configurations) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [name, options] of Object.entries(target.configurations)) {
|
|
||||||
if (options !== undefined) {
|
|
||||||
yield [name, options];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const TOKENS_FILE_CONTENT = `
|
const TOKENS_FILE_CONTENT = `
|
||||||
import { InjectionToken } from '@angular/core';
|
import { InjectionToken } from '@angular/core';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
|||||||
19
packages/angular/src/utils/targets.ts
Normal file
19
packages/angular/src/utils/targets.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { TargetConfiguration } from '@nx/devkit';
|
||||||
|
|
||||||
|
export function* allTargetOptions<T>(
|
||||||
|
target: TargetConfiguration<T>
|
||||||
|
): Iterable<[string | undefined, T]> {
|
||||||
|
if (target.options) {
|
||||||
|
yield [undefined, target.options];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target.configurations) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [name, options] of Object.entries(target.configurations)) {
|
||||||
|
if (options !== undefined) {
|
||||||
|
yield [name, options];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user