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,
|
||||
"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",
|
||||
"path": "/nx-api/angular/generators/directive",
|
||||
|
||||
@ -177,6 +177,15 @@
|
||||
"path": "/nx-api/angular/generators/component-test",
|
||||
"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": {
|
||||
"description": "Generate an Angular directive.",
|
||||
"file": "generated/packages/angular/generators/directive.json",
|
||||
|
||||
@ -172,6 +172,15 @@
|
||||
"path": "angular/generators/component-test",
|
||||
"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.",
|
||||
"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-story](/nx-api/angular/generators/component-story)
|
||||
- [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)
|
||||
- [federate-module](/nx-api/angular/generators/federate-module)
|
||||
- [init](/nx-api/angular/generators/init)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { names } from '@nx/devkit';
|
||||
import {
|
||||
checkFilesDoNotExist,
|
||||
checkFilesExist,
|
||||
cleanupProject,
|
||||
getSize,
|
||||
@ -8,6 +9,7 @@ import {
|
||||
newProject,
|
||||
readFile,
|
||||
removeFile,
|
||||
rmDist,
|
||||
runCLI,
|
||||
runCommandUntil,
|
||||
runE2ETests,
|
||||
@ -552,4 +554,52 @@ describe('Angular Projects', () => {
|
||||
`Successfully ran target test for project ${libName}`
|
||||
);
|
||||
}, 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",
|
||||
"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": {
|
||||
"factory": "./src/generators/directive/directive",
|
||||
"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,
|
||||
type Tree,
|
||||
} 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 {
|
||||
addProviderToAppConfig,
|
||||
addProviderToModule,
|
||||
} from '../../../utils/nx-devkit/ast-utils';
|
||||
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
|
||||
import { SourceFile } from 'typescript';
|
||||
import { insertImport } from '@nx/js';
|
||||
import { type Schema } from '../schema';
|
||||
|
||||
let tsModule: typeof import('typescript');
|
||||
let tsquery: typeof import('@phenomnomnominal/tsquery').tsquery;
|
||||
|
||||
export function addHydration(tree: Tree, options: Schema) {
|
||||
const projectConfig = readProjectConfiguration(tree, options.project);
|
||||
|
||||
if (!tsModule) {
|
||||
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 = (
|
||||
source: SourceFile,
|
||||
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,
|
||||
'provideClientHydration',
|
||||
|
||||
@ -84,7 +84,9 @@ export function updateProjectConfigForBrowserBuilder(
|
||||
|
||||
projectConfig.targets.server = {
|
||||
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: {
|
||||
outputPath: joinPathFragments(baseOutputPath, 'server'),
|
||||
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 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);
|
||||
generateSSRFiles(tree, options, isUsingApplicationBuilder);
|
||||
|
||||
@ -5,7 +5,6 @@ import {
|
||||
readJson,
|
||||
removeDependenciesFromPackageJson,
|
||||
visitNotIgnoredFiles,
|
||||
type TargetConfiguration,
|
||||
type Tree,
|
||||
} from '@nx/devkit';
|
||||
import { dirname, relative } from 'path';
|
||||
@ -13,6 +12,7 @@ import {
|
||||
getInstalledPackageVersionInfo,
|
||||
versions,
|
||||
} from '../../generators/utils/version-utils';
|
||||
import { allTargetOptions } from '../../utils/targets';
|
||||
import { getProjectsFilteredByDependencies } from '../utils/projects';
|
||||
|
||||
const UNIVERSAL_PACKAGES = [
|
||||
@ -141,24 +141,6 @@ export default async function (tree: 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 = `
|
||||
import { InjectionToken } from '@angular/core';
|
||||
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