From 358f85e4d2d1362d27b4f5ce326303bbba3ee8af Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 23 Aug 2024 10:58:30 +0000 Subject: [PATCH 01/11] build: update dependency webpack to v5.94.0 --- package.json | 2 +- .../angular_devkit/build_angular/package.json | 2 +- .../angular_devkit/build_webpack/package.json | 2 +- packages/ngtools/webpack/package.json | 2 +- packages/ngtools/webpack/src/ivy/plugin.ts | 6 ++- yarn.lock | 43 +++++-------------- 6 files changed, 20 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index 6c8746966682..b1623b0d07fd 100644 --- a/package.json +++ b/package.json @@ -203,7 +203,7 @@ "verdaccio-auth-memory": "^10.0.0", "vite": "5.4.2", "watchpack": "2.4.2", - "webpack": "5.93.0", + "webpack": "5.94.0", "webpack-dev-middleware": "7.4.2", "webpack-dev-server": "5.0.4", "webpack-merge": "6.0.1", diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json index fa2f07be9769..d15c14cc23b9 100644 --- a/packages/angular_devkit/build_angular/package.json +++ b/packages/angular_devkit/build_angular/package.json @@ -63,7 +63,7 @@ "tslib": "2.6.3", "vite": "5.4.2", "watchpack": "2.4.2", - "webpack": "5.93.0", + "webpack": "5.94.0", "webpack-dev-middleware": "7.4.2", "webpack-dev-server": "5.0.4", "webpack-merge": "6.0.1", diff --git a/packages/angular_devkit/build_webpack/package.json b/packages/angular_devkit/build_webpack/package.json index f5f9a480a570..e51d17edfcfc 100644 --- a/packages/angular_devkit/build_webpack/package.json +++ b/packages/angular_devkit/build_webpack/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "@angular-devkit/core": "0.0.0-PLACEHOLDER", - "webpack": "5.93.0" + "webpack": "5.94.0" }, "peerDependencies": { "webpack": "^5.30.0", diff --git a/packages/ngtools/webpack/package.json b/packages/ngtools/webpack/package.json index dedf1668aaeb..6c3381585059 100644 --- a/packages/ngtools/webpack/package.json +++ b/packages/ngtools/webpack/package.json @@ -30,6 +30,6 @@ "@angular/compiler": "19.0.0-next.1", "@angular/compiler-cli": "19.0.0-next.1", "typescript": "5.5.4", - "webpack": "5.93.0" + "webpack": "5.94.0" } } diff --git a/packages/ngtools/webpack/src/ivy/plugin.ts b/packages/ngtools/webpack/src/ivy/plugin.ts index 2cc02f938027..f6b957264fd6 100644 --- a/packages/ngtools/webpack/src/ivy/plugin.ts +++ b/packages/ngtools/webpack/src/ivy/plugin.ts @@ -324,7 +324,11 @@ export class AngularWebpackPlugin { compilationFileEmitters.set(compilation, fileEmitters); compilation.compiler.webpack.NormalModule.getCompilationHooks(compilation).loader.tap( PLUGIN_NAME, - (loaderContext: { [AngularPluginSymbol]?: FileEmitterCollection }) => { + (context) => { + const loaderContext = context as typeof context & { + [AngularPluginSymbol]?: FileEmitterCollection; + }; + loaderContext[AngularPluginSymbol] = fileEmitters; }, ); diff --git a/yarn.lock b/yarn.lock index 154cda54a554..78d3301b868d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -118,7 +118,7 @@ __metadata: undici: "npm:6.19.8" vite: "npm:5.4.2" watchpack: "npm:2.4.2" - webpack: "npm:5.93.0" + webpack: "npm:5.94.0" webpack-dev-middleware: "npm:7.4.2" webpack-dev-server: "npm:5.0.4" webpack-merge: "npm:6.0.1" @@ -176,7 +176,7 @@ __metadata: "@angular-devkit/architect": "npm:0.0.0-EXPERIMENTAL-PLACEHOLDER" "@angular-devkit/core": "npm:0.0.0-PLACEHOLDER" rxjs: "npm:7.8.1" - webpack: "npm:5.93.0" + webpack: "npm:5.94.0" peerDependencies: webpack: ^5.30.0 webpack-dev-server: ^5.0.2 @@ -779,7 +779,7 @@ __metadata: verdaccio-auth-memory: "npm:^10.0.0" vite: "npm:5.4.2" watchpack: "npm:2.4.2" - webpack: "npm:5.93.0" + webpack: "npm:5.94.0" webpack-dev-middleware: "npm:7.4.2" webpack-dev-server: "npm:5.0.4" webpack-merge: "npm:6.0.1" @@ -3697,7 +3697,7 @@ __metadata: "@angular/compiler": "npm:19.0.0-next.1" "@angular/compiler-cli": "npm:19.0.0-next.1" typescript: "npm:5.5.4" - webpack: "npm:5.93.0" + webpack: "npm:5.94.0" peerDependencies: "@angular/compiler-cli": ^19.0.0-next.0 typescript: ">=5.4 <5.6" @@ -4866,26 +4866,6 @@ __metadata: languageName: node linkType: hard -"@types/eslint-scope@npm:^3.7.3": - version: 3.7.7 - resolution: "@types/eslint-scope@npm:3.7.7" - dependencies: - "@types/eslint": "npm:*" - "@types/estree": "npm:*" - checksum: 10c0/a0ecbdf2f03912679440550817ff77ef39a30fa8bfdacaf6372b88b1f931828aec392f52283240f0d648cf3055c5ddc564544a626bcf245f3d09fcb099ebe3cc - languageName: node - linkType: hard - -"@types/eslint@npm:*": - version: 9.6.0 - resolution: "@types/eslint@npm:9.6.0" - dependencies: - "@types/estree": "npm:*" - "@types/json-schema": "npm:*" - checksum: 10c0/69301356bc73b85e381ae00931291de2e96d1cc49a112c592c74ee32b2f85412203dea6a333b4315fd9839bb14f364f265cbfe7743fc5a78492ee0326dd6a2c1 - languageName: node - linkType: hard - "@types/estree@npm:*, @types/estree@npm:1.0.5, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.5": version: 1.0.5 resolution: "@types/estree@npm:1.0.5" @@ -5012,7 +4992,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": +"@types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db @@ -8821,7 +8801,7 @@ __metadata: languageName: node linkType: hard -"enhanced-resolve@npm:^5.17.0": +"enhanced-resolve@npm:^5.17.1": version: 5.17.1 resolution: "enhanced-resolve@npm:5.17.1" dependencies: @@ -18139,11 +18119,10 @@ __metadata: languageName: node linkType: hard -"webpack@npm:5.93.0": - version: 5.93.0 - resolution: "webpack@npm:5.93.0" +"webpack@npm:5.94.0": + version: 5.94.0 + resolution: "webpack@npm:5.94.0" dependencies: - "@types/eslint-scope": "npm:^3.7.3" "@types/estree": "npm:^1.0.5" "@webassemblyjs/ast": "npm:^1.12.1" "@webassemblyjs/wasm-edit": "npm:^1.12.1" @@ -18152,7 +18131,7 @@ __metadata: acorn-import-attributes: "npm:^1.9.5" browserslist: "npm:^4.21.10" chrome-trace-event: "npm:^1.0.2" - enhanced-resolve: "npm:^5.17.0" + enhanced-resolve: "npm:^5.17.1" es-module-lexer: "npm:^1.2.1" eslint-scope: "npm:5.1.1" events: "npm:^3.2.0" @@ -18172,7 +18151,7 @@ __metadata: optional: true bin: webpack: bin/webpack.js - checksum: 10c0/f0c72f1325ff57a4cc461bb978e6e1296f2a7d45c9765965271aa686ccdd448512956f4d7fdcf8c164d073af046c5a0aba17ce85ea98e33e5e2bfbfe13aa5808 + checksum: 10c0/b4d1b751f634079bd177a89eef84d80fa5bb8d6fc15d72ab40fc2b9ca5167a79b56585e1a849e9e27e259803ee5c4365cb719e54af70a43c06358ec268ff4ebf languageName: node linkType: hard From bcbbee32fd3250dbf196f2344c250ca08f2243d1 Mon Sep 17 00:00:00 2001 From: Angular Robot Date: Fri, 23 Aug 2024 11:15:04 +0000 Subject: [PATCH 02/11] build: update angular --- tests/legacy-cli/e2e/ng-snapshot/package.json | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/legacy-cli/e2e/ng-snapshot/package.json b/tests/legacy-cli/e2e/ng-snapshot/package.json index 001ac6f694a6..788fe248c00e 100644 --- a/tests/legacy-cli/e2e/ng-snapshot/package.json +++ b/tests/legacy-cli/e2e/ng-snapshot/package.json @@ -2,21 +2,21 @@ "description": "snapshot versions of Angular for e2e testing", "private": true, "dependencies": { - "@angular/animations": "github:angular/animations-builds#fafe74153fa09685f1c1cef33f574061bb4d4f01", - "@angular/cdk": "github:angular/cdk-builds#af84c0fb5f4f4bc3f0c5223928190ed1717176b8", - "@angular/common": "github:angular/common-builds#7527cab8003fb9778c12ebdc0dbc474a3f36835e", - "@angular/compiler": "github:angular/compiler-builds#e7ef7eaa0c84eb0bcb420290f05cad9e24a120ed", - "@angular/compiler-cli": "github:angular/compiler-cli-builds#26a7998ee03d10234270902deed60c205d23b31f", - "@angular/core": "github:angular/core-builds#447d1b69be44f231973ff589481862b01800c53a", - "@angular/forms": "github:angular/forms-builds#e5ee85d7fcd86eafdcdea40676f748eadc18739c", - "@angular/language-service": "github:angular/language-service-builds#b16480cec0c566853f413ebb8ef048d373cf834e", - "@angular/localize": "github:angular/localize-builds#2edfad144603b442512caf55f1c23d3539b346f4", - "@angular/material": "github:angular/material-builds#9404bb90ab7ea077765cf233426e377558a23d91", - "@angular/material-moment-adapter": "github:angular/material-moment-adapter-builds#a3be09b120e8a61d5c0c68fbd7c2690952eb3e3b", - "@angular/platform-browser": "github:angular/platform-browser-builds#e5d2793029693fad0f415bd8a49db4f28b8489e5", - "@angular/platform-browser-dynamic": "github:angular/platform-browser-dynamic-builds#7c39d66326aa3b4cf3ea267531bf79d1d708378a", - "@angular/platform-server": "github:angular/platform-server-builds#a3f60aba6d6ec5b0f4f144e4ceeba36126868aa4", - "@angular/router": "github:angular/router-builds#e56c078722daf0f70e003c5a673f3383293f236b", - "@angular/service-worker": "github:angular/service-worker-builds#d2b19cb047f4c1b63e6e0ae85460e3df61eccc97" + "@angular/animations": "github:angular/animations-builds#f786bf3c6d7083df17782d8f21b2b940ff2b7644", + "@angular/cdk": "github:angular/cdk-builds#3e46ea5a40c6430cf06403600531198ec9eeb926", + "@angular/common": "github:angular/common-builds#70d127892ded3140febee71b255663b853bc991a", + "@angular/compiler": "github:angular/compiler-builds#a140f4e1045d3561bfd142bb2f036c29da3edd0c", + "@angular/compiler-cli": "github:angular/compiler-cli-builds#5067490cfeb052463d7fa3e4fc784697f1551c30", + "@angular/core": "github:angular/core-builds#3adc404180df46fa95af113ec1e4f73ae376e5c7", + "@angular/forms": "github:angular/forms-builds#12059b94c3996801a5387f192b69c2de955c25aa", + "@angular/language-service": "github:angular/language-service-builds#f974afb7e7e2a9511f271b1c127ea5077a84f61a", + "@angular/localize": "github:angular/localize-builds#b9eda03408794a184669271f159a0a54599cbb21", + "@angular/material": "github:angular/material-builds#0534afc370872185b96febac6697f4cbe37f8de5", + "@angular/material-moment-adapter": "github:angular/material-moment-adapter-builds#138c0430d1aa27c60990950fe9db717277ade17b", + "@angular/platform-browser": "github:angular/platform-browser-builds#f36a1df568da88e8f6a57540df1e6de2931d7f25", + "@angular/platform-browser-dynamic": "github:angular/platform-browser-dynamic-builds#5efc8c309ab9809fc34fa23d196a237901316a1e", + "@angular/platform-server": "github:angular/platform-server-builds#64816d507a89405021d017e2f3517bae09320169", + "@angular/router": "github:angular/router-builds#61bc1cdb76f1e8c9101f3a16b2c9bf7bf62fbc70", + "@angular/service-worker": "github:angular/service-worker-builds#e5e0b41230fb02e9c5a69ab7ec1ce07fc869088a" } } From ac102aa9c6300a7612a272c49b77a7580475391d Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 23 Aug 2024 18:50:14 +0000 Subject: [PATCH 03/11] ci: run modules and packages tests using a single bazel invocation Optimize Bazel build with unified module and package tests --- .github/workflows/ci.yml | 6 ++---- .github/workflows/pr.yml | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 550fe062bc02..455fee49e4ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,10 +63,8 @@ jobs: uses: angular/dev-infra/github-actions/bazel/configure-remote@8eed650ff101cb2be3949febba829b722e89ff80 - name: Install node modules run: yarn install --immutable - - name: Run module tests - run: yarn bazel test //modules/... - - name: Run package tests - run: yarn bazel test //packages/... + - name: Run module and package tests + run: yarn bazel test //modules/... //packages/... e2e: strategy: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 7c9e136a3c64..e8d74b4c4b2b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -97,10 +97,8 @@ jobs: uses: angular/dev-infra/github-actions/bazel/configure-remote@8eed650ff101cb2be3949febba829b722e89ff80 - name: Install node modules run: yarn install --immutable - - name: Run module tests - run: yarn bazel test //modules/... - - name: Run package tests - run: yarn bazel test //packages/... + - name: Run module and package tests + run: yarn bazel test //modules/... //packages/... e2e: strategy: From a381a3db187f7b20e5ec8d1e1a1f1bd860426fcd Mon Sep 17 00:00:00 2001 From: aparzi Date: Fri, 23 Aug 2024 23:22:29 +0200 Subject: [PATCH 04/11] feat(@schematics/angular): add option to export component as default Introduces option `--export-default` to control whether the generated component uses a default export instead of a named export. Closes: #25023 --- ...me@dasherize__.__type@dasherize__.ts.template | 2 +- .../schematics/angular/component/index_spec.ts | 16 ++++++++++++++++ .../schematics/angular/component/schema.json | 5 +++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/schematics/angular/component/files/__name@dasherize@if-flat__/__name@dasherize__.__type@dasherize__.ts.template b/packages/schematics/angular/component/files/__name@dasherize@if-flat__/__name@dasherize__.__type@dasherize__.ts.template index 21c884bd80e8..4a420ef9a866 100644 --- a/packages/schematics/angular/component/files/__name@dasherize@if-flat__/__name@dasherize__.__type@dasherize__.ts.template +++ b/packages/schematics/angular/component/files/__name@dasherize@if-flat__/__name@dasherize__.__type@dasherize__.ts.template @@ -19,6 +19,6 @@ import { <% if(changeDetection !== 'Default') { %>ChangeDetectionStrategy, <% }% encapsulation: ViewEncapsulation.<%= viewEncapsulation %><% } if (changeDetection !== 'Default') { %>, changeDetection: ChangeDetectionStrategy.<%= changeDetection %><% } %> }) -export class <%= classify(name) %><%= classify(type) %> { +export <% if(exportDefault) {%>default <%}%>class <%= classify(name) %><%= classify(type) %> { } diff --git a/packages/schematics/angular/component/index_spec.ts b/packages/schematics/angular/component/index_spec.ts index ee1d51c3c1a0..aa54f1b9ec99 100644 --- a/packages/schematics/angular/component/index_spec.ts +++ b/packages/schematics/angular/component/index_spec.ts @@ -496,4 +496,20 @@ describe('Component Schematic', () => { await expectAsync(schematicRunner.runSchematic('component', options, appTree)).toBeRejected(); }); }); + + it('should export the component as default when exportDefault is true', async () => { + const options = { ...defaultOptions, exportDefault: true }; + + const tree = await schematicRunner.runSchematic('component', options, appTree); + const tsContent = tree.readContent('/projects/bar/src/app/foo/foo.component.ts'); + expect(tsContent).toContain('export default class FooComponent'); + }); + + it('should export the component as a named export when exportDefault is false', async () => { + const options = { ...defaultOptions, exportDefault: false }; + + const tree = await schematicRunner.runSchematic('component', options, appTree); + const tsContent = tree.readContent('/projects/bar/src/app/foo/foo.component.ts'); + expect(tsContent).toContain('export class FooComponent'); + }); }); diff --git a/packages/schematics/angular/component/schema.json b/packages/schematics/angular/component/schema.json index e2e3914b41b9..9b95d4f1b8f3 100644 --- a/packages/schematics/angular/component/schema.json +++ b/packages/schematics/angular/component/schema.json @@ -130,6 +130,11 @@ "type": "boolean", "default": false, "description": "The declaring NgModule exports this component." + }, + "exportDefault": { + "type": "boolean", + "default": false, + "description": "Use default export for the component instead of a named export." } }, "required": ["name", "project"] From 607a97cdeb8fc73451ef954c2478d63e98210fbc Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Thu, 22 Aug 2024 18:38:07 +0000 Subject: [PATCH 05/11] refactor(@angular/ssr): bundle Critters This commit bundles the Critters library to ensure compatibility with Nodeless environments. Additionally, all licenses for bundled libraries, including Critters, are now included in the package. This helps maintain compliance with open-source license requirements. --- package.json | 1 + packages/angular/build/BUILD.bazel | 1 + packages/angular/build/src/typings.d.ts | 19 ++ packages/angular/ssr/BUILD.bazel | 11 +- packages/angular/ssr/package.json | 1 - packages/angular/ssr/src/app.ts | 42 ++- .../ssr/src/common-engine/common-engine.ts | 29 +- .../src/common-engine/inline-css-processor.ts | 260 +----------------- .../ssr/src/utils/inline-critical-css.ts | 214 ++++++++++++++ .../ssr/third_party/critters/BUILD.bazel | 55 ++++ .../third_party/critters/esbuild.config.mjs | 197 +++++++++++++ .../ssr/third_party/critters/index.d.ts | 9 + scripts/build.mts | 4 +- tools/defaults.bzl | 23 +- yarn.lock | 45 ++- 15 files changed, 619 insertions(+), 292 deletions(-) create mode 100644 packages/angular/build/src/typings.d.ts create mode 100644 packages/angular/ssr/src/utils/inline-critical-css.ts create mode 100644 packages/angular/ssr/third_party/critters/BUILD.bazel create mode 100644 packages/angular/ssr/third_party/critters/esbuild.config.mjs create mode 100644 packages/angular/ssr/third_party/critters/index.d.ts diff --git a/package.json b/package.json index b1623b0d07fd..d3aef362d9fe 100644 --- a/package.json +++ b/package.json @@ -199,6 +199,7 @@ "tslib": "2.6.3", "typescript": "5.5.4", "undici": "6.19.8", + "unenv": "^1.10.0", "verdaccio": "5.32.1", "verdaccio-auth-memory": "^10.0.0", "vite": "5.4.2", diff --git a/packages/angular/build/BUILD.bazel b/packages/angular/build/BUILD.bazel index 49481ff36e87..b60ab7d83299 100644 --- a/packages/angular/build/BUILD.bazel +++ b/packages/angular/build/BUILD.bazel @@ -79,6 +79,7 @@ ts_library( "@npm//browserslist", "@npm//critters", "@npm//esbuild", + "@npm//esbuild-wasm", "@npm//fast-glob", "@npm//https-proxy-agent", "@npm//listr2", diff --git a/packages/angular/build/src/typings.d.ts b/packages/angular/build/src/typings.d.ts new file mode 100644 index 000000000000..c784b3c4220a --- /dev/null +++ b/packages/angular/build/src/typings.d.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +// The `bundled_critters` causes issues with module mappings in Bazel, +// leading to unexpected behavior with esbuild. Specifically, the problem occurs +// when esbuild resolves to a different module or version than expected, due to +// how Bazel handles module mappings. +// +// This change aims to resolve esbuild types correctly and maintain consistency +// in the Bazel build process. + +declare module 'esbuild' { + export * from 'esbuild-wasm'; +} diff --git a/packages/angular/ssr/BUILD.bazel b/packages/angular/ssr/BUILD.bazel index dbd1662aee0f..0d0dea781e32 100644 --- a/packages/angular/ssr/BUILD.bazel +++ b/packages/angular/ssr/BUILD.bazel @@ -18,12 +18,12 @@ ts_library( ), module_name = "@angular/ssr", deps = [ + "//packages/angular/ssr/third_party/critters:bundled_critters_lib", "@npm//@angular/common", "@npm//@angular/core", "@npm//@angular/platform-server", "@npm//@angular/router", "@npm//@types/node", - "@npm//critters", ], ) @@ -32,8 +32,15 @@ ng_package( package_name = "@angular/ssr", srcs = [ ":package.json", + "//packages/angular/ssr/third_party/critters:bundled_critters_lib", + ], + externals = [ + "express", + "../../third_party/critters", + ], + nested_packages = [ + "//packages/angular/ssr/schematics:npm_package", ], - nested_packages = ["//packages/angular/ssr/schematics:npm_package"], tags = ["release-package"], deps = [ ":ssr", diff --git a/packages/angular/ssr/package.json b/packages/angular/ssr/package.json index 608de72116e1..e449d89cb981 100644 --- a/packages/angular/ssr/package.json +++ b/packages/angular/ssr/package.json @@ -13,7 +13,6 @@ "save": "dependencies" }, "dependencies": { - "critters": "0.0.24", "tslib": "^2.3.0" }, "peerDependencies": { diff --git a/packages/angular/ssr/src/app.ts b/packages/angular/ssr/src/app.ts index f8bc12fa17f0..bc116876aef3 100644 --- a/packages/angular/ssr/src/app.ts +++ b/packages/angular/ssr/src/app.ts @@ -14,6 +14,7 @@ import { Hooks } from './hooks'; import { getAngularAppManifest } from './manifest'; import { ServerRouter } from './routes/router'; import { REQUEST, REQUEST_CONTEXT, RESPONSE_INIT } from './tokens'; +import { InlineCriticalCssProcessor } from './utils/inline-critical-css'; import { renderAngular } from './utils/ng'; /** @@ -52,6 +53,11 @@ export class AngularServerApp { */ private router: ServerRouter | undefined; + /** + * The `inlineCriticalCssProcessor` is responsible for handling critical CSS inlining. + */ + private inlineCriticalCssProcessor: InlineCriticalCssProcessor | undefined; + /** * Renders a response for the given HTTP request using the server application. * @@ -177,10 +183,38 @@ export class AngularServerApp { html = await hooks.run('html:transform:pre', { html }); } - return new Response( - await renderAngular(html, manifest.bootstrap(), new URL(request.url), platformProviders), - responseInit, - ); + html = await renderAngular(html, manifest.bootstrap(), new URL(request.url), platformProviders); + + if (manifest.inlineCriticalCss) { + // Optionally inline critical CSS. + const inlineCriticalCssProcessor = this.getOrCreateInlineCssProcessor(); + html = await inlineCriticalCssProcessor.process(html); + } + + return new Response(html, responseInit); + } + + /** + * Retrieves or creates the inline critical CSS processor. + * If one does not exist, it initializes a new instance. + * + * @returns The inline critical CSS processor instance. + */ + private getOrCreateInlineCssProcessor(): InlineCriticalCssProcessor { + let inlineCriticalCssProcessor = this.inlineCriticalCssProcessor; + + if (!inlineCriticalCssProcessor) { + inlineCriticalCssProcessor = new InlineCriticalCssProcessor(); + inlineCriticalCssProcessor.readFile = (path: string) => { + const fileName = path.split('/').pop() ?? path; + + return this.assets.getServerAsset(fileName); + }; + + this.inlineCriticalCssProcessor = inlineCriticalCssProcessor; + } + + return inlineCriticalCssProcessor; } } diff --git a/packages/angular/ssr/src/common-engine/common-engine.ts b/packages/angular/ssr/src/common-engine/common-engine.ts index 02a7ba6c55a6..acbfb277cc1e 100644 --- a/packages/angular/ssr/src/common-engine/common-engine.ts +++ b/packages/angular/ssr/src/common-engine/common-engine.ts @@ -11,7 +11,7 @@ import { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/plat import * as fs from 'node:fs'; import { dirname, join, normalize, resolve } from 'node:path'; import { URL } from 'node:url'; -import { InlineCriticalCssProcessor, InlineCriticalCssResult } from './inline-css-processor'; +import { CommonEngineInlineCriticalCssProcessor } from './inline-css-processor'; import { noopRunMethodAndMeasurePerf, printPerformanceLogs, @@ -55,14 +55,10 @@ export interface CommonEngineRenderOptions { export class CommonEngine { private readonly templateCache = new Map(); - private readonly inlineCriticalCssProcessor: InlineCriticalCssProcessor; + private readonly inlineCriticalCssProcessor = new CommonEngineInlineCriticalCssProcessor(); private readonly pageIsSSG = new Map(); - constructor(private options?: CommonEngineOptions) { - this.inlineCriticalCssProcessor = new InlineCriticalCssProcessor({ - minify: false, - }); - } + constructor(private options?: CommonEngineOptions) {} /** * Render an HTML document for a specific URL with specified @@ -81,17 +77,12 @@ export class CommonEngine { html = await runMethod('Render Page', () => this.renderApplication(opts)); if (opts.inlineCriticalCss !== false) { - const { content, errors, warnings } = await runMethod('Inline Critical CSS', () => + const content = await runMethod('Inline Critical CSS', () => // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.inlineCriticalCss(html!, opts), ); html = content; - - // eslint-disable-next-line no-console - warnings?.forEach((m) => console.warn(m)); - // eslint-disable-next-line no-console - errors?.forEach((m) => console.error(m)); } } @@ -102,13 +93,11 @@ export class CommonEngine { return html; } - private inlineCriticalCss( - html: string, - opts: CommonEngineRenderOptions, - ): Promise { - return this.inlineCriticalCssProcessor.process(html, { - outputPath: opts.publicPath ?? (opts.documentFilePath ? dirname(opts.documentFilePath) : ''), - }); + private inlineCriticalCss(html: string, opts: CommonEngineRenderOptions): Promise { + const outputPath = + opts.publicPath ?? (opts.documentFilePath ? dirname(opts.documentFilePath) : ''); + + return this.inlineCriticalCssProcessor.process(html, outputPath); } private async retrieveSSGPage(opts: CommonEngineRenderOptions): Promise { diff --git a/packages/angular/ssr/src/common-engine/inline-css-processor.ts b/packages/angular/ssr/src/common-engine/inline-css-processor.ts index 9664621f02d3..e19a522ec401 100644 --- a/packages/angular/ssr/src/common-engine/inline-css-processor.ts +++ b/packages/angular/ssr/src/common-engine/inline-css-processor.ts @@ -6,258 +6,24 @@ * found in the LICENSE file at https://angular.dev/license */ -import Critters from 'critters'; import { readFile } from 'node:fs/promises'; +import { InlineCriticalCssProcessor } from '../utils/inline-critical-css'; -/** - * Pattern used to extract the media query set by Critters in an `onload` handler. - */ -const MEDIA_SET_HANDLER_PATTERN = /^this\.media=["'](.*)["'];?$/; - -/** - * Name of the attribute used to save the Critters media query so it can be re-assigned on load. - */ -const CSP_MEDIA_ATTR = 'ngCspMedia'; - -/** - * Script text used to change the media value of the link tags. - * - * NOTE: - * We do not use `document.querySelectorAll('link').forEach((s) => s.addEventListener('load', ...)` - * because this does not always fire on Chome. - * See: https://github.com/angular/angular-cli/issues/26932 and https://crbug.com/1521256 - */ -const LINK_LOAD_SCRIPT_CONTENT = [ - '(() => {', - ` const CSP_MEDIA_ATTR = '${CSP_MEDIA_ATTR}';`, - ' const documentElement = document.documentElement;', - ' const listener = (e) => {', - ' const target = e.target;', - ` if (!target || target.tagName !== 'LINK' || !target.hasAttribute(CSP_MEDIA_ATTR)) {`, - ' return;', - ' }', - - ' target.media = target.getAttribute(CSP_MEDIA_ATTR);', - ' target.removeAttribute(CSP_MEDIA_ATTR);', - - // Remove onload listener when there are no longer styles that need to be loaded. - ' if (!document.head.querySelector(`link[${CSP_MEDIA_ATTR}]`)) {', - ` documentElement.removeEventListener('load', listener);`, - ' }', - ' };', - - // We use an event with capturing (the true parameter) because load events don't bubble. - ` documentElement.addEventListener('load', listener, true);`, - '})();', -].join('\n'); - -export interface InlineCriticalCssProcessOptions { - outputPath?: string; -} - -export interface InlineCriticalCssProcessorOptions { - minify?: boolean; - deployUrl?: string; -} - -export interface InlineCriticalCssResult { - content: string; - warnings?: string[]; - errors?: string[]; -} - -/** Partial representation of an `HTMLElement`. */ -interface PartialHTMLElement { - getAttribute(name: string): string | null; - setAttribute(name: string, value: string): void; - hasAttribute(name: string): boolean; - removeAttribute(name: string): void; - appendChild(child: PartialHTMLElement): void; - insertBefore(newNode: PartialHTMLElement, referenceNode?: PartialHTMLElement): void; - remove(): void; - name: string; - textContent: string; - tagName: string | null; - children: PartialHTMLElement[]; - next: PartialHTMLElement | null; - prev: PartialHTMLElement | null; -} - -/** Partial representation of an HTML `Document`. */ -interface PartialDocument { - head: PartialHTMLElement; - createElement(tagName: string): PartialHTMLElement; - querySelector(selector: string): PartialHTMLElement | null; -} - -/** Signature of the `Critters.embedLinkedStylesheet` method. */ -type EmbedLinkedStylesheetFn = ( - link: PartialHTMLElement, - document: PartialDocument, -) => Promise; - -class CrittersExtended extends Critters { - readonly warnings: string[] = []; - readonly errors: string[] = []; - private initialEmbedLinkedStylesheet: EmbedLinkedStylesheetFn; - private addedCspScriptsDocuments = new WeakSet(); - private documentNonces = new WeakMap(); - - // Inherited from `Critters`, but not exposed in the typings. - protected declare embedLinkedStylesheet: EmbedLinkedStylesheetFn; - - constructor( - readonly optionsExtended: InlineCriticalCssProcessorOptions & InlineCriticalCssProcessOptions, - private readonly resourceCache: Map, - ) { - super({ - logger: { - warn: (s: string) => this.warnings.push(s), - error: (s: string) => this.errors.push(s), - info: () => {}, - }, - logLevel: 'warn', - path: optionsExtended.outputPath, - publicPath: optionsExtended.deployUrl, - compress: !!optionsExtended.minify, - pruneSource: false, - reduceInlineStyles: false, - mergeStylesheets: false, - // Note: if `preload` changes to anything other than `media`, the logic in - // `embedLinkedStylesheetOverride` will have to be updated. - preload: 'media', - noscriptFallback: true, - inlineFonts: true, - }); - - // We can't use inheritance to override `embedLinkedStylesheet`, because it's not declared in - // the `Critters` .d.ts which means that we can't call the `super` implementation. TS doesn't - // allow for `super` to be cast to a different type. - this.initialEmbedLinkedStylesheet = this.embedLinkedStylesheet; - this.embedLinkedStylesheet = this.embedLinkedStylesheetOverride; - } - - public override async readFile(path: string): Promise { - let resourceContent = this.resourceCache.get(path); - if (resourceContent === undefined) { - resourceContent = await readFile(path, 'utf-8'); - this.resourceCache.set(path, resourceContent); - } - - return resourceContent; - } - - /** - * Override of the Critters `embedLinkedStylesheet` method - * that makes it work with Angular's CSP APIs. - */ - private embedLinkedStylesheetOverride: EmbedLinkedStylesheetFn = async (link, document) => { - if (link.getAttribute('media') === 'print' && link.next?.name === 'noscript') { - // Workaround for https://github.com/GoogleChromeLabs/critters/issues/64 - // NB: this is only needed for the webpack based builders. - const media = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN); - if (media) { - link.removeAttribute('onload'); - link.setAttribute('media', media[1]); - link?.next?.remove(); - } - } - - const returnValue = await this.initialEmbedLinkedStylesheet(link, document); - const cspNonce = this.findCspNonce(document); - - if (cspNonce) { - const crittersMedia = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN); - - if (crittersMedia) { - // If there's a Critters-generated `onload` handler and the file has an Angular CSP nonce, - // we have to remove the handler, because it's incompatible with CSP. We save the value - // in a different attribute and we generate a script tag with the nonce that uses - // `addEventListener` to apply the media query instead. - link.removeAttribute('onload'); - link.setAttribute(CSP_MEDIA_ATTR, crittersMedia[1]); - this.conditionallyInsertCspLoadingScript(document, cspNonce, link); - } - - // Ideally we would hook in at the time Critters inserts the `style` tags, but there isn't - // a way of doing that at the moment so we fall back to doing it any time a `link` tag is - // inserted. We mitigate it by only iterating the direct children of the `` which - // should be pretty shallow. - document.head.children.forEach((child) => { - if (child.tagName === 'style' && !child.hasAttribute('nonce')) { - child.setAttribute('nonce', cspNonce); - } - }); - } - - return returnValue; - }; - - /** - * Finds the CSP nonce for a specific document. - */ - private findCspNonce(document: PartialDocument): string | null { - if (this.documentNonces.has(document)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return this.documentNonces.get(document)!; - } - - // HTML attribute are case-insensitive, but the parser used by Critters is case-sensitive. - const nonceElement = document.querySelector('[ngCspNonce], [ngcspnonce]'); - const cspNonce = - nonceElement?.getAttribute('ngCspNonce') || nonceElement?.getAttribute('ngcspnonce') || null; - - this.documentNonces.set(document, cspNonce); - - return cspNonce; - } - - /** - * Inserts the `script` tag that swaps the critical CSS at runtime, - * if one hasn't been inserted into the document already. - */ - private conditionallyInsertCspLoadingScript( - document: PartialDocument, - nonce: string, - link: PartialHTMLElement, - ): void { - if (this.addedCspScriptsDocuments.has(document)) { - return; - } - - if (document.head.textContent.includes(LINK_LOAD_SCRIPT_CONTENT)) { - // Script was already added during the build. - this.addedCspScriptsDocuments.add(document); - - return; - } - - const script = document.createElement('script'); - script.setAttribute('nonce', nonce); - script.textContent = LINK_LOAD_SCRIPT_CONTENT; - // Prepend the script to the head since it needs to - // run as early as possible, before the `link` tags. - document.head.insertBefore(script, link); - this.addedCspScriptsDocuments.add(document); - } -} - -export class InlineCriticalCssProcessor { +export class CommonEngineInlineCriticalCssProcessor { private readonly resourceCache = new Map(); - constructor(protected readonly options: InlineCriticalCssProcessorOptions) {} - - async process( - html: string, - options: InlineCriticalCssProcessOptions, - ): Promise { - const critters = new CrittersExtended({ ...this.options, ...options }, this.resourceCache); - const content = await critters.process(html); + async process(html: string, outputPath: string | undefined): Promise { + const critters = new InlineCriticalCssProcessor(outputPath); + critters.readFile = async (path: string): Promise => { + let resourceContent = this.resourceCache.get(path); + if (resourceContent === undefined) { + resourceContent = await readFile(path, 'utf-8'); + this.resourceCache.set(path, resourceContent); + } - return { - content, - errors: critters.errors.length ? critters.errors : undefined, - warnings: critters.warnings.length ? critters.warnings : undefined, + return resourceContent; }; + + return critters.process(html); } } diff --git a/packages/angular/ssr/src/utils/inline-critical-css.ts b/packages/angular/ssr/src/utils/inline-critical-css.ts new file mode 100644 index 000000000000..36ad4f47b2a5 --- /dev/null +++ b/packages/angular/ssr/src/utils/inline-critical-css.ts @@ -0,0 +1,214 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import Critters from '../../third_party/critters'; + +/** + * Pattern used to extract the media query set by Critters in an `onload` handler. + */ +const MEDIA_SET_HANDLER_PATTERN = /^this\.media=["'](.*)["'];?$/; + +/** + * Name of the attribute used to save the Critters media query so it can be re-assigned on load. + */ +const CSP_MEDIA_ATTR = 'ngCspMedia'; + +/** + * Script text used to change the media value of the link tags. + * + * NOTE: + * We do not use `document.querySelectorAll('link').forEach((s) => s.addEventListener('load', ...)` + * because this does not always fire on Chome. + * See: https://github.com/angular/angular-cli/issues/26932 and https://crbug.com/1521256 + */ +const LINK_LOAD_SCRIPT_CONTENT = [ + '(() => {', + ` const CSP_MEDIA_ATTR = '${CSP_MEDIA_ATTR}';`, + ' const documentElement = document.documentElement;', + ' const listener = (e) => {', + ' const target = e.target;', + ` if (!target || target.tagName !== 'LINK' || !target.hasAttribute(CSP_MEDIA_ATTR)) {`, + ' return;', + ' }', + + ' target.media = target.getAttribute(CSP_MEDIA_ATTR);', + ' target.removeAttribute(CSP_MEDIA_ATTR);', + + // Remove onload listener when there are no longer styles that need to be loaded. + ' if (!document.head.querySelector(`link[${CSP_MEDIA_ATTR}]`)) {', + ` documentElement.removeEventListener('load', listener);`, + ' }', + ' };', + + // We use an event with capturing (the true parameter) because load events don't bubble. + ` documentElement.addEventListener('load', listener, true);`, + '})();', +].join('\n'); + +/** Partial representation of an `HTMLElement`. */ +interface PartialHTMLElement { + getAttribute(name: string): string | null; + setAttribute(name: string, value: string): void; + hasAttribute(name: string): boolean; + removeAttribute(name: string): void; + appendChild(child: PartialHTMLElement): void; + insertBefore(newNode: PartialHTMLElement, referenceNode?: PartialHTMLElement): void; + remove(): void; + name: string; + textContent: string; + tagName: string | null; + children: PartialHTMLElement[]; + next: PartialHTMLElement | null; + prev: PartialHTMLElement | null; +} + +/** Partial representation of an HTML `Document`. */ +interface PartialDocument { + head: PartialHTMLElement; + createElement(tagName: string): PartialHTMLElement; + querySelector(selector: string): PartialHTMLElement | null; +} + +/** Signature of the `Critters.embedLinkedStylesheet` method. */ +type EmbedLinkedStylesheetFn = ( + link: PartialHTMLElement, + document: PartialDocument, +) => Promise; + +export class InlineCriticalCssProcessor extends Critters { + private initialEmbedLinkedStylesheet: EmbedLinkedStylesheetFn; + private addedCspScriptsDocuments = new WeakSet(); + private documentNonces = new WeakMap(); + + // Inherited from `Critters`, but not exposed in the typings. + protected declare embedLinkedStylesheet: EmbedLinkedStylesheetFn; + + constructor(readonly outputPath?: string) { + super({ + logger: { + // eslint-disable-next-line no-console + warn: (s: string) => console.warn(s), + // eslint-disable-next-line no-console + error: (s: string) => console.error(s), + info: () => {}, + }, + logLevel: 'warn', + path: outputPath, + publicPath: undefined, + compress: false, + pruneSource: false, + reduceInlineStyles: false, + mergeStylesheets: false, + // Note: if `preload` changes to anything other than `media`, the logic in + // `embedLinkedStylesheetOverride` will have to be updated. + preload: 'media', + noscriptFallback: true, + inlineFonts: true, + }); + + // We can't use inheritance to override `embedLinkedStylesheet`, because it's not declared in + // the `Critters` .d.ts which means that we can't call the `super` implementation. TS doesn't + // allow for `super` to be cast to a different type. + this.initialEmbedLinkedStylesheet = this.embedLinkedStylesheet; + this.embedLinkedStylesheet = this.embedLinkedStylesheetOverride; + } + + /** + * Override of the Critters `embedLinkedStylesheet` method + * that makes it work with Angular's CSP APIs. + */ + private embedLinkedStylesheetOverride: EmbedLinkedStylesheetFn = async (link, document) => { + if (link.getAttribute('media') === 'print' && link.next?.name === 'noscript') { + // Workaround for https://github.com/GoogleChromeLabs/critters/issues/64 + // NB: this is only needed for the webpack based builders. + const media = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN); + if (media) { + link.removeAttribute('onload'); + link.setAttribute('media', media[1]); + link?.next?.remove(); + } + } + + const returnValue = await this.initialEmbedLinkedStylesheet(link, document); + const cspNonce = this.findCspNonce(document); + + if (cspNonce) { + const crittersMedia = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN); + + if (crittersMedia) { + // If there's a Critters-generated `onload` handler and the file has an Angular CSP nonce, + // we have to remove the handler, because it's incompatible with CSP. We save the value + // in a different attribute and we generate a script tag with the nonce that uses + // `addEventListener` to apply the media query instead. + link.removeAttribute('onload'); + link.setAttribute(CSP_MEDIA_ATTR, crittersMedia[1]); + this.conditionallyInsertCspLoadingScript(document, cspNonce, link); + } + + // Ideally we would hook in at the time Critters inserts the `style` tags, but there isn't + // a way of doing that at the moment so we fall back to doing it any time a `link` tag is + // inserted. We mitigate it by only iterating the direct children of the `` which + // should be pretty shallow. + document.head.children.forEach((child) => { + if (child.tagName === 'style' && !child.hasAttribute('nonce')) { + child.setAttribute('nonce', cspNonce); + } + }); + } + + return returnValue; + }; + + /** + * Finds the CSP nonce for a specific document. + */ + private findCspNonce(document: PartialDocument): string | null { + if (this.documentNonces.has(document)) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.documentNonces.get(document)!; + } + + // HTML attribute are case-insensitive, but the parser used by Critters is case-sensitive. + const nonceElement = document.querySelector('[ngCspNonce], [ngcspnonce]'); + const cspNonce = + nonceElement?.getAttribute('ngCspNonce') || nonceElement?.getAttribute('ngcspnonce') || null; + + this.documentNonces.set(document, cspNonce); + + return cspNonce; + } + + /** + * Inserts the `script` tag that swaps the critical CSS at runtime, + * if one hasn't been inserted into the document already. + */ + private conditionallyInsertCspLoadingScript( + document: PartialDocument, + nonce: string, + link: PartialHTMLElement, + ): void { + if (this.addedCspScriptsDocuments.has(document)) { + return; + } + + if (document.head.textContent.includes(LINK_LOAD_SCRIPT_CONTENT)) { + // Script was already added during the build. + this.addedCspScriptsDocuments.add(document); + + return; + } + + const script = document.createElement('script'); + script.setAttribute('nonce', nonce); + script.textContent = LINK_LOAD_SCRIPT_CONTENT; + // Prepend the script to the head since it needs to + // run as early as possible, before the `link` tags. + document.head.insertBefore(script, link); + this.addedCspScriptsDocuments.add(document); + } +} diff --git a/packages/angular/ssr/third_party/critters/BUILD.bazel b/packages/angular/ssr/third_party/critters/BUILD.bazel new file mode 100644 index 000000000000..8dde77495c4e --- /dev/null +++ b/packages/angular/ssr/third_party/critters/BUILD.bazel @@ -0,0 +1,55 @@ +load("@npm//@angular/build-tooling/bazel/esbuild:index.bzl", "esbuild", "esbuild_config") +load("//tools:defaults.bzl", "js_library") + +package(default_visibility = ["//visibility:public"]) + +esbuild( + name = "bundled_critters", + config = ":esbuild_config", + entry_point = "@npm//:node_modules/critters/dist/critters.mjs", + metafile = True, + splitting = True, + deps = [ + "@npm//critters", + "@npm//unenv", + ], +) + +esbuild_config( + name = "esbuild_config", + config_file = "esbuild.config.mjs", +) + +js_library( + name = "bundled_critters_lib", + srcs = [ + "index.d.ts", + ":bundled_critters_files", + ], + deps = [ + "@npm//critters", + ], +) + +# Filter out esbuild metadata files and only copy the necessary files +genrule( + name = "bundled_critters_files", + srcs = [ + ":bundled_critters", + ], + outs = [ + "index.js", + "index.js.map", + "THIRD_PARTY_LICENSES.txt", + ], + cmd = """ + for f in $(locations :bundled_critters); do + # Only process files inside the bundled_critters directory + if [[ "$${f}" == *bundled_critters ]]; then + cp "$${f}/index.js" $(location :index.js) + cp "$${f}/index.js.map" $(location :index.js.map) + cp "$${f}/THIRD_PARTY_LICENSES.txt" $(location :THIRD_PARTY_LICENSES.txt) + fi + done + """, +) diff --git a/packages/angular/ssr/third_party/critters/esbuild.config.mjs b/packages/angular/ssr/third_party/critters/esbuild.config.mjs new file mode 100644 index 000000000000..b50e9b61524a --- /dev/null +++ b/packages/angular/ssr/third_party/critters/esbuild.config.mjs @@ -0,0 +1,197 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { readFile, writeFile } from 'node:fs/promises'; +import { join, basename, dirname } from 'node:path'; +import { nodeless } from 'unenv'; + +const { path, fs } = nodeless.alias; + +export default { + format: 'esm', + platform: 'browser', + entryNames: 'index', + legalComments: 'eof', + alias: { + fs, + path, + }, + plugins: [ + { + name: 'angular-license-extractor', + setup(build) { + build.onEnd(async (result) => { + const licenses = await extractLicenses(result.metafile); + + return writeFile(join(build.initialOptions.outdir, 'THIRD_PARTY_LICENSES.txt'), licenses); + }); + }, + }, + ], +}; + +// Note: The following content has been adapted from the Angular CLI license extractor. + +/** + * The path segment used to signify that a file is part of a package. + */ +const NODE_MODULE_SEGMENT = 'node_modules'; + +/** + * String constant for the NPM recommended custom license wording. + * + * See: https://docs.npmjs.com/cli/v9/configuring-npm/package-json#license + * + * Example: + * ``` + * { + * "license" : "SEE LICENSE IN " + * } + * ``` + */ +const CUSTOM_LICENSE_TEXT = 'SEE LICENSE IN '; + +/** + * A list of commonly named license files found within packages. + */ +const LICENSE_FILES = ['LICENSE', 'LICENSE.txt', 'LICENSE.md']; + +/** + * Header text that will be added to the top of the output license extraction file. + */ +const EXTRACTION_FILE_HEADER = ''; + +/** + * The package entry separator to use within the output license extraction file. + */ +const EXTRACTION_FILE_SEPARATOR = '-'.repeat(80) + '\n'; + +/** + * Extracts license information for each node module package included in the output + * files of the built code. This includes JavaScript and CSS output files. The esbuild + * metafile generated during the bundling steps is used as the source of information + * regarding what input files where included and where they are located. A path segment + * of `node_modules` is used to indicate that a file belongs to a package and its license + * should be include in the output licenses file. + * + * The package name and license field are extracted from the `package.json` file for the + * package. If a license file (e.g., `LICENSE`) is present in the root of the package, it + * will also be included in the output licenses file. + * + * @param metafile An esbuild metafile object. + * @returns A string containing the content of the output licenses file. + */ +async function extractLicenses(metafile) { + let extractedLicenseContent = `${EXTRACTION_FILE_HEADER}\n${EXTRACTION_FILE_SEPARATOR}`; + + const seenPaths = new Set(); + const seenPackages = new Set(); + + for (const entry of Object.values(metafile.outputs)) { + for (const [inputPath, { bytesInOutput }] of Object.entries(entry.inputs)) { + // Skip if not included in output + if (bytesInOutput <= 0) { + continue; + } + + // Skip already processed paths + if (seenPaths.has(inputPath)) { + continue; + } + seenPaths.add(inputPath); + + // Skip non-package paths + if (!inputPath.includes(NODE_MODULE_SEGMENT)) { + continue; + } + + // Extract the package name from the path + let baseDirectory = join(process.cwd(), inputPath); + let nameOrScope, nameOrFile; + let found = false; + while (baseDirectory !== dirname(baseDirectory)) { + const segment = basename(baseDirectory); + if (segment === NODE_MODULE_SEGMENT) { + found = true; + break; + } + + nameOrFile = nameOrScope; + nameOrScope = segment; + baseDirectory = dirname(baseDirectory); + } + + // Skip non-package path edge cases that are not caught in the includes check above + if (!found || !nameOrScope) { + continue; + } + + const packageName = nameOrScope.startsWith('@') + ? `${nameOrScope}/${nameOrFile}` + : nameOrScope; + const packageDirectory = join(baseDirectory, packageName); + + // Load the package's metadata to find the package's name, version, and license type + const packageJsonPath = join(packageDirectory, 'package.json'); + let packageJson; + try { + packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8')); + } catch { + // Invalid package + continue; + } + + // Skip already processed packages + const packageId = `${packageName}@${packageJson.version}`; + if (seenPackages.has(packageId)) { + continue; + } + seenPackages.add(packageId); + + // Attempt to find license text inside package + let licenseText = ''; + if ( + typeof packageJson.license === 'string' && + packageJson.license.toLowerCase().startsWith(CUSTOM_LICENSE_TEXT) + ) { + // Attempt to load the package's custom license + let customLicensePath; + const customLicenseFile = normalize( + packageJson.license.slice(CUSTOM_LICENSE_TEXT.length + 1).trim(), + ); + if (customLicenseFile.startsWith('..') || isAbsolute(customLicenseFile)) { + // Path is attempting to access files outside of the package + // TODO: Issue warning? + } else { + customLicensePath = join(packageDirectory, customLicenseFile); + try { + licenseText = await readFile(customLicensePath, 'utf-8'); + break; + } catch {} + } + } else { + // Search for a license file within the root of the package + for (const potentialLicense of LICENSE_FILES) { + const packageLicensePath = join(packageDirectory, potentialLicense); + try { + licenseText = await readFile(packageLicensePath, 'utf-8'); + break; + } catch {} + } + } + + // Generate the package's license entry in the output content + extractedLicenseContent += `Package: ${packageJson.name}\n`; + extractedLicenseContent += `License: ${JSON.stringify(packageJson.license, null, 2)}\n`; + extractedLicenseContent += `\n${licenseText}\n`; + extractedLicenseContent += EXTRACTION_FILE_SEPARATOR; + } + } + + return extractedLicenseContent; +} diff --git a/packages/angular/ssr/third_party/critters/index.d.ts b/packages/angular/ssr/third_party/critters/index.d.ts new file mode 100644 index 000000000000..c53119f0a037 --- /dev/null +++ b/packages/angular/ssr/third_party/critters/index.d.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export {default} from 'critters'; diff --git a/scripts/build.mts b/scripts/build.mts index 828d73a822f6..6c5312759df6 100644 --- a/scripts/build.mts +++ b/scripts/build.mts @@ -95,9 +95,7 @@ async function _build(logger: Console, mode: BuildMode): Promise { logger.group(`Building (mode=${mode})...`); logger.group('Finding targets...'); - const queryTargetsCmd = - `${bazelCmd} query --output=label "attr(name, npm_package_archive, //packages/...` + - ' except //packages/angular/ssr/schematics/...)"'; + const queryTargetsCmd = `${bazelCmd} query --output=label "attr(name, npm_package_archive, //packages/...)"`; const targets = (await _exec(queryTargetsCmd, true, logger)).split(/\r?\n/); logger.groupEnd(); diff --git a/tools/defaults.bzl b/tools/defaults.bzl index 20e20d699b13..5b385d8b7614 100644 --- a/tools/defaults.bzl +++ b/tools/defaults.bzl @@ -24,6 +24,8 @@ NPM_PACKAGE_SUBSTITUTIONS = { "0.0.0-ENGINES-NODE": RELEASE_ENGINES_NODE, "0.0.0-ENGINES-NPM": RELEASE_ENGINES_NPM, "0.0.0-ENGINES-YARN": RELEASE_ENGINES_YARN, + # The below is needed for @angular/ssr FESM file. + "\\./(.+)/packages/angular/ssr/third_party/critters": "../third_party/critters/index.js", } NO_STAMP_PACKAGE_SUBSTITUTIONS = dict(NPM_PACKAGE_SUBSTITUTIONS, **{ @@ -218,13 +220,14 @@ def pkg_npm(name, pkg_deps = [], use_prodmode_output = False, **kwargs): **kwargs ) - pkg_tar( - name = name + "_archive", - srcs = [":%s" % name], - extension = "tgz", - strip_prefix = "./%s" % name, - visibility = visibility, - ) + if pkg_json: + pkg_tar( + name = name + "_archive", + srcs = [":%s" % name], + extension = "tgz", + strip_prefix = "./%s" % name, + visibility = visibility, + ) def ng_module(name, tsconfig = None, entry_point = None, testonly = False, deps = [], module_name = None, package_name = None, **kwargs): """Default values for ng_module""" @@ -268,12 +271,6 @@ def ng_module(name, tsconfig = None, entry_point = None, testonly = False, deps def ng_package(deps = [], **kwargs): _ng_package( deps = deps, - externals = [ - "xhr2", - "critters", - "express-engine", - "express", - ], substitutions = select({ "//:stamp": NPM_PACKAGE_SUBSTITUTIONS, "//conditions:default": NO_STAMP_PACKAGE_SUBSTITUTIONS, diff --git a/yarn.lock b/yarn.lock index 78d3301b868d..a4ee0a2e6537 100644 --- a/yarn.lock +++ b/yarn.lock @@ -775,6 +775,7 @@ __metadata: tslib: "npm:2.6.3" typescript: "npm:5.5.4" undici: "npm:6.19.8" + unenv: "npm:^1.10.0" verdaccio: "npm:5.32.1" verdaccio-auth-memory: "npm:^10.0.0" vite: "npm:5.4.2" @@ -964,7 +965,6 @@ __metadata: "@angular/platform-browser": "npm:19.0.0-next.1" "@angular/platform-server": "npm:19.0.0-next.1" "@angular/router": "npm:19.0.0-next.1" - critters: "npm:0.0.24" tslib: "npm:^2.3.0" zone.js: "npm:^0.15.0" peerDependencies: @@ -7923,6 +7923,13 @@ __metadata: languageName: node linkType: hard +"consola@npm:^3.2.3": + version: 3.2.3 + resolution: "consola@npm:3.2.3" + checksum: 10c0/c606220524ec88a05bb1baf557e9e0e04a0c08a9c35d7a08652d99de195c4ddcb6572040a7df57a18ff38bbc13ce9880ad032d56630cef27bef72768ef0ac078 + languageName: node + linkType: hard + "content-disposition@npm:0.5.4, content-disposition@npm:~0.5.2": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" @@ -8402,6 +8409,13 @@ __metadata: languageName: node linkType: hard +"defu@npm:^6.1.4": + version: 6.1.4 + resolution: "defu@npm:6.1.4" + checksum: 10c0/2d6cc366262dc0cb8096e429368e44052fdf43ed48e53ad84cc7c9407f890301aa5fcb80d0995abaaf842b3949f154d060be4160f7a46cb2bc2f7726c81526f5 + languageName: node + linkType: hard + "degenerator@npm:^5.0.0": version: 5.0.1 resolution: "degenerator@npm:5.0.1" @@ -12974,7 +12988,7 @@ __metadata: languageName: node linkType: hard -"mime@npm:3.0.0": +"mime@npm:3.0.0, mime@npm:^3.0.0": version: 3.0.0 resolution: "mime@npm:3.0.0" bin: @@ -13437,6 +13451,13 @@ __metadata: languageName: node linkType: hard +"node-fetch-native@npm:^1.6.4": + version: 1.6.4 + resolution: "node-fetch-native@npm:1.6.4" + checksum: 10c0/78334dc6def5d1d95cfe87b33ac76c4833592c5eb84779ad2b0c23c689f9dd5d1cfc827035ada72d6b8b218f717798968c5a99aeff0a1a8bf06657e80592f9c3 + languageName: node + linkType: hard + "node-fetch@npm:*": version: 3.3.2 resolution: "node-fetch@npm:3.3.2" @@ -14331,6 +14352,13 @@ __metadata: languageName: node linkType: hard +"pathe@npm:^1.1.2": + version: 1.1.2 + resolution: "pathe@npm:1.1.2" + checksum: 10c0/64ee0a4e587fb0f208d9777a6c56e4f9050039268faaaaecd50e959ef01bf847b7872785c36483fa5cdcdbdfdb31fef2ff222684d4fc21c330ab60395c681897 + languageName: node + linkType: hard + "peek-stream@npm:^1.1.0": version: 1.1.3 resolution: "peek-stream@npm:1.1.3" @@ -17451,6 +17479,19 @@ __metadata: languageName: node linkType: hard +"unenv@npm:^1.10.0": + version: 1.10.0 + resolution: "unenv@npm:1.10.0" + dependencies: + consola: "npm:^3.2.3" + defu: "npm:^6.1.4" + mime: "npm:^3.0.0" + node-fetch-native: "npm:^1.6.4" + pathe: "npm:^1.1.2" + checksum: 10c0/354180647e21204b6c303339e7364b920baadb2672b540a88af267bc827636593e0bf79f59753dcc6b7ab5d4c83e71d69a9171a3596befb8bf77e0bb3c7612b9 + languageName: node + linkType: hard + "unicode-canonical-property-names-ecmascript@npm:^2.0.0": version: 2.0.0 resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.0" From faee5c6fb3bd538c0bf50454e6064cd936201f64 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 23 Aug 2024 07:02:37 +0000 Subject: [PATCH 06/11] refactor: add TypeScript declaration merging for Critters This refactor adds a workaround to enable TypeScript declaration merging for the Critters class. We initially tried using interface merging on the default-exported Critters class: ```typescript interface Critters { embedLinkedStylesheet(link: PartialHTMLElement, document: PartialDocument): Promise; } ``` However, since Critters is exported as a default class, TypeScript's declaration merging does not apply. To solve this, we introduced a new class, CrittersBase, which extends Critters, and added the required method in the CrittersBase interface: ```typescript interface CrittersBase { embedLinkedStylesheet(link: PartialHTMLElement, document: PartialDocument): Promise; } class CrittersBase extends Critters {} ``` --- .../utils/index-file/inline-critical-css.ts | 97 +++++++++-------- packages/angular/ssr/src/app.ts | 26 +---- .../src/common-engine/inline-css-processor.ts | 5 +- .../ssr/src/utils/inline-critical-css.ts | 102 ++++++++++-------- 4 files changed, 114 insertions(+), 116 deletions(-) diff --git a/packages/angular/build/src/utils/index-file/inline-critical-css.ts b/packages/angular/build/src/utils/index-file/inline-critical-css.ts index fe68c8abe105..ad531422989c 100644 --- a/packages/angular/build/src/utils/index-file/inline-critical-css.ts +++ b/packages/angular/build/src/utils/index-file/inline-critical-css.ts @@ -20,36 +20,46 @@ const MEDIA_SET_HANDLER_PATTERN = /^this\.media=["'](.*)["'];?$/; const CSP_MEDIA_ATTR = 'ngCspMedia'; /** - * Script text used to change the media value of the link tags. + * Script that dynamically updates the `media` attribute of `` tags based on a custom attribute (`CSP_MEDIA_ATTR`). * * NOTE: * We do not use `document.querySelectorAll('link').forEach((s) => s.addEventListener('load', ...)` - * because this does not always fire on Chome. + * because load events are not always triggered reliably on Chrome. * See: https://github.com/angular/angular-cli/issues/26932 and https://crbug.com/1521256 + * + * The script: + * - Ensures the event target is a `` tag with the `CSP_MEDIA_ATTR` attribute. + * - Updates the `media` attribute with the value of `CSP_MEDIA_ATTR` and then removes the attribute. + * - Removes the event listener when all relevant `` tags have been processed. + * - Uses event capturing (the `true` parameter) since load events do not bubble up the DOM. */ -const LINK_LOAD_SCRIPT_CONTENT = [ - '(() => {', - ` const CSP_MEDIA_ATTR = '${CSP_MEDIA_ATTR}';`, - ' const documentElement = document.documentElement;', - ' const listener = (e) => {', - ' const target = e.target;', - ` if (!target || target.tagName !== 'LINK' || !target.hasAttribute(CSP_MEDIA_ATTR)) {`, - ' return;', - ' }', - - ' target.media = target.getAttribute(CSP_MEDIA_ATTR);', - ' target.removeAttribute(CSP_MEDIA_ATTR);', - - // Remove onload listener when there are no longer styles that need to be loaded. - ' if (!document.head.querySelector(`link[${CSP_MEDIA_ATTR}]`)) {', - ` documentElement.removeEventListener('load', listener);`, - ' }', - ' };', - - // We use an event with capturing (the true parameter) because load events don't bubble. - ` documentElement.addEventListener('load', listener, true);`, - '})();', -].join('\n'); +const LINK_LOAD_SCRIPT_CONTENT = ` +(() => { + const CSP_MEDIA_ATTR = '${CSP_MEDIA_ATTR}'; + const documentElement = document.documentElement; + + // Listener for load events on link tags. + const listener = (e) => { + const target = e.target; + if ( + !target || + target.tagName !== 'LINK' || + !target.hasAttribute(CSP_MEDIA_ATTR) + ) { + return; + } + + target.media = target.getAttribute(CSP_MEDIA_ATTR); + target.removeAttribute(CSP_MEDIA_ATTR); + + if (!document.head.querySelector(\`link[\${CSP_MEDIA_ATTR}]\`)) { + documentElement.removeEventListener('load', listener); + } + }; + + documentElement.addEventListener('load', listener, true); +})(); +`.trim(); export interface InlineCriticalCssProcessOptions { outputPath: string; @@ -85,22 +95,22 @@ interface PartialDocument { querySelector(selector: string): PartialHTMLElement | null; } -/** Signature of the `Critters.embedLinkedStylesheet` method. */ -type EmbedLinkedStylesheetFn = ( - link: PartialHTMLElement, - document: PartialDocument, -) => Promise; +/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ -class CrittersExtended extends Critters { +// We use Typescript declaration merging because `embedLinkedStylesheet` it's not declared in +// the `Critters` types which means that we can't call the `super` implementation. +interface CrittersBase { + embedLinkedStylesheet(link: PartialHTMLElement, document: PartialDocument): Promise; +} +class CrittersBase extends Critters {} +/* eslint-enable @typescript-eslint/no-unsafe-declaration-merging */ + +class CrittersExtended extends CrittersBase { readonly warnings: string[] = []; readonly errors: string[] = []; - private initialEmbedLinkedStylesheet: EmbedLinkedStylesheetFn; private addedCspScriptsDocuments = new WeakSet(); private documentNonces = new WeakMap(); - // Inherited from `Critters`, but not exposed in the typings. - protected declare embedLinkedStylesheet: EmbedLinkedStylesheetFn; - constructor( private readonly optionsExtended: InlineCriticalCssProcessorOptions & InlineCriticalCssProcessOptions, @@ -119,17 +129,11 @@ class CrittersExtended extends Critters { reduceInlineStyles: false, mergeStylesheets: false, // Note: if `preload` changes to anything other than `media`, the logic in - // `embedLinkedStylesheetOverride` will have to be updated. + // `embedLinkedStylesheet` will have to be updated. preload: 'media', noscriptFallback: true, inlineFonts: true, }); - - // We can't use inheritance to override `embedLinkedStylesheet`, because it's not declared in - // the `Critters` .d.ts which means that we can't call the `super` implementation. TS doesn't - // allow for `super` to be cast to a different type. - this.initialEmbedLinkedStylesheet = this.embedLinkedStylesheet; - this.embedLinkedStylesheet = this.embedLinkedStylesheetOverride; } public override readFile(path: string): Promise { @@ -142,7 +146,10 @@ class CrittersExtended extends Critters { * Override of the Critters `embedLinkedStylesheet` method * that makes it work with Angular's CSP APIs. */ - private embedLinkedStylesheetOverride: EmbedLinkedStylesheetFn = async (link, document) => { + override async embedLinkedStylesheet( + link: PartialHTMLElement, + document: PartialDocument, + ): Promise { if (link.getAttribute('media') === 'print' && link.next?.name === 'noscript') { // Workaround for https://github.com/GoogleChromeLabs/critters/issues/64 // NB: this is only needed for the webpack based builders. @@ -154,7 +161,7 @@ class CrittersExtended extends Critters { } } - const returnValue = await this.initialEmbedLinkedStylesheet(link, document); + const returnValue = await super.embedLinkedStylesheet(link, document); const cspNonce = this.findCspNonce(document); if (cspNonce) { @@ -182,7 +189,7 @@ class CrittersExtended extends Critters { } return returnValue; - }; + } /** * Finds the CSP nonce for a specific document. diff --git a/packages/angular/ssr/src/app.ts b/packages/angular/ssr/src/app.ts index bc116876aef3..06925a239a64 100644 --- a/packages/angular/ssr/src/app.ts +++ b/packages/angular/ssr/src/app.ts @@ -187,34 +187,16 @@ export class AngularServerApp { if (manifest.inlineCriticalCss) { // Optionally inline critical CSS. - const inlineCriticalCssProcessor = this.getOrCreateInlineCssProcessor(); - html = await inlineCriticalCssProcessor.process(html); - } - - return new Response(html, responseInit); - } - - /** - * Retrieves or creates the inline critical CSS processor. - * If one does not exist, it initializes a new instance. - * - * @returns The inline critical CSS processor instance. - */ - private getOrCreateInlineCssProcessor(): InlineCriticalCssProcessor { - let inlineCriticalCssProcessor = this.inlineCriticalCssProcessor; - - if (!inlineCriticalCssProcessor) { - inlineCriticalCssProcessor = new InlineCriticalCssProcessor(); - inlineCriticalCssProcessor.readFile = (path: string) => { + this.inlineCriticalCssProcessor ??= new InlineCriticalCssProcessor((path: string) => { const fileName = path.split('/').pop() ?? path; return this.assets.getServerAsset(fileName); - }; + }); - this.inlineCriticalCssProcessor = inlineCriticalCssProcessor; + html = await this.inlineCriticalCssProcessor.process(html); } - return inlineCriticalCssProcessor; + return new Response(html, responseInit); } } diff --git a/packages/angular/ssr/src/common-engine/inline-css-processor.ts b/packages/angular/ssr/src/common-engine/inline-css-processor.ts index e19a522ec401..d9e8d0e2c2d3 100644 --- a/packages/angular/ssr/src/common-engine/inline-css-processor.ts +++ b/packages/angular/ssr/src/common-engine/inline-css-processor.ts @@ -13,8 +13,7 @@ export class CommonEngineInlineCriticalCssProcessor { private readonly resourceCache = new Map(); async process(html: string, outputPath: string | undefined): Promise { - const critters = new InlineCriticalCssProcessor(outputPath); - critters.readFile = async (path: string): Promise => { + const critters = new InlineCriticalCssProcessor(async (path) => { let resourceContent = this.resourceCache.get(path); if (resourceContent === undefined) { resourceContent = await readFile(path, 'utf-8'); @@ -22,7 +21,7 @@ export class CommonEngineInlineCriticalCssProcessor { } return resourceContent; - }; + }, outputPath); return critters.process(html); } diff --git a/packages/angular/ssr/src/utils/inline-critical-css.ts b/packages/angular/ssr/src/utils/inline-critical-css.ts index 36ad4f47b2a5..648a4d51577c 100644 --- a/packages/angular/ssr/src/utils/inline-critical-css.ts +++ b/packages/angular/ssr/src/utils/inline-critical-css.ts @@ -19,36 +19,46 @@ const MEDIA_SET_HANDLER_PATTERN = /^this\.media=["'](.*)["'];?$/; const CSP_MEDIA_ATTR = 'ngCspMedia'; /** - * Script text used to change the media value of the link tags. + * Script that dynamically updates the `media` attribute of `` tags based on a custom attribute (`CSP_MEDIA_ATTR`). * * NOTE: * We do not use `document.querySelectorAll('link').forEach((s) => s.addEventListener('load', ...)` - * because this does not always fire on Chome. + * because load events are not always triggered reliably on Chrome. * See: https://github.com/angular/angular-cli/issues/26932 and https://crbug.com/1521256 + * + * The script: + * - Ensures the event target is a `` tag with the `CSP_MEDIA_ATTR` attribute. + * - Updates the `media` attribute with the value of `CSP_MEDIA_ATTR` and then removes the attribute. + * - Removes the event listener when all relevant `` tags have been processed. + * - Uses event capturing (the `true` parameter) since load events do not bubble up the DOM. */ -const LINK_LOAD_SCRIPT_CONTENT = [ - '(() => {', - ` const CSP_MEDIA_ATTR = '${CSP_MEDIA_ATTR}';`, - ' const documentElement = document.documentElement;', - ' const listener = (e) => {', - ' const target = e.target;', - ` if (!target || target.tagName !== 'LINK' || !target.hasAttribute(CSP_MEDIA_ATTR)) {`, - ' return;', - ' }', - - ' target.media = target.getAttribute(CSP_MEDIA_ATTR);', - ' target.removeAttribute(CSP_MEDIA_ATTR);', - - // Remove onload listener when there are no longer styles that need to be loaded. - ' if (!document.head.querySelector(`link[${CSP_MEDIA_ATTR}]`)) {', - ` documentElement.removeEventListener('load', listener);`, - ' }', - ' };', - - // We use an event with capturing (the true parameter) because load events don't bubble. - ` documentElement.addEventListener('load', listener, true);`, - '})();', -].join('\n'); +const LINK_LOAD_SCRIPT_CONTENT = ` +(() => { + const CSP_MEDIA_ATTR = '${CSP_MEDIA_ATTR}'; + const documentElement = document.documentElement; + + // Listener for load events on link tags. + const listener = (e) => { + const target = e.target; + if ( + !target || + target.tagName !== 'LINK' || + !target.hasAttribute(CSP_MEDIA_ATTR) + ) { + return; + } + + target.media = target.getAttribute(CSP_MEDIA_ATTR); + target.removeAttribute(CSP_MEDIA_ATTR); + + if (!document.head.querySelector(\`link[\${CSP_MEDIA_ATTR}]\`)) { + documentElement.removeEventListener('load', listener); + } + }; + + documentElement.addEventListener('load', listener, true); +})(); +`.trim(); /** Partial representation of an `HTMLElement`. */ interface PartialHTMLElement { @@ -74,21 +84,24 @@ interface PartialDocument { querySelector(selector: string): PartialHTMLElement | null; } -/** Signature of the `Critters.embedLinkedStylesheet` method. */ -type EmbedLinkedStylesheetFn = ( - link: PartialHTMLElement, - document: PartialDocument, -) => Promise; +/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ -export class InlineCriticalCssProcessor extends Critters { - private initialEmbedLinkedStylesheet: EmbedLinkedStylesheetFn; +// We use Typescript declaration merging because `embedLinkedStylesheet` it's not declared in +// the `Critters` types which means that we can't call the `super` implementation. +interface CrittersBase { + embedLinkedStylesheet(link: PartialHTMLElement, document: PartialDocument): Promise; +} +class CrittersBase extends Critters {} +/* eslint-enable @typescript-eslint/no-unsafe-declaration-merging */ + +export class InlineCriticalCssProcessor extends CrittersBase { private addedCspScriptsDocuments = new WeakSet(); private documentNonces = new WeakMap(); - // Inherited from `Critters`, but not exposed in the typings. - protected declare embedLinkedStylesheet: EmbedLinkedStylesheetFn; - - constructor(readonly outputPath?: string) { + constructor( + public override readFile: (path: string) => Promise, + readonly outputPath?: string, + ) { super({ logger: { // eslint-disable-next-line no-console @@ -105,24 +118,21 @@ export class InlineCriticalCssProcessor extends Critters { reduceInlineStyles: false, mergeStylesheets: false, // Note: if `preload` changes to anything other than `media`, the logic in - // `embedLinkedStylesheetOverride` will have to be updated. + // `embedLinkedStylesheet` will have to be updated. preload: 'media', noscriptFallback: true, inlineFonts: true, }); - - // We can't use inheritance to override `embedLinkedStylesheet`, because it's not declared in - // the `Critters` .d.ts which means that we can't call the `super` implementation. TS doesn't - // allow for `super` to be cast to a different type. - this.initialEmbedLinkedStylesheet = this.embedLinkedStylesheet; - this.embedLinkedStylesheet = this.embedLinkedStylesheetOverride; } /** * Override of the Critters `embedLinkedStylesheet` method * that makes it work with Angular's CSP APIs. */ - private embedLinkedStylesheetOverride: EmbedLinkedStylesheetFn = async (link, document) => { + override async embedLinkedStylesheet( + link: PartialHTMLElement, + document: PartialDocument, + ): Promise { if (link.getAttribute('media') === 'print' && link.next?.name === 'noscript') { // Workaround for https://github.com/GoogleChromeLabs/critters/issues/64 // NB: this is only needed for the webpack based builders. @@ -134,7 +144,7 @@ export class InlineCriticalCssProcessor extends Critters { } } - const returnValue = await this.initialEmbedLinkedStylesheet(link, document); + const returnValue = await super.embedLinkedStylesheet(link, document); const cspNonce = this.findCspNonce(document); if (cspNonce) { @@ -162,7 +172,7 @@ export class InlineCriticalCssProcessor extends Critters { } return returnValue; - }; + } /** * Finds the CSP nonce for a specific document. From 96693b70233b832cadaf29ae41acfb156ea4f0ec Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 23 Aug 2024 11:52:08 +0000 Subject: [PATCH 07/11] build: add function to validate and update 3rd party license files - Implemented functionality to compare and update third-party license files - Added handling for '--accept' argument to update the golden license file - Included tests to ensure the Critters license file matches the golden reference This update ensures license file consistency and provides an easy way to update the reference file when necessary. --- .prettierignore | 4 +- package.json | 3 + packages/angular/ssr/package.json | 2 + packages/angular/ssr/test/BUILD.bazel | 2 +- .../angular/ssr/test/npm_package/BUILD.bazel | 35 ++ .../THIRD_PARTY_LICENSES.txt.golden | 350 ++++++++++++++++++ .../ssr/test/npm_package/package_spec.ts | 62 ++++ .../test/npm_package/update-package-golden.ts | 15 + .../angular/ssr/test/npm_package/utils.ts | 32 ++ .../third_party/critters/esbuild.config.mjs | 19 +- .../ssr/third_party/critters/index.d.ts | 2 +- yarn.lock | 14 +- 12 files changed, 529 insertions(+), 11 deletions(-) create mode 100644 packages/angular/ssr/test/npm_package/BUILD.bazel create mode 100644 packages/angular/ssr/test/npm_package/THIRD_PARTY_LICENSES.txt.golden create mode 100644 packages/angular/ssr/test/npm_package/package_spec.ts create mode 100644 packages/angular/ssr/test/npm_package/update-package-golden.ts create mode 100644 packages/angular/ssr/test/npm_package/utils.ts diff --git a/.prettierignore b/.prettierignore index c78211cd0633..ae6046eb4baf 100644 --- a/.prettierignore +++ b/.prettierignore @@ -12,6 +12,6 @@ /CONTRIBUTING.md .yarn/ dist/ -third_party/ +third_party/github.com/ /tests/legacy-cli/e2e/assets/ -/tools/test/*.json \ No newline at end of file +/tools/test/*.json diff --git a/package.json b/package.json index d3aef362d9fe..8974f67de22a 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "@bazel/buildifier": "7.1.2", "@bazel/concatjs": "patch:@bazel/concatjs@npm%3A5.8.1#~/.yarn/patches/@bazel-concatjs-npm-5.8.1-1bf81df846.patch", "@bazel/jasmine": "patch:@bazel/jasmine@npm%3A5.8.1#~/.yarn/patches/@bazel-jasmine-npm-5.8.1-3370fee155.patch", + "@bazel/runfiles": "^5.8.1", "@discoveryjs/json-ext": "0.6.1", "@inquirer/confirm": "3.1.22", "@inquirer/prompts": "5.3.8", @@ -91,6 +92,7 @@ "@rollup/plugin-node-resolve": "^13.0.5", "@types/babel__core": "7.20.5", "@types/browser-sync": "^2.27.0", + "@types/diff": "^5.2.1", "@types/express": "^4.16.0", "@types/http-proxy": "^1.17.4", "@types/ini": "^4.0.0", @@ -130,6 +132,7 @@ "critters": "0.0.24", "css-loader": "7.1.2", "debug": "^4.1.1", + "diff": "^5.2.0", "esbuild": "0.23.1", "esbuild-wasm": "0.23.1", "eslint": "8.57.0", diff --git a/packages/angular/ssr/package.json b/packages/angular/ssr/package.json index e449d89cb981..811944267e97 100644 --- a/packages/angular/ssr/package.json +++ b/packages/angular/ssr/package.json @@ -28,6 +28,8 @@ "@angular/platform-browser": "19.0.0-next.1", "@angular/platform-server": "19.0.0-next.1", "@angular/router": "19.0.0-next.1", + "@bazel/runfiles": "^5.8.1", + "diff": "^5.2.0", "zone.js": "^0.15.0" }, "schematics": "./schematics/collection.json", diff --git a/packages/angular/ssr/test/BUILD.bazel b/packages/angular/ssr/test/BUILD.bazel index abc2a4eefdb1..75adb017fb96 100644 --- a/packages/angular/ssr/test/BUILD.bazel +++ b/packages/angular/ssr/test/BUILD.bazel @@ -13,7 +13,7 @@ ts_library( testonly = True, srcs = glob( include = ["**/*_spec.ts"], - exclude = ESM_TESTS, + exclude = ESM_TESTS + ["npm_package/**"], ), deps = [ "//packages/angular/ssr", diff --git a/packages/angular/ssr/test/npm_package/BUILD.bazel b/packages/angular/ssr/test/npm_package/BUILD.bazel new file mode 100644 index 000000000000..bd8f302e5019 --- /dev/null +++ b/packages/angular/ssr/test/npm_package/BUILD.bazel @@ -0,0 +1,35 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary") +load("@npm//@bazel/jasmine:index.bzl", "jasmine_node_test") +load("//tools:defaults.bzl", "ts_library") + +ts_library( + name = "unit_test_lib", + testonly = True, + srcs = glob(["**/*.ts"]), + deps = [ + "@npm//@bazel/runfiles", + "@npm//@types/diff", + ], +) + +jasmine_node_test( + name = "test", + srcs = [":unit_test_lib"], + data = [ + "THIRD_PARTY_LICENSES.txt.golden", + "//packages/angular/ssr:npm_package", + "@npm//diff", + ], +) + +nodejs_binary( + name = "test.accept", + testonly = True, + data = [ + "THIRD_PARTY_LICENSES.txt.golden", + ":unit_test_lib", + "//packages/angular/ssr:npm_package", + "@npm//diff", + ], + entry_point = ":update-package-golden.ts", +) diff --git a/packages/angular/ssr/test/npm_package/THIRD_PARTY_LICENSES.txt.golden b/packages/angular/ssr/test/npm_package/THIRD_PARTY_LICENSES.txt.golden new file mode 100644 index 000000000000..067f2370fd4c --- /dev/null +++ b/packages/angular/ssr/test/npm_package/THIRD_PARTY_LICENSES.txt.golden @@ -0,0 +1,350 @@ + +-------------------------------------------------------------------------------- +Package: ansi-styles +License: "MIT" + + +-------------------------------------------------------------------------------- +Package: boolbase +License: "ISC" + + +-------------------------------------------------------------------------------- +Package: chalk +License: "MIT" + + +-------------------------------------------------------------------------------- +Package: color-convert +License: "MIT" + +Copyright (c) 2011-2016 Heather Arthur + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------------- +Package: color-name +License: "MIT" + +The MIT License (MIT) +Copyright (c) 2015 Dmitry Ivanov + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------------- +Package: critters +License: "Apache-2.0" + + +-------------------------------------------------------------------------------- +Package: css-select +License: "BSD-2-Clause" + +Copyright (c) Felix Böhm +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-------------------------------------------------------------------------------- +Package: css-what +License: "BSD-2-Clause" + +Copyright (c) Felix Böhm +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-------------------------------------------------------------------------------- +Package: dom-serializer +License: "MIT" + +License + +(The MIT License) + +Copyright (c) 2014 The cheeriojs contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------------- +Package: domelementtype +License: "BSD-2-Clause" + +Copyright (c) Felix Böhm +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-------------------------------------------------------------------------------- +Package: domhandler +License: "BSD-2-Clause" + +Copyright (c) Felix Böhm +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-------------------------------------------------------------------------------- +Package: domutils +License: "BSD-2-Clause" + +Copyright (c) Felix Böhm +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-------------------------------------------------------------------------------- +Package: entities +License: "BSD-2-Clause" + +Copyright (c) Felix Böhm +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-------------------------------------------------------------------------------- +Package: htmlparser2 +License: "MIT" + +Copyright 2010, 2011, Chris Winberry . All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +-------------------------------------------------------------------------------- +Package: nanoid +License: "MIT" + +The MIT License (MIT) + +Copyright 2017 Andrey Sitnik + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------------- +Package: nth-check +License: "BSD-2-Clause" + +Copyright (c) Felix Böhm +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-------------------------------------------------------------------------------- +Package: pathe +License: "MIT" + +MIT License + +Copyright (c) Pooya Parsa - Daniel Roe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- + +Copyright Joyent, Inc. and other Node contributors. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------------- +Package: picocolors +License: "ISC" + +ISC License + +Copyright (c) 2021 Alexey Raspopov, Kostiantyn Denysov, Anton Verinov + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-------------------------------------------------------------------------------- +Package: postcss +License: "MIT" + +The MIT License (MIT) + +Copyright 2013 Andrey Sitnik + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------------- +Package: postcss-media-query-parser +License: "MIT" + + +-------------------------------------------------------------------------------- +Package: supports-color +License: "MIT" + + +-------------------------------------------------------------------------------- +Package: unenv +License: "MIT" + +MIT License + +Copyright (c) Pooya Parsa + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +-------------------------------------------------------------------------------- diff --git a/packages/angular/ssr/test/npm_package/package_spec.ts b/packages/angular/ssr/test/npm_package/package_spec.ts new file mode 100644 index 000000000000..2aadbe6540cc --- /dev/null +++ b/packages/angular/ssr/test/npm_package/package_spec.ts @@ -0,0 +1,62 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { createPatch } from 'diff'; +import { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { + ANGULAR_SSR_PACKAGE_PATH, + CRITTERS_ACTUAL_LICENSE_FILE_PATH, + CRITTERS_GOLDEN_LICENSE_FILE_PATH, +} from './utils'; + +describe('NPM Package Tests', () => { + it('should not include the contents of third_party/critters/index.js in the FESM bundle', async () => { + const fesmFilePath = join(ANGULAR_SSR_PACKAGE_PATH, 'fesm2022/ssr.mjs'); + const fesmContent = await readFile(fesmFilePath, 'utf-8'); + expect(fesmContent).toContain(`import Critters from '../third_party/critters/index.js'`); + }); + + describe('third_party/critters/THIRD_PARTY_LICENSES.txt', () => { + it('should exist', () => { + expect(existsSync(CRITTERS_ACTUAL_LICENSE_FILE_PATH)).toBe(true); + }); + + it('should match the expected golden file', async () => { + const [expectedContent, actualContent] = await Promise.all([ + readFile(CRITTERS_GOLDEN_LICENSE_FILE_PATH, 'utf-8'), + readFile(CRITTERS_ACTUAL_LICENSE_FILE_PATH, 'utf-8'), + ]); + + if (expectedContent.trim() === actualContent.trim()) { + return; + } + + const patch = createPatch( + CRITTERS_GOLDEN_LICENSE_FILE_PATH, + expectedContent, + actualContent, + 'Golden License File', + 'Current License File', + { context: 5 }, + ); + + const errorMessage = `The content of the actual license file differs from the expected golden reference. + Diff: + ${patch} + To accept the new golden file, execute: + yarn bazel run ${process.env['BAZEL_TARGET']}.accept + `; + + const error = new Error(errorMessage); + error.stack = error.stack?.replace(` Diff:\n ${patch}`, ''); + throw error; + }); + }); +}); diff --git a/packages/angular/ssr/test/npm_package/update-package-golden.ts b/packages/angular/ssr/test/npm_package/update-package-golden.ts new file mode 100644 index 000000000000..0754d4d89249 --- /dev/null +++ b/packages/angular/ssr/test/npm_package/update-package-golden.ts @@ -0,0 +1,15 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { readFileSync, writeFileSync } from 'node:fs'; +import { CRITTERS_ACTUAL_LICENSE_FILE_PATH, CRITTERS_GOLDEN_LICENSE_FILE_PATH } from './utils'; + +/** + * Updates the golden reference license file. + */ +writeFileSync(CRITTERS_GOLDEN_LICENSE_FILE_PATH, readFileSync(CRITTERS_ACTUAL_LICENSE_FILE_PATH)); diff --git a/packages/angular/ssr/test/npm_package/utils.ts b/packages/angular/ssr/test/npm_package/utils.ts new file mode 100644 index 000000000000..fc1a3adfc518 --- /dev/null +++ b/packages/angular/ssr/test/npm_package/utils.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { runfiles } from '@bazel/runfiles'; +import { dirname, join } from 'node:path'; + +/** + * Resolve paths for the Critters license file and the golden reference file. + */ +export const ANGULAR_SSR_PACKAGE_PATH = dirname( + runfiles.resolve('angular_cli/packages/angular/ssr/npm_package/package.json'), +); + +/** + * Path to the actual license file for the Critters library. + * This file is located in the `third_party/critters` directory of the Angular CLI npm package. + */ +export const CRITTERS_ACTUAL_LICENSE_FILE_PATH = join( + ANGULAR_SSR_PACKAGE_PATH, + 'third_party/critters/THIRD_PARTY_LICENSES.txt', +); + +/** + * Path to the golden reference license file for the Critters library. + * This file is used as a reference for comparison and is located in the same directory as this script. + */ +export const CRITTERS_GOLDEN_LICENSE_FILE_PATH = join(__dirname, 'THIRD_PARTY_LICENSES.txt.golden'); diff --git a/packages/angular/ssr/third_party/critters/esbuild.config.mjs b/packages/angular/ssr/third_party/critters/esbuild.config.mjs index b50e9b61524a..31b367d33965 100644 --- a/packages/angular/ssr/third_party/critters/esbuild.config.mjs +++ b/packages/angular/ssr/third_party/critters/esbuild.config.mjs @@ -87,10 +87,9 @@ const EXTRACTION_FILE_SEPARATOR = '-'.repeat(80) + '\n'; * @returns A string containing the content of the output licenses file. */ async function extractLicenses(metafile) { - let extractedLicenseContent = `${EXTRACTION_FILE_HEADER}\n${EXTRACTION_FILE_SEPARATOR}`; - const seenPaths = new Set(); const seenPackages = new Set(); + const extractedLicenses = {}; for (const entry of Object.values(metafile.outputs)) { for (const [inputPath, { bytesInOutput }] of Object.entries(entry.inputs)) { @@ -186,12 +185,20 @@ async function extractLicenses(metafile) { } // Generate the package's license entry in the output content - extractedLicenseContent += `Package: ${packageJson.name}\n`; - extractedLicenseContent += `License: ${JSON.stringify(packageJson.license, null, 2)}\n`; - extractedLicenseContent += `\n${licenseText}\n`; + let extractedLicenseContent = `Package: ${packageJson.name}\n`; + extractedLicenseContent += `License: ${JSON.stringify(packageJson.license, undefined, 2)}\n`; + extractedLicenseContent += `\n${licenseText.trim().replace(/\r?\n/g, '\n')}\n`; extractedLicenseContent += EXTRACTION_FILE_SEPARATOR; + + extractedLicenses[packageJson.name] = extractedLicenseContent; } } - return extractedLicenseContent; + // Get the keys of the object and sort them and etract and join the values corresponding to the sorted keys + const joinedLicenseContent = Object.keys(extractedLicenses) + .sort() + .map((pkgName) => extractedLicenses[pkgName]) + .join(''); + + return `${EXTRACTION_FILE_HEADER}\n${EXTRACTION_FILE_SEPARATOR}${joinedLicenseContent}`; } diff --git a/packages/angular/ssr/third_party/critters/index.d.ts b/packages/angular/ssr/third_party/critters/index.d.ts index c53119f0a037..2d7efd309d20 100644 --- a/packages/angular/ssr/third_party/critters/index.d.ts +++ b/packages/angular/ssr/third_party/critters/index.d.ts @@ -6,4 +6,4 @@ * found in the LICENSE file at https://angular.io/license */ -export {default} from 'critters'; +export { default } from 'critters'; diff --git a/yarn.lock b/yarn.lock index a4ee0a2e6537..4e6d02c37ab2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -659,6 +659,7 @@ __metadata: "@bazel/buildifier": "npm:7.1.2" "@bazel/concatjs": "patch:@bazel/concatjs@npm%3A5.8.1#~/.yarn/patches/@bazel-concatjs-npm-5.8.1-1bf81df846.patch" "@bazel/jasmine": "patch:@bazel/jasmine@npm%3A5.8.1#~/.yarn/patches/@bazel-jasmine-npm-5.8.1-3370fee155.patch" + "@bazel/runfiles": "npm:^5.8.1" "@discoveryjs/json-ext": "npm:0.6.1" "@inquirer/confirm": "npm:3.1.22" "@inquirer/prompts": "npm:5.3.8" @@ -667,6 +668,7 @@ __metadata: "@rollup/plugin-node-resolve": "npm:^13.0.5" "@types/babel__core": "npm:7.20.5" "@types/browser-sync": "npm:^2.27.0" + "@types/diff": "npm:^5.2.1" "@types/express": "npm:^4.16.0" "@types/http-proxy": "npm:^1.17.4" "@types/ini": "npm:^4.0.0" @@ -706,6 +708,7 @@ __metadata: critters: "npm:0.0.24" css-loader: "npm:7.1.2" debug: "npm:^4.1.1" + diff: "npm:^5.2.0" esbuild: "npm:0.23.1" esbuild-wasm: "npm:0.23.1" eslint: "npm:8.57.0" @@ -965,6 +968,8 @@ __metadata: "@angular/platform-browser": "npm:19.0.0-next.1" "@angular/platform-server": "npm:19.0.0-next.1" "@angular/router": "npm:19.0.0-next.1" + "@bazel/runfiles": "npm:^5.8.1" + diff: "npm:^5.2.0" tslib: "npm:^2.3.0" zone.js: "npm:^0.15.0" peerDependencies: @@ -4866,6 +4871,13 @@ __metadata: languageName: node linkType: hard +"@types/diff@npm:^5.2.1": + version: 5.2.1 + resolution: "@types/diff@npm:5.2.1" + checksum: 10c0/62dcab32197ac67f212939cdd79aa3953327a482bec55c6a38ad9de8a0662a9f920b59504609a322fc242593bd9afb3d2704702f4bc98087a13171234b952361 + languageName: node + linkType: hard + "@types/estree@npm:*, @types/estree@npm:1.0.5, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.5": version: 1.0.5 resolution: "@types/estree@npm:1.0.5" @@ -8549,7 +8561,7 @@ __metadata: languageName: node linkType: hard -"diff@npm:^5.0.0, diff@npm:^5.1.0": +"diff@npm:^5.0.0, diff@npm:^5.1.0, diff@npm:^5.2.0": version: 5.2.0 resolution: "diff@npm:5.2.0" checksum: 10c0/aed0941f206fe261ecb258dc8d0ceea8abbde3ace5827518ff8d302f0fc9cc81ce116c4d8f379151171336caf0516b79e01abdc1ed1201b6440d895a66689eb4 From ad65c3fbc298ea2824b67f1e6588866c9e7b142e Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 23 Aug 2024 21:16:36 +0000 Subject: [PATCH 08/11] build: use Bazel `diff_test` to compare file differences Leverage the built-in `diff_test` feature from Bazel to check for file changes. For details, see: https://github.com/bazelbuild/bazel-skylib/blob/main/docs/diff_test_doc.md --- package.json | 2 - packages/angular/ssr/package.json | 1 - .../angular/ssr/test/npm_package/BUILD.bazel | 47 +++++++++++---- .../ssr/test/npm_package/package_spec.ts | 60 +++++++------------ .../test/npm_package/update-package-golden.ts | 15 ----- .../angular/ssr/test/npm_package/utils.ts | 32 ---------- yarn.lock | 12 +--- 7 files changed, 59 insertions(+), 110 deletions(-) delete mode 100644 packages/angular/ssr/test/npm_package/update-package-golden.ts delete mode 100644 packages/angular/ssr/test/npm_package/utils.ts diff --git a/package.json b/package.json index 8974f67de22a..5e380d55cd2a 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,6 @@ "@rollup/plugin-node-resolve": "^13.0.5", "@types/babel__core": "7.20.5", "@types/browser-sync": "^2.27.0", - "@types/diff": "^5.2.1", "@types/express": "^4.16.0", "@types/http-proxy": "^1.17.4", "@types/ini": "^4.0.0", @@ -132,7 +131,6 @@ "critters": "0.0.24", "css-loader": "7.1.2", "debug": "^4.1.1", - "diff": "^5.2.0", "esbuild": "0.23.1", "esbuild-wasm": "0.23.1", "eslint": "8.57.0", diff --git a/packages/angular/ssr/package.json b/packages/angular/ssr/package.json index 811944267e97..747189433350 100644 --- a/packages/angular/ssr/package.json +++ b/packages/angular/ssr/package.json @@ -29,7 +29,6 @@ "@angular/platform-server": "19.0.0-next.1", "@angular/router": "19.0.0-next.1", "@bazel/runfiles": "^5.8.1", - "diff": "^5.2.0", "zone.js": "^0.15.0" }, "schematics": "./schematics/collection.json", diff --git a/packages/angular/ssr/test/npm_package/BUILD.bazel b/packages/angular/ssr/test/npm_package/BUILD.bazel index bd8f302e5019..48a239493667 100644 --- a/packages/angular/ssr/test/npm_package/BUILD.bazel +++ b/packages/angular/ssr/test/npm_package/BUILD.bazel @@ -1,4 +1,5 @@ -load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary") +load("@bazel_skylib//rules:diff_test.bzl", "diff_test") +load("@bazel_skylib//rules:write_file.bzl", "write_file") load("@npm//@bazel/jasmine:index.bzl", "jasmine_node_test") load("//tools:defaults.bzl", "ts_library") @@ -8,7 +9,6 @@ ts_library( srcs = glob(["**/*.ts"]), deps = [ "@npm//@bazel/runfiles", - "@npm//@types/diff", ], ) @@ -16,20 +16,43 @@ jasmine_node_test( name = "test", srcs = [":unit_test_lib"], data = [ - "THIRD_PARTY_LICENSES.txt.golden", "//packages/angular/ssr:npm_package", - "@npm//diff", ], ) -nodejs_binary( - name = "test.accept", - testonly = True, - data = [ - "THIRD_PARTY_LICENSES.txt.golden", - ":unit_test_lib", +genrule( + name = "critters_license_file", + srcs = [ "//packages/angular/ssr:npm_package", - "@npm//diff", ], - entry_point = ":update-package-golden.ts", + outs = [ + "THIRD_PARTY_LICENSES.txt", + ], + cmd = """ + cp $(location //packages/angular/ssr:npm_package)/third_party/critters/THIRD_PARTY_LICENSES.txt $(location :THIRD_PARTY_LICENSES.txt) + """, +) + +diff_test( + name = "critters_license_test", + failure_message = """ + + To accept the new golden file, execute: + yarn bazel run //packages/angular/ssr/test/npm_package:critters_license_test.accept + """, + file1 = ":THIRD_PARTY_LICENSES.txt.golden", + file2 = ":critters_license_file", +) + +write_file( + name = "critters_license_test.accept", + out = "critters_license_file_accept.sh", + content = + [ + "#!/usr/bin/env bash", + "cd ${BUILD_WORKSPACE_DIRECTORY}", + "yarn bazel build //packages/angular/ssr:npm_package", + "cp -fv dist/bin/packages/angular/ssr/npm_package/third_party/critters/THIRD_PARTY_LICENSES.txt packages/angular/ssr/test/npm_package/THIRD_PARTY_LICENSES.txt.golden", + ], + is_executable = True, ) diff --git a/packages/angular/ssr/test/npm_package/package_spec.ts b/packages/angular/ssr/test/npm_package/package_spec.ts index 2aadbe6540cc..c4f9fad7f402 100644 --- a/packages/angular/ssr/test/npm_package/package_spec.ts +++ b/packages/angular/ssr/test/npm_package/package_spec.ts @@ -6,15 +6,32 @@ * found in the LICENSE file at https://angular.dev/license */ -import { createPatch } from 'diff'; +import { runfiles } from '@bazel/runfiles'; import { existsSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; -import { join } from 'node:path'; -import { +import { dirname, join } from 'node:path'; + +/** + * Resolve paths for the Critters license file and the golden reference file. + */ +const ANGULAR_SSR_PACKAGE_PATH = dirname( + runfiles.resolve('angular_cli/packages/angular/ssr/npm_package/package.json'), +); + +/** + * Path to the actual license file for the Critters library. + * This file is located in the `third_party/critters` directory of the Angular CLI npm package. + */ +const CRITTERS_ACTUAL_LICENSE_FILE_PATH = join( ANGULAR_SSR_PACKAGE_PATH, - CRITTERS_ACTUAL_LICENSE_FILE_PATH, - CRITTERS_GOLDEN_LICENSE_FILE_PATH, -} from './utils'; + 'third_party/critters/THIRD_PARTY_LICENSES.txt', +); + +/** + * Path to the golden reference license file for the Critters library. + * This file is used as a reference for comparison and is located in the same directory as this script. + */ +const CRITTERS_GOLDEN_LICENSE_FILE_PATH = join(__dirname, 'THIRD_PARTY_LICENSES.txt.golden'); describe('NPM Package Tests', () => { it('should not include the contents of third_party/critters/index.js in the FESM bundle', async () => { @@ -27,36 +44,5 @@ describe('NPM Package Tests', () => { it('should exist', () => { expect(existsSync(CRITTERS_ACTUAL_LICENSE_FILE_PATH)).toBe(true); }); - - it('should match the expected golden file', async () => { - const [expectedContent, actualContent] = await Promise.all([ - readFile(CRITTERS_GOLDEN_LICENSE_FILE_PATH, 'utf-8'), - readFile(CRITTERS_ACTUAL_LICENSE_FILE_PATH, 'utf-8'), - ]); - - if (expectedContent.trim() === actualContent.trim()) { - return; - } - - const patch = createPatch( - CRITTERS_GOLDEN_LICENSE_FILE_PATH, - expectedContent, - actualContent, - 'Golden License File', - 'Current License File', - { context: 5 }, - ); - - const errorMessage = `The content of the actual license file differs from the expected golden reference. - Diff: - ${patch} - To accept the new golden file, execute: - yarn bazel run ${process.env['BAZEL_TARGET']}.accept - `; - - const error = new Error(errorMessage); - error.stack = error.stack?.replace(` Diff:\n ${patch}`, ''); - throw error; - }); }); }); diff --git a/packages/angular/ssr/test/npm_package/update-package-golden.ts b/packages/angular/ssr/test/npm_package/update-package-golden.ts deleted file mode 100644 index 0754d4d89249..000000000000 --- a/packages/angular/ssr/test/npm_package/update-package-golden.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import { readFileSync, writeFileSync } from 'node:fs'; -import { CRITTERS_ACTUAL_LICENSE_FILE_PATH, CRITTERS_GOLDEN_LICENSE_FILE_PATH } from './utils'; - -/** - * Updates the golden reference license file. - */ -writeFileSync(CRITTERS_GOLDEN_LICENSE_FILE_PATH, readFileSync(CRITTERS_ACTUAL_LICENSE_FILE_PATH)); diff --git a/packages/angular/ssr/test/npm_package/utils.ts b/packages/angular/ssr/test/npm_package/utils.ts deleted file mode 100644 index fc1a3adfc518..000000000000 --- a/packages/angular/ssr/test/npm_package/utils.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import { runfiles } from '@bazel/runfiles'; -import { dirname, join } from 'node:path'; - -/** - * Resolve paths for the Critters license file and the golden reference file. - */ -export const ANGULAR_SSR_PACKAGE_PATH = dirname( - runfiles.resolve('angular_cli/packages/angular/ssr/npm_package/package.json'), -); - -/** - * Path to the actual license file for the Critters library. - * This file is located in the `third_party/critters` directory of the Angular CLI npm package. - */ -export const CRITTERS_ACTUAL_LICENSE_FILE_PATH = join( - ANGULAR_SSR_PACKAGE_PATH, - 'third_party/critters/THIRD_PARTY_LICENSES.txt', -); - -/** - * Path to the golden reference license file for the Critters library. - * This file is used as a reference for comparison and is located in the same directory as this script. - */ -export const CRITTERS_GOLDEN_LICENSE_FILE_PATH = join(__dirname, 'THIRD_PARTY_LICENSES.txt.golden'); diff --git a/yarn.lock b/yarn.lock index 4e6d02c37ab2..2f7f0984ca0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -668,7 +668,6 @@ __metadata: "@rollup/plugin-node-resolve": "npm:^13.0.5" "@types/babel__core": "npm:7.20.5" "@types/browser-sync": "npm:^2.27.0" - "@types/diff": "npm:^5.2.1" "@types/express": "npm:^4.16.0" "@types/http-proxy": "npm:^1.17.4" "@types/ini": "npm:^4.0.0" @@ -708,7 +707,6 @@ __metadata: critters: "npm:0.0.24" css-loader: "npm:7.1.2" debug: "npm:^4.1.1" - diff: "npm:^5.2.0" esbuild: "npm:0.23.1" esbuild-wasm: "npm:0.23.1" eslint: "npm:8.57.0" @@ -969,7 +967,6 @@ __metadata: "@angular/platform-server": "npm:19.0.0-next.1" "@angular/router": "npm:19.0.0-next.1" "@bazel/runfiles": "npm:^5.8.1" - diff: "npm:^5.2.0" tslib: "npm:^2.3.0" zone.js: "npm:^0.15.0" peerDependencies: @@ -4871,13 +4868,6 @@ __metadata: languageName: node linkType: hard -"@types/diff@npm:^5.2.1": - version: 5.2.1 - resolution: "@types/diff@npm:5.2.1" - checksum: 10c0/62dcab32197ac67f212939cdd79aa3953327a482bec55c6a38ad9de8a0662a9f920b59504609a322fc242593bd9afb3d2704702f4bc98087a13171234b952361 - languageName: node - linkType: hard - "@types/estree@npm:*, @types/estree@npm:1.0.5, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.5": version: 1.0.5 resolution: "@types/estree@npm:1.0.5" @@ -8561,7 +8551,7 @@ __metadata: languageName: node linkType: hard -"diff@npm:^5.0.0, diff@npm:^5.1.0, diff@npm:^5.2.0": +"diff@npm:^5.0.0, diff@npm:^5.1.0": version: 5.2.0 resolution: "diff@npm:5.2.0" checksum: 10c0/aed0941f206fe261ecb258dc8d0ceea8abbde3ace5827518ff8d302f0fc9cc81ce116c4d8f379151171336caf0516b79e01abdc1ed1201b6440d895a66689eb4 From 617c2d9a33446e8016c18bbd8cda6b229e2e138f Mon Sep 17 00:00:00 2001 From: Angular Robot Date: Fri, 23 Aug 2024 20:17:02 +0000 Subject: [PATCH 09/11] build: update all non-major dependencies --- package.json | 4 +- .../angular_devkit/build_angular/package.json | 4 +- yarn.lock | 43 +++++++++++++------ 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 5e380d55cd2a..af9a1a0ea461 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "@angular/router": "19.0.0-next.1", "@angular/service-worker": "19.0.0-next.1", "@babel/core": "7.25.2", - "@babel/generator": "7.25.4", + "@babel/generator": "7.25.5", "@babel/helper-annotate-as-pure": "7.24.7", "@babel/helper-split-export-declaration": "7.24.7", "@babel/plugin-syntax-import-attributes": "7.24.7", @@ -197,7 +197,7 @@ "terser": "5.31.6", "tree-kill": "1.2.2", "ts-node": "^10.9.1", - "tslib": "2.6.3", + "tslib": "2.7.0", "typescript": "5.5.4", "undici": "6.19.8", "unenv": "^1.10.0", diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json index d15c14cc23b9..eab2e271bd33 100644 --- a/packages/angular_devkit/build_angular/package.json +++ b/packages/angular_devkit/build_angular/package.json @@ -12,7 +12,7 @@ "@angular-devkit/core": "0.0.0-PLACEHOLDER", "@angular/build": "0.0.0-PLACEHOLDER", "@babel/core": "7.25.2", - "@babel/generator": "7.25.4", + "@babel/generator": "7.25.5", "@babel/helper-annotate-as-pure": "7.24.7", "@babel/helper-split-export-declaration": "7.24.7", "@babel/plugin-transform-async-generator-functions": "7.25.4", @@ -60,7 +60,7 @@ "source-map-support": "0.5.21", "terser": "5.31.6", "tree-kill": "1.2.2", - "tslib": "2.6.3", + "tslib": "2.7.0", "vite": "5.4.2", "watchpack": "2.4.2", "webpack": "5.94.0", diff --git a/yarn.lock b/yarn.lock index 2f7f0984ca0e..76f985a75abf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -65,7 +65,7 @@ __metadata: "@angular-devkit/core": "npm:0.0.0-PLACEHOLDER" "@angular/build": "npm:0.0.0-PLACEHOLDER" "@babel/core": "npm:7.25.2" - "@babel/generator": "npm:7.25.4" + "@babel/generator": "npm:7.25.5" "@babel/helper-annotate-as-pure": "npm:7.24.7" "@babel/helper-split-export-declaration": "npm:7.24.7" "@babel/plugin-transform-async-generator-functions": "npm:7.25.4" @@ -114,7 +114,7 @@ __metadata: source-map-support: "npm:0.5.21" terser: "npm:5.31.6" tree-kill: "npm:1.2.2" - tslib: "npm:2.6.3" + tslib: "npm:2.7.0" undici: "npm:6.19.8" vite: "npm:5.4.2" watchpack: "npm:2.4.2" @@ -646,7 +646,7 @@ __metadata: "@angular/router": "npm:19.0.0-next.1" "@angular/service-worker": "npm:19.0.0-next.1" "@babel/core": "npm:7.25.2" - "@babel/generator": "npm:7.25.4" + "@babel/generator": "npm:7.25.5" "@babel/helper-annotate-as-pure": "npm:7.24.7" "@babel/helper-split-export-declaration": "npm:7.24.7" "@babel/plugin-syntax-import-attributes": "npm:7.24.7" @@ -773,7 +773,7 @@ __metadata: terser: "npm:5.31.6" tree-kill: "npm:1.2.2" ts-node: "npm:^10.9.1" - tslib: "npm:2.6.3" + tslib: "npm:2.7.0" typescript: "npm:5.5.4" undici: "npm:6.19.8" unenv: "npm:^1.10.0" @@ -1024,15 +1024,15 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:7.25.4, @babel/generator@npm:^7.25.4": - version: 7.25.4 - resolution: "@babel/generator@npm:7.25.4" +"@babel/generator@npm:7.25.5": + version: 7.25.5 + resolution: "@babel/generator@npm:7.25.5" dependencies: "@babel/types": "npm:^7.25.4" "@jridgewell/gen-mapping": "npm:^0.3.5" "@jridgewell/trace-mapping": "npm:^0.3.25" jsesc: "npm:^2.5.1" - checksum: 10c0/a2d8cc39e759214740f836360c8d9c17aa93e16e41afe73368a9e7ccd1d5c3303a420ce3aca1c9a31fdb93d1899de471d5aac97d1c64f741f8750a25a6e91fbc + checksum: 10c0/eb8af30c39476e4f4d6b953f355fcf092258291f78d65fb759b7d5e5e6fd521b5bfee64a4e2e4290279f0dcd25ccf8c49a61807828b99b5830d2b734506da1fd languageName: node linkType: hard @@ -1048,6 +1048,18 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.25.4": + version: 7.25.4 + resolution: "@babel/generator@npm:7.25.4" + dependencies: + "@babel/types": "npm:^7.25.4" + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.25" + jsesc: "npm:^2.5.1" + checksum: 10c0/a2d8cc39e759214740f836360c8d9c17aa93e16e41afe73368a9e7ccd1d5c3303a420ce3aca1c9a31fdb93d1899de471d5aac97d1c64f741f8750a25a6e91fbc + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:7.24.7, @babel/helper-annotate-as-pure@npm:^7.18.6, @babel/helper-annotate-as-pure@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-annotate-as-pure@npm:7.24.7" @@ -17176,10 +17188,10 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.6.3, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.5.2": - version: 2.6.3 - resolution: "tslib@npm:2.6.3" - checksum: 10c0/2598aef53d9dbe711af75522464b2104724d6467b26a60f2bdac8297d2b5f1f6b86a71f61717384aa8fd897240467aaa7bcc36a0700a0faf751293d1331db39a +"tslib@npm:2.7.0": + version: 2.7.0 + resolution: "tslib@npm:2.7.0" + checksum: 10c0/469e1d5bf1af585742128827000711efa61010b699cb040ab1800bcd3ccdd37f63ec30642c9e07c4439c1db6e46345582614275daca3e0f4abae29b0083f04a6 languageName: node linkType: hard @@ -17190,6 +17202,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.5.2": + version: 2.6.3 + resolution: "tslib@npm:2.6.3" + checksum: 10c0/2598aef53d9dbe711af75522464b2104724d6467b26a60f2bdac8297d2b5f1f6b86a71f61717384aa8fd897240467aaa7bcc36a0700a0faf751293d1331db39a + languageName: node + linkType: hard + "tsscmp@npm:1.0.6": version: 1.0.6 resolution: "tsscmp@npm:1.0.6" From 3fd7b68c38584acb3f4fc1033d4412e4b055c8e2 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Sat, 24 Aug 2024 06:50:49 +0000 Subject: [PATCH 10/11] test: disable `buildOptimizer` for server tests Attempting to reduce flaky tests. --- .../build_angular/src/builders/server/tests/setup.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/angular_devkit/build_angular/src/builders/server/tests/setup.ts b/packages/angular_devkit/build_angular/src/builders/server/tests/setup.ts index 4621a9a7135f..8172b22e63d1 100644 --- a/packages/angular_devkit/build_angular/src/builders/server/tests/setup.ts +++ b/packages/angular_devkit/build_angular/src/builders/server/tests/setup.ts @@ -28,4 +28,5 @@ export const BASE_OPTIONS = Object.freeze({ // Disable optimizations optimization: false, + buildOptimizer: false, }); From 1ac220d9bca49cedfc6834bf4b066b42764b2ce6 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Sat, 24 Aug 2024 06:56:47 +0000 Subject: [PATCH 11/11] Revert "build: mark server tests as flaky" This reverts commit 60d24b24c5e5993bc93fd4646c76056ec0c15244. --- packages/angular_devkit/build_angular/BUILD.bazel | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/angular_devkit/build_angular/BUILD.bazel b/packages/angular_devkit/build_angular/BUILD.bazel index f1173c0be635..0b0ac4a8e3d6 100644 --- a/packages/angular_devkit/build_angular/BUILD.bazel +++ b/packages/angular_devkit/build_angular/BUILD.bazel @@ -343,7 +343,6 @@ LARGE_SPECS = { "shards": 1, }, "server": { - "flaky": True, "extra_deps": [ "@npm//@angular/animations", ],