feat(angular): support angular v16 (#15592)

This commit is contained in:
Leosvel Pérez Espinosa 2023-05-03 16:02:07 +01:00 committed by GitHub
parent 9c3698982b
commit f45aa076da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
109 changed files with 4254 additions and 3461 deletions

View File

@ -993,8 +993,8 @@ Use this to expose a compatible Angular Builder
#### Parameters #### Parameters
| Name | Type | | Name | Type |
| :--------- | :-------------------------------------------------------------- | | :--------- | :------------------------------------------------------ |
| `executor` | [`Executor`](../../devkit/documents/nx_devkit#executor)<`any`\> | | `executor` | [`Executor`](../../devkit/documents/nx_devkit#executor) |
#### Returns #### Returns

View File

@ -6,7 +6,7 @@
"outputCapture": "direct-nodejs", "outputCapture": "direct-nodejs",
"$schema": "http://json-schema.org/schema", "$schema": "http://json-schema.org/schema",
"title": "ng-packagr Target", "title": "ng-packagr Target",
"description": "Builds a library with support for incremental builds.\n\nThis executor is meant to be used with buildable libraries in an incremental build scenario. It is similar to the `@nrwl/angular:package` executor but with some key differences:\n- It doesn't run `ngcc` automatically (`ngcc` needs to be run separately beforehand if needed, this can be done in a `postinstall` hook on `package.json`).\n- It only produces ESM2020 bundles.\n- It doesn't generate package exports in the `package.json`.", "description": "Builds a library with support for incremental builds.\n\nThis executor is meant to be used with buildable libraries in an incremental build scenario. It is similar to the `@nx/angular:package` executor but with some key differences:\n- For supported Angular versions lower than v16, it doesn't run `ngcc` automatically (`ngcc` is no longer available from Angular v16 onwards, for lower versions, it needs to be run separately beforehand if needed, this can be done in a `postinstall` hook on `package.json`).\n- It only produces ESM2022 bundles (ESM2020 for supported Angular versions lower than v16).",
"cli": "nx", "cli": "nx",
"type": "object", "type": "object",
"presets": [ "presets": [

View File

@ -247,6 +247,11 @@
"description": "Extract all licenses in a separate file, in the case of production builds only.", "description": "Extract all licenses in a separate file, in the case of production builds only.",
"default": true "default": true
}, },
"buildOptimizer": {
"type": "boolean",
"description": "Enables advanced build optimizations. _Note: only supported in Angular versions >= 16.0.0_.",
"default": true
},
"namedChunks": { "namedChunks": {
"type": "boolean", "type": "boolean",
"description": "Use file name for lazy loaded chunks.", "description": "Use file name for lazy loaded chunks.",

View File

@ -19,7 +19,7 @@
"appId": { "appId": {
"type": "string", "type": "string",
"format": "html-selector", "format": "html-selector",
"description": "The `appId` to use with `withServerTransition`.", "description": "The `appId` to use with `withServerTransition`. _Note: This is only used in Angular versions <16.0.0. It's deprecated since Angular 16._",
"default": "serverApp" "default": "serverApp"
}, },
"main": { "main": {
@ -49,6 +49,10 @@
"description": "The name of the root module class.", "description": "The name of the root module class.",
"default": "AppServerModule" "default": "AppServerModule"
}, },
"standalone": {
"type": "boolean",
"description": "Use Standalone Components to bootstrap SSR. _Note: This is only supported in Angular versions >= 14.1.0_."
},
"skipFormat": { "skipFormat": {
"type": "boolean", "type": "boolean",
"description": "Skip formatting the workspace after the generator completes.", "description": "Skip formatting the workspace after the generator completes.",

View File

@ -993,8 +993,8 @@ Use this to expose a compatible Angular Builder
#### Parameters #### Parameters
| Name | Type | | Name | Type |
| :--------- | :-------------------------------------------------------------- | | :--------- | :------------------------------------------------------ |
| `executor` | [`Executor`](../../devkit/documents/nx_devkit#executor)<`any`\> | | `executor` | [`Executor`](../../devkit/documents/nx_devkit#executor) |
#### Returns #### Returns

View File

@ -177,7 +177,7 @@ describe('Tailwind support', () => {
libSpacing: typeof spacing['root'] libSpacing: typeof spacing['root']
) => { ) => {
const builtComponentContent = readFile( const builtComponentContent = readFile(
`dist/libs/${lib}/esm2020/lib/foo.component.mjs` `dist/libs/${lib}/esm2022/lib/foo.component.mjs`
); );
let expectedStylesRegex = new RegExp( let expectedStylesRegex = new RegExp(
`styles: \\[\\"\\.custom\\-btn(\\[_ngcontent\\-%COMP%\\])?{margin:${libSpacing.md};padding:${libSpacing.sm}}(\\\\n)?\\"\\]` `styles: \\[\\"\\.custom\\-btn(\\[_ngcontent\\-%COMP%\\])?{margin:${libSpacing.md};padding:${libSpacing.sm}}(\\\\n)?\\"\\]`

View File

@ -601,9 +601,10 @@ function updateGeneratedRuleImplementation(
true true
); );
const transformer = const transformer = <T extends ts.SourceFile>(
<T extends ts.SourceFile>(context: ts.TransformationContext) => context: ts.TransformationContext
(rootNode: T) => { ) =>
((rootNode: T) => {
function visit(node: ts.Node): ts.Node { function visit(node: ts.Node): ts.Node {
/** /**
* Add an ESLint messageId which will show the knownLintErrorMessage * Add an ESLint messageId which will show the knownLintErrorMessage
@ -752,7 +753,7 @@ function updateGeneratedRuleImplementation(
...rootNode.statements, ...rootNode.statements,
]); ]);
return ts.visitNode(importAdded, visit); return ts.visitNode(importAdded, visit);
}; }) as ts.Transformer<T>;
const result: ts.TransformationResult<ts.SourceFile> = const result: ts.TransformationResult<ts.SourceFile> =
ts.transform<ts.SourceFile>(newRuleSourceFile, [transformer]); ts.transform<ts.SourceFile>(newRuleSourceFile, [transformer]);

View File

@ -493,7 +493,7 @@ describe('nest libraries', function () {
const nestlib = uniq('nestlib'); const nestlib = uniq('nestlib');
runCLI(`generate @nx/nest:lib ${nestlib} --buildable`); runCLI(`generate @nx/nest:lib ${nestlib} --buildable`);
packageInstall('@nestjs/swagger', undefined, '~6.0.0'); packageInstall('@nestjs/swagger', undefined, '~6.3.0');
updateProjectConfig(nestlib, (config) => { updateProjectConfig(nestlib, (config) => {
config.targets.build.options.transformers = [ config.targets.build.options.transformers = [

View File

@ -219,10 +219,10 @@ export async function h() { return 'c'; }
const json = JSON.parse(content); const json = JSON.parse(content);
/** /**
* Set target as es3!! * Set target as es5!!
*/ */
json.compilerOptions.target = 'es3'; json.compilerOptions.target = 'es5';
return JSON.stringify(json, null, 2); return JSON.stringify(json, null, 2);
}); });
// What we're testing // What we're testing

View File

@ -1,7 +1,6 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"sourceMap": false,
"outDir": "../../dist/out-tsc", "outDir": "../../dist/out-tsc",
"allowJs": true, "allowJs": true,
"types": ["cypress", "node"], "types": ["cypress", "node"],

View File

@ -1,5 +1,8 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": {
"sourceMap": false
},
"files": [], "files": [],
"include": [ "include": [
"cypress.config.ts", "cypress.config.ts",

View File

@ -26,21 +26,22 @@
}, },
"devDependencies": { "devDependencies": {
"@actions/core": "^1.10.0", "@actions/core": "^1.10.0",
"@angular-devkit/architect": "~0.1502.0", "@angular-devkit/architect": "~0.1600.0-rc.4",
"@angular-devkit/build-angular": "~15.2.0", "@angular-devkit/build-angular": "~16.0.0-rc.4",
"@angular-devkit/core": "~15.2.0", "@angular-devkit/core": "~16.0.0-rc.4",
"@angular-devkit/schematics": "~15.2.0", "@angular-devkit/schematics": "~16.0.0-rc.4",
"@angular-eslint/eslint-plugin": "~15.0.0", "@angular-eslint/eslint-plugin": "~16.0.0-alpha.1",
"@angular-eslint/eslint-plugin-template": "~15.0.0", "@angular-eslint/eslint-plugin-template": "~16.0.0-alpha.1",
"@angular-eslint/template-parser": "~15.0.0", "@angular-eslint/template-parser": "~16.0.0-alpha.1",
"@angular/cli": "~15.2.0", "@angular/cli": "~16.0.0-rc.4",
"@angular/common": "~15.2.0", "@angular/common": "~16.0.0-rc.4",
"@angular/compiler": "~15.2.0", "@angular/compiler": "~16.0.0-rc.4",
"@angular/compiler-cli": "~15.2.0", "@angular/compiler-cli": "~16.0.0-rc.4",
"@angular/core": "~15.2.0", "@angular/core": "~16.0.0-rc.4",
"@angular/router": "~15.2.0", "@angular/router": "~16.0.0-rc.4",
"@babel/core": "^7.15.0", "@babel/core": "^7.15.0",
"@babel/helper-create-regexp-features-plugin": "^7.14.5", "@babel/helper-create-regexp-features-plugin": "^7.14.5",
"@babel/plugin-transform-runtime": "^7.15.0",
"@babel/preset-react": "^7.14.5", "@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.15.0", "@babel/preset-typescript": "^7.15.0",
"@floating-ui/react": "0.19.2", "@floating-ui/react": "0.19.2",
@ -61,7 +62,7 @@
"@ngrx/effects": "~15.3.0", "@ngrx/effects": "~15.3.0",
"@ngrx/router-store": "~15.3.0", "@ngrx/router-store": "~15.3.0",
"@ngrx/store": "~15.3.0", "@ngrx/store": "~15.3.0",
"@nguniversal/builders": "~15.2.0", "@nguniversal/builders": "~16.0.0-rc.2",
"nx-cloud": "16.0.5", "nx-cloud": "16.0.5",
"@nx/webpack": "16.0.0-rc.0", "@nx/webpack": "16.0.0-rc.0",
"@parcel/watcher": "2.0.4", "@parcel/watcher": "2.0.4",
@ -75,13 +76,13 @@
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.0.4", "@rollup/plugin-node-resolve": "^13.0.4",
"@rollup/plugin-url": "^7.0.0", "@rollup/plugin-url": "^7.0.0",
"@schematics/angular": "~15.2.0", "@schematics/angular": "~16.0.0-rc.4",
"@storybook/addon-essentials": "^7.0.2", "@storybook/addon-essentials": "^7.0.8",
"@storybook/angular": "^7.0.2", "@storybook/angular": "^7.0.8",
"@storybook/core-server": "^7.0.2", "@storybook/core-server": "^7.0.8",
"@storybook/react": "^7.0.2", "@storybook/react": "^7.0.8",
"@storybook/react-webpack5": "^7.0.2", "@storybook/react-webpack5": "^7.0.8",
"@storybook/types": "^7.0.2", "@storybook/types": "^7.0.8",
"@svgr/rollup": "^6.1.2", "@svgr/rollup": "^6.1.2",
"@svgr/webpack": "^6.1.2", "@svgr/webpack": "^6.1.2",
"@swc-node/register": "^1.4.2", "@swc-node/register": "^1.4.2",
@ -198,7 +199,7 @@
"mini-css-extract-plugin": "~2.4.7", "mini-css-extract-plugin": "~2.4.7",
"minimatch": "3.0.5", "minimatch": "3.0.5",
"next-sitemap": "^3.1.10", "next-sitemap": "^3.1.10",
"ng-packagr": "~15.2.2", "ng-packagr": "~16.0.0-rc.1",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.7",
"nx": "16.0.0-rc.0", "nx": "16.0.0-rc.0",
"octokit": "^2.0.14", "octokit": "^2.0.14",
@ -225,7 +226,7 @@
"rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-postcss": "^4.0.1", "rollup-plugin-postcss": "^4.0.1",
"rollup-plugin-typescript2": "0.34.1", "rollup-plugin-typescript2": "0.34.1",
"rxjs": "6.6.7", "rxjs": "^7.8.0",
"sass": "1.55.0", "sass": "1.55.0",
"sass-loader": "^12.2.0", "sass-loader": "^12.2.0",
"semver": "7.3.4", "semver": "7.3.4",
@ -251,13 +252,13 @@
"tslint-to-eslint-config": "^2.13.0", "tslint-to-eslint-config": "^2.13.0",
"typedoc": "0.23.28", "typedoc": "0.23.28",
"typedoc-plugin-markdown": "3.14.0", "typedoc-plugin-markdown": "3.14.0",
"typescript": "~4.9.5", "typescript": "~5.0.2",
"unzipper": "^0.10.11", "unzipper": "^0.10.11",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"use-sync-external-store": "^1.2.0", "use-sync-external-store": "^1.2.0",
"verdaccio": "^5.0.4", "verdaccio": "^5.0.4",
"vite": "^4.0.1", "vite": "^4.0.1",
"webpack": "^5.75.0", "webpack": "^5.80.0",
"webpack-dev-server": "^4.9.3", "webpack-dev-server": "^4.9.3",
"webpack-merge": "^5.8.0", "webpack-merge": "^5.8.0",
"webpack-node-externals": "^3.0.0", "webpack-node-externals": "^3.0.0",
@ -275,7 +276,6 @@
"@nx/react": "16.0.0-rc.0", "@nx/react": "16.0.0-rc.0",
"@nx/jest": "16.0.0-rc.0", "@nx/jest": "16.0.0-rc.0",
"@nx/next": "16.0.0-rc.0", "@nx/next": "16.0.0-rc.0",
"@nx/angular": "16.0.0-rc.0",
"@nx/eslint-plugin": "16.0.0-rc.0" "@nx/eslint-plugin": "16.0.0-rc.0"
}, },
"author": "Victor Savkin", "author": "Victor Savkin",
@ -347,4 +347,3 @@
] ]
} }
} }

View File

@ -200,6 +200,42 @@
}, },
"description": "Remove exported `@angular/platform-server` `renderModule` method. The `renderModule` method is now exported by the Angular CLI.", "description": "Remove exported `@angular/platform-server` `renderModule` method. The `renderModule` method is now exported by the Angular CLI.",
"factory": "./src/migrations/update-16-1-0/remove-render-module-platform-server-exports" "factory": "./src/migrations/update-16-1-0/remove-render-module-platform-server-exports"
},
"update-angular-cli-version-16-0-0-rc-4": {
"cli": "nx",
"version": "16.1.0-beta.0",
"requires": {
"@angular/core": ">=16.0.0-rc.4"
},
"description": "Update the @angular/cli package version to ~16.0.0-rc.4.",
"factory": "./src/migrations/update-16-1-0/update-angular-cli"
},
"remove-ngcc-invocation": {
"cli": "nx",
"version": "16.1.0-beta.0",
"requires": {
"@angular/core": ">=16.0.0-rc.4"
},
"description": "Remove 'ngcc' invocation if exists from the 'postinstall' script in package.json.",
"factory": "./src/migrations/update-16-1-0/remove-ngcc-invocation"
},
"extract-app-config-for-standalone": {
"cli": "nx",
"version": "16.1.0-beta.0",
"requires": {
"@angular/core": ">=16.0.0-rc.4"
},
"description": "Extract the app config for standalone apps",
"factory": "./src/migrations/update-16-1-0/extract-standalone-config-from-bootstrap"
},
"update-server-executor-config": {
"cli": "nx",
"version": "16.1.0-beta.0",
"requires": {
"@angular/core": ">=16.0.0-rc.4"
},
"description": "Update server executors' configuration to disable 'buildOptimizer' for non optimized builds.",
"factory": "./src/migrations/update-16-1-0/update-server-executor-config"
} }
}, },
"packageJsonUpdates": { "packageJsonUpdates": {
@ -984,6 +1020,96 @@
"alwaysAddToPackageJson": false "alwaysAddToPackageJson": false
} }
} }
},
"16.1.0": {
"version": "16.1.0-beta.0",
"x-prompt": "Do you want to update the Angular version to v16?",
"requires": {
"@angular/core": "^15.2.0",
"typescript": ">=4.9.3 <5.1"
},
"packages": {
"@angular/core": {
"version": "~16.0.0-rc.4",
"alwaysAddToPackageJson": true
},
"zone.js": {
"version": "~0.13.0",
"alwaysAddToPackageJson": false
},
"@angular-devkit/architect": {
"version": "~0.1600.0-rc.4",
"alwaysAddToPackageJson": false
},
"@angular-devkit/build-angular": {
"version": "~16.0.0-rc.4",
"alwaysAddToPackageJson": false
},
"@angular-devkit/build-webpack": {
"version": "~0.1600.0-rc.4",
"alwaysAddToPackageJson": false
},
"@angular-devkit/core": {
"version": "~16.0.0-rc.4",
"alwaysAddToPackageJson": false
},
"@angular-devkit/schematics": {
"version": "~16.0.0-rc.4",
"alwaysAddToPackageJson": false
},
"@schematics/angular": {
"version": "~16.0.0-rc.4",
"alwaysAddToPackageJson": false
},
"ng-packagr": {
"version": "~16.0.0-rc.1",
"alwaysAddToPackageJson": false
},
"@nguniversal/build-angular": {
"version": "~16.0.0-rc.2",
"alwaysAddToPackageJson": false
},
"@nguniversal/builders": {
"version": "~16.0.0-rc.2",
"alwaysAddToPackageJson": false
},
"@nguniversal/common": {
"version": "~16.0.0-rc.2",
"alwaysAddToPackageJson": false
},
"@nguniversal/express-engine": {
"version": "~16.0.0-rc.2",
"alwaysAddToPackageJson": false
},
"@angular/material": {
"version": "~16.0.0-rc.2",
"alwaysAddToPackageJson": false
},
"@angular/cdk": {
"version": "~16.0.0-rc.2",
"alwaysAddToPackageJson": false
}
}
},
"16.1.0-angular-eslint": {
"version": "16.1.0-beta.0",
"requires": {
"eslint": "^7.20.0 || ^8.0.0"
},
"packages": {
"@angular-eslint/eslint-plugin": {
"version": "~16.0.0-alpha.1",
"alwaysAddToPackageJson": false
},
"@angular-eslint/eslint-plugin-template": {
"version": "~16.0.0-alpha.1",
"alwaysAddToPackageJson": false
},
"@angular-eslint/template-parser": {
"version": "~16.0.0-alpha.1",
"alwaysAddToPackageJson": false
}
}
} }
} }
} }

View File

@ -46,6 +46,7 @@
}, },
"dependencies": { "dependencies": {
"@phenomnomnominal/tsquery": "~5.0.1", "@phenomnomnominal/tsquery": "~5.0.1",
"@typescript-eslint/type-utils": "^5.36.1",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
"http-server": "^14.1.0", "http-server": "^14.1.0",
@ -55,7 +56,7 @@
"semver": "7.3.4", "semver": "7.3.4",
"ts-node": "10.9.1", "ts-node": "10.9.1",
"tsconfig-paths": "^4.1.2", "tsconfig-paths": "^4.1.2",
"webpack": "^5.75.0", "webpack": "^5.80.0",
"webpack-merge": "5.7.3", "webpack-merge": "5.7.3",
"enquirer": "^2.3.6", "enquirer": "^2.3.6",
"@nx/cypress": "file:../cypress", "@nx/cypress": "file:../cypress",
@ -67,11 +68,11 @@
"@nx/workspace": "file:../workspace" "@nx/workspace": "file:../workspace"
}, },
"peerDependencies": { "peerDependencies": {
"@angular-devkit/build-angular": ">= 14.0.0 < 16.0.0", "@angular-devkit/build-angular": "^16.0.0-rc.4",
"@angular-devkit/schematics": ">= 14.0.0 < 16.0.0", "@angular-devkit/schematics": "^16.0.0-rc.4",
"@schematics/angular": ">= 14.0.0 < 16.0.0", "@schematics/angular": "^16.0.0-rc.4",
"@angular-devkit/core": ">= 14.0.0 < 16.0.0", "@angular-devkit/core": "^16.0.0-rc.4",
"@nguniversal/builders": ">= 14.0.0 < 16.0.0", "@nguniversal/builders": "^16.0.0-rc.2",
"rxjs": "^6.5.3 || ^7.5.0" "rxjs": "^6.5.3 || ^7.5.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {

View File

@ -1,4 +1,4 @@
import { ServerBuilderOptions } from '@angular-devkit/build-angular'; import type { ServerBuilderOptions } from '@angular-devkit/build-angular';
export interface Schema extends ServerBuilderOptions { export interface Schema extends ServerBuilderOptions {
customWebpackConfig?: { customWebpackConfig?: {

View File

@ -191,6 +191,11 @@
"description": "Extract all licenses in a separate file, in the case of production builds only.", "description": "Extract all licenses in a separate file, in the case of production builds only.",
"default": true "default": true
}, },
"buildOptimizer": {
"type": "boolean",
"description": "Enables advanced build optimizations. _Note: only supported in Angular versions >= 16.0.0_.",
"default": true
},
"namedChunks": { "namedChunks": {
"type": "boolean", "type": "boolean",
"description": "Use file name for lazy loaded chunks.", "description": "Use file name for lazy loaded chunks.",

View File

@ -7,6 +7,7 @@ import type { Schema } from './schema';
export function validateOptions(options: Schema): void { export function validateOptions(options: Schema): void {
const angularVersionInfo = getInstalledAngularVersionInfo(); const angularVersionInfo = getInstalledAngularVersionInfo();
validateAssets(options, angularVersionInfo); validateAssets(options, angularVersionInfo);
validateBuildOptimizer(options, angularVersionInfo);
validateBundleDependencies(options, angularVersionInfo); validateBundleDependencies(options, angularVersionInfo);
validateVendorChunk(options, angularVersionInfo); validateVendorChunk(options, angularVersionInfo);
} }
@ -22,6 +23,16 @@ function validateAssets(options: Schema, { version }: VersionInfo): void {
} }
} }
function validateBuildOptimizer(
options: Schema,
{ major, version }: VersionInfo
): void {
if (major < 16 && options.buildOptimizer) {
throw new Error(stripIndents`The "buildOptimizer" option is supported from Angular >= 16.0.0. You are currently using "${version}".
You can resolve this error by removing the "buildOptimizer" option.`);
}
}
function validateBundleDependencies( function validateBundleDependencies(
options: Schema, options: Schema,
{ major, version }: VersionInfo { major, version }: VersionInfo

View File

@ -3,7 +3,8 @@
* *
* Changes made: * Changes made:
* - Use our own StylesheetProcessor files instead of the ones provide by ng-packagr. * - Use our own StylesheetProcessor files instead of the ones provide by ng-packagr.
* - Excludes the ngcc compilation for faster builds. * - Excludes the ngcc compilation for faster builds (angular < v16)
* - Support ESM2020 for Angular < 16.
*/ */
import type { Transform } from 'ng-packagr/lib/graph/transform'; import type { Transform } from 'ng-packagr/lib/graph/transform';
@ -15,6 +16,7 @@ import {
import { setDependenciesTsConfigPaths } from 'ng-packagr/lib/ts/tsconfig'; import { setDependenciesTsConfigPaths } from 'ng-packagr/lib/ts/tsconfig';
import * as path from 'path'; import * as path from 'path';
import * as ts from 'typescript'; import * as ts from 'typescript';
import { getInstalledAngularVersionInfo } from '../../../../utilities/angular-version-utils';
import { compileSourceFiles } from '../../ngc/compile-source-files'; import { compileSourceFiles } from '../../ngc/compile-source-files';
import { StylesheetProcessor as StylesheetProcessorClass } from '../../styles/stylesheet-processor'; import { StylesheetProcessor as StylesheetProcessorClass } from '../../styles/stylesheet-processor';
import { NgPackagrOptions } from '../options.di'; import { NgPackagrOptions } from '../options.di';
@ -33,8 +35,14 @@ export const nxCompileNgcTransformFactory = (
entryPoints entryPoints
); );
const angularVersion = getInstalledAngularVersionInfo();
// Compile TypeScript sources // Compile TypeScript sources
const { esm2020, declarations } = entryPoint.data.destinationFiles; const { declarations } = entryPoint.data.destinationFiles;
const esmModulePath =
angularVersion.major < 16
? (entryPoint.data.destinationFiles as any).esm2020
: entryPoint.data.destinationFiles.esm2022;
const { basePath, cssUrl, styleIncludePaths } = const { basePath, cssUrl, styleIncludePaths } =
entryPoint.data.entryPoint; entryPoint.data.entryPoint;
const { moduleResolutionCache } = entryPoint.cache; const { moduleResolutionCache } = entryPoint.cache;
@ -53,13 +61,15 @@ export const nxCompileNgcTransformFactory = (
tsConfig, tsConfig,
moduleResolutionCache, moduleResolutionCache,
{ {
outDir: path.dirname(esm2020), outDir: path.dirname(esmModulePath),
declarationDir: path.dirname(declarations), declarationDir: path.dirname(declarations),
declaration: true, declaration: true,
target: ts.ScriptTarget.ES2020, target:
angularVersion.major >= 16
? ts.ScriptTarget.ES2022
: ts.ScriptTarget.ES2020,
}, },
entryPoint.cache.stylesheetProcessor as any, entryPoint.cache.stylesheetProcessor as any,
null,
options.watch options.watch
); );
} catch (error) { } catch (error) {

View File

@ -32,8 +32,8 @@ import { pipe } from 'rxjs';
* The transformation pipeline is pluggable through the dependency injection system. * The transformation pipeline is pluggable through the dependency injection system.
* Sub-transformations are passed to this factory function as arguments. * Sub-transformations are passed to this factory function as arguments.
* *
* @param compileTs Transformation compiling typescript sources to ES2020 modules. * @param compileTs Transformation compiling typescript sources to ES2022 modules.
* @param writePackage Transformation writing a distribution-ready `package.json` (for publishing to npm registry). * @param writeBundles Transformation flattening ES2022 modules to ESM2022, UMD, and minified UMD.
*/ */
export const nxEntryPointTransformFactory = ( export const nxEntryPointTransformFactory = (
compileTs: Transform, compileTs: Transform,

View File

@ -2,8 +2,8 @@
* Adapted from the original ng-packagr. * Adapted from the original ng-packagr.
* *
* Changes made: * Changes made:
* - Change the package.json metadata to only use the ESM2020 output as it's the only one generated. * - Change the package.json metadata to only use the ESM2022 output.
* - Remove package exports. * - Change the package.json metadata to only use the ESM2020 output (Angular < 16).
*/ */
import { logger } from '@nx/devkit'; import { logger } from '@nx/devkit';
@ -33,6 +33,10 @@ import { globFiles } from 'ng-packagr/lib/utils/glob';
import { ensureUnixPath } from 'ng-packagr/lib/utils/path'; import { ensureUnixPath } from 'ng-packagr/lib/utils/path';
import { AssetPattern } from 'ng-packagr/ng-package.schema'; import { AssetPattern } from 'ng-packagr/ng-package.schema';
import * as path from 'path'; import * as path from 'path';
import {
getInstalledAngularVersionInfo,
VersionInfo,
} from '../../../../utilities/angular-version-utils';
export const nxWritePackageTransform = (options: NgPackagrOptions) => export const nxWritePackageTransform = (options: NgPackagrOptions) =>
transformFromPromise(async (graph) => { transformFromPromise(async (graph) => {
@ -42,11 +46,13 @@ export const nxWritePackageTransform = (options: NgPackagrOptions) =>
const ngPackage = ngPackageNode.data; const ngPackage = ngPackageNode.data;
const { destinationFiles } = entryPoint.data; const { destinationFiles } = entryPoint.data;
const angularVersion = getInstalledAngularVersionInfo();
if (!ngEntryPoint.isSecondaryEntryPoint) { if (!ngEntryPoint.isSecondaryEntryPoint) {
logger.log('Copying assets'); logger.log('Copying assets');
try { try {
await copyAssets(graph, entryPoint, ngPackageNode); await copyAssets(graph, entryPoint, ngPackageNode, angularVersion);
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -64,11 +70,28 @@ export const nxWritePackageTransform = (options: NgPackagrOptions) =>
ngEntryPoint, ngEntryPoint,
ngPackage, ngPackage,
{ {
module: relativeUnixFromDestPath(destinationFiles.esm2020), // backward compat for Angular < 16
es2020: relativeUnixFromDestPath(destinationFiles.esm2020), ...(angularVersion.major < 16
esm2020: relativeUnixFromDestPath(destinationFiles.esm2020), ? {
module: relativeUnixFromDestPath(
(destinationFiles as any).esm2020
),
es2020: relativeUnixFromDestPath(
(destinationFiles as any).esm2020
),
esm2020: relativeUnixFromDestPath(
(destinationFiles as any).esm2020
),
}
: {
module: relativeUnixFromDestPath(destinationFiles.esm2022),
}),
typings: relativeUnixFromDestPath(destinationFiles.declarations), typings: relativeUnixFromDestPath(destinationFiles.declarations),
exports: generatePackageExports(ngEntryPoint, graph), exports: generatePackageExports(
ngEntryPoint,
graph,
angularVersion
),
// webpack v4+ specific flag to enable advanced optimizations and code splitting // webpack v4+ specific flag to enable advanced optimizations and code splitting
sideEffects: ngEntryPoint.packageJson.sideEffects ?? false, sideEffects: ngEntryPoint.packageJson.sideEffects ?? false,
}, },
@ -109,7 +132,8 @@ type AssetEntry = Exclude<AssetPattern, string>;
async function copyAssets( async function copyAssets(
graph: BuildGraph, graph: BuildGraph,
entryPointNode: EntryPointNode, entryPointNode: EntryPointNode,
ngPackageNode: PackageNode ngPackageNode: PackageNode,
angularVersion: VersionInfo
): Promise<void> { ): Promise<void> {
const ngPackage = ngPackageNode.data; const ngPackage = ngPackageNode.data;
@ -164,14 +188,24 @@ async function copyAssets(
} }
for (const asset of assets) { for (const asset of assets) {
const filePaths = await globFiles(asset.glob, { const globOptions: Parameters<typeof globFiles>[1] = {
cwd: asset.input, cwd: asset.input,
ignore: [...(asset.ignore ?? []), ...globsForceIgnored], ignore: [...(asset.ignore ?? []), ...globsForceIgnored],
cache: ngPackageNode.cache.globCache,
dot: true, dot: true,
nodir: true, };
follow: asset.followSymlinks,
}); if (angularVersion.major < 16) {
// versions lower than v16 support these properties
(globOptions as any).cache = (ngPackageNode.cache as any).globCache;
(globOptions as any).nodir = true;
(globOptions as any).follow = asset.followSymlinks;
} else {
// starting in v16 these properties are supported
globOptions.onlyFiles = true;
globOptions.followSymbolicLinks = asset.followSymlinks;
}
const filePaths = await globFiles(asset.glob, globOptions);
for (const filePath of filePaths) { for (const filePath of filePaths) {
const fileSrcFullPath = path.join(asset.input, filePath); const fileSrcFullPath = path.join(asset.input, filePath);
const fileDestFullPath = path.join(asset.output, filePath); const fileDestFullPath = path.join(asset.output, filePath);
@ -334,12 +368,16 @@ type PackageExports = Record<string, ConditionalExport>;
* https://nodejs.org/api/packages.html#packages_conditional_exports * https://nodejs.org/api/packages.html#packages_conditional_exports
*/ */
type ConditionalExport = { type ConditionalExport = {
node?: string;
types?: string; types?: string;
esm2022?: string;
esm?: string;
default?: string;
// backward compat for Angular < 16
node?: string;
esm2020?: string; esm2020?: string;
es2020?: string; es2020?: string;
es2015?: string; es2015?: string;
default?: string;
}; };
/** /**
@ -348,7 +386,8 @@ type ConditionalExport = {
*/ */
function generatePackageExports( function generatePackageExports(
{ destinationPath, packageJson }: NgEntryPoint, { destinationPath, packageJson }: NgEntryPoint,
graph: BuildGraph graph: BuildGraph,
angularVersion: VersionInfo
): PackageExports { ): PackageExports {
const exports: PackageExports = packageJson.exports const exports: PackageExports = packageJson.exports
? JSON.parse(JSON.stringify(packageJson.exports)) ? JSON.parse(JSON.stringify(packageJson.exports))
@ -396,12 +435,26 @@ function generatePackageExports(
? `./${destinationFiles.directory}` ? `./${destinationFiles.directory}`
: '.'; : '.';
insertMappingOrError(subpath, { // backward compat for Angular < 16
const mapping =
angularVersion.major < 16
? {
types: relativeUnixFromDestPath(destinationFiles.declarations), types: relativeUnixFromDestPath(destinationFiles.declarations),
es2020: relativeUnixFromDestPath(destinationFiles.esm2020), es2020: relativeUnixFromDestPath((destinationFiles as any).esm2020),
esm2020: relativeUnixFromDestPath(destinationFiles.esm2020), esm2020: relativeUnixFromDestPath(
default: relativeUnixFromDestPath(destinationFiles.esm2020), (destinationFiles as any).esm2020
}); ),
default: relativeUnixFromDestPath(
(destinationFiles as any).esm2020
),
}
: {
esm2022: relativeUnixFromDestPath(destinationFiles.esm2022),
esm: relativeUnixFromDestPath(destinationFiles.esm2022),
default: relativeUnixFromDestPath(destinationFiles.esm2022),
};
insertMappingOrError(subpath, mapping);
} }
return exports; return exports;

View File

@ -2,8 +2,9 @@
* Adapted from the original ng-packagr source. * Adapted from the original ng-packagr source.
* *
* Changes made: * Changes made:
* - Made sure ngccProcessor is optional. * - Remove ngccProcessor (Angular < 16)
* - Use custom cacheCompilerHost instead of the one provided by ng-packagr. * - Use custom cacheCompilerHost instead of the one provided by ng-packagr.
* - Support Angular Compiler `incrementalDriver` for Angular < 16.
*/ */
import type { import type {
@ -17,12 +18,11 @@ import {
isPackage, isPackage,
PackageNode, PackageNode,
} from 'ng-packagr/lib/ng-package/nodes'; } from 'ng-packagr/lib/ng-package/nodes';
import { NgccProcessor } from 'ng-packagr/lib/ngc/ngcc-processor';
import { augmentProgramWithVersioning } from 'ng-packagr/lib/ts/cache-compiler-host'; import { augmentProgramWithVersioning } from 'ng-packagr/lib/ts/cache-compiler-host';
import { ngccTransformCompilerHost } from 'ng-packagr/lib/ts/ngcc-transform-compiler-host';
import * as log from 'ng-packagr/lib/utils/log'; import * as log from 'ng-packagr/lib/utils/log';
import { ngCompilerCli } from 'ng-packagr/lib/utils/ng-compiler-cli'; import { ngCompilerCli } from 'ng-packagr/lib/utils/ng-compiler-cli';
import * as ts from 'typescript'; import * as ts from 'typescript';
import { getInstalledAngularVersionInfo } from '../../../utilities/angular-version-utils';
import { StylesheetProcessor } from '../styles/stylesheet-processor'; import { StylesheetProcessor } from '../styles/stylesheet-processor';
import { cacheCompilerHost } from '../ts/cache-compiler-host'; import { cacheCompilerHost } from '../ts/cache-compiler-host';
@ -32,7 +32,6 @@ export async function compileSourceFiles(
moduleResolutionCache: ts.ModuleResolutionCache, moduleResolutionCache: ts.ModuleResolutionCache,
extraOptions?: Partial<CompilerOptions>, extraOptions?: Partial<CompilerOptions>,
stylesheetProcessor?: StylesheetProcessor, stylesheetProcessor?: StylesheetProcessor,
ngccProcessor?: NgccProcessor,
watch?: boolean watch?: boolean
) { ) {
const { NgtscProgram, formatDiagnostics } = await ngCompilerCli(); const { NgtscProgram, formatDiagnostics } = await ngCompilerCli();
@ -45,7 +44,7 @@ export async function compileSourceFiles(
const ngPackageNode: PackageNode = graph.find(isPackage); const ngPackageNode: PackageNode = graph.find(isPackage);
const inlineStyleLanguage = ngPackageNode.data.inlineStyleLanguage; const inlineStyleLanguage = ngPackageNode.data.inlineStyleLanguage;
let tsCompilerHost = cacheCompilerHost( const tsCompilerHost = cacheCompilerHost(
graph, graph,
entryPoint, entryPoint,
tsConfigOptions, tsConfigOptions,
@ -54,15 +53,6 @@ export async function compileSourceFiles(
inlineStyleLanguage inlineStyleLanguage
); );
if (ngccProcessor) {
tsCompilerHost = ngccTransformCompilerHost(
tsCompilerHost,
tsConfigOptions,
ngccProcessor,
moduleResolutionCache
);
}
const cache = entryPoint.cache; const cache = entryPoint.cache;
const sourceFileCache = cache.sourcesFileCache; const sourceFileCache = cache.sourcesFileCache;
@ -165,9 +155,15 @@ export async function compileSourceFiles(
} }
// Collect sources that are required to be emitted // Collect sources that are required to be emitted
const angularVersion = getInstalledAngularVersionInfo();
const incrementalCompilation: typeof angularCompiler.incrementalCompilation =
angularVersion.major < 16
? (angularCompiler as any).incrementalDriver
: angularCompiler.incrementalCompilation;
if ( if (
!ignoreForEmit.has(sourceFile) && !ignoreForEmit.has(sourceFile) &&
!angularCompiler.incrementalDriver.safeToSkipEmit(sourceFile) !incrementalCompilation.safeToSkipEmit(sourceFile)
) { ) {
// If required to emit, diagnostics may have also changed // If required to emit, diagnostics may have also changed
if (!ignoreForDiagnostics.has(sourceFile)) { if (!ignoreForDiagnostics.has(sourceFile)) {

View File

@ -3,7 +3,7 @@
"outputCapture": "direct-nodejs", "outputCapture": "direct-nodejs",
"$schema": "http://json-schema.org/schema", "$schema": "http://json-schema.org/schema",
"title": "ng-packagr Target", "title": "ng-packagr Target",
"description": "Builds a library with support for incremental builds.\n\nThis executor is meant to be used with buildable libraries in an incremental build scenario. It is similar to the `@nrwl/angular:package` executor but with some key differences:\n- It doesn't run `ngcc` automatically (`ngcc` needs to be run separately beforehand if needed, this can be done in a `postinstall` hook on `package.json`).\n- It only produces ESM2020 bundles.\n- It doesn't generate package exports in the `package.json`.", "description": "Builds a library with support for incremental builds.\n\nThis executor is meant to be used with buildable libraries in an incremental build scenario. It is similar to the `@nx/angular:package` executor but with some key differences:\n- For supported Angular versions lower than v16, it doesn't run `ngcc` automatically (`ngcc` is no longer available from Angular v16 onwards, for lower versions, it needs to be run separately beforehand if needed, this can be done in a `postinstall` hook on `package.json`).\n- It only produces ESM2022 bundles (ESM2020 for supported Angular versions lower than v16).",
"cli": "nx", "cli": "nx",
"type": "object", "type": "object",
"presets": [ "presets": [

View File

@ -3,6 +3,8 @@
* *
* Changes made: * Changes made:
* - Use our own StylesheetProcessor files instead of the ones provide by ng-packagr. * - Use our own StylesheetProcessor files instead of the ones provide by ng-packagr.
* - Support ngcc for Angular < 16.
* - Support ESM2020 for Angular < 16.
*/ */
import { import {
@ -14,14 +16,14 @@ import {
isEntryPoint, isEntryPoint,
isEntryPointInProgress, isEntryPointInProgress,
} from 'ng-packagr/lib/ng-package/nodes'; } from 'ng-packagr/lib/ng-package/nodes';
import { NgccProcessor } from 'ng-packagr/lib/ngc/ngcc-processor';
import { setDependenciesTsConfigPaths } from 'ng-packagr/lib/ts/tsconfig'; import { setDependenciesTsConfigPaths } from 'ng-packagr/lib/ts/tsconfig';
import { ngccCompilerCli } from 'ng-packagr/lib/utils/ng-compiler-cli';
import * as ora from 'ora'; import * as ora from 'ora';
import * as path from 'path'; import * as path from 'path';
import * as ts from 'typescript'; import * as ts from 'typescript';
import { getInstalledAngularVersionInfo } from '../../../../utilities/angular-version-utils';
import { compileSourceFiles } from '../../ngc/compile-source-files'; import { compileSourceFiles } from '../../ngc/compile-source-files';
import { StylesheetProcessor as StylesheetProcessorClass } from '../../styles/stylesheet-processor'; import { StylesheetProcessor as StylesheetProcessorClass } from '../../styles/stylesheet-processor';
import { ngccCompilerCli } from '../../utils/ng-compiler-cli';
import { NgPackagrOptions } from '../options.di'; import { NgPackagrOptions } from '../options.di';
export const compileNgcTransformFactory = ( export const compileNgcTransformFactory = (
@ -43,20 +45,29 @@ export const compileNgcTransformFactory = (
entryPoints entryPoints
); );
const angularVersion = getInstalledAngularVersionInfo();
// Compile TypeScript sources // Compile TypeScript sources
const { esm2020, declarations } = entryPoint.data.destinationFiles; const { declarations } = entryPoint.data.destinationFiles;
const esmModulePath =
angularVersion.major < 16
? (entryPoint.data.destinationFiles as any).esm2020
: entryPoint.data.destinationFiles.esm2022;
const { basePath, cssUrl, styleIncludePaths } = const { basePath, cssUrl, styleIncludePaths } =
entryPoint.data.entryPoint; entryPoint.data.entryPoint;
const { moduleResolutionCache, ngccProcessingCache } = entryPoint.cache; const { moduleResolutionCache } = entryPoint.cache;
spinner.start( spinner.start(
`Compiling with Angular sources in Ivy ${ `Compiling with Angular sources in Ivy ${
tsConfig.options.compilationMode || 'full' tsConfig.options.compilationMode || 'full'
} compilation mode.` } compilation mode.`
); );
const ngccProcessor = new NgccProcessor( let ngccProcessor: any;
if (angularVersion && angularVersion.major < 16) {
ngccProcessor =
new (require('ng-packagr/lib/ngc/ngcc-processor').NgccProcessor)(
await ngccCompilerCli(), await ngccCompilerCli(),
ngccProcessingCache, (entryPoint.cache as any).ngccProcessingCache,
tsConfig.project, tsConfig.project,
tsConfig.options, tsConfig.options,
entryPoints entryPoints
@ -65,6 +76,7 @@ export const compileNgcTransformFactory = (
// Only run the async version of NGCC during the primary entrypoint processing. // Only run the async version of NGCC during the primary entrypoint processing.
await ngccProcessor.process(); await ngccProcessor.process();
} }
}
entryPoint.cache.stylesheetProcessor ??= new StylesheetProcessor( entryPoint.cache.stylesheetProcessor ??= new StylesheetProcessor(
basePath, basePath,
@ -80,10 +92,13 @@ export const compileNgcTransformFactory = (
tsConfig, tsConfig,
moduleResolutionCache, moduleResolutionCache,
{ {
outDir: path.dirname(esm2020), outDir: path.dirname(esmModulePath),
declarationDir: path.dirname(declarations), declarationDir: path.dirname(declarations),
declaration: true, declaration: true,
target: ts.ScriptTarget.ES2020, target:
angularVersion.major >= 16
? ts.ScriptTarget.ES2022
: ts.ScriptTarget.ES2020,
}, },
entryPoint.cache.stylesheetProcessor as any, entryPoint.cache.stylesheetProcessor as any,
ngccProcessor, ngccProcessor,

View File

@ -3,6 +3,8 @@
* *
* Changes made: * Changes made:
* - Use custom cacheCompilerHost instead of the one provided by ng-packagr. * - Use custom cacheCompilerHost instead of the one provided by ng-packagr.
* - Support ngcc for Angular < 16.
* - Support Angular Compiler `incrementalDriver` for Angular < 16.
*/ */
import type { import type {
@ -16,11 +18,10 @@ import {
isPackage, isPackage,
PackageNode, PackageNode,
} from 'ng-packagr/lib/ng-package/nodes'; } from 'ng-packagr/lib/ng-package/nodes';
import { NgccProcessor } from 'ng-packagr/lib/ngc/ngcc-processor';
import { ngccTransformCompilerHost } from 'ng-packagr/lib/ts/ngcc-transform-compiler-host';
import * as log from 'ng-packagr/lib/utils/log'; import * as log from 'ng-packagr/lib/utils/log';
import { ngCompilerCli } from 'ng-packagr/lib/utils/ng-compiler-cli'; import { ngCompilerCli } from 'ng-packagr/lib/utils/ng-compiler-cli';
import * as ts from 'typescript'; import * as ts from 'typescript';
import { getInstalledAngularVersionInfo } from '../../../utilities/angular-version-utils';
import { StylesheetProcessor } from '../styles/stylesheet-processor'; import { StylesheetProcessor } from '../styles/stylesheet-processor';
import { import {
augmentProgramWithVersioning, augmentProgramWithVersioning,
@ -33,7 +34,7 @@ export async function compileSourceFiles(
moduleResolutionCache: ts.ModuleResolutionCache, moduleResolutionCache: ts.ModuleResolutionCache,
extraOptions?: Partial<CompilerOptions>, extraOptions?: Partial<CompilerOptions>,
stylesheetProcessor?: StylesheetProcessor, stylesheetProcessor?: StylesheetProcessor,
ngccProcessor?: NgccProcessor, ngccProcessor?: any,
watch?: boolean watch?: boolean
) { ) {
const { NgtscProgram, formatDiagnostics } = await ngCompilerCli(); const { NgtscProgram, formatDiagnostics } = await ngCompilerCli();
@ -46,19 +47,24 @@ export async function compileSourceFiles(
const ngPackageNode: PackageNode = graph.find(isPackage); const ngPackageNode: PackageNode = graph.find(isPackage);
const inlineStyleLanguage = ngPackageNode.data.inlineStyleLanguage; const inlineStyleLanguage = ngPackageNode.data.inlineStyleLanguage;
const tsCompilerHost = ngccTransformCompilerHost( let tsCompilerHost = cacheCompilerHost(
cacheCompilerHost(
graph, graph,
entryPoint, entryPoint,
tsConfigOptions, tsConfigOptions,
moduleResolutionCache, moduleResolutionCache,
stylesheetProcessor, stylesheetProcessor,
inlineStyleLanguage inlineStyleLanguage
), );
if (ngccProcessor) {
tsCompilerHost =
require('ng-packagr/lib/ts/ngcc-transform-compiler-host').ngccTransformCompilerHost(
tsCompilerHost,
tsConfigOptions, tsConfigOptions,
ngccProcessor, ngccProcessor,
moduleResolutionCache moduleResolutionCache
); );
}
const cache = entryPoint.cache; const cache = entryPoint.cache;
const sourceFileCache = cache.sourcesFileCache; const sourceFileCache = cache.sourcesFileCache;
@ -162,9 +168,15 @@ export async function compileSourceFiles(
} }
// Collect sources that are required to be emitted // Collect sources that are required to be emitted
const angularVersion = getInstalledAngularVersionInfo();
const incrementalCompilation: typeof angularCompiler.incrementalCompilation =
angularVersion.major < 16
? (angularCompiler as any).incrementalDriver
: angularCompiler.incrementalCompilation;
if ( if (
!ignoreForEmit.has(sourceFile) && !ignoreForEmit.has(sourceFile) &&
!angularCompiler.incrementalDriver.safeToSkipEmit(sourceFile) !incrementalCompilation.safeToSkipEmit(sourceFile)
) { ) {
// If required to emit, diagnostics may have also changed // If required to emit, diagnostics may have also changed
if (!ignoreForDiagnostics.has(sourceFile)) { if (!ignoreForDiagnostics.has(sourceFile)) {

View File

@ -0,0 +1,9 @@
export async function ngccCompilerCli(): Promise<any> {
const compilerCliModule = await new Function(
`return import('@angular/compiler-cli/ngcc');`
)();
return compilerCliModule.process
? compilerCliModule
: compilerCliModule.default;
}

View File

@ -61,8 +61,13 @@ function getTailwindConfigPath(
projectRoot: string, projectRoot: string,
workspaceRoot: string workspaceRoot: string
): string | undefined { ): string | undefined {
// valid tailwind config files https://github.com/tailwindlabs/tailwindcss/blob/master/src/util/resolveConfigPath.js#L46 // valid tailwind config files https://github.com/tailwindlabs/tailwindcss/blob/master/src/util/resolveConfigPath.js#L4
const tailwindConfigFiles = ['tailwind.config.js', 'tailwind.config.cjs']; const tailwindConfigFiles = [
'tailwind.config.js',
'tailwind.config.cjs',
'tailwind.config.mjs',
'tailwind.config.ts',
];
for (const basePath of [projectRoot, workspaceRoot]) { for (const basePath of [projectRoot, workspaceRoot]) {
for (const configFile of tailwindConfigFiles) { for (const configFile of tailwindConfigFiles) {

View File

@ -208,27 +208,37 @@ exports[`app --minimal should skip "nx-welcome.component.ts" file and references
exports[`app --standalone should generate a standalone app correctly with routing 1`] = ` exports[`app --standalone should generate a standalone app correctly with routing 1`] = `
"import { bootstrapApplication } from '@angular/platform-browser'; "import { bootstrapApplication } from '@angular/platform-browser';
import { import { appConfig } from './app/app.config';
provideRouter,
withEnabledBlockingInitialNavigation,
} from '@angular/router';
import { appRoutes } from './app/app.routes';
import { AppComponent } from './app/app.component'; import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, { bootstrapApplication(AppComponent, appConfig).catch((err) =>
providers: [provideRouter(appRoutes, withEnabledBlockingInitialNavigation())], console.error(err)
}).catch((err) => console.error(err)); );
" "
`; `;
exports[`app --standalone should generate a standalone app correctly with routing 2`] = ` exports[`app --standalone should generate a standalone app correctly with routing 2`] = `
"import { ApplicationConfig } from '@angular/core';
import {
provideRouter,
withEnabledBlockingInitialNavigation,
} from '@angular/router';
import { appRoutes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(appRoutes, withEnabledBlockingInitialNavigation())],
};
"
`;
exports[`app --standalone should generate a standalone app correctly with routing 3`] = `
"import { Route } from '@angular/router'; "import { Route } from '@angular/router';
export const appRoutes: Route[] = []; export const appRoutes: Route[] = [];
" "
`; `;
exports[`app --standalone should generate a standalone app correctly with routing 3`] = ` exports[`app --standalone should generate a standalone app correctly with routing 4`] = `
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { NxWelcomeComponent } from './nx-welcome.component'; import { NxWelcomeComponent } from './nx-welcome.component';
@ -246,7 +256,7 @@ export class AppComponent {
" "
`; `;
exports[`app --standalone should generate a standalone app correctly with routing 4`] = ` exports[`app --standalone should generate a standalone app correctly with routing 5`] = `
"import { TestBed } from '@angular/core/testing'; "import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { NxWelcomeComponent } from './nx-welcome.component'; import { NxWelcomeComponent } from './nx-welcome.component';
@ -279,15 +289,25 @@ describe('AppComponent', () => {
exports[`app --standalone should generate a standalone app correctly without routing 1`] = ` exports[`app --standalone should generate a standalone app correctly without routing 1`] = `
"import { bootstrapApplication } from '@angular/platform-browser'; "import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component'; import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, { bootstrapApplication(AppComponent, appConfig).catch((err) =>
providers: [], console.error(err)
}).catch((err) => console.error(err)); );
" "
`; `;
exports[`app --standalone should generate a standalone app correctly without routing 2`] = ` exports[`app --standalone should generate a standalone app correctly without routing 2`] = `
"import { ApplicationConfig } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [],
};
"
`;
exports[`app --standalone should generate a standalone app correctly without routing 3`] = `
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
import { NxWelcomeComponent } from './nx-welcome.component'; import { NxWelcomeComponent } from './nx-welcome.component';
@ -304,7 +324,7 @@ export class AppComponent {
" "
`; `;
exports[`app --standalone should generate a standalone app correctly without routing 3`] = ` exports[`app --standalone should generate a standalone app correctly without routing 4`] = `
"import { TestBed } from '@angular/core/testing'; "import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { NxWelcomeComponent } from './nx-welcome.component'; import { NxWelcomeComponent } from './nx-welcome.component';

View File

@ -747,6 +747,9 @@ describe('app', () => {
expect( expect(
appTree.read('apps/standalone/src/main.ts', 'utf-8') appTree.read('apps/standalone/src/main.ts', 'utf-8')
).toMatchSnapshot(); ).toMatchSnapshot();
expect(
appTree.read('apps/standalone/src/app/app.config.ts', 'utf-8')
).toMatchSnapshot();
expect( expect(
appTree.read('apps/standalone/src/app/app.routes.ts', 'utf-8') appTree.read('apps/standalone/src/app/app.routes.ts', 'utf-8')
).toMatchSnapshot(); ).toMatchSnapshot();
@ -775,6 +778,9 @@ describe('app', () => {
expect( expect(
appTree.read('apps/standalone/src/main.ts', 'utf-8') appTree.read('apps/standalone/src/main.ts', 'utf-8')
).toMatchSnapshot(); ).toMatchSnapshot();
expect(
appTree.read('apps/standalone/src/app/app.config.ts', 'utf-8')
).toMatchSnapshot();
expect( expect(
appTree.read('apps/standalone/src/app/app.component.ts', 'utf-8') appTree.read('apps/standalone/src/app/app.component.ts', 'utf-8')
).toMatchSnapshot(); ).toMatchSnapshot();

View File

@ -0,0 +1,7 @@
import { ApplicationConfig } from '@angular/core';<% if (routing) { %>
import { provideRouter, withEnabledBlockingInitialNavigation } from '@angular/router';
import { appRoutes } from './app.routes';<% } %>
export const appConfig: ApplicationConfig = {
providers: [<% if (routing) { %>provideRouter(appRoutes, withEnabledBlockingInitialNavigation()) <% } %>]
};

View File

@ -1,11 +1,5 @@
import { bootstrapApplication } from '@angular/platform-browser';<% if(routing) { %> import { bootstrapApplication } from '@angular/platform-browser';
import { import { appConfig } from './app/app.config';
provideRouter,
withEnabledBlockingInitialNavigation,
} from '@angular/router';
import { appRoutes } from './app/app.routes';<% } %>
import { AppComponent } from './app/app.component'; import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, { bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err));
providers: [<% if(routing) { %>provideRouter(appRoutes, withEnabledBlockingInitialNavigation())<% } %>],
}).catch((err) => console.error(err));

View File

@ -551,9 +551,9 @@ exports[`convert-tslint-to-eslint should work for Angular applications 1`] = `
{ {
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
"@angular-eslint/eslint-plugin": "~15.0.0", "@angular-eslint/eslint-plugin": "~16.0.0-alpha.1",
"@angular-eslint/eslint-plugin-template": "~15.0.0", "@angular-eslint/eslint-plugin-template": "~16.0.0-alpha.1",
"@angular-eslint/template-parser": "~15.0.0", "@angular-eslint/template-parser": "~16.0.0-alpha.1",
"@nx/eslint-plugin": "0.0.1", "@nx/eslint-plugin": "0.0.1",
"@nx/linter": "0.0.1", "@nx/linter": "0.0.1",
"@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/eslint-plugin": "^5.58.0",
@ -903,9 +903,9 @@ exports[`convert-tslint-to-eslint should work for Angular libraries 1`] = `
{ {
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
"@angular-eslint/eslint-plugin": "~15.0.0", "@angular-eslint/eslint-plugin": "~16.0.0-alpha.1",
"@angular-eslint/eslint-plugin-template": "~15.0.0", "@angular-eslint/eslint-plugin-template": "~16.0.0-alpha.1",
"@angular-eslint/template-parser": "~15.0.0", "@angular-eslint/template-parser": "~16.0.0-alpha.1",
"@nx/eslint-plugin": "0.0.1", "@nx/eslint-plugin": "0.0.1",
"@nx/linter": "0.0.1", "@nx/linter": "0.0.1",
"@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/eslint-plugin": "^5.58.0",

View File

@ -11,7 +11,7 @@ import { NxWelcomeComponent } from './nx-welcome.component';
@NgModule({ @NgModule({
declarations: [AppComponent, NxWelcomeComponent], declarations: [AppComponent, NxWelcomeComponent],
imports: [ imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }), BrowserModule,
RouterModule.forRoot(appRoutes, { initialNavigation: 'enabledBlocking' }), RouterModule.forRoot(appRoutes, { initialNavigation: 'enabledBlocking' }),
], ],
providers: [], providers: [],
@ -22,7 +22,12 @@ export class AppModule {}
`; `;
exports[`Host App Generator --ssr should generate the correct files 2`] = ` exports[`Host App Generator --ssr should generate the correct files 2`] = `
"import('./bootstrap').catch((err) => console.error(err)); "import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err));
" "
`; `;
@ -138,9 +143,11 @@ exports[`Host App Generator --ssr should generate the correct files 9`] = `
{ {
"configurations": { "configurations": {
"development": { "development": {
"buildOptimizer": false,
"extractLicenses": false, "extractLicenses": false,
"optimization": false, "optimization": false,
"sourceMap": true, "sourceMap": true,
"vendorChunk": true,
}, },
"production": { "production": {
"outputHashing": "media", "outputHashing": "media",
@ -179,6 +186,205 @@ exports[`Host App Generator --ssr should generate the correct files 10`] = `
} }
`; `;
exports[`Host App Generator --ssr should generate the correct files for standalone 1`] = `
"import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err)
);
"
`;
exports[`Host App Generator --ssr should generate the correct files for standalone 2`] = `
"import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { config } from './app/app.config.server';
const bootstrap = () => bootstrapApplication(AppComponent, config);
export default bootstrap;
"
`;
exports[`Host App Generator --ssr should generate the correct files for standalone 3`] = `
"import 'zone.js/dist/zone-node';
import { APP_BASE_HREF } from '@angular/common';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import * as cors from 'cors';
import { existsSync } from 'fs';
import { join } from 'path';
import bootstrap from './bootstrap.server';
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const browserBundles = join(process.cwd(), 'dist/apps/test/browser');
server.use(cors());
const indexHtml = existsSync(join(browserBundles, 'index.original.html'))
? 'index.original.html'
: 'index';
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
server.engine(
'html',
ngExpressEngine({
bootstrap,
})
);
server.set('view engine', 'html');
server.set('views', browserBundles);
// Serve static files from /browser
server.get(
'*.*',
express.static(browserBundles, {
maxAge: '1y',
})
);
// All regular routes use the Universal engine
server.get('*', (req, res) => {
// keep it async to avoid blocking the server thread
res.render(indexHtml, {
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],
req,
});
});
return server;
}
function run(): void {
const port = process.env['PORT'] || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(\`Node Express server listening on http://localhost:\${port}\`);
});
}
run();
export default bootstrap;
"
`;
exports[`Host App Generator --ssr should generate the correct files for standalone 4`] = `
"import('./src/main.server');
"
`;
exports[`Host App Generator --ssr should generate the correct files for standalone 5`] = `
"module.exports = {
name: 'test',
remotes: [],
};
"
`;
exports[`Host App Generator --ssr should generate the correct files for standalone 6`] = `
"const { withModuleFederationForSSR } = require('@nx/angular/module-federation');
const config = require('./module-federation.config');
module.exports = withModuleFederationForSSR(config);
"
`;
exports[`Host App Generator --ssr should generate the correct files for standalone 7`] = `
"import { NxWelcomeComponent } from './nx-welcome.component';
import { Route } from '@angular/router';
export const appRoutes: Route[] = [
{
path: '',
component: NxWelcomeComponent,
},
];
"
`;
exports[`Host App Generator --ssr should generate the correct files for standalone 8`] = `
"import { ApplicationConfig } from '@angular/core';
import {
provideRouter,
withEnabledBlockingInitialNavigation,
} from '@angular/router';
import { appRoutes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(appRoutes, withEnabledBlockingInitialNavigation())],
};
"
`;
exports[`Host App Generator --ssr should generate the correct files for standalone 9`] = `
"import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';
const serverConfig: ApplicationConfig = {
providers: [provideServerRendering()],
};
export const config = mergeApplicationConfig(appConfig, serverConfig);
"
`;
exports[`Host App Generator --ssr should generate the correct files for standalone 10`] = `
{
"configurations": {
"development": {
"buildOptimizer": false,
"extractLicenses": false,
"optimization": false,
"sourceMap": true,
"vendorChunk": true,
},
"production": {
"outputHashing": "media",
},
},
"defaultConfiguration": "production",
"dependsOn": [
"build",
],
"executor": "@nx/angular:webpack-server",
"options": {
"customWebpackConfig": {
"path": "apps/test/webpack.server.config.js",
},
"main": "apps/test/server.ts",
"outputPath": "dist/apps/test/server",
"tsConfig": "apps/test/tsconfig.server.json",
},
}
`;
exports[`Host App Generator --ssr should generate the correct files for standalone 11`] = `
{
"configurations": {
"development": {
"browserTarget": "test:build:development",
"serverTarget": "test:server:development",
},
"production": {
"browserTarget": "test:build:production",
"serverTarget": "test:server:production",
},
},
"defaultConfiguration": "development",
"executor": "@nx/angular:module-federation-dev-ssr",
}
`;
exports[`Host App Generator should generate a host app with a remote 1`] = ` exports[`Host App Generator should generate a host app with a remote 1`] = `
"const { withModuleFederation } = require('@nx/angular/module-federation'); "const { withModuleFederation } = require('@nx/angular/module-federation');
const config = require('./module-federation.config'); const config = require('./module-federation.config');
@ -202,16 +408,12 @@ module.exports = withModuleFederation(config);
exports[`Host App Generator should generate a host with remotes using standalone components 1`] = ` exports[`Host App Generator should generate a host with remotes using standalone components 1`] = `
"import { bootstrapApplication } from '@angular/platform-browser'; "import { bootstrapApplication } from '@angular/platform-browser';
import { import { appConfig } from './app/app.config';
provideRouter,
withEnabledBlockingInitialNavigation,
} from '@angular/router';
import { appRoutes } from './app/app.routes';
import { AppComponent } from './app/app.component'; import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, { bootstrapApplication(AppComponent, appConfig).catch((err) =>
providers: [provideRouter(appRoutes, withEnabledBlockingInitialNavigation())], console.error(err)
}).catch((err) => console.error(err)); );
" "
`; `;

View File

@ -7,7 +7,7 @@ import * as cors from 'cors';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { AppServerModule } from './bootstrap.server'; import<% if(standalone) { %> bootstrap <% } else { %> { AppServerModule } <% } %>from './bootstrap.server';
// The Express app is exported so that it can be used by serverless Functions. // The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express { export function app(): express.Express {
@ -23,7 +23,7 @@ export function app(): express.Express {
server.engine( server.engine(
'html', 'html',
ngExpressEngine({ ngExpressEngine({
bootstrap: AppServerModule, <% if(standalone) { %>bootstrap<% } else { %>bootstrap: AppServerModule,<% } %>
}) })
); );
@ -63,4 +63,4 @@ function run(): void {
run(); run();
export * from './bootstrap.server'; <% if(standalone) { %>export default bootstrap;<% } else { %>export * from './bootstrap.server';<% } %>

View File

@ -229,6 +229,49 @@ describe('Host App Generator', () => {
expect(project.targets.server).toMatchSnapshot(); expect(project.targets.server).toMatchSnapshot();
expect(project.targets['serve-ssr']).toMatchSnapshot(); expect(project.targets['serve-ssr']).toMatchSnapshot();
}); });
it('should generate the correct files for standalone', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
// ACT
await generateTestHostApplication(tree, {
name: 'test',
standalone: true,
ssr: true,
});
// ASSERT
const project = readProjectConfiguration(tree, 'test');
expect(tree.exists(`apps/test/src/app/app.module.ts`)).toBeFalsy();
expect(
tree.read(`apps/test/src/bootstrap.ts`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`apps/test/src/bootstrap.server.ts`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`apps/test/src/main.server.ts`, 'utf-8')
).toMatchSnapshot();
expect(tree.read(`apps/test/server.ts`, 'utf-8')).toMatchSnapshot();
expect(
tree.read(`apps/test/module-federation.config.js`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`apps/test/webpack.server.config.js`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`apps/test/src/app/app.routes.ts`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`apps/test/src/app/app.config.ts`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`apps/test/src/app/app.config.server.ts`, 'utf-8')
).toMatchSnapshot();
expect(project.targets.server).toMatchSnapshot();
expect(project.targets['serve-ssr']).toMatchSnapshot();
});
}); });
it('should error correctly when Angular version does not support standalone', async () => { it('should error correctly when Angular version does not support standalone', async () => {

View File

@ -22,6 +22,7 @@ export async function addSsr(tree: Tree, options: Schema, appName: string) {
await setupSsr(tree, { await setupSsr(tree, {
project: appName, project: appName,
standalone: options.standalone,
}); });
tree.rename( tree.rename(
@ -33,17 +34,9 @@ export async function addSsr(tree: Tree, options: Schema, appName: string) {
"import('./src/main.server');" "import('./src/main.server');"
); );
tree.rename(
joinPathFragments(project.sourceRoot, 'main.ts'),
joinPathFragments(project.sourceRoot, 'bootstrap.ts')
);
tree.write(
joinPathFragments(project.sourceRoot, 'main.ts'),
`import("./bootstrap")`
);
generateFiles(tree, joinPathFragments(__dirname, '../files'), project.root, { generateFiles(tree, joinPathFragments(__dirname, '../files'), project.root, {
appName, appName,
standalone: options.standalone,
tmpl: '', tmpl: '',
}); });

View File

@ -173,6 +173,8 @@ async function addUnitTestRunner(
switch (options.unitTestRunner) { switch (options.unitTestRunner) {
case UnitTestRunner.Jest: case UnitTestRunner.Jest:
if (!options.skipPackageJson) { if (!options.skipPackageJson) {
process.env.npm_config_legacy_peer_deps ??= 'true';
addDependenciesToPackageJsonIfDontExist( addDependenciesToPackageJsonIfDontExist(
tree, tree,
{}, {},

View File

@ -2,8 +2,8 @@
"name": "<% if(npmScope) { %><%= npmScope %>/<% } %><%= libFileName %>", "name": "<% if(npmScope) { %><%= npmScope %>/<% } %><%= libFileName %>",
"version": "0.0.1", "version": "0.0.1",
"peerDependencies": { "peerDependencies": {
"@angular/common": "^15.2.0", "@angular/common": "<%= angularPeerDepVersion %>",
"@angular/core": "^15.2.0" "@angular/core": "<%= angularPeerDepVersion %>"
}, },
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"

View File

@ -7,8 +7,10 @@ import {
offsetFromRoot, offsetFromRoot,
} from '@nx/devkit'; } from '@nx/devkit';
import { getRootTsConfigFileName } from '@nx/js'; import { getRootTsConfigFileName } from '@nx/js';
import { parse } from 'semver';
import { UnitTestRunner } from '../../../utils/test-runners'; import { UnitTestRunner } from '../../../utils/test-runners';
import type { AngularProjectConfiguration } from '../../../utils/types'; import type { AngularProjectConfiguration } from '../../../utils/types';
import { getInstalledAngularVersion } from '../../utils/version-utils';
import type { NormalizedSchema } from './normalized-schema'; import type { NormalizedSchema } from './normalized-schema';
export function createFiles( export function createFiles(
@ -26,6 +28,9 @@ export function createFiles(
options.libraryOptions.fileName options.libraryOptions.fileName
); );
const version = getInstalledAngularVersion(tree);
const { major, minor } = parse(version);
const substitutions = { const substitutions = {
libName: options.libraryOptions.name, libName: options.libraryOptions.name,
libFileName: options.libraryOptions.fileName, libFileName: options.libraryOptions.fileName,
@ -39,6 +44,7 @@ export function createFiles(
pathToComponent, pathToComponent,
npmScope, npmScope,
rootOffset, rootOffset,
angularPeerDepVersion: `^${major}.${minor}.0`,
tpl: '', tpl: '',
}; };

View File

@ -1654,6 +1654,31 @@ describe('lib', () => {
}); });
}); });
it('should create a local package.json', async () => {
// ACT
await runLibraryGeneratorWithOpts({
publishable: true,
importPath: '@myorg/lib',
});
// ASSERT
const tsconfigJson = readJson(tree, 'libs/my-lib/package.json');
expect(tsconfigJson).toMatchInlineSnapshot(`
{
"dependencies": {
"tslib": "^2.3.0",
},
"name": "@myorg/lib",
"peerDependencies": {
"@angular/common": "^14.1.0",
"@angular/core": "^14.1.0",
},
"sideEffects": false,
"version": "0.0.1",
}
`);
});
it('should generate a library with a standalone component as entry point with angular 14.1.0', async () => { it('should generate a library with a standalone component as entry point with angular 14.1.0', async () => {
await runLibraryGeneratorWithOpts({ standalone: true }); await runLibraryGeneratorWithOpts({ standalone: true });

View File

@ -658,18 +658,28 @@ export const appRoutes: Routes = [
exports[`ngrx Standalone APIs should add a root module with feature module when minimal is set to false 1`] = ` exports[`ngrx Standalone APIs should add a root module with feature module when minimal is set to false 1`] = `
"import { bootstrapApplication } from '@angular/platform-browser'; "import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err)
);
"
`;
exports[`ngrx Standalone APIs should add a root module with feature module when minimal is set to false 2`] = `
"import { ApplicationConfig } from '@angular/core';
import { import {
provideRouter, provideRouter,
withEnabledBlockingInitialNavigation, withEnabledBlockingInitialNavigation,
} from '@angular/router'; } from '@angular/router';
import { appRoutes } from './app/app.routes'; import { appRoutes } from './app.routes';
import { AppComponent } from './app/app.component';
import { provideStore, provideState } from '@ngrx/store'; import { provideStore, provideState } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects'; import { provideEffects } from '@ngrx/effects';
import * as fromUsers from './+state/users.reducer'; import * as fromUsers from './+state/users.reducer';
import { UsersEffects } from './+state/users.effects'; import { UsersEffects } from './+state/users.effects';
bootstrapApplication(AppComponent, { export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideEffects(UsersEffects), provideEffects(UsersEffects),
provideState(fromUsers.USERS_FEATURE_KEY, fromUsers.usersReducer), provideState(fromUsers.USERS_FEATURE_KEY, fromUsers.usersReducer),
@ -677,46 +687,66 @@ bootstrapApplication(AppComponent, {
provideStore(), provideStore(),
provideRouter(appRoutes, withEnabledBlockingInitialNavigation()), provideRouter(appRoutes, withEnabledBlockingInitialNavigation()),
], ],
}).catch((err) => console.error(err)); };
" "
`; `;
exports[`ngrx Standalone APIs should add an empty provideStore when minimal and root are set to true 1`] = ` exports[`ngrx Standalone APIs should add an empty provideStore when minimal and root are set to true 1`] = `
"import { bootstrapApplication } from '@angular/platform-browser'; "import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err)
);
"
`;
exports[`ngrx Standalone APIs should add an empty provideStore when minimal and root are set to true 2`] = `
"import { ApplicationConfig } from '@angular/core';
import { import {
provideRouter, provideRouter,
withEnabledBlockingInitialNavigation, withEnabledBlockingInitialNavigation,
} from '@angular/router'; } from '@angular/router';
import { appRoutes } from './app/app.routes'; import { appRoutes } from './app.routes';
import { AppComponent } from './app/app.component';
import { provideStore, provideState } from '@ngrx/store'; import { provideStore, provideState } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects'; import { provideEffects } from '@ngrx/effects';
bootstrapApplication(AppComponent, { export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideEffects(), provideEffects(),
provideStore(), provideStore(),
provideRouter(appRoutes, withEnabledBlockingInitialNavigation()), provideRouter(appRoutes, withEnabledBlockingInitialNavigation()),
], ],
}).catch((err) => console.error(err)); };
" "
`; `;
exports[`ngrx Standalone APIs should add facade provider when facade is true 1`] = ` exports[`ngrx Standalone APIs should add facade provider when facade is true 1`] = `
"import { bootstrapApplication } from '@angular/platform-browser'; "import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err)
);
"
`;
exports[`ngrx Standalone APIs should add facade provider when facade is true 2`] = `
"import { ApplicationConfig } from '@angular/core';
import { import {
provideRouter, provideRouter,
withEnabledBlockingInitialNavigation, withEnabledBlockingInitialNavigation,
} from '@angular/router'; } from '@angular/router';
import { appRoutes } from './app/app.routes'; import { appRoutes } from './app.routes';
import { AppComponent } from './app/app.component';
import { provideStore, provideState } from '@ngrx/store'; import { provideStore, provideState } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects'; import { provideEffects } from '@ngrx/effects';
import * as fromUsers from './+state/users.reducer'; import * as fromUsers from './+state/users.reducer';
import { UsersEffects } from './+state/users.effects'; import { UsersEffects } from './+state/users.effects';
import { UsersFacade } from './+state/users.facade'; import { UsersFacade } from './+state/users.facade';
bootstrapApplication(AppComponent, { export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideEffects(UsersEffects), provideEffects(UsersEffects),
provideState(fromUsers.USERS_FEATURE_KEY, fromUsers.usersReducer), provideState(fromUsers.USERS_FEATURE_KEY, fromUsers.usersReducer),
@ -725,7 +755,7 @@ bootstrapApplication(AppComponent, {
UsersFacade, UsersFacade,
provideRouter(appRoutes, withEnabledBlockingInitialNavigation()), provideRouter(appRoutes, withEnabledBlockingInitialNavigation()),
], ],
}).catch((err) => console.error(err)); };
" "
`; `;

View File

@ -5,7 +5,7 @@ import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'
import type { SourceFile } from 'typescript'; import type { SourceFile } from 'typescript';
import { import {
addImportToModule, addImportToModule,
addProviderToBootstrapApplication, addProviderToAppConfig,
addProviderToModule, addProviderToModule,
} from '../../../utils/nx-devkit/ast-utils'; } from '../../../utils/nx-devkit/ast-utils';
import type { NormalizedNgRxGeneratorOptions } from './normalize-options'; import type { NormalizedNgRxGeneratorOptions } from './normalize-options';
@ -23,8 +23,8 @@ function addRootStoreImport(
storeForRoot: string storeForRoot: string
) { ) {
if (isParentStandalone) { if (isParentStandalone) {
if (tree.read(parentPath, 'utf-8').includes('bootstrapApplication')) { if (tree.read(parentPath, 'utf-8').includes('ApplicationConfig')) {
addProviderToBootstrapApplication(tree, parentPath, provideRootStore); addProviderToAppConfig(tree, parentPath, provideRootStore);
} else { } else {
addProviderToRoute(tree, parentPath, route, provideRootStore); addProviderToRoute(tree, parentPath, route, provideRootStore);
} }
@ -44,8 +44,8 @@ function addRootEffectsImport(
effectsForEmptyRoot: string effectsForEmptyRoot: string
) { ) {
if (isParentStandalone) { if (isParentStandalone) {
if (tree.read(parentPath, 'utf-8').includes('bootstrapApplication')) { if (tree.read(parentPath, 'utf-8').includes('ApplicationConfig')) {
addProviderToBootstrapApplication(tree, parentPath, provideRootEffects); addProviderToAppConfig(tree, parentPath, provideRootEffects);
} else { } else {
addProviderToRoute(tree, parentPath, route, provideRootEffects); addProviderToRoute(tree, parentPath, route, provideRootEffects);
} }
@ -91,12 +91,8 @@ function addStoreForFeatureImport(
storeForFeature: string storeForFeature: string
) { ) {
if (isParentStandalone) { if (isParentStandalone) {
if (tree.read(parentPath, 'utf-8').includes('bootstrapApplication')) { if (tree.read(parentPath, 'utf-8').includes('ApplicationConfig')) {
addProviderToBootstrapApplication( addProviderToAppConfig(tree, parentPath, provideStoreForFeature);
tree,
parentPath,
provideStoreForFeature
);
} else { } else {
addProviderToRoute(tree, parentPath, route, provideStoreForFeature); addProviderToRoute(tree, parentPath, route, provideStoreForFeature);
} }
@ -121,12 +117,8 @@ function addEffectsForFeatureImport(
effectsForFeature: string effectsForFeature: string
) { ) {
if (isParentStandalone) { if (isParentStandalone) {
if (tree.read(parentPath, 'utf-8').includes('bootstrapApplication')) { if (tree.read(parentPath, 'utf-8').includes('ApplicationConfig')) {
addProviderToBootstrapApplication( addProviderToAppConfig(tree, parentPath, provideEffectsForFeature);
tree,
parentPath,
provideEffectsForFeature
);
} else { } else {
addProviderToRoute(tree, parentPath, route, provideEffectsForFeature); addProviderToRoute(tree, parentPath, route, provideEffectsForFeature);
} }
@ -257,8 +249,8 @@ export function addImportsToModule(
if (options.facade) { if (options.facade) {
sourceFile = addImport(sourceFile, facadeName, facadePath); sourceFile = addImport(sourceFile, facadeName, facadePath);
if (isParentStandalone) { if (isParentStandalone) {
if (tree.read(parentPath, 'utf-8').includes('bootstrapApplication')) { if (tree.read(parentPath, 'utf-8').includes('ApplicationConfig')) {
addProviderToBootstrapApplication(tree, parentPath, facadeName); addProviderToAppConfig(tree, parentPath, facadeName);
} else { } else {
addProviderToRoute(tree, parentPath, options.route, facadeName); addProviderToRoute(tree, parentPath, options.route, facadeName);
} }

View File

@ -13,6 +13,8 @@ export function addNgRxToPackageJson(
: '~0.8.3'; : '~0.8.3';
const ngrxVersion = versions(tree).ngrxVersion; const ngrxVersion = versions(tree).ngrxVersion;
process.env.npm_config_legacy_peer_deps ??= 'true';
return addDependenciesToPackageJson( return addDependenciesToPackageJson(
tree, tree,
{ {

View File

@ -30,7 +30,7 @@ describe('ngrx', () => {
const defaultStandaloneOptions: NgRxGeneratorOptions = { const defaultStandaloneOptions: NgRxGeneratorOptions = {
directory: '+state', directory: '+state',
minimal: true, minimal: true,
parent: 'apps/my-app/src/main.ts', parent: 'apps/my-app/src/app/app.config.ts',
name: 'users', name: 'users',
}; };
@ -534,6 +534,9 @@ describe('ngrx', () => {
}); });
expect(tree.read('/apps/my-app/src/main.ts', 'utf-8')).toMatchSnapshot(); expect(tree.read('/apps/my-app/src/main.ts', 'utf-8')).toMatchSnapshot();
expect(
tree.read('/apps/my-app/src/app/app.config.ts', 'utf-8')
).toMatchSnapshot();
expect(tree.exists('/apps/my-app/src/app/+state/users.actions.ts')).toBe( expect(tree.exists('/apps/my-app/src/app/+state/users.actions.ts')).toBe(
false false
); );
@ -562,6 +565,9 @@ describe('ngrx', () => {
}); });
expect(tree.read('/apps/my-app/src/main.ts', 'utf-8')).toMatchSnapshot(); expect(tree.read('/apps/my-app/src/main.ts', 'utf-8')).toMatchSnapshot();
expect(
tree.read('/apps/my-app/src/app/app.config.ts', 'utf-8')
).toMatchSnapshot();
}); });
it('should add a feature module when route is undefined', async () => { it('should add a feature module when route is undefined', async () => {
@ -619,6 +625,9 @@ describe('ngrx', () => {
}); });
expect(tree.read('/apps/my-app/src/main.ts', 'utf-8')).toMatchSnapshot(); expect(tree.read('/apps/my-app/src/main.ts', 'utf-8')).toMatchSnapshot();
expect(
tree.read('/apps/my-app/src/app/app.config.ts', 'utf-8')
).toMatchSnapshot();
}); });
it('should add facade provider when facade is true and --root is false', async () => { it('should add facade provider when facade is true and --root is false', async () => {

View File

@ -9,7 +9,7 @@ import { AppComponent } from './app.component';
@NgModule({ @NgModule({
declarations: [AppComponent], declarations: [AppComponent],
imports: [ imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }), BrowserModule,
RouterModule.forRoot( RouterModule.forRoot(
[ [
{ {
@ -181,9 +181,11 @@ exports[`MF Remote App Generator --ssr should generate the correct files 11`] =
{ {
"configurations": { "configurations": {
"development": { "development": {
"buildOptimizer": false,
"extractLicenses": false, "extractLicenses": false,
"optimization": false, "optimization": false,
"sourceMap": true, "sourceMap": true,
"vendorChunk": true,
}, },
"production": { "production": {
"outputHashing": "media", "outputHashing": "media",

View File

@ -7,7 +7,7 @@ import * as cors from 'cors';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { AppServerModule } from './bootstrap.server'; import<% if(standalone) { %> bootstrap <% } else { %> { AppServerModule } <% } %>from './bootstrap.server';
// The Express app is exported so that it can be used by serverless Functions. // The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express { export function app(): express.Express {
@ -24,7 +24,7 @@ export function app(): express.Express {
server.engine( server.engine(
'html', 'html',
ngExpressEngine({ ngExpressEngine({
bootstrap: AppServerModule, <% if(standalone) { %>bootstrap<% } else { %>bootstrap: AppServerModule,<% } %>
}) })
); );
@ -71,4 +71,4 @@ function run(): void {
run(); run();
export * from './bootstrap.server'; <% if(standalone) { %>export default bootstrap;<% } else { %>export * from './bootstrap.server';<% } %>

View File

@ -0,0 +1,7 @@
import {bootstrapApplication} from '@angular/platform-browser';
import {RemoteEntryComponent} from './app/remote-entry/entry.component';
import {config} from './app/app.config.server';
const bootstrap = () => bootstrapApplication(RemoteEntryComponent, config);
export default bootstrap;

View File

@ -18,27 +18,52 @@ import {
export async function addSsr( export async function addSsr(
tree: Tree, tree: Tree,
{ appName, port }: { appName: string; port: number } {
appName,
port,
standalone,
}: { appName: string; port: number; standalone: boolean }
) { ) {
let project = readProjectConfiguration(tree, appName); let project = readProjectConfiguration(tree, appName);
await setupSsr(tree, { await setupSsr(tree, {
project: appName, project: appName,
standalone,
}); });
tree.rename( tree.rename(
joinPathFragments(project.sourceRoot, 'main.server.ts'), joinPathFragments(project.sourceRoot, 'main.server.ts'),
joinPathFragments(project.sourceRoot, 'bootstrap.server.ts') joinPathFragments(project.sourceRoot, 'bootstrap.server.ts')
); );
tree.write( tree.write(
joinPathFragments(project.root, 'server.ts'), joinPathFragments(project.root, 'server.ts'),
"import('./src/main.server');" "import('./src/main.server');"
); );
generateFiles(tree, joinPathFragments(__dirname, '../files'), project.root, { generateFiles(
tree,
joinPathFragments(__dirname, '../files/base'),
project.root,
{
appName, appName,
standalone,
tmpl: '', tmpl: '',
}); }
);
if (standalone) {
generateFiles(
tree,
joinPathFragments(__dirname, '../files/standalone'),
project.root,
{
appName,
standalone,
tmpl: '',
}
);
}
// update project.json // update project.json
project = readProjectConfiguration(tree, appName); project = readProjectConfiguration(tree, appName);

View File

@ -61,7 +61,11 @@ export async function remote(tree: Tree, options: Schema) {
let installTasks = [appInstallTask]; let installTasks = [appInstallTask];
if (options.ssr) { if (options.ssr) {
let ssrInstallTask = await addSsr(tree, { appName, port }); let ssrInstallTask = await addSsr(tree, {
appName,
port,
standalone: options.standalone,
});
installTasks.push(ssrInstallTask); installTasks.push(ssrInstallTask);
} }

View File

@ -4,9 +4,11 @@ exports[`setupSSR should create the files correctly for ssr 1`] = `
{ {
"configurations": { "configurations": {
"development": { "development": {
"buildOptimizer": false,
"extractLicenses": false, "extractLicenses": false,
"optimization": false, "optimization": false,
"sourceMap": true, "sourceMap": true,
"vendorChunk": true,
}, },
"production": { "production": {
"outputHashing": "media", "outputHashing": "media",
@ -100,13 +102,117 @@ export * from './src/main.server';
" "
`; `;
exports[`setupSSR should create the files correctly for ssr when app is standalone 1`] = `
{
"configurations": {
"development": {
"buildOptimizer": false,
"extractLicenses": false,
"optimization": false,
"sourceMap": true,
"vendorChunk": true,
},
"production": {
"outputHashing": "media",
},
},
"defaultConfiguration": "production",
"dependsOn": [
"build",
],
"executor": "@angular-devkit/build-angular:server",
"options": {
"main": "apps/app1/server.ts",
"outputPath": "dist/apps/app1/server",
"tsConfig": "apps/app1/tsconfig.server.json",
},
}
`;
exports[`setupSSR should create the files correctly for ssr when app is standalone 2`] = `
"import 'zone.js/dist/zone-node';
import { APP_BASE_HREF } from '@angular/common';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { existsSync } from 'fs';
import { join } from 'path';
import bootstrap from './src/main.server';
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const distFolder = join(process.cwd(), 'dist/apps/app1/browser');
const indexHtml = existsSync(join(distFolder, 'index.original.html'))
? 'index.original.html'
: 'index';
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
server.engine(
'html',
ngExpressEngine({
bootstrap,
})
);
server.set('view engine', 'html');
server.set('views', distFolder);
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get(
'*.*',
express.static(distFolder, {
maxAge: '1y',
})
);
// All regular routes use the Universal engine
server.get('*', (req, res) => {
res.render(indexHtml, {
req,
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],
});
});
return server;
}
function run(): void {
const port = process.env['PORT'] || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(\`Node Express server listening on http://localhost:\${port}\`);
});
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = (mainModule && mainModule.filename) || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}
export default bootstrap;
"
`;
exports[`setupSSR should use fileReplacements if they already exist 1`] = ` exports[`setupSSR should use fileReplacements if they already exist 1`] = `
{ {
"configurations": { "configurations": {
"development": { "development": {
"buildOptimizer": false,
"extractLicenses": false, "extractLicenses": false,
"optimization": false, "optimization": false,
"sourceMap": true, "sourceMap": true,
"vendorChunk": true,
}, },
"production": { "production": {
"fileReplacements": [ "fileReplacements": [

View File

@ -0,0 +1,60 @@
import 'zone.js/dist/zone-node';
import {APP_BASE_HREF} from '@angular/common';
import {ngExpressEngine} from '@nguniversal/express-engine';
import * as express from 'express';
import {existsSync} from 'fs';
import {join} from 'path';
import bootstrap from './src/<%= main.slice(0, -3) %>';
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const distFolder = join(process.cwd(), 'dist/apps/<%= project %>/browser');
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
server.engine('html', ngExpressEngine({
bootstrap
}));
server.set('view engine', 'html');
server.set('views', distFolder);
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));
// All regular routes use the Universal engine
server.get('*', (req, res) => {
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});
return server;
}
function run(): void {
const port = process.env['PORT'] || <%= serverPort %>;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}
export default bootstrap;

View File

@ -0,0 +1,7 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { config } from './app/app.config.server';
const bootstrap = () => bootstrapApplication(AppComponent, config);
export default bootstrap;

View File

@ -0,0 +1,11 @@
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';
const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering()
]
};
export const config = mergeApplicationConfig(appConfig, serverConfig);

View File

@ -4,40 +4,53 @@ import {
joinPathFragments, joinPathFragments,
readProjectConfiguration, readProjectConfiguration,
} from '@nx/devkit'; } from '@nx/devkit';
import {
getInstalledAngularMajorVersion,
getInstalledAngularVersionInfo,
} from '../../utils/version-utils';
import type { Schema } from '../schema';
import { lt } from 'semver'; import { lt } from 'semver';
import { getInstalledAngularVersionInfo } from '../../utils/version-utils';
import type { Schema } from '../schema';
export function generateSSRFiles(tree: Tree, schema: Schema) { export function generateSSRFiles(tree: Tree, schema: Schema) {
const projectRoot = readProjectConfiguration(tree, schema.project).root; const projectRoot = readProjectConfiguration(tree, schema.project).root;
const pathToFiles = joinPathFragments(__dirname, '..', 'files');
generateFiles(tree, joinPathFragments(pathToFiles, 'base'), projectRoot, {
...schema,
tpl: '',
});
if (schema.standalone) {
generateFiles( generateFiles(
tree, tree,
joinPathFragments(__dirname, '..', 'files', 'base'), joinPathFragments(pathToFiles, 'standalone'),
projectRoot,
{ ...schema, tpl: '' }
);
} else {
generateFiles(
tree,
joinPathFragments(pathToFiles, 'ngmodule', 'base'),
projectRoot, projectRoot,
{ ...schema, tpl: '' } { ...schema, tpl: '' }
); );
const { major: angularMajorVersion, version: angularVersion } = const { major: angularMajorVersion, version: angularVersion } =
getInstalledAngularVersionInfo(tree); getInstalledAngularVersionInfo(tree);
if (angularMajorVersion < 15) { if (angularMajorVersion < 15) {
generateFiles( generateFiles(
tree, tree,
joinPathFragments(__dirname, '..', 'files', 'v14'), joinPathFragments(pathToFiles, 'ngmodule', 'v14'),
projectRoot, projectRoot,
{ ...schema, tpl: '' } { ...schema, tpl: '' }
); );
} }
if (lt(angularVersion, '15.2.0')) { if (lt(angularVersion, '15.2.0')) {
generateFiles( generateFiles(
tree, tree,
joinPathFragments(__dirname, '..', 'files', 'pre-v15-2'), joinPathFragments(pathToFiles, 'ngmodule', 'pre-v15-2'),
projectRoot, projectRoot,
{ ...schema, tpl: '' } { ...schema, tpl: '' }
); );
} }
} }
}

View File

@ -2,3 +2,4 @@ export * from './generate-files';
export * from './normalize-options'; export * from './normalize-options';
export * from './update-app-module'; export * from './update-app-module';
export * from './update-project-config'; export * from './update-project-config';
export * from './validate-options';

View File

@ -1,10 +1,12 @@
import type { Tree } from '@nx/devkit'; import type { Tree } from '@nx/devkit';
import { readNxJson } from '@nx/devkit'; import { isNgStandaloneApp } from '../../../utils/nx-devkit/ast-utils';
import type { Schema } from '../schema'; import type { Schema } from '../schema';
export function normalizeOptions(tree: Tree, options: Schema) { export function normalizeOptions(tree: Tree, options: Schema) {
const isStandaloneApp = isNgStandaloneApp(tree, options.project);
return { return {
project: options.project ?? readNxJson(tree).defaultProject, project: options.project,
appId: options.appId ?? 'serverApp', appId: options.appId ?? 'serverApp',
main: options.main ?? 'main.server.ts', main: options.main ?? 'main.server.ts',
serverFileName: options.serverFileName ?? 'server.ts', serverFileName: options.serverFileName ?? 'server.ts',
@ -12,5 +14,6 @@ export function normalizeOptions(tree: Tree, options: Schema) {
rootModuleFileName: options.rootModuleFileName ?? 'app.server.module.ts', rootModuleFileName: options.rootModuleFileName ?? 'app.server.module.ts',
rootModuleClassName: options.rootModuleClassName ?? 'AppServerModule', rootModuleClassName: options.rootModuleClassName ?? 'AppServerModule',
skipFormat: options.skipFormat ?? false, skipFormat: options.skipFormat ?? false,
standalone: options.standalone ?? isStandaloneApp,
}; };
} }

View File

@ -1,9 +1,15 @@
import type { Tree } from '@nx/devkit'; import type { Tree } from '@nx/devkit';
import { joinPathFragments, readProjectConfiguration } from '@nx/devkit'; import { joinPathFragments, readProjectConfiguration } from '@nx/devkit';
import type { Schema } from '../schema';
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
import { getInstalledAngularMajorVersion } from '../../utils/version-utils';
import type { Schema } from '../schema';
export function updateAppModule(tree: Tree, schema: Schema) { export function updateAppModule(tree: Tree, schema: Schema) {
const angularMajorVersion = getInstalledAngularMajorVersion(tree);
if (angularMajorVersion >= 16) {
return;
}
ensureTypescript(); ensureTypescript();
const { tsquery } = require('@phenomnomnominal/tsquery'); const { tsquery } = require('@phenomnomnominal/tsquery');
// read the content of app module // read the content of app module

View File

@ -1,3 +1,7 @@
import type {
BrowserBuilderOptions,
ServerBuilderOptions,
} from '@angular-devkit/build-angular';
import type { Tree } from '@nx/devkit'; import type { Tree } from '@nx/devkit';
import { import {
joinPathFragments, joinPathFragments,
@ -10,11 +14,17 @@ import type { Schema } from '../schema';
export function updateProjectConfig(tree: Tree, schema: Schema) { export function updateProjectConfig(tree: Tree, schema: Schema) {
let projectConfig = readProjectConfiguration(tree, schema.project); let projectConfig = readProjectConfiguration(tree, schema.project);
const buildTarget = projectConfig.targets.build;
projectConfig.targets.build.options.outputPath = `dist/apps/${schema.project}/browser`; buildTarget.options.outputPath = `dist/apps/${schema.project}/browser`;
const buildTargetFileReplacements = const buildConfigurations = projectConfig.targets.build.configurations;
projectConfig.targets.build.configurations?.production?.fileReplacements; const configurations: Record<string, {}> = {};
if (buildConfigurations) {
for (const [key, options] of Object.entries(buildConfigurations)) {
configurations[key] = getServerOptions(options);
}
}
projectConfig.targets.server = { projectConfig.targets.server = {
dependsOn: ['build'], dependsOn: ['build'],
@ -23,20 +33,9 @@ export function updateProjectConfig(tree: Tree, schema: Schema) {
outputPath: `dist/${projectConfig.root}/server`, outputPath: `dist/${projectConfig.root}/server`,
main: joinPathFragments(projectConfig.root, schema.serverFileName), main: joinPathFragments(projectConfig.root, schema.serverFileName),
tsConfig: joinPathFragments(projectConfig.root, 'tsconfig.server.json'), tsConfig: joinPathFragments(projectConfig.root, 'tsconfig.server.json'),
...(buildTarget.options ? getServerOptions(buildTarget.options) : {}),
}, },
configurations: { configurations,
production: {
outputHashing: 'media',
...(buildTargetFileReplacements
? { fileReplacements: buildTargetFileReplacements }
: {}),
},
development: {
optimization: false,
sourceMap: true,
extractLicenses: false,
},
},
defaultConfiguration: 'production', defaultConfiguration: 'production',
}; };
@ -89,3 +88,27 @@ export function updateProjectConfig(tree: Tree, schema: Schema) {
updateNxJson(tree, nxJson); updateNxJson(tree, nxJson);
} }
} }
function getServerOptions(
options: Partial<BrowserBuilderOptions> = {}
): Partial<ServerBuilderOptions> {
return {
buildOptimizer: options?.buildOptimizer,
outputHashing:
options?.outputHashing === 'all'
? ('media' as any)
: options?.outputHashing,
fileReplacements: options?.fileReplacements,
optimization:
options?.optimization === undefined ? undefined : !!options?.optimization,
sourceMap: options?.sourceMap,
stylePreprocessorOptions: options?.stylePreprocessorOptions,
resourcesOutputPath: options?.resourcesOutputPath,
deployUrl: options?.deployUrl,
i18nMissingTranslation: options?.i18nMissingTranslation,
preserveSymlinks: options?.preserveSymlinks,
extractLicenses: options?.extractLicenses,
inlineStyleLanguage: options?.inlineStyleLanguage,
vendorChunk: options?.vendorChunk,
};
}

View File

@ -0,0 +1,11 @@
import type { Tree } from '@nx/devkit';
import {
validateProject,
validateStandaloneOption,
} from '../../utils/validations';
import type { Schema } from '../schema';
export function validateOptions(tree: Tree, options: Schema): void {
validateProject(tree, options.project);
validateStandaloneOption(tree, options.standalone);
}

View File

@ -6,5 +6,6 @@ export interface Schema {
serverPort?: number; serverPort?: number;
rootModuleFileName?: string; rootModuleFileName?: string;
rootModuleClassName?: string; rootModuleClassName?: string;
standalone?: boolean;
skipFormat?: boolean; skipFormat?: boolean;
} }

View File

@ -19,7 +19,7 @@
"appId": { "appId": {
"type": "string", "type": "string",
"format": "html-selector", "format": "html-selector",
"description": "The `appId` to use with `withServerTransition`.", "description": "The `appId` to use with `withServerTransition`. _Note: This is only used in Angular versions <16.0.0. It's deprecated since Angular 16._",
"default": "serverApp" "default": "serverApp"
}, },
"main": { "main": {
@ -49,6 +49,10 @@
"description": "The name of the root module class.", "description": "The name of the root module class.",
"default": "AppServerModule" "default": "AppServerModule"
}, },
"standalone": {
"type": "boolean",
"description": "Use Standalone Components to bootstrap SSR. _Note: This is only supported in Angular versions >= 14.1.0_."
},
"skipFormat": { "skipFormat": {
"type": "boolean", "type": "boolean",
"description": "Skip formatting the workspace after the generator completes.", "description": "Skip formatting the workspace after the generator completes.",

View File

@ -80,7 +80,7 @@ describe('setupSSR', () => {
@NgModule({ @NgModule({
declarations: [AppComponent, NxWelcomeComponent], declarations: [AppComponent, NxWelcomeComponent],
imports: [BrowserModule.withServerTransition({ appId: 'serverApp' })], imports: [BrowserModule],
providers: [], providers: [],
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })
@ -148,6 +148,95 @@ describe('setupSSR', () => {
).toMatchSnapshot(); ).toMatchSnapshot();
}); });
it('should create the files correctly for ssr when app is standalone', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await generateTestApplication(tree, {
name: 'app1',
standalone: true,
});
// ACT
await setupSsr(tree, { project: 'app1' });
// ASSERT
expect(
readProjectConfiguration(tree, 'app1').targets.server
).toMatchSnapshot();
expect(tree.read('apps/app1/server.ts', 'utf-8')).toMatchSnapshot();
expect(tree.read('apps/app1/src/main.server.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { config } from './app/app.config.server';
const bootstrap = () => bootstrapApplication(AppComponent, config);
export default bootstrap;
"
`);
expect(tree.read('apps/app1/tsconfig.server.json', 'utf-8'))
.toMatchInlineSnapshot(`
"/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.app.json",
"compilerOptions": {
"outDir": "../../out-tsc/server",
"target": "es2019",
"types": ["node"]
},
"files": ["src/main.server.ts", "server.ts"]
}
"
`);
expect(tree.read('apps/app1/src/app/app.config.server.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';
const serverConfig: ApplicationConfig = {
providers: [provideServerRendering()],
};
export const config = mergeApplicationConfig(appConfig, serverConfig);
"
`);
const packageJson = readJson<PackageJson>(tree, 'package.json');
const dependencies = {
'@nguniversal/express-engine': ngUniversalVersion,
'@angular/platform-server': angularVersion,
};
for (const [dep, version] of Object.entries(dependencies)) {
expect(packageJson.dependencies[dep]).toEqual(version);
}
const devDeps = {
'@nguniversal/builders': ngUniversalVersion,
};
for (const [dep, version] of Object.entries(devDeps)) {
expect(packageJson.devDependencies[dep]).toEqual(version);
}
const nxJson = readJson<NxJsonConfiguration>(tree, 'nx.json');
expect(nxJson.tasksRunnerOptions).toMatchInlineSnapshot(`
{
"default": {
"options": {
"cacheableOperations": [
"build",
"lint",
"test",
"e2e",
"server",
],
},
"runner": "nx/tasks-runners/default",
},
}
`);
});
describe('compat', () => { describe('compat', () => {
it('should install the correct versions when using older versions of Angular', async () => { it('should install the correct versions when using older versions of Angular', async () => {
// ARRANGE // ARRANGE
@ -212,6 +301,40 @@ describe('setupSSR', () => {
`); `);
}); });
it('should add "withServerTransition" call to app module for angular versions lower than 16', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
updateJson(tree, 'package.json', (json) => ({
...json,
dependencies: { ...json.dependencies, '@angular/core': '^15.2.0' },
}));
await generateTestApplication(tree, {
name: 'app1',
});
// ACT
await setupSsr(tree, { project: 'app1' });
// ASSERT
expect(tree.read('apps/app1/src/app/app.module.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { NxWelcomeComponent } from './nx-welcome.component';
@NgModule({
declarations: [AppComponent, NxWelcomeComponent],
imports: [BrowserModule.withServerTransition({ appId: 'serverApp' })],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
"
`);
});
it('should wrap bootstrap call for Angular versions lower than 15.2', async () => { it('should wrap bootstrap call for Angular versions lower than 15.2', async () => {
// ARRANGE // ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });

View File

@ -10,14 +10,20 @@ import {
normalizeOptions, normalizeOptions,
updateAppModule, updateAppModule,
updateProjectConfig, updateProjectConfig,
validateOptions,
} from './lib'; } from './lib';
import type { Schema } from './schema'; import type { Schema } from './schema';
export async function setupSsr(tree: Tree, schema: Schema) { export async function setupSsr(tree: Tree, schema: Schema) {
validateOptions(tree, schema);
const options = normalizeOptions(tree, schema); const options = normalizeOptions(tree, schema);
generateSSRFiles(tree, options); generateSSRFiles(tree, options);
if (!options.standalone) {
updateAppModule(tree, options); updateAppModule(tree, options);
}
updateProjectConfig(tree, options); updateProjectConfig(tree, options);
const pkgVersions = versions(tree); const pkgVersions = versions(tree);

View File

@ -3,7 +3,6 @@ import type {
__String, __String,
CallExpression, CallExpression,
ClassDeclaration, ClassDeclaration,
Decorator,
ImportDeclaration, ImportDeclaration,
ObjectLiteralExpression, ObjectLiteralExpression,
PropertyAssignment, PropertyAssignment,
@ -48,38 +47,15 @@ export function insertNgModuleProperty(
const ngModuleName = ngModuleNamedImport.name.escapedText; const ngModuleName = ngModuleNamedImport.name.escapedText;
/** const ngModuleClassDeclaration = findDecoratedClass(sourceFile, ngModuleName);
* Ensure backwards compatibility with TS < 4.8 due to the API change in TS4.8.
* The getDecorators util is only in TS 4.8, so we need the previous logic to handle TS < 4.8. const { getDecorators } = getTsEsLintTypeUtils();
* const ngModuleDecorator = getDecorators(ngModuleClassDeclaration).find(
* TODO: clean this up using another util or when we don't need to support TS < 4.8 anymore.
*/
let ngModuleClassDeclaration: ClassDeclaration;
let ngModuleDecorator: Decorator;
try {
ngModuleClassDeclaration = findDecoratedClass(sourceFile, ngModuleName);
ngModuleDecorator = tsModule
.getDecorators(ngModuleClassDeclaration)
.find(
(decorator) => (decorator) =>
tsModule.isCallExpression(decorator.expression) && tsModule.isCallExpression(decorator.expression) &&
tsModule.isIdentifier(decorator.expression.expression) && tsModule.isIdentifier(decorator.expression.expression) &&
decorator.expression.expression.escapedText === ngModuleName decorator.expression.expression.escapedText === ngModuleName
); );
} catch {
// Support for TS < 4.8
ngModuleClassDeclaration = findDecoratedClassLegacy(
sourceFile,
ngModuleName
);
// @ts-ignore
ngModuleDecorator = ngModuleClassDeclaration.decorators.find(
(decorator) =>
tsModule.isCallExpression(decorator.expression) &&
tsModule.isIdentifier(decorator.expression.expression) &&
decorator.expression.expression.escapedText === ngModuleName
);
}
const ngModuleCall = ngModuleDecorator.expression as CallExpression; const ngModuleCall = ngModuleDecorator.expression as CallExpression;
@ -203,8 +179,10 @@ function findDecoratedClass(
const classDeclarations = sourceFile.statements.filter( const classDeclarations = sourceFile.statements.filter(
tsModule.isClassDeclaration tsModule.isClassDeclaration
); );
const { getDecorators } = getTsEsLintTypeUtils();
return classDeclarations.find((declaration) => { return classDeclarations.find((declaration) => {
const decorators = tsModule.getDecorators(declaration); const decorators = getDecorators(declaration);
if (decorators) { if (decorators) {
return decorators.some( return decorators.some(
(decorator) => (decorator) =>
@ -217,29 +195,6 @@ function findDecoratedClass(
}); });
} }
function findDecoratedClassLegacy(
sourceFile: SourceFile,
ngModuleName: __String
) {
if (!tsModule) {
tsModule = ensureTypescript();
}
const classDeclarations = sourceFile.statements.filter(
tsModule.isClassDeclaration
);
return classDeclarations.find(
(declaration) =>
declaration.decorators &&
(declaration.decorators as any[]).some(
(decorator) =>
tsModule.isCallExpression(decorator.expression) &&
tsModule.isIdentifier(decorator.expression.expression) &&
decorator.expression.expression.escapedText === ngModuleName
)
);
}
function findPropertyAssignment( function findPropertyAssignment(
ngModuleOptions: ObjectLiteralExpression, ngModuleOptions: ObjectLiteralExpression,
propertyName: ngModuleDecoratorProperty propertyName: ngModuleDecoratorProperty
@ -255,3 +210,8 @@ function findPropertyAssignment(
property.name.escapedText === propertyName property.name.escapedText === propertyName
) as PropertyAssignment; ) as PropertyAssignment;
} }
let tsUtils: typeof import('@typescript-eslint/type-utils');
function getTsEsLintTypeUtils(): typeof import('@typescript-eslint/type-utils') {
return tsUtils ?? require('@typescript-eslint/type-utils');
}

View File

@ -1,33 +1,11 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { updateJson } from '@nx/devkit'; import { updateJson } from '@nx/devkit';
import { import {
getGeneratorDirectoryForInstalledAngularVersion,
getInstalledAngularMajorVersion, getInstalledAngularMajorVersion,
getInstalledAngularVersion, getInstalledAngularVersion,
} from './version-utils'; } from './version-utils';
describe('angularVersionUtils', () => { describe('angularVersionUtils', () => {
test.each(['14.0.0', '~14.1.0', '^14.2.0', '~14.3.0-beta.0'])(
'should return correct directory name for v14',
(ngVersion) => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
updateJson(tree, 'package.json', (json) => ({
...json,
dependencies: {
'@angular/core': ngVersion,
},
}));
// ACT
const directoryName =
getGeneratorDirectoryForInstalledAngularVersion(tree);
// ASSERT
expect(directoryName).toEqual('angular-v14');
}
);
test.each(['14.0.0', '~14.1.0', '^14.2.0', '~14.3.0-beta.0'])( test.each(['14.0.0', '~14.1.0', '^14.2.0', '~14.3.0-beta.0'])(
'should return correct major version', 'should return correct major version',
(ngVersion) => { (ngVersion) => {
@ -69,28 +47,4 @@ describe('angularVersionUtils', () => {
// ASSERT // ASSERT
expect(angularVersion).toEqual(expectedVersion); expect(angularVersion).toEqual(expectedVersion);
}); });
test.each([
'15.0.0',
'~15.1.0',
'^13.2.0',
'~15.3.0-beta.0',
'latest',
'next',
])('should return null for anything other than v14', (ngVersion) => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
updateJson(tree, 'package.json', (json) => ({
...json,
dependencies: {
'@angular/core': ngVersion,
},
}));
// ACT
const directoryName = getGeneratorDirectoryForInstalledAngularVersion(tree);
// ASSERT
expect(directoryName).toBe(null);
});
}); });

View File

@ -5,18 +5,6 @@ import * as latestVersions from '../../utils/versions';
import { angularVersion } from '../../utils/versions'; import { angularVersion } from '../../utils/versions';
import { backwardCompatibleVersions } from '../../utils/backward-compatible-versions'; import { backwardCompatibleVersions } from '../../utils/backward-compatible-versions';
export function getGeneratorDirectoryForInstalledAngularVersion(
tree: Tree
): string | null {
const majorAngularVersion = getInstalledAngularMajorVersion(tree);
const directoryDictionary = {
14: 'angular-v14',
};
return directoryDictionary[majorAngularVersion] ?? null;
}
export function getInstalledAngularVersion(tree: Tree): string { export function getInstalledAngularVersion(tree: Tree): string {
const pkgJson = readJson(tree, 'package.json'); const pkgJson = readJson(tree, 'package.json');
const installedAngularVersion = const installedAngularVersion =
@ -99,6 +87,8 @@ export function versions(tree: Tree) {
switch (majorAngularVersion) { switch (majorAngularVersion) {
case 14: case 14:
return backwardCompatibleVersions.angularV14; return backwardCompatibleVersions.angularV14;
case 15:
return backwardCompatibleVersions.angularV15;
default: default:
return latestVersions; return latestVersions;
} }

View File

@ -0,0 +1,123 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import extractStandaloneConfig from './extract-standalone-config-from-bootstrap';
import { addProjectConfiguration } from '@nx/devkit';
const TEST_MAIN_FILE = `import { bootstrapApplication } from '@angular/platform-browser';
import {
provideRouter,
withEnabledBlockingInitialNavigation,
} from '@angular/router';
import { appRoutes } from './app/app.routes';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [provideRouter(appRoutes, withEnabledBlockingInitialNavigation())],
}).catch((err) => console.error(err));`;
describe('extractStandaloneConfigFromBootstrap', () => {
it('should extract the config correctly from a standard main.ts file', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app1', {
name: 'app1',
root: 'apps/app1',
sourceRoot: 'apps/app1/src',
projectType: 'application',
targets: {
build: {
options: {
main: 'apps/app1/src/main.ts',
},
},
},
});
tree.write('apps/app1/src/main.ts', TEST_MAIN_FILE);
// ACT
await extractStandaloneConfig(tree);
// ASSERT
expect(tree.read('apps/app1/src/main.ts', 'utf-8')).toMatchInlineSnapshot(`
"import { appConfig } from './app/app.config';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err)
);
"
`);
expect(tree.exists('apps/app1/src/app/app.config.ts')).toBeTruthy();
expect(tree.read('apps/app1/src/app/app.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ApplicationConfig } from '@angular/core';
import {
provideRouter,
withEnabledBlockingInitialNavigation,
} from '@angular/router';
import { appRoutes } from './app/app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(appRoutes, withEnabledBlockingInitialNavigation())],
};
"
`);
});
it('should extract the config correctly when the main.ts imports bootstrap from bootstrap.ts file', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app1', {
name: 'app1',
root: 'apps/app1',
sourceRoot: 'apps/app1/src',
projectType: 'application',
targets: {
build: {
options: {
main: 'apps/app1/src/main.ts',
},
},
},
});
tree.write('apps/app1/src/main.ts', `import('./bootstrap');`);
tree.write('apps/app1/src/bootstrap.ts', TEST_MAIN_FILE);
// ACT
await extractStandaloneConfig(tree);
// ASSERT
expect(tree.read('apps/app1/src/main.ts', 'utf-8')).toMatchInlineSnapshot(`
"import('./bootstrap');
"
`);
expect(tree.read('apps/app1/src/bootstrap.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { appConfig } from './app/app.config';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err)
);
"
`);
expect(tree.exists('apps/app1/src/app/app.config.ts')).toBeTruthy();
expect(tree.read('apps/app1/src/app/app.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ApplicationConfig } from '@angular/core';
import {
provideRouter,
withEnabledBlockingInitialNavigation,
} from '@angular/router';
import { appRoutes } from './app/app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(appRoutes, withEnabledBlockingInitialNavigation())],
};
"
`);
});
});

View File

@ -0,0 +1,191 @@
import type { ProjectConfiguration, Tree } from '@nx/devkit';
import { formatFiles, getProjects, joinPathFragments } from '@nx/devkit';
import type { Node, SourceFile } from 'typescript';
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
let tsModule: typeof import('typescript');
let tsquery: typeof import('@phenomnomnominal/tsquery').tsquery;
function getBootstrapCallFileInfo<T>(
project: ProjectConfiguration,
tree: Tree
) {
const IMPORT_BOOTSTRAP_FILE =
'CallExpression:has(ImportKeyword) > StringLiteral';
let bootstrapCallFilePath = project.targets?.build?.options?.main;
let bootstrapCallFileContents = tree.read(bootstrapCallFilePath, 'utf-8');
const ast = tsquery.ast(bootstrapCallFileContents);
const importBootstrapNodes = tsquery(ast, IMPORT_BOOTSTRAP_FILE, {
visitAllChildren: true,
});
if (
importBootstrapNodes.length > 0 &&
importBootstrapNodes[0].getText().includes('./bootstrap')
) {
bootstrapCallFilePath = joinPathFragments(
project.sourceRoot,
'bootstrap.ts'
);
bootstrapCallFileContents = tree.read(bootstrapCallFilePath, 'utf-8');
}
return { bootstrapCallFilePath, bootstrapCallFileContents };
}
function getImportTokenMap(bootstrapCallFileContentsAst: SourceFile) {
const importTokenMap = new Map<string, string>();
const importedTokensNodes = tsquery(
bootstrapCallFileContentsAst,
'ImportDeclaration > ImportClause',
{ visitAllChildren: true }
);
for (const node of importedTokensNodes) {
importTokenMap.set(node.getText(), node.parent.getText());
}
return importTokenMap;
}
function getImportsRequiredForAppConfig(
importTokenMap: Map<string, string>,
appConfigNode: Node
) {
const importsRequiredForAppConfig = new Set<string>();
const checkImportsForTokens = (nodeText: string) => {
const keys = importTokenMap.keys();
for (const key of keys) {
if (key.includes(nodeText))
importsRequiredForAppConfig.add(importTokenMap.get(key));
}
};
const visitEachChild = (node: Node) => {
node.forEachChild((node) => {
const nodeText = node.getText();
checkImportsForTokens(nodeText);
visitEachChild(node);
});
};
visitEachChild(appConfigNode);
return importsRequiredForAppConfig;
}
function getAppConfigFileContents(
importsRequiredForAppConfig: Set<string>,
appConfigText: string
) {
const buildAppConfigFileContents = (
importStatements: string[],
appConfig: string
) => `import { ApplicationConfig } from '@angular/core';${importStatements.join(
'\n'
)}
export const appConfig: ApplicationConfig = ${appConfig}`;
const appConfigFileContents = buildAppConfigFileContents(
Array.from(importsRequiredForAppConfig),
appConfigText
);
return appConfigFileContents;
}
function getBootstrapCallFileContents(
bootstrapCallFileContents: string,
appConfigNode: Node,
importsRequiredForAppConfig: Set<string>
) {
let newBootstrapCallFileContents = `import { appConfig } from './app/app.config';
${bootstrapCallFileContents.slice(
0,
appConfigNode.getStart()
)}appConfig${bootstrapCallFileContents.slice(appConfigNode.getEnd())}`;
for (const importStatement of importsRequiredForAppConfig) {
newBootstrapCallFileContents = newBootstrapCallFileContents.replace(
importStatement,
''
);
}
return newBootstrapCallFileContents;
}
export default async function extractStandaloneConfig(tree: Tree) {
if (!tsModule) {
tsModule = ensureTypescript();
}
if (!tsquery) {
tsquery = require('@phenomnomnominal/tsquery').tsquery;
}
const projects = getProjects(tree);
const BOOTSTRAP_APPLICATION_CALL_SELECTOR =
'CallExpression:has(Identifier[name=bootstrapApplication])';
const BOOTSTRAP_APPLICATION_CALL_CONFIG_SELECTOR =
'CallExpression:has(Identifier[name=bootstrapApplication]) > ObjectLiteralExpression';
for (const [projectName, project] of projects.entries()) {
if (project.projectType !== 'application') {
continue;
}
if (project.targets?.build?.options?.main === undefined) {
continue;
}
const { bootstrapCallFilePath, bootstrapCallFileContents } =
getBootstrapCallFileInfo(project, tree);
const bootstrapCallFileContentsAst = tsquery.ast(bootstrapCallFileContents);
const nodes: Node[] = tsquery(
bootstrapCallFileContentsAst,
BOOTSTRAP_APPLICATION_CALL_SELECTOR,
{ visitAllChildren: true }
);
if (nodes.length === 0) {
continue;
}
const importTokenMap = getImportTokenMap(bootstrapCallFileContentsAst);
const bootstrapCallNode = nodes[0];
const appConfigNodes = tsquery(
bootstrapCallNode,
BOOTSTRAP_APPLICATION_CALL_CONFIG_SELECTOR,
{ visitAllChildren: true }
);
if (appConfigNodes.length === 0) {
continue;
}
const appConfigNode = appConfigNodes[0];
const appConfigText = appConfigNode.getText();
const importsRequiredForAppConfig = getImportsRequiredForAppConfig(
importTokenMap,
appConfigNode
);
const appConfigFilePath = joinPathFragments(
project.sourceRoot,
'app/app.config.ts'
);
const appConfigFileContents = getAppConfigFileContents(
importsRequiredForAppConfig,
appConfigText
);
tree.write(appConfigFilePath, appConfigFileContents);
let newBootstrapCallFileContents = getBootstrapCallFileContents(
bootstrapCallFileContents,
appConfigNode,
importsRequiredForAppConfig
);
tree.write(bootstrapCallFilePath, newBootstrapCallFileContents);
}
await formatFiles(tree);
}

View File

@ -0,0 +1,125 @@
import type { Tree } from '@nx/devkit';
import { readJson, updateJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import removeNgccInvocation from './remove-ngcc-invocation';
describe('remove-ngcc-invocation migration', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should not throw when there is no scripts entry', async () => {
updateJson(tree, 'package.json', (json) => {
delete json.scripts;
return json;
});
await expect(removeNgccInvocation(tree)).resolves.not.toThrow();
});
it('should not throw when there is no postinstall script', async () => {
updateJson(tree, 'package.json', (json) => {
json.scripts = {};
return json;
});
await expect(removeNgccInvocation(tree)).resolves.not.toThrow();
});
it('should handle postinstall script without ngcc invocation', async () => {
updateJson(tree, 'package.json', (json) => ({
...json,
scripts: {
postinstall:
'node ./some-awesome-script.js && node ./another-awesome-script.js',
},
}));
await removeNgccInvocation(tree);
const { scripts } = readJson(tree, 'package.json');
expect(scripts.postinstall).toBe(
'node ./some-awesome-script.js && node ./another-awesome-script.js'
);
});
it('should handle postinstall script with only ngcc invocation', async () => {
updateJson(tree, 'package.json', (json) => ({
...json,
scripts: {
postinstall: 'ngcc --properties es2020 browser module main',
},
}));
await removeNgccInvocation(tree);
const { scripts } = readJson(tree, 'package.json');
expect(scripts.postinstall).toBeUndefined();
});
it('should handle postinstall script with extra leading command', async () => {
updateJson(tree, 'package.json', (json) => ({
...json,
scripts: {
postinstall:
'node ./some-awesome-script.js && ngcc --properties es2020 browser module main',
},
}));
await removeNgccInvocation(tree);
const { scripts } = readJson(tree, 'package.json');
expect(scripts.postinstall).toBe('node ./some-awesome-script.js');
});
it('should handle postinstall script with extra trailing command', async () => {
updateJson(tree, 'package.json', (json) => ({
...json,
scripts: {
postinstall:
'ngcc --properties es2020 browser module main && node ./some-awesome-script.js',
},
}));
await removeNgccInvocation(tree);
const { scripts } = readJson(tree, 'package.json');
expect(scripts.postinstall).toBe('node ./some-awesome-script.js');
});
it('should handle postinstall script with extra leading and trailing commands', async () => {
updateJson(tree, 'package.json', (json) => ({
...json,
scripts: {
postinstall:
'node ./some-awesome-script.js && ngcc --properties es2020 browser module main && node ./another-awesome-script.js',
},
}));
await removeNgccInvocation(tree);
const { scripts } = readJson(tree, 'package.json');
expect(scripts.postinstall).toBe(
'node ./some-awesome-script.js && node ./another-awesome-script.js'
);
});
it('should remove ngcc invocation with an arbitrary amount of spaces around "&&"', async () => {
updateJson(tree, 'package.json', (json) => ({
...json,
scripts: {
postinstall:
'node ./some-awesome-script.js && ngcc --properties es2020 browser module main &&node ./another-awesome-script.js',
},
}));
await removeNgccInvocation(tree);
const { scripts } = readJson(tree, 'package.json');
expect(scripts.postinstall).toBe(
'node ./some-awesome-script.js &&node ./another-awesome-script.js'
);
});
});

View File

@ -0,0 +1,25 @@
import type { Tree } from '@nx/devkit';
import { formatFiles, updateJson } from '@nx/devkit';
export default async function (tree: Tree) {
updateJson(tree, 'package.json', (json) => {
if (!json.scripts?.postinstall?.includes('ngcc ')) {
return json;
}
json.scripts.postinstall = json.scripts.postinstall
// special case when ngcc is at the start so we remove the && as well
.replace(/^(ngcc.*?&& *)(.*)/, '$2')
// everything else
.replace(/(.*?)((&& *)?ngcc.*?)((?=&)|$)(.*)/, '$1$5')
.trim();
if (json.scripts.postinstall === '') {
json.scripts.postinstall = undefined;
}
return json;
});
await formatFiles(tree);
}

View File

@ -0,0 +1,42 @@
import { readJson, Tree, writeJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import updateAngularCli, { angularCliVersion } from './update-angular-cli';
describe('update-angular-cli migration', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
});
it('should update @angular/cli version when defined as a dev dependency', async () => {
writeJson(tree, 'package.json', {
devDependencies: { '@angular/cli': '~13.3.0' },
});
await updateAngularCli(tree);
const { devDependencies } = readJson(tree, 'package.json');
expect(devDependencies['@angular/cli']).toEqual(angularCliVersion);
});
it('should update @angular/cli version when defined as a dependency', async () => {
writeJson(tree, 'package.json', {
dependencies: { '@angular/cli': '~13.3.0' },
});
await updateAngularCli(tree);
const { dependencies } = readJson(tree, 'package.json');
expect(dependencies['@angular/cli']).toEqual(angularCliVersion);
});
it('should not add @angular/cli to package.json when it is not set', async () => {
const initialPackageJson = readJson(tree, 'package.json');
await updateAngularCli(tree);
const packageJson = readJson(tree, 'package.json');
expect(packageJson).toStrictEqual(initialPackageJson);
});
});

View File

@ -0,0 +1,23 @@
import { formatFiles, Tree, updateJson } from '@nx/devkit';
export const angularCliVersion = '~16.0.0-rc.4';
export default async function (tree: Tree) {
let shouldFormat = false;
updateJson(tree, 'package.json', (json) => {
if (json.devDependencies?.['@angular/cli']) {
json.devDependencies['@angular/cli'] = angularCliVersion;
shouldFormat = true;
} else if (json.dependencies?.['@angular/cli']) {
json.dependencies['@angular/cli'] = angularCliVersion;
shouldFormat = true;
}
return json;
});
if (shouldFormat) {
await formatFiles(tree);
}
}

View File

@ -0,0 +1,108 @@
import type { Tree } from '@nx/devkit';
import { addProjectConfiguration, readProjectConfiguration } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import migration from './update-server-executor-config';
describe.each([
'@angular-devkit/build-angular:server',
'@nx/angular:server',
'@nrwl/angular:server',
])('update-server-executor-config migration', (executor) => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it(`should add 'buildOptimizer: false' to config with 'optimization: false' (${executor})`, async () => {
addProjectConfiguration(tree, 'app1', {
root: 'apps/app1',
targets: {
server: {
executor,
configurations: {
development: { optimization: false },
production: { optimization: true },
},
},
},
});
await migration(tree);
const project = readProjectConfiguration(tree, 'app1');
expect(
project.targets.server.configurations.development.buildOptimizer
).toBe(false);
});
it(`should not add 'buildOptimizer' option to config when 'optimization' is not defined (${executor})`, async () => {
addProjectConfiguration(tree, 'app1', {
root: 'apps/app1',
targets: {
server: {
executor,
options: {},
configurations: {
development: { optimization: false },
production: { optimization: true },
},
},
},
});
await migration(tree);
const project = readProjectConfiguration(tree, 'app1');
expect(project.targets.server.options.buildOptimizer).toBeUndefined();
});
it(`should add 'buildOptimizer: true' to config with 'optimization: true' (${executor})`, async () => {
addProjectConfiguration(tree, 'app1', {
root: 'apps/app1',
targets: {
server: {
executor,
options: {},
configurations: {
development: { optimization: false },
production: { optimization: true },
},
},
},
});
await migration(tree);
const project = readProjectConfiguration(tree, 'app1');
expect(
project.targets.server.configurations.production.buildOptimizer
).toBe(true);
});
it(`should not change 'buildOptimizer' if already set (${executor})`, async () => {
addProjectConfiguration(tree, 'app1', {
root: 'apps/app1',
targets: {
server: {
executor,
options: {},
configurations: {
development: {
optimization: false,
buildOptimizer: true,
},
production: { optimization: true },
},
},
},
});
await migration(tree);
const project = readProjectConfiguration(tree, 'app1');
expect(
project.targets.server.configurations.development.buildOptimizer
).toBe(true);
});
});

View File

@ -0,0 +1,45 @@
import type { ServerBuilderOptions } from '@angular-devkit/build-angular';
import type { Tree } from '@nx/devkit';
import {
formatFiles,
readProjectConfiguration,
updateProjectConfiguration,
} from '@nx/devkit';
import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils';
const executors = [
'@angular-devkit/build-angular:server',
'@nx/angular:server',
'@nrwl/angular:server',
];
export default async function (tree: Tree) {
executors.forEach((executor) => {
forEachExecutorOptions<ServerBuilderOptions>(
tree,
executor,
(_options, projectName, targetName, configurationName) => {
const projectConfiguration = readProjectConfiguration(
tree,
projectName
);
const configToUpdate: ServerBuilderOptions = configurationName
? projectConfiguration.targets[targetName].configurations[
configurationName
]
: projectConfiguration.targets[targetName].options;
if (
configToUpdate.buildOptimizer === undefined &&
configToUpdate.optimization !== undefined
) {
configToUpdate.buildOptimizer = !!configToUpdate.optimization;
}
updateProjectConfiguration(tree, projectName, projectConfiguration);
}
);
});
await formatFiles(tree);
}

View File

@ -0,0 +1,30 @@
import type {
ProjectConfiguration,
ProjectGraphProjectNode,
Tree,
} from '@nx/devkit';
import { createProjectGraphAsync, readProjectConfiguration } from '@nx/devkit';
export async function getProjectsFilteredByDependencies(
tree: Tree,
dependencies: string[]
): Promise<
Array<{
project: ProjectConfiguration;
graphNode: ProjectGraphProjectNode;
}>
> {
const projectGraph = await createProjectGraphAsync();
return Object.entries(projectGraph.dependencies)
.filter(([node, dep]) =>
dep.some(
({ target }) =>
!projectGraph.externalNodes?.[node] && dependencies.includes(target)
)
)
.map(([projectName]) => ({
project: readProjectConfiguration(tree, projectName),
graphNode: projectGraph.nodes[projectName],
}));
}

View File

@ -1,6 +1,6 @@
import * as latestVersions from './versions'; import * as latestVersions from './versions';
type SupportedVersions = 'angularV14'; type SupportedVersions = 'angularV14' | 'angularV15';
export type PackageVersionNames = Exclude< export type PackageVersionNames = Exclude<
keyof typeof latestVersions, keyof typeof latestVersions,
'nxVersion' 'nxVersion'
@ -16,7 +16,7 @@ export const backwardCompatibleVersions: Record<
angularDevkitVersion: '~14.2.0', angularDevkitVersion: '~14.2.0',
ngPackagrVersion: '~14.2.0', ngPackagrVersion: '~14.2.0',
ngrxVersion: '~14.0.0', ngrxVersion: '~14.0.0',
rxjsVersion: '~7.5.0', rxjsVersion: '~7.8.0',
zoneJsVersion: '~0.11.4', zoneJsVersion: '~0.11.4',
angularJsVersion: '1.7.9', angularJsVersion: '1.7.9',
tsLibVersion: '^2.3.0', tsLibVersion: '^2.3.0',
@ -38,4 +38,31 @@ export const backwardCompatibleVersions: Record<
typesNodeVersion: '16.11.7', typesNodeVersion: '16.11.7',
jasmineMarblesVersion: '^0.9.2', jasmineMarblesVersion: '^0.9.2',
}, },
angularV15: {
angularVersion: '~15.2.0',
angularDevkitVersion: '~15.2.0',
ngPackagrVersion: '~15.2.2',
ngrxVersion: '~15.3.0',
rxjsVersion: '~7.8.0',
zoneJsVersion: '~0.12.0',
angularJsVersion: '1.7.9',
tsLibVersion: '^2.3.0',
ngUniversalVersion: '~15.1.0',
corsVersion: '~2.8.5',
typesCorsVersion: '~2.8.5',
expressVersion: '~4.18.2',
typesExpressVersion: '4.17.14',
moduleFederationNodeVersion: '~0.10.1',
angularEslintVersion: '~15.0.0',
tailwindVersion: '^3.0.2',
postcssVersion: '^8.4.5',
postcssImportVersion: '~14.1.0',
postcssPresetEnvVersion: '~7.5.0',
postcssUrlVersion: '~10.1.3',
autoprefixerVersion: '^10.4.0',
tsNodeVersion: '10.9.1',
jestPresetAngularVersion: '~13.0.0',
typesNodeVersion: '16.11.7',
jasmineMarblesVersion: '^0.9.2',
},
}; };

View File

@ -21,6 +21,10 @@ export class FileChangeRecorder {
this.tree.write(this.filePath, this.mutableContent.toString()); this.tree.write(this.filePath, this.mutableContent.toString());
} }
hasChanged(): boolean {
return this.mutableContent.hasChanged();
}
insertLeft(index: number, content: string): void { insertLeft(index: number, content: string): void {
this.mutableContent.appendLeft(index, content); this.mutableContent.appendLeft(index, content);
} }

View File

@ -3,6 +3,7 @@ import {
addImportToDirective, addImportToDirective,
addImportToModule, addImportToModule,
addImportToPipe, addImportToPipe,
addProviderToAppConfig,
addProviderToBootstrapApplication, addProviderToBootstrapApplication,
isStandalone, isStandalone,
} from './ast-utils'; } from './ast-utils';
@ -302,4 +303,35 @@ bootstrapApplication(AppComponent, {
}).catch((err) => console.error(err));" }).catch((err) => console.error(err));"
`); `);
}); });
it('should add a provider to the appConfig', () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
tree.write(
'app.config.ts',
`import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes) ]
};`
);
// ACT
addProviderToAppConfig(tree, 'app.config.ts', 'provideStore()');
// ASSERT
expect(tree.read('app.config.ts', 'utf-8')).toMatchInlineSnapshot(`
"import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideStore(),provideRouter(routes) ]
};"
`);
});
}); });

View File

@ -1,6 +1,6 @@
import type * as ts from 'typescript'; import type * as ts from 'typescript';
import { findNodes } from '@nx/js';
import { import {
findNodes,
getImport, getImport,
getSourceNodes, getSourceNodes,
insertChange, insertChange,
@ -676,6 +676,28 @@ function getListOfRoutes(
return null; return null;
} }
export function isNgStandaloneApp(tree: Tree, projectName: string) {
const project = readProjectConfiguration(tree, projectName);
const mainFile = project.targets?.build?.options?.main;
if (project.projectType !== 'application' || !mainFile) {
return false;
}
ensureTypescript();
const { tsquery } = require('@phenomnomnominal/tsquery');
const mainFileContents = tree.read(mainFile, 'utf-8');
const BOOTSTRAP_APPLICATION_SELECTOR =
'CallExpression:has(Identifier[name=bootstrapApplication])';
const ast = tsquery.ast(mainFileContents);
const nodes = tsquery(ast, BOOTSTRAP_APPLICATION_SELECTOR, {
visitAllChildren: true,
});
return nodes.length > 0;
}
/** /**
* Add a provider to bootstrapApplication call for Standalone Applications * Add a provider to bootstrapApplication call for Standalone Applications
* @param tree Virtual Tree * @param tree Virtual Tree
@ -716,6 +738,47 @@ export function addProviderToBootstrapApplication(
tree.write(filePath, newFileContents); tree.write(filePath, newFileContents);
} }
/**
* Add a provider to appConfig for Standalone Applications
* NOTE: The appConfig must be marked with type ApplicationConfig and the providers must be declared as an array in the config
* @param tree Virtual Tree
* @param filePath Path to the file containing the bootstrapApplication call
* @param providerToAdd Provider to add
*/
export function addProviderToAppConfig(
tree: Tree,
filePath: string,
providerToAdd: string
) {
ensureTypescript();
const { tsquery } = require('@phenomnomnominal/tsquery');
const PROVIDERS_ARRAY_SELECTOR =
'VariableDeclaration:has(TypeReference > Identifier[name=ApplicationConfig]) > ObjectLiteralExpression PropertyAssignment:has(Identifier[name=providers]) > ArrayLiteralExpression';
const fileContents = tree.read(filePath, 'utf-8');
const ast = tsquery.ast(fileContents);
const providersArrayNodes = tsquery(ast, PROVIDERS_ARRAY_SELECTOR, {
visitAllChildren: true,
});
if (providersArrayNodes.length === 0) {
throw new Error(
`'providers' does not exist in the application configuration at '${filePath}'.`
);
}
const arrayNode = providersArrayNodes[0];
const newFileContents = `${fileContents.slice(
0,
arrayNode.getStart() + 1
)}${providerToAdd},${fileContents.slice(
arrayNode.getStart() + 1,
fileContents.length
)}`;
tree.write(filePath, newFileContents);
}
/** /**
* Add a provider to an NgModule * Add a provider to an NgModule
* @param host Virtual Tree * @param host Virtual Tree

View File

@ -5,6 +5,7 @@ export {
addImportToPipe, addImportToPipe,
addImportToModule, addImportToModule,
addProviderToBootstrapApplication, addProviderToBootstrapApplication,
addProviderToAppConfig,
addProviderToComponent, addProviderToComponent,
addProviderToModule, addProviderToModule,
} from './nx-devkit/ast-utils'; } from './nx-devkit/ast-utils';

View File

@ -1,22 +1,22 @@
export const nxVersion = require('../../package.json').version; export const nxVersion = require('../../package.json').version;
export const angularVersion = '~15.2.0'; export const angularVersion = '~16.0.0-rc.4';
export const angularDevkitVersion = '~15.2.0'; export const angularDevkitVersion = '~16.0.0-rc.4';
export const ngPackagrVersion = '~15.2.2'; export const ngPackagrVersion = '~16.0.0-rc.1';
export const ngrxVersion = '~15.3.0'; export const ngrxVersion = '~15.3.0';
export const rxjsVersion = '~7.8.0'; export const rxjsVersion = '~7.8.0';
export const zoneJsVersion = '~0.12.0'; export const zoneJsVersion = '~0.12.0';
export const angularJsVersion = '1.7.9'; export const angularJsVersion = '1.7.9';
export const tsLibVersion = '^2.3.0'; export const tsLibVersion = '^2.3.0';
export const ngUniversalVersion = '~15.1.0'; export const ngUniversalVersion = '~16.0.0-rc.2';
export const corsVersion = '~2.8.5'; export const corsVersion = '~2.8.5';
export const typesCorsVersion = '~2.8.5'; export const typesCorsVersion = '~2.8.5';
export const expressVersion = '~4.18.2'; export const expressVersion = '~4.18.2';
export const typesExpressVersion = '4.17.14'; export const typesExpressVersion = '4.17.14';
export const moduleFederationNodeVersion = '~0.10.1'; export const moduleFederationNodeVersion = '~0.10.1';
export const angularEslintVersion = '~15.0.0'; export const angularEslintVersion = '~16.0.0-alpha.1';
export const tailwindVersion = '^3.0.2'; export const tailwindVersion = '^3.0.2';
export const postcssVersion = '^8.4.5'; export const postcssVersion = '^8.4.5';
export const postcssImportVersion = '~14.1.0'; export const postcssImportVersion = '~14.1.0';

View File

@ -32,6 +32,8 @@ import { cypressInitGenerator } from '../init/init';
// app // app
import { Schema } from './schema'; import { Schema } from './schema';
import { addLinterToCyProject } from '../../utils/add-linter'; import { addLinterToCyProject } from '../../utils/add-linter';
import { checkAndCleanWithSemver } from '@nx/devkit/src/utils/semver';
import { major } from 'semver';
export interface CypressProjectSchema extends Schema { export interface CypressProjectSchema extends Schema {
projectName: string; projectName: string;
@ -84,7 +86,9 @@ function createFiles(tree: Tree, options: CypressProjectSchema) {
function addProject(tree: Tree, options: CypressProjectSchema) { function addProject(tree: Tree, options: CypressProjectSchema) {
let e2eProjectConfig: ProjectConfiguration; let e2eProjectConfig: ProjectConfiguration;
const detectedCypressVersion = installedCypressVersion() ?? cypressVersion; const detectedCypressVersion =
installedCypressVersion() ??
major(checkAndCleanWithSemver('cypress', cypressVersion));
const cypressConfig = const cypressConfig =
detectedCypressVersion < 10 ? 'cypress.json' : 'cypress.config.ts'; detectedCypressVersion < 10 ? 'cypress.json' : 'cypress.config.ts';

View File

@ -1,11 +1,20 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as ts from 'typescript';
import { readTsPathMappings } from './typescript'; import { readTsPathMappings } from './typescript';
let readConfigFileResult: any;
let parseJsonConfigFileContentResult: any;
jest.mock('typescript', () => ({
...jest.requireActual('typescript'),
readConfigFile: jest.fn().mockImplementation(() => readConfigFileResult),
parseJsonConfigFileContent: jest
.fn()
.mockImplementation(() => parseJsonConfigFileContentResult),
}));
describe('readTsPathMappings', () => { describe('readTsPathMappings', () => {
it('should normalize paths', () => { it('should normalize paths', () => {
jest.spyOn(fs, 'existsSync').mockReturnValue(true); jest.spyOn(fs, 'existsSync').mockReturnValue(true);
jest.spyOn(ts, 'readConfigFile').mockReturnValue({ readConfigFileResult = {
config: { config: {
options: { options: {
paths: { paths: {
@ -14,9 +23,8 @@ describe('readTsPathMappings', () => {
}, },
}, },
}, },
} as any); };
parseJsonConfigFileContentResult = {
jest.spyOn(ts, 'parseJsonConfigFileContent').mockReturnValue({
options: { options: {
paths: { paths: {
'@myorg/lib1': ['./libs/lib1/src/index.ts'], '@myorg/lib1': ['./libs/lib1/src/index.ts'],
@ -25,7 +33,7 @@ describe('readTsPathMappings', () => {
}, },
fileNames: [], fileNames: [],
errors: [], errors: [],
}); };
const paths = readTsPathMappings('/path/to/tsconfig.json'); const paths = readTsPathMappings('/path/to/tsconfig.json');

View File

@ -1 +1 @@
export const typescriptVersion = '~4.9.5'; export const typescriptVersion = '~5.0.2';

View File

@ -35,6 +35,7 @@
"dependencies": { "dependencies": {
"@nx/devkit": "file:../devkit", "@nx/devkit": "file:../devkit",
"@nx/js": "file:../js", "@nx/js": "file:../js",
"@typescript-eslint/type-utils": "^5.58.0",
"@typescript-eslint/utils": "^5.58.0", "@typescript-eslint/utils": "^5.58.0",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"confusing-browser-globals": "^1.0.9", "confusing-browser-globals": "^1.0.9",

View File

@ -1,13 +1,15 @@
import { import {
joinPathFragments, joinPathFragments,
logger,
ProjectGraphProjectNode, ProjectGraphProjectNode,
readJsonFile, readJsonFile,
workspaceRoot,
} from '@nx/devkit'; } from '@nx/devkit';
import { findNodes } from '@nx/js'; import { findNodes } from '@nx/js';
import { getModifiers } from '@typescript-eslint/type-utils';
import { existsSync, readFileSync } from 'fs'; import { existsSync, readFileSync } from 'fs';
import { dirname } from 'path'; import { dirname } from 'path';
import ts = require('typescript'); import ts = require('typescript');
import { logger, workspaceRoot } from '@nx/devkit';
function tryReadBaseJson() { function tryReadBaseJson() {
try { try {
@ -126,8 +128,7 @@ export function getRelativeImportPath(exportedMember, filePath, basePath) {
} }
if ( if (
parent.modifiers && getModifiers(parent)?.find(
parent.modifiers.find(
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword (modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword
) )
) { ) {

View File

@ -68,6 +68,18 @@
"alwaysAddToPackageJson": false "alwaysAddToPackageJson": false
} }
} }
},
"16.1.0": {
"version": "16.1.0-beta.0",
"x-prompt": "Do you want to update to TypeScript v5.0?",
"requires": {
"typescript": ">=4.9.5 <5.0.0"
},
"packages": {
"typescript": {
"version": "~5.0.2"
}
}
} }
} }
} }

View File

@ -8,4 +8,4 @@ export const swcHelpersVersion = '~0.5.0';
export const swcNodeVersion = '~1.4.2'; export const swcNodeVersion = '~1.4.2';
export const tsLibVersion = '^2.3.0'; export const tsLibVersion = '^2.3.0';
export const typesNodeVersion = '18.7.1'; export const typesNodeVersion = '18.7.1';
export const typescriptVersion = '~4.9.5'; export const typescriptVersion = '~5.0.2';

View File

@ -42,6 +42,35 @@
"alwaysAddToPackageJson": false "alwaysAddToPackageJson": false
} }
} }
},
"16.1.0": {
"version": "16.1.0-beta.0",
"packages": {
"@nestjs/common": {
"version": "^9.1.0",
"alwaysAddToPackageJson": false
},
"@nestjs/core": {
"version": "^9.1.0",
"alwaysAddToPackageJson": false
},
"@nestjs/platform-express": {
"version": "^9.1.0",
"alwaysAddToPackageJson": false
},
"@nestjs/schematics": {
"version": "^9.1.0",
"alwaysAddToPackageJson": false
},
"@nestjs/swagger": {
"version": "^6.3.0",
"alwaysAddToPackageJson": false
},
"@nestjs/testing": {
"version": "^9.1.0",
"alwaysAddToPackageJson": false
}
}
} }
} }
} }

View File

@ -1,6 +1,6 @@
export const nxVersion = require('../../package.json').version; export const nxVersion = require('../../package.json').version;
export const nestJsVersion = '^9.0.0'; export const nestJsVersion = '^9.1.0';
export const rxjsVersion = '^7.0.0'; export const rxjsVersion = '^7.8.0';
export const reflectMetadataVersion = '^0.1.13'; export const reflectMetadataVersion = '^0.1.13';
export const tsLibVersion = '^2.3.0'; export const tsLibVersion = '^2.3.0';

View File

@ -1 +1 @@
export const typescriptVersion = '~4.9.5'; export const typescriptVersion = '~5.0.2';

View File

@ -44,7 +44,7 @@
"rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-postcss": "^4.0.1", "rollup-plugin-postcss": "^4.0.1",
"rollup-plugin-typescript2": "0.34.1", "rollup-plugin-typescript2": "0.34.1",
"rxjs": "^6.5.4", "rxjs": "^7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"@nx/devkit": "file:../devkit", "@nx/devkit": "file:../devkit",
"@nx/js": "file:../js" "@nx/js": "file:../js"

View File

@ -142,7 +142,7 @@ export async function* rollupExecutor(
}) })
) )
), ),
scan<RollupExecutorEvent>( scan<RollupExecutorEvent, RollupExecutorEvent>(
(acc, result) => { (acc, result) => {
if (!acc.success) return acc; if (!acc.success) return acc;
return result; return result;

View File

@ -56,6 +56,114 @@
} }
}, },
"packageJsonUpdates": { "packageJsonUpdates": {
"16.1.0": {
"version": "16.1.0-beta.0",
"requires": {
"@storybook/core-server": ">=7.0.0 <7.0.8"
},
"packages": {
"@storybook/core-server": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/angular": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/react": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/web-components-vite": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/web-components-webpack5": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/builder-vite": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/builder-webpack5": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-a11y": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-actions": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-backgrounds": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-controls": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-docs": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-essentials": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-mdx-gfm": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-highlight": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-interactions": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-jest": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-links": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-measure": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-outline": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-storyshots": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-storyshots-puppeteer": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-storysource": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-toolbars": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-viewport": {
"version": "^7.0.8",
"alwaysAddToPackageJson": false
}
}
},
"16.0.0": { "16.0.0": {
"version": "16.0.0-beta.1", "version": "16.0.0-beta.1",
"packages": { "packages": {

View File

@ -17,7 +17,7 @@ exports[`@nx/storybook:init dependencies for package.json should add angular rel
"existing": "1.0.0", "existing": "1.0.0",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.5.0",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"typescript": "~4.9.5", "typescript": "~5.0.2",
"webpack": "^5.64.0", "webpack": "^5.64.0",
}, },
"name": "test-name", "name": "test-name",

Some files were not shown because too many files have changed in this diff Show More