From 2b9c00f686145a8613dc2ce7f494193622e02625 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Wed, 29 Jan 2025 15:02:46 +0000 Subject: [PATCH 01/26] fix(@angular/build): prevent fallback to serving main.js for unknown requests Previously, when an unknown `main.js` file was requested, the system would automatically fall back to serving the default `main.js`. This behavior could cause unexpected issues, such as incorrect resource loading or misleading errors. This fix ensures that only valid `main.js` files are served, preventing unintended fallbacks and improving request handling. Closes #29524 (cherry picked from commit 9a46be8d68fbc5acf88f43916985f781db79bcf1) --- .../tests/behavior/build-assets_spec.ts | 14 +++++++++++++- .../build/src/builders/dev-server/vite-server.ts | 6 +++++- .../tools/vite/plugins/angular-memory-plugin.ts | 16 ++-------------- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/angular/build/src/builders/dev-server/tests/behavior/build-assets_spec.ts b/packages/angular/build/src/builders/dev-server/tests/behavior/build-assets_spec.ts index ef33e9e5ec0c..c1ee820d6f1b 100644 --- a/packages/angular/build/src/builders/dev-server/tests/behavior/build-assets_spec.ts +++ b/packages/angular/build/src/builders/dev-server/tests/behavior/build-assets_spec.ts @@ -142,7 +142,7 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT expect(await response?.headers.get('Location')).toBe('/login/'); }); - it('serves a JavaScript asset named as a bundle', async () => { + it('serves a JavaScript asset named as a bundle (main.js)', async () => { await harness.writeFile('public/test/main.js', javascriptFileContent); setupTarget(harness, { @@ -162,5 +162,17 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT expect(result?.success).toBeTrue(); expect(await response?.text()).toContain(javascriptFileContent); }); + + it('should return 404 when a JavaScript asset named as a bundle (main.js) does not exist', async () => { + setupTarget(harness, {}); + + harness.useTarget('serve', { + ...BASE_OPTIONS, + }); + + const { result, response } = await executeOnceAndFetch(harness, 'unknown/main.js'); + expect(result?.success).toBeTrue(); + expect(response?.status).toBe(404); + }); }); }); diff --git a/packages/angular/build/src/builders/dev-server/vite-server.ts b/packages/angular/build/src/builders/dev-server/vite-server.ts index fb4062625f47..e76573c30151 100644 --- a/packages/angular/build/src/builders/dev-server/vite-server.ts +++ b/packages/angular/build/src/builders/dev-server/vite-server.ts @@ -808,7 +808,11 @@ export async function setupServer( }, // This is needed when `externalDependencies` is used to prevent Vite load errors. // NOTE: If Vite adds direct support for externals, this can be removed. - preTransformRequests: externalMetadata.explicitBrowser.length === 0, + // NOTE: Vite breaks the resolution of browser modules in SSR + // when accessing a url with two or more segments (e.g., 'foo/bar'), + // as they are not re-based from the base href. + preTransformRequests: + externalMetadata.explicitBrowser.length === 0 && ssrMode === ServerSsrMode.NoSsr, }, ssr: { // Note: `true` and `/.*/` have different sematics. When true, the `external` option is ignored. diff --git a/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts b/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts index b585e05aa7e0..f8a4c004ec0e 100644 --- a/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts +++ b/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts @@ -62,24 +62,12 @@ export async function createAngularMemoryPlugin( return source; } - if (importer) { + if (importer && source[0] === '.') { const normalizedImporter = normalizePath(importer); - if (source[0] === '.' && normalizedImporter.startsWith(virtualProjectRoot)) { + if (normalizedImporter.startsWith(virtualProjectRoot)) { // Remove query if present const [importerFile] = normalizedImporter.split('?', 1); source = '/' + join(dirname(relative(virtualProjectRoot, importerFile)), source); - } else if ( - !ssr && - source[0] === '/' && - importer.endsWith('index.html') && - normalizedImporter.startsWith(virtualProjectRoot) - ) { - // This is only needed when using SSR and `angularSsrMiddleware` (old style) to correctly resolve - // .js files when using lazy-loading. - // Remove query if present - const [importerFile] = normalizedImporter.split('?', 1); - source = - '/' + join(dirname(relative(virtualProjectRoot, importerFile)), basename(source)); } } From 0eea8fc8214b6292f2483e9c1cfdf41142a62757 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Wed, 29 Jan 2025 18:16:44 +0000 Subject: [PATCH 02/26] refactor(@angular/build): remove unused import Remove unused import. (cherry picked from commit 249f9dc26caa4f3fe3cf9799588955cd27f51791) --- .../build/src/tools/vite/plugins/angular-memory-plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts b/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts index f8a4c004ec0e..2d7793dc41b0 100644 --- a/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts +++ b/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts @@ -8,7 +8,7 @@ import assert from 'node:assert'; import { readFile } from 'node:fs/promises'; -import { basename, dirname, join, relative } from 'node:path'; +import { dirname, join, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; import type { Plugin } from 'vite'; import { loadEsmModule } from '../../../utils/load-esm'; From 45abd15b781bb5bb067a7a52e7a809bb9d141c75 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Thu, 30 Jan 2025 10:38:39 +0000 Subject: [PATCH 03/26] fix(@angular/build): prevent server manifest generation when no server features are enabled This change ensures that the server manifest is not generated if none of the server-related features are enabled. Closes #29443 (cherry picked from commit 9b0d730871a3a17a2c5ba04f5941a3d0e4fa5845) --- .../build/src/builders/application/execute-post-bundle.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/angular/build/src/builders/application/execute-post-bundle.ts b/packages/angular/build/src/builders/application/execute-post-bundle.ts index d704c49eefca..5171ca254d5d 100644 --- a/packages/angular/build/src/builders/application/execute-post-bundle.ts +++ b/packages/angular/build/src/builders/application/execute-post-bundle.ts @@ -66,7 +66,7 @@ export async function executePostBundleSteps( const { baseHref = '/', serviceWorker, - i18nOptions, + ssrOptions, indexHtmlOptions, optimizationOptions, sourcemapOptions, @@ -113,7 +113,7 @@ export async function executePostBundleSteps( // Create server manifest const initialFilesPaths = new Set(initialFiles.keys()); - if (serverEntryPoint) { + if (serverEntryPoint && (outputMode || prerenderOptions || appShellOptions || ssrOptions)) { const { manifestContent, serverAssetsChunks } = generateAngularServerAppManifest( additionalHtmlOutputFiles, outputFiles, From f53523067355bcfea9bbc7f31981455ca7dbf798 Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Fri, 31 Jan 2025 15:09:02 +0000 Subject: [PATCH 04/26] ci: configure codeql locally within the repo to allow for customization Moving to the configuration being in the repo allows us to specify which specific rules are run in analysis. (cherry picked from commit 09cb935efb9dfb6575375fac9e184cd4fcfd9674) --- .github/workflows/codeql.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000000..b239c0cb13ed --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,35 @@ +name: 'CodeQL' + +on: + push: + branches: ['main', '*.*.x'] + schedule: + - cron: '39 9 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: 'ubuntu-latest' + permissions: + security-events: write + packages: read + strategy: + fail-fast: false + matrix: + include: + - language: javascript-typescript + build-mode: none + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - name: Initialize CodeQL + uses: github/codeql-action/init@1a7989f3955e0c69f0e0ccc14aee54a387a0fd31 #v3.28.8 + with: + languages: javascript-typescript + build-mode: none + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@86b04fb0e47484f7282357688f21d5d0e32175fe #v3.28.8 + with: + category: '/language:javascript-typescript' From 5bfbf2e157e77ea0ec30cbb935eb65edc0e8e50c Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Fri, 31 Jan 2025 15:24:10 +0000 Subject: [PATCH 05/26] ci: disable evalutations that arecausing codeql timeouts Disabling js/bad-code-sanitization and js/regex-injection because a recent update caused tons of timeouts and we don't have anything where cryptographic usage is ultra important in our use cases. (cherry picked from commit 65534522effd7e5da8cd800c74d554ffbb93d094) --- .github/codeql/config.yml | 8 ++++++++ .github/workflows/codeql.yml | 5 +---- 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 .github/codeql/config.yml diff --git a/.github/codeql/config.yml b/.github/codeql/config.yml new file mode 100644 index 000000000000..ad81a268eda4 --- /dev/null +++ b/.github/codeql/config.yml @@ -0,0 +1,8 @@ +name: 'Angular CLI CodeQL config' + +query-filters: + # TODO(josephperrott): reevaluate if these can be reenabled. + - exclude: + id: js/bad-code-sanitization + - exclude: + id: js/regex-injection diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b239c0cb13ed..99de6caf443e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -15,10 +15,6 @@ jobs: packages: read strategy: fail-fast: false - matrix: - include: - - language: javascript-typescript - build-mode: none steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -29,6 +25,7 @@ jobs: with: languages: javascript-typescript build-mode: none + config-file: .github/codeql/config.yml - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@86b04fb0e47484f7282357688f21d5d0e32175fe #v3.28.8 with: From 2dec1e74a3b6547dd725d9efaa0f4f1e671a81a3 Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Fri, 31 Jan 2025 18:16:00 +0000 Subject: [PATCH 06/26] ci: empty default permissions for CodeQL action (cherry picked from commit 64a1b1524b245e80dfaa0c7c459501a4b45df49c) --- .github/workflows/codeql.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 99de6caf443e..3f5f0e105f5b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -6,6 +6,8 @@ on: schedule: - cron: '39 9 * * 1' +permissions: {} + jobs: analyze: name: Analyze From 5bf5e5fd20e3c33a274a936dd1ce00e07b860226 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Thu, 30 Jan 2025 10:09:47 +0000 Subject: [PATCH 07/26] fix(@angular/ssr): prioritize the first matching route over subsequent ones Ensures that the SSR router gives precedence to the first matching route, addressing the issue where later conflicting routes. This change prevents the incorrect prioritization of routes and ensures the intended route is matched first, aligning routing behavior. Closes: #29539 (cherry picked from commit 6448f80bfb4a8900ca78857917314bd15fa4144d) --- packages/angular/ssr/src/routes/ng-routes.ts | 22 ++++++++---- .../angular/ssr/test/routes/ng-routes_spec.ts | 34 +++++++++++++++++++ 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/packages/angular/ssr/src/routes/ng-routes.ts b/packages/angular/ssr/src/routes/ng-routes.ts index 2e104054a814..0f7c270e6830 100644 --- a/packages/angular/ssr/src/routes/ng-routes.ts +++ b/packages/angular/ssr/src/routes/ng-routes.ts @@ -557,7 +557,6 @@ export async function getRoutesFromAngularRouterConfig( // Wait until the application is stable. await applicationRef.whenStable(); - const routesResults: RouteTreeNodeMetadata[] = []; const errors: string[] = []; let baseHref = @@ -581,11 +580,12 @@ export async function getRoutesFromAngularRouterConfig( if (errors.length) { return { baseHref, - routes: routesResults, + routes: [], errors, }; } + const routesResults: RouteTreeNodeMetadata[] = []; if (router.config.length) { // Retrieve all routes from the Angular router configuration. const traverseRoutes = traverseRoutesConfig({ @@ -599,11 +599,19 @@ export async function getRoutesFromAngularRouterConfig( entryPointToBrowserMapping, }); - for await (const result of traverseRoutes) { - if ('error' in result) { - errors.push(result.error); - } else { - routesResults.push(result); + const seenRoutes: Set = new Set(); + for await (const routeMetadata of traverseRoutes) { + if ('error' in routeMetadata) { + errors.push(routeMetadata.error); + continue; + } + + // If a result already exists for the exact same route, subsequent matches should be ignored. + // This aligns with Angular's app router behavior, which prioritizes the first route. + const routePath = routeMetadata.route; + if (!seenRoutes.has(routePath)) { + routesResults.push(routeMetadata); + seenRoutes.add(routePath); } } diff --git a/packages/angular/ssr/test/routes/ng-routes_spec.ts b/packages/angular/ssr/test/routes/ng-routes_spec.ts index 44d246e60a2f..6691f54d41a6 100644 --- a/packages/angular/ssr/test/routes/ng-routes_spec.ts +++ b/packages/angular/ssr/test/routes/ng-routes_spec.ts @@ -570,4 +570,38 @@ describe('extractRoutesAndCreateRouteTree', () => { expect(errors).toHaveSize(0); expect(routeTree.toObject()).toHaveSize(2); }); + + it('should give precedence to the first matching route over subsequent ones', async () => { + setAngularAppTestingManifest( + [ + { + path: '', + children: [ + { path: 'home', component: DummyComponent }, + { path: '**', component: DummyComponent }, + ], + }, + // The following routes should be ignored due to Angular's routing behavior: + // - ['', '**'] and ['**'] are equivalent, and the first match takes precedence. + // - ['', 'home'] and ['home'] are equivalent, and the first match takes precedence. + { + path: 'home', + redirectTo: 'never', + }, + { + path: '**', + redirectTo: 'never', + }, + ], + [{ path: '**', renderMode: RenderMode.Server }], + ); + + const { routeTree, errors } = await extractRoutesAndCreateRouteTree({ url }); + expect(errors).toHaveSize(0); + expect(routeTree.toObject()).toEqual([ + { route: '/', renderMode: RenderMode.Server }, + { route: '/home', renderMode: RenderMode.Server }, + { route: '/**', renderMode: RenderMode.Server }, + ]); + }); }); From 3f704267223d1881ea40e9de4e6381b9d0e43fe6 Mon Sep 17 00:00:00 2001 From: "ilir.beqiri" Date: Sat, 1 Feb 2025 15:35:43 +0100 Subject: [PATCH 08/26] fix(@schematics/angular): remove additional newline after standalone property (cherry picked from commit 4a5b76a8eee0bbbc4f08b568fee55ca22dff9927) --- .../__name@dasherize__.__type@dasherize__.ts.template | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 f787ad3acf8e..3c9ffd21516e 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 @@ -3,8 +3,7 @@ import { <% if(changeDetection !== 'Default') { %>ChangeDetectionStrategy, <% }% @Component({<% if(!skipSelector) {%> selector: '<%= selector %>',<%}%><% if(standalone) {%> imports: [],<%} else { %> - standalone: false, - <% }%><% if(inlineTemplate) { %> + standalone: false,<% }%><% if(inlineTemplate) { %> template: `

<%= dasherize(name) %> works! From 4b4eecaaa035fa1b6ea6e271488b99f2d231f317 Mon Sep 17 00:00:00 2001 From: Jan Martin Date: Mon, 3 Feb 2025 12:33:51 -0800 Subject: [PATCH 09/26] build: Preserve newlines when gathering stdout If the chunk happens to end in a new line or other meaningful whitespace, stripping it can lead to two words (e.g. targets) being squished together and broken. (cherry picked from commit c48a2da2878bb356dc337cf05e409ff0a7c8eb8d) --- scripts/build-schema.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-schema.mts b/scripts/build-schema.mts index 08ea76496735..c03a18c1b165 100644 --- a/scripts/build-schema.mts +++ b/scripts/build-schema.mts @@ -38,7 +38,7 @@ function _exec(cmd: string, captureStdout: boolean): Promise { proc.stdout.on('data', (data) => { console.info(data.toString().trim()); if (captureStdout) { - output += data.toString().trim(); + output += data.toString(); } }); proc.stderr.on('data', (data) => console.info(data.toString().trim())); From e9778dba0d75e7f528b600d51504a583485bd033 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Mon, 3 Feb 2025 22:32:07 +0000 Subject: [PATCH 10/26] fix(@schematics/angular): skip ssr migration when `@angular/ssr` is not a dependency This commit updates the `update-ssr-imports` migration to not run when the @angular/ssr` package is not listed as a dependency. Closes #29560 (cherry picked from commit c716ce15236ef9fe3f25b31a53a30b33c0a47c52) --- .../angular/migrations/update-ssr-imports/migration.ts | 5 +++++ .../migrations/update-ssr-imports/migration_spec.ts | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/packages/schematics/angular/migrations/update-ssr-imports/migration.ts b/packages/schematics/angular/migrations/update-ssr-imports/migration.ts index 847200791b61..7e17888a1c54 100644 --- a/packages/schematics/angular/migrations/update-ssr-imports/migration.ts +++ b/packages/schematics/angular/migrations/update-ssr-imports/migration.ts @@ -8,6 +8,7 @@ import { DirEntry, Rule, UpdateRecorder } from '@angular-devkit/schematics'; import * as ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript'; +import { getPackageJsonDependency } from '../../utility/dependencies'; function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { @@ -46,6 +47,10 @@ function* visit(directory: DirEntry): IterableIterator { */ export default function (): Rule { return (tree) => { + if (!getPackageJsonDependency(tree, '@angular/ssr')) { + return; + } + for (const sourceFile of visit(tree.root)) { let recorder: UpdateRecorder | undefined; diff --git a/packages/schematics/angular/migrations/update-ssr-imports/migration_spec.ts b/packages/schematics/angular/migrations/update-ssr-imports/migration_spec.ts index 6cbc7ebbee6e..9c8919b0febe 100644 --- a/packages/schematics/angular/migrations/update-ssr-imports/migration_spec.ts +++ b/packages/schematics/angular/migrations/update-ssr-imports/migration_spec.ts @@ -19,6 +19,14 @@ describe('CommonEngine migration', () => { let tree: UnitTestTree; beforeEach(() => { tree = new UnitTestTree(new EmptyTree()); + tree.create( + 'package.json', + JSON.stringify({ + dependencies: { + '@angular/ssr': '0.0.0', + }, + }), + ); }); function runMigration(): Promise { From de66572ea23df13dfeb371d9462ddfe8d6b2ea5c Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 31 Jan 2025 12:36:21 +0000 Subject: [PATCH 11/26] refactor(@angular/ssr): simplify preload append logic in metadata Replace `filter` and `map` with a `for...of` loop to improve readability in the preload append logic within metadata. (cherry picked from commit 25dbe7cfc193d844d8247435846cf5ce92a6cf5c) --- packages/angular/ssr/src/routes/ng-routes.ts | 28 +++++++++++++------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/angular/ssr/src/routes/ng-routes.ts b/packages/angular/ssr/src/routes/ng-routes.ts index 0f7c270e6830..ee31072d6b0e 100644 --- a/packages/angular/ssr/src/routes/ng-routes.ts +++ b/packages/angular/ssr/src/routes/ng-routes.ts @@ -281,21 +281,31 @@ function appendPreloadToMetadata( metadata: ServerConfigRouteTreeNodeMetadata, includeDynamicImports: boolean, ): void { - if (!entryPointToBrowserMapping) { + const existingPreloads = metadata.preload ?? []; + if (!entryPointToBrowserMapping || existingPreloads.length >= MODULE_PRELOAD_MAX) { return; } const preload = entryPointToBrowserMapping[entryName]; + if (!preload?.length) { + return; + } + + // Merge existing preloads with new ones, ensuring uniqueness and limiting the total to the maximum allowed. + const combinedPreloads: Set = new Set(existingPreloads); + for (const { dynamicImport, path } of preload) { + if (dynamicImport && !includeDynamicImports) { + continue; + } + + combinedPreloads.add(path); - if (preload?.length) { - // Merge existing preloads with new ones, ensuring uniqueness and limiting the total to the maximum allowed. - const preloadPaths = - preload - .filter(({ dynamicImport }) => includeDynamicImports || !dynamicImport) - .map(({ path }) => path) ?? []; - const combinedPreloads = [...(metadata.preload ?? []), ...preloadPaths]; - metadata.preload = Array.from(new Set(combinedPreloads)).slice(0, MODULE_PRELOAD_MAX); + if (combinedPreloads.size === MODULE_PRELOAD_MAX) { + break; + } } + + metadata.preload = Array.from(combinedPreloads); } /** From 58c2122fda84fa4388ca9ec6a0390f3eb40a2a72 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 28 Jan 2025 18:08:22 -0500 Subject: [PATCH 12/26] refactor(@angular/build): allow component update invalidation from client If HMR is enabled, a component update has the potential to be unsupported at runtime or may cause an exception. While build time analysis attempts to verify that an update is possible, there could be cases that are as of yet unknown. For those cases, the runtime can now signal this information back to the development server which will clear the errant component update and trigger a full page reload. This action will be logged to the development server console along with an optional message from the client. (cherry picked from commit 9525eee739848af52ea998c5d4bc13827bcdaf55) --- .../src/builders/dev-server/vite-server.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/angular/build/src/builders/dev-server/vite-server.ts b/packages/angular/build/src/builders/dev-server/vite-server.ts index e76573c30151..89b93d2a8f51 100644 --- a/packages/angular/build/src/builders/dev-server/vite-server.ts +++ b/packages/angular/build/src/builders/dev-server/vite-server.ts @@ -457,6 +457,41 @@ export async function* serveWithVite( } }); + // Setup component HMR invalidation + // Invalidation occurs when the runtime cannot update a component + server.hot.on( + 'angular:invalidate', + (data: { id: string; message?: string; error?: boolean }) => { + if (typeof data?.id !== 'string') { + context.logger.warn( + 'Development server client sent invalid internal invalidate event.', + ); + } + + // Clear invalid template update + templateUpdates.delete(data.id); + + // Some cases are expected unsupported update scenarios but some may be errors. + // If an error occurred, log the error in addition to the invalidation. + if (data.error) { + context.logger.error( + `Component update failed${data.message ? `: ${data.message}` : '.'}` + + '\nPlease consider reporting the error at https://github.com/angular/angular-cli/issues', + ); + } else { + context.logger.warn( + `Component update unsupported${data.message ? `: ${data.message}` : '.'}`, + ); + } + + server?.ws.send({ + type: 'full-reload', + path: '*', + }); + context.logger.info('Page reload sent to client(s).'); + }, + ); + const urls = server.resolvedUrls; if (urls && (urls.local.length || urls.network.length)) { serverUrl = new URL(urls.local[0] ?? urls.network[0]); From 27f8331865de35044ddeda7a8c05bb2700b0be6a Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 4 Feb 2025 12:41:02 -0500 Subject: [PATCH 13/26] fix(@angular/build): avoid pre-transform errors with Vite pre-bundling Vite 6.0 change the option location of the `preTransformRequests` to the `dev` section of the Vite configuration. While the previous `server` section option of the same name is still present, it currently does not change behavior when configured. (cherry picked from commit 5c1360179cec2f0fad6b2adb4a8e4d6930738976) --- .../behavior/build-external-dependencies_spec.ts | 16 ++++++++++++++++ .../build/src/builders/dev-server/vite-server.ts | 16 +++++++++------- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/angular/build/src/builders/dev-server/tests/behavior/build-external-dependencies_spec.ts b/packages/angular/build/src/builders/dev-server/tests/behavior/build-external-dependencies_spec.ts index 583f988ba12a..476ea0cec47a 100644 --- a/packages/angular/build/src/builders/dev-server/tests/behavior/build-external-dependencies_spec.ts +++ b/packages/angular/build/src/builders/dev-server/tests/behavior/build-external-dependencies_spec.ts @@ -83,5 +83,21 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT expect(text).toContain(`import { BehaviorSubject } from "rxjs";`); expect(text).toContain(`import { map } from "rxjs/operators";`); }); + + // TODO: Enable when Vite has a custom logger setup to redirect logging into the builder system + xit('does not show pre-transform errors in the console for external dependencies', async () => { + harness.useTarget('serve', { + ...BASE_OPTIONS, + }); + + const { result, logs } = await executeOnceAndFetch(harness, 'main.js'); + + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('Pre-transform error'), + }), + ); + }); }); }); diff --git a/packages/angular/build/src/builders/dev-server/vite-server.ts b/packages/angular/build/src/builders/dev-server/vite-server.ts index 89b93d2a8f51..7ac866ad893a 100644 --- a/packages/angular/build/src/builders/dev-server/vite-server.ts +++ b/packages/angular/build/src/builders/dev-server/vite-server.ts @@ -803,6 +803,15 @@ export async function setupServer( mainFields: ['es2020', 'browser', 'module', 'main'], preserveSymlinks, }, + dev: { + // This is needed when `externalDependencies` is used to prevent Vite load errors. + // NOTE: If Vite adds direct support for externals, this can be removed. + // NOTE: Vite breaks the resolution of browser modules in SSR + // when accessing a url with two or more segments (e.g., 'foo/bar'), + // as they are not re-based from the base href. + preTransformRequests: + externalMetadata.explicitBrowser.length === 0 && ssrMode === ServerSsrMode.NoSsr, + }, server: { warmup: { ssrFiles, @@ -841,13 +850,6 @@ export async function setupServer( ...[...assets.values()].map(({ source }) => source), ], }, - // This is needed when `externalDependencies` is used to prevent Vite load errors. - // NOTE: If Vite adds direct support for externals, this can be removed. - // NOTE: Vite breaks the resolution of browser modules in SSR - // when accessing a url with two or more segments (e.g., 'foo/bar'), - // as they are not re-based from the base href. - preTransformRequests: - externalMetadata.explicitBrowser.length === 0 && ssrMode === ServerSsrMode.NoSsr, }, ssr: { // Note: `true` and `/.*/` have different sematics. When true, the `external` option is ignored. From 8f6ee7ed933ea7394e14fe46d141427839008040 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:39:17 -0500 Subject: [PATCH 14/26] fix(@angular/build): ensure full rebuild after initial error build in watch mode If an initial build of an application results in an error during watch mode (including `ng serve`), the following non-error rebuild will now always be a full build result. This ensures that all new files are available for later incremental build result updates. (cherry picked from commit b24089ef8630e028883b097d57c9246b6ef085ed) --- .../src/builders/application/build-action.ts | 9 ++- .../tests/behavior/build-errors_spec.ts | 66 +++++++++++++++++++ .../src/builders/dev-server/vite-server.ts | 2 + .../e2e/tests/build/jit-ngmodule.ts | 6 ++ 4 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 packages/angular/build/src/builders/dev-server/tests/behavior/build-errors_spec.ts diff --git a/packages/angular/build/src/builders/application/build-action.ts b/packages/angular/build/src/builders/application/build-action.ts index 1236c6887855..0ded5bb5ef28 100644 --- a/packages/angular/build/src/builders/application/build-action.ts +++ b/packages/angular/build/src/builders/application/build-action.ts @@ -152,6 +152,10 @@ export async function* runEsBuildBuildAction( return; } + // Used to force a full result on next rebuild if there were initial errors. + // This ensures at least one full result is emitted. + let hasInitialErrors = result.errors.length > 0; + // Wait for changes and rebuild as needed const currentWatchFiles = new Set(result.watchFiles); try { @@ -201,10 +205,13 @@ export async function* runEsBuildBuildAction( result, outputOptions, changes, - incrementalResults ? rebuildState : undefined, + incrementalResults && !hasInitialErrors ? rebuildState : undefined, )) { yield outputResult; } + + // Clear initial build errors flag if no errors are now present + hasInitialErrors &&= result.errors.length > 0; } } finally { // Stop the watcher and cleanup incremental rebuild state diff --git a/packages/angular/build/src/builders/dev-server/tests/behavior/build-errors_spec.ts b/packages/angular/build/src/builders/dev-server/tests/behavior/build-errors_spec.ts new file mode 100644 index 000000000000..82467da0d249 --- /dev/null +++ b/packages/angular/build/src/builders/dev-server/tests/behavior/build-errors_spec.ts @@ -0,0 +1,66 @@ +/** + * @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 { concatMap, count, take, timeout } from 'rxjs'; +import { executeDevServer } from '../../index'; +import { describeServeBuilder } from '../jasmine-helpers'; +import { BASE_OPTIONS, BUILD_TIMEOUT, DEV_SERVER_BUILDER_INFO } from '../setup'; +import { logging } from '@angular-devkit/core'; + +describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget) => { + describe('Behavior: "Rebuild Error Detection"', () => { + beforeEach(() => { + setupTarget(harness); + }); + + it('Emits full build result with incremental enabled and initial build has errors', async () => { + harness.useTarget('serve', { + ...BASE_OPTIONS, + watch: true, + }); + + // Missing ending `>` on the div will cause an error + await harness.appendToFile('src/app/app.component.html', '

Hello, world! { + switch (index) { + case 0: + expect(result?.success).toBeFalse(); + debugger; + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('Unexpected character "EOF"'), + }), + ); + + await harness.appendToFile('src/app/app.component.html', '>'); + + break; + case 1: + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('Unexpected character "EOF"'), + }), + ); + break; + } + }), + take(2), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(2); + }); + }); +}); diff --git a/packages/angular/build/src/builders/dev-server/vite-server.ts b/packages/angular/build/src/builders/dev-server/vite-server.ts index 7ac866ad893a..d704e3a5161f 100644 --- a/packages/angular/build/src/builders/dev-server/vite-server.ts +++ b/packages/angular/build/src/builders/dev-server/vite-server.ts @@ -213,6 +213,8 @@ export async function* serveWithVite( }, }); } + + yield { baseUrl: '', success: false }; continue; } // Clear existing error overlay on successful result diff --git a/tests/legacy-cli/e2e/tests/build/jit-ngmodule.ts b/tests/legacy-cli/e2e/tests/build/jit-ngmodule.ts index a66311f0e27f..8d24b35e4e0f 100644 --- a/tests/legacy-cli/e2e/tests/build/jit-ngmodule.ts +++ b/tests/legacy-cli/e2e/tests/build/jit-ngmodule.ts @@ -32,6 +32,12 @@ export default async function () { }; } + // Remove bundle budgets due to the increased size from JIT + build.configurations.production = { + ...build.configurations.production, + budgets: undefined, + }; + build.options.aot = false; }); // Test it works From f4de3d228fd1fe25ffd179623f9945e32e462f1b Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Wed, 5 Feb 2025 21:45:36 +0000 Subject: [PATCH 15/26] release: cut the v19.1.6 release --- .../npm_translate_lock_MzA5NzUwNzMx | 2 +- CHANGELOG.md | 28 +++++++++++++++++++ package.json | 2 +- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/.aspect/rules/external_repository_action_cache/npm_translate_lock_MzA5NzUwNzMx b/.aspect/rules/external_repository_action_cache/npm_translate_lock_MzA5NzUwNzMx index 37fad62bbec4..7830a5c17b0b 100755 --- a/.aspect/rules/external_repository_action_cache/npm_translate_lock_MzA5NzUwNzMx +++ b/.aspect/rules/external_repository_action_cache/npm_translate_lock_MzA5NzUwNzMx @@ -3,7 +3,7 @@ # This file should be checked into version control along with the pnpm-lock.yaml file. .npmrc=-1406867100 modules/testing/builder/package.json=973445093 -package.json=1299651399 +package.json=-458933914 packages/angular/build/package.json=954937711 packages/angular/cli/package.json=349838588 packages/angular/pwa/package.json=-1352285148 diff --git a/CHANGELOG.md b/CHANGELOG.md index a0246cae326c..0fdff0aaa725 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ + + +# 19.1.6 (2025-02-05) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------- | +| [3f7042672](https://github.com/angular/angular-cli/commit/3f704267223d1881ea40e9de4e6381b9d0e43fe6) | fix | remove additional newline after standalone property | +| [e9778dba0](https://github.com/angular/angular-cli/commit/e9778dba0d75e7f528b600d51504a583485bd033) | fix | skip ssr migration when `@angular/ssr` is not a dependency | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------------- | +| [27f833186](https://github.com/angular/angular-cli/commit/27f8331865de35044ddeda7a8c05bb2700b0be6a) | fix | avoid pre-transform errors with Vite pre-bundling | +| [8f6ee7ed9](https://github.com/angular/angular-cli/commit/8f6ee7ed933ea7394e14fe46d141427839008040) | fix | ensure full rebuild after initial error build in watch mode | +| [2b9c00f68](https://github.com/angular/angular-cli/commit/2b9c00f686145a8613dc2ce7f494193622e02625) | fix | prevent fallback to serving main.js for unknown requests | +| [45abd15b7](https://github.com/angular/angular-cli/commit/45abd15b781bb5bb067a7a52e7a809bb9d141c75) | fix | prevent server manifest generation when no server features are enabled | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [5bf5e5fd2](https://github.com/angular/angular-cli/commit/5bf5e5fd20e3c33a274a936dd1ce00e07b860226) | fix | prioritize the first matching route over subsequent ones | + + + # 19.1.5 (2025-01-29) diff --git a/package.json b/package.json index 1a5c33cda15c..358107453b18 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "19.1.5", + "version": "19.1.6", "private": true, "description": "Software Development Kit for Angular", "keywords": [ From f5d97457622897b41e73a859dd1f218fa962be15 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Thu, 6 Feb 2025 19:29:14 +0000 Subject: [PATCH 16/26] fix(@angular/ssr): accurately calculate content length for static pages with `\r\n` JS engines convert `\r\n` to `\n` in template literals, potentially leading to incorrect byte length calculations. This fix ensures the correct content length is determined. Closes #29567 (cherry picked from commit 414736bc0f56ea3b5c1a32ed54da7da4c5c3320e) --- .../build/src/utils/server-rendering/manifest.ts | 12 ++++++++++-- .../server-routes-output-mode-server.ts | 9 ++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/angular/build/src/utils/server-rendering/manifest.ts b/packages/angular/build/src/utils/server-rendering/manifest.ts index a8be8d833efa..4d1459e221c2 100644 --- a/packages/angular/build/src/utils/server-rendering/manifest.ts +++ b/packages/angular/build/src/utils/server-rendering/manifest.ts @@ -8,6 +8,7 @@ import type { Metafile } from 'esbuild'; import { extname } from 'node:path'; +import { runInThisContext } from 'node:vm'; import { NormalizedApplicationBuildOptions } from '../../builders/application/options'; import { type BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context'; import { createOutputFile } from '../../tools/esbuild/utils'; @@ -139,20 +140,27 @@ export function generateAngularServerAppManifest( } { const serverAssetsChunks: BuildOutputFile[] = []; const serverAssets: Record = {}; + for (const file of [...additionalHtmlOutputFiles.values(), ...outputFiles]) { const extension = extname(file.path); if (extension === '.html' || (inlineCriticalCss && extension === '.css')) { const jsChunkFilePath = `assets-chunks/${file.path.replace(/[./]/g, '_')}.mjs`; + const escapedContent = escapeUnsafeChars(file.text); + serverAssetsChunks.push( createOutputFile( jsChunkFilePath, - `export default \`${escapeUnsafeChars(file.text)}\`;`, + `export default \`${escapedContent}\`;`, BuildOutputFileType.ServerApplication, ), ); + // This is needed because JavaScript engines script parser convert `\r\n` to `\n` in template literals, + // which can result in an incorrect byte length. + const size = runInThisContext(`new TextEncoder().encode(\`${escapedContent}\`).byteLength`); + serverAssets[file.path] = - `{size: ${file.size}, hash: '${file.hash}', text: () => import('./${jsChunkFilePath}').then(m => m.default)}`; + `{size: ${size}, hash: '${file.hash}', text: () => import('./${jsChunkFilePath}').then(m => m.default)}`; } } diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts index 2d14c0ceecbb..822b9ea9bb7e 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts @@ -1,7 +1,7 @@ import { join } from 'node:path'; import { existsSync } from 'node:fs'; import assert from 'node:assert'; -import { expectFileToMatch, writeFile } from '../../../utils/fs'; +import { expectFileToMatch, readFile, replaceInFile, writeFile } from '../../../utils/fs'; import { execAndWaitForOutputToMatch, ng, noSilentNg, silentNg } from '../../../utils/process'; import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages'; import { useSha } from '../../../utils/project'; @@ -20,6 +20,12 @@ export default async function () { await useSha(); await installWorkspacePackages(); + // Test scenario to verify that the content length, including \r\n, is accurate + await replaceInFile('src/app/app.component.ts', "title = '", "title = 'Title\\r\\n"); + + // Ensure text has been updated. + assert.match(await readFile('src/app/app.component.ts'), /title = 'Title/); + // Add routes await writeFile( 'src/app/app.routes.ts', @@ -165,6 +171,7 @@ export default async function () { const port = await spawnServer(); for (const [pathname, { content, headers, serverContext }] of Object.entries(responseExpects)) { + // NOTE: A global 'UND_ERR_SOCKET' may occur due to an incorrect Content-Length header value. const res = await fetch(`http://localhost:${port}${pathname}`); const text = await res.text(); From bf939ebf12bee34bc742305de3cba50a60319e99 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Thu, 6 Feb 2025 19:47:36 +0000 Subject: [PATCH 17/26] refactor(@angular/build): also add `server.preTransformRequests` https://github.com/angular/angular-cli/commit/5c1360179cec2f0fad6b2adb4a8e4d6930738976 moved `preTransformRequests` from the `server` to `dev` section. But vite, still uses the `server` section in such cases https://github.com/vitejs/vite/blob/bcdb51a1ac082f4e8ed6f820787d6745dfaa972d/packages/vite/src/node/server/index.ts#L673 and https://github.com/vitejs/vite/blob/bcdb51a1ac082f4e8ed6f820787d6745dfaa972d/packages/vite/src/node/server/middlewares/indexHtml.ts#L475 (cherry picked from commit 17a7b8cf014e873f847b8d8d1661b6349029c9c5) --- .../src/builders/dev-server/vite-server.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/angular/build/src/builders/dev-server/vite-server.ts b/packages/angular/build/src/builders/dev-server/vite-server.ts index d704e3a5161f..8bd8ab58fe87 100644 --- a/packages/angular/build/src/builders/dev-server/vite-server.ts +++ b/packages/angular/build/src/builders/dev-server/vite-server.ts @@ -734,6 +734,7 @@ function updateResultRecord( } } +// eslint-disable-next-line max-lines-per-function export async function setupServer( serverOptions: NormalizedDevServerOptions, outputFiles: Map, @@ -776,6 +777,15 @@ export async function setupServer( break; } + /** + * Required when using `externalDependencies` to prevent Vite load errors. + * + * @note Can be removed if Vite introduces native support for externals. + * @note Vite misresolves browser modules in SSR when accessing URLs with multiple segments + * (e.g., 'foo/bar'), as they are not correctly re-based from the base href. + */ + const preTransformRequests = + externalMetadata.explicitBrowser.length === 0 && ssrMode === ServerSsrMode.NoSsr; const cacheDir = join(serverOptions.cacheOptions.path, serverOptions.buildTarget.project, 'vite'); const configuration: InlineConfig = { configFile: false, @@ -806,15 +816,10 @@ export async function setupServer( preserveSymlinks, }, dev: { - // This is needed when `externalDependencies` is used to prevent Vite load errors. - // NOTE: If Vite adds direct support for externals, this can be removed. - // NOTE: Vite breaks the resolution of browser modules in SSR - // when accessing a url with two or more segments (e.g., 'foo/bar'), - // as they are not re-based from the base href. - preTransformRequests: - externalMetadata.explicitBrowser.length === 0 && ssrMode === ServerSsrMode.NoSsr, + preTransformRequests, }, server: { + preTransformRequests, warmup: { ssrFiles, }, From a13a49d95be61d2a2458962d57318f301dede502 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Mon, 10 Feb 2025 13:40:38 +0000 Subject: [PATCH 18/26] fix(@angular/build): exclude unmodified files from logs with `--localize` Ensures that only modified files are displayed in logs when using the `--localize` flag, preventing unnecessary noise. Closes #29586 (cherry picked from commit 880a50c50cafb3ab2e5713aed0c4a20be6648ced) --- .../build/src/builders/application/execute-build.ts | 5 ++--- packages/angular/build/src/tools/esbuild/utils.ts | 7 ++++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/angular/build/src/builders/application/execute-build.ts b/packages/angular/build/src/builders/application/execute-build.ts index d6e121a6bd20..51351c3aa372 100644 --- a/packages/angular/build/src/builders/application/execute-build.ts +++ b/packages/angular/build/src/builders/application/execute-build.ts @@ -184,9 +184,6 @@ export async function executeBuild( executionResult.outputFiles.push(...outputFiles); - const changedFiles = - rebuildState && executionResult.findChangedFiles(rebuildState.previousOutputInfo); - // Analyze files for bundle budget failures if present let budgetFailures: BudgetCalculatorResult[] | undefined; if (options.budgets) { @@ -288,6 +285,8 @@ export async function executeBuild( } if (!jsonLogs) { + const changedFiles = + rebuildState && executionResult.findChangedFiles(rebuildState.previousOutputInfo); executionResult.addLog( logBuildStats( metafile, diff --git a/packages/angular/build/src/tools/esbuild/utils.ts b/packages/angular/build/src/tools/esbuild/utils.ts index 3e62c6e7bb1a..6b8e44def11d 100644 --- a/packages/angular/build/src/tools/esbuild/utils.ts +++ b/packages/angular/build/src/tools/esbuild/utils.ts @@ -40,6 +40,11 @@ export function logBuildStats( ssrOutputEnabled?: boolean, verbose?: boolean, ): string { + // Remove the i18n subpath in case the build is using i18n. + // en-US/main.js -> main.js + const normalizedChangedFiles: Set = new Set( + [...(changedFiles ?? [])].map((f) => basename(f)), + ); const browserStats: BundleStats[] = []; const serverStats: BundleStats[] = []; let unchangedCount = 0; @@ -52,7 +57,7 @@ export function logBuildStats( } // Show only changed files if a changed list is provided - if (changedFiles && !changedFiles.has(file)) { + if (normalizedChangedFiles.size && !normalizedChangedFiles.has(file)) { ++unchangedCount; continue; } From de73b1c0c2d5748818d2e94f93f2640d4c6b949c Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Mon, 10 Feb 2025 13:59:47 +0000 Subject: [PATCH 19/26] fix(@schematics/angular): include default export for Express app This update is required for Firebase functions compatibility. Closes #29488 (cherry picked from commit aa0ae457b0f2fe9ad76b52aaca08044cfaf5eff9) --- .../files/application-builder-common-engine/server.ts.template | 2 ++ .../angular/ssr/files/application-builder/server.ts.template | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/schematics/angular/ssr/files/application-builder-common-engine/server.ts.template b/packages/schematics/angular/ssr/files/application-builder-common-engine/server.ts.template index 7e8b13608374..63a70ae893f6 100644 --- a/packages/schematics/angular/ssr/files/application-builder-common-engine/server.ts.template +++ b/packages/schematics/angular/ssr/files/application-builder-common-engine/server.ts.template @@ -63,3 +63,5 @@ if (isMainModule(import.meta.url)) { console.log(`Node Express server listening on http://localhost:${port}`); }); } + +export default app; diff --git a/packages/schematics/angular/ssr/files/application-builder/server.ts.template b/packages/schematics/angular/ssr/files/application-builder/server.ts.template index 877173580ff0..1d07f023a713 100644 --- a/packages/schematics/angular/ssr/files/application-builder/server.ts.template +++ b/packages/schematics/angular/ssr/files/application-builder/server.ts.template @@ -61,6 +61,6 @@ if (isMainModule(import.meta.url)) { } /** - * The request handler used by the Angular CLI (dev-server and during build). + * Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions. */ export const reqHandler = createNodeRequestHandler(app); From 0826315fac1c3fd2d22aa0ea544bd59ef9ed8781 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Mon, 10 Feb 2025 12:47:05 +0000 Subject: [PATCH 20/26] fix(@angular/build): handle unlocalizable files correctly in localized prerender Ensure proper handling of unlocalizable files during localized prerendering to prevent errors. Closes #29587 (cherry picked from commit b5530698962a0421e882f60e2975026cf348e795) --- .../angular/build/src/builders/application/i18n.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/angular/build/src/builders/application/i18n.ts b/packages/angular/build/src/builders/application/i18n.ts index f526286e35a7..151694173246 100644 --- a/packages/angular/build/src/builders/application/i18n.ts +++ b/packages/angular/build/src/builders/application/i18n.ts @@ -64,6 +64,11 @@ export async function inlineI18n( // For each active locale, use the inliner to process the output files of the build. const updatedOutputFiles = []; const updatedAssetFiles = []; + // Root and SSR entry files are not modified. + const unModifiedOutputFiles = executionResult.outputFiles.filter( + ({ type }) => type === BuildOutputFileType.Root || type === BuildOutputFileType.ServerRoot, + ); + try { for (const locale of i18nOptions.inlineLocales) { // A locale specific set of files is returned from the inliner. @@ -87,7 +92,7 @@ export async function inlineI18n( ...options, baseHref: getLocaleBaseHref(baseHref, i18nOptions, locale) ?? baseHref, }, - localeOutputFiles, + [...unModifiedOutputFiles, ...localeOutputFiles], executionResult.assetFiles, initialFiles, locale, @@ -124,9 +129,7 @@ export async function inlineI18n( // Update the result with all localized files. executionResult.outputFiles = [ // Root and SSR entry files are not modified. - ...executionResult.outputFiles.filter( - ({ type }) => type === BuildOutputFileType.Root || type === BuildOutputFileType.ServerRoot, - ), + ...unModifiedOutputFiles, // Updated files for each locale. ...updatedOutputFiles, ]; From 8890a5f76c252fe383a632880df476e5f63ef931 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 4 Feb 2025 13:19:11 -0500 Subject: [PATCH 21/26] fix(@angular/build): always provide Vite client helpers with development server In addition to the WebSocket code, the Vite client module contains helper functions which may be injected into modules at request time. These helpers are required for certain behavior to function. Previously, when `--no-live-reload` was used, these helpers may not have been available which led to runtime errors. These runtime errors will no longer occur. However, the browser console will now log that the Vite client cannot connect to the development server WebSocket. This is expected in this case since live reload functionality was disabled and the server side is intentionally not available. (cherry picked from commit beefed839f782216c9e4ee28673a95b6be8fb26c) --- .../src/builders/dev-server/vite-server.ts | 2 +- .../vite/plugins/angular-memory-plugin.ts | 20 +++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/angular/build/src/builders/dev-server/vite-server.ts b/packages/angular/build/src/builders/dev-server/vite-server.ts index 8bd8ab58fe87..fcdf5e42934a 100644 --- a/packages/angular/build/src/builders/dev-server/vite-server.ts +++ b/packages/angular/build/src/builders/dev-server/vite-server.ts @@ -898,7 +898,7 @@ export async function setupServer( outputFiles, templateUpdates, external: externalMetadata.explicitBrowser, - skipViteClient: serverOptions.liveReload === false && serverOptions.hmr === false, + disableViteTransport: !serverOptions.liveReload, }), ], // Browser only optimizeDeps. (This does not run for SSR dependencies). diff --git a/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts b/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts index 2d7793dc41b0..036d2c400b2a 100644 --- a/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts +++ b/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts @@ -19,7 +19,7 @@ interface AngularMemoryPluginOptions { outputFiles: AngularMemoryOutputFiles; templateUpdates?: ReadonlyMap; external?: string[]; - skipViteClient?: boolean; + disableViteTransport?: boolean; } const ANGULAR_PREFIX = '/@ng/'; @@ -91,7 +91,7 @@ export async function createAngularMemoryPlugin( const codeContents = outputFiles.get(relativeFile)?.contents; if (codeContents === undefined) { if (relativeFile.endsWith('/node_modules/vite/dist/client/client.mjs')) { - return options.skipViteClient ? '' : loadViteClientCode(file); + return loadViteClientCode(file, options.disableViteTransport); } return undefined; @@ -118,9 +118,9 @@ export async function createAngularMemoryPlugin( * @param file The absolute path to the Vite client code. * @returns */ -async function loadViteClientCode(file: string): Promise { +async function loadViteClientCode(file: string, disableViteTransport = false): Promise { const originalContents = await readFile(file, 'utf-8'); - const updatedContents = originalContents.replace( + let updatedContents = originalContents.replace( `"You can also disable this overlay by setting ", h("code", { part: "config-option-name" }, "server.hmr.overlay"), " to ", @@ -133,5 +133,17 @@ async function loadViteClientCode(file: string): Promise { assert(originalContents !== updatedContents, 'Failed to update Vite client error overlay text.'); + if (disableViteTransport) { + const previousUpdatedContents = updatedContents; + + updatedContents = updatedContents.replace('transport.connect(handleMessage)', ''); + assert( + previousUpdatedContents !== updatedContents, + 'Failed to update Vite client WebSocket disable.', + ); + + updatedContents = updatedContents.replace('console.debug("[vite] connecting...")', ''); + } + return updatedContents; } From c26ea1619095102b21176435af826cf53f0054b1 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Mon, 10 Feb 2025 12:20:52 +0000 Subject: [PATCH 22/26] fix(@angular/ssr): properly handle baseHref with protocol Enhances handling of `baseHref` when it includes a full URL with a protocol. Closes #29590 (cherry picked from commit 833dc986dbfd8902c0cf6ce9c8eeea9d759a25ce) --- .../src/utils/server-rendering/prerender.ts | 8 +++++--- packages/angular/ssr/src/routes/ng-routes.ts | 7 ++----- .../angular/ssr/test/routes/ng-routes_spec.ts | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/angular/build/src/utils/server-rendering/prerender.ts b/packages/angular/build/src/utils/server-rendering/prerender.ts index 5ffbedd94c08..76b9de2bc2fe 100644 --- a/packages/angular/build/src/utils/server-rendering/prerender.ts +++ b/packages/angular/build/src/utils/server-rendering/prerender.ts @@ -228,12 +228,14 @@ async function renderPages( try { const renderingPromises: Promise[] = []; const appShellRouteWithLeadingSlash = appShellRoute && addLeadingSlash(appShellRoute); - const baseHrefWithLeadingSlash = addLeadingSlash(baseHref); + const baseHrefPathnameWithLeadingSlash = new URL(baseHref, 'http://localhost').pathname; for (const { route, redirectTo } of serializableRouteTreeNode) { // Remove the base href from the file output path. - const routeWithoutBaseHref = addTrailingSlash(route).startsWith(baseHrefWithLeadingSlash) - ? addLeadingSlash(route.slice(baseHrefWithLeadingSlash.length)) + const routeWithoutBaseHref = addTrailingSlash(route).startsWith( + baseHrefPathnameWithLeadingSlash, + ) + ? addLeadingSlash(route.slice(baseHrefPathnameWithLeadingSlash.length)) : route; const outPath = posix.join(removeLeadingSlash(routeWithoutBaseHref), 'index.html'); diff --git a/packages/angular/ssr/src/routes/ng-routes.ts b/packages/angular/ssr/src/routes/ng-routes.ts index ee31072d6b0e..b3f00f7a2617 100644 --- a/packages/angular/ssr/src/routes/ng-routes.ts +++ b/packages/angular/ssr/src/routes/ng-routes.ts @@ -569,13 +569,10 @@ export async function getRoutesFromAngularRouterConfig( const errors: string[] = []; - let baseHref = + const rawBaseHref = injector.get(APP_BASE_HREF, null, { optional: true }) ?? injector.get(PlatformLocation).getBaseHrefFromDOM(); - - if (baseHref.startsWith('./')) { - baseHref = baseHref.slice(2); - } + const { pathname: baseHref } = new URL(rawBaseHref, 'http://localhost'); const compiler = injector.get(Compiler); const serverRoutesConfig = injector.get(SERVER_ROUTES_CONFIG, null, { optional: true }); diff --git a/packages/angular/ssr/test/routes/ng-routes_spec.ts b/packages/angular/ssr/test/routes/ng-routes_spec.ts index 6691f54d41a6..276e26dba93c 100644 --- a/packages/angular/ssr/test/routes/ng-routes_spec.ts +++ b/packages/angular/ssr/test/routes/ng-routes_spec.ts @@ -510,6 +510,25 @@ describe('extractRoutesAndCreateRouteTree', () => { ]); }); + it('handles a baseHref starting with a protocol', async () => { + setAngularAppTestingManifest( + [{ path: 'home', component: DummyComponent }], + [{ path: '**', renderMode: RenderMode.Server }], + /** baseHref*/ 'http://foo.com/example/', + ); + + const { routeTree, errors } = await extractRoutesAndCreateRouteTree({ + url, + invokeGetPrerenderParams: true, + includePrerenderFallbackRoutes: true, + }); + + expect(errors).toHaveSize(0); + expect(routeTree.toObject()).toEqual([ + { route: '/example/home', renderMode: RenderMode.Server }, + ]); + }); + it('should not bootstrap the root component', async () => { @Component({ standalone: true, From 36afdf51317ad87e698cf5d3edb3a16e3f71d55a Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Tue, 11 Feb 2025 10:47:30 +0000 Subject: [PATCH 23/26] refactor(@angular/build): remove outdated `allowedHosts` warning The warning is no longer accurate, as `allowedHosts` is now used in Vite. (cherry picked from commit 8faaf51d61d7e4ab6be247e5dc40f10558a6cf2e) --- .../build_angular/src/builders/dev-server/builder.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts index e27413816904..aa5b84f324aa 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts @@ -67,12 +67,6 @@ export function execute( ); } - if (options.allowedHosts?.length) { - context.logger.warn( - `The "allowedHosts" option will not be used because it is not supported by the "${builderName}" builder.`, - ); - } - if (options.publicHost) { context.logger.warn( `The "publicHost" option will not be used because it is not supported by the "${builderName}" builder.`, From d2e1c8e9f5c03a410d8204a5f9b11b4ad9cc9eaa Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Tue, 11 Feb 2025 19:13:59 +0000 Subject: [PATCH 24/26] perf(@angular/build): cache translated i18n bundles for faster builds When disk caching is enabled, translated i18n bundles are stored on disk, improving performance and speeding up both incremental and non-incremental builds. (cherry picked from commit b50b6ee920165d8a2fbfdeb57376ca21aed4a91a) --- .../build/src/builders/application/i18n.ts | 3 +- .../build/src/tools/esbuild/i18n-inliner.ts | 128 ++++++++++++++---- 2 files changed, 105 insertions(+), 26 deletions(-) diff --git a/packages/angular/build/src/builders/application/i18n.ts b/packages/angular/build/src/builders/application/i18n.ts index 151694173246..478a8893ca10 100644 --- a/packages/angular/build/src/builders/application/i18n.ts +++ b/packages/angular/build/src/builders/application/i18n.ts @@ -39,7 +39,7 @@ export async function inlineI18n( warnings: string[]; prerenderedRoutes: PrerenderedRoutesRecord; }> { - const { i18nOptions, optimizationOptions, baseHref } = options; + const { i18nOptions, optimizationOptions, baseHref, cacheOptions } = options; // Create the multi-threaded inliner with common options and the files generated from the build. const inliner = new I18nInliner( @@ -47,6 +47,7 @@ export async function inlineI18n( missingTranslation: i18nOptions.missingTranslationBehavior ?? 'warning', outputFiles: executionResult.outputFiles, shouldOptimize: optimizationOptions.scripts, + persistentCachePath: cacheOptions.enabled ? cacheOptions.path : undefined, }, maxWorkers, ); diff --git a/packages/angular/build/src/tools/esbuild/i18n-inliner.ts b/packages/angular/build/src/tools/esbuild/i18n-inliner.ts index 2fcb8e6dbd55..e2fac79709b7 100644 --- a/packages/angular/build/src/tools/esbuild/i18n-inliner.ts +++ b/packages/angular/build/src/tools/esbuild/i18n-inliner.ts @@ -7,8 +7,11 @@ */ import assert from 'node:assert'; +import { createHash } from 'node:crypto'; +import { extname, join } from 'node:path'; import { WorkerPool } from '../../utils/worker-pool'; import { BuildOutputFile, BuildOutputFileType } from './bundler-context'; +import type { LmbdCacheStore } from './lmdb-cache-store'; import { createOutputFile } from './utils'; /** @@ -24,6 +27,7 @@ export interface I18nInlinerOptions { missingTranslation: 'error' | 'warning' | 'ignore'; outputFiles: BuildOutputFile[]; shouldOptimize?: boolean; + persistentCachePath?: string; } /** @@ -33,26 +37,30 @@ export interface I18nInlinerOptions { * localize function (`$localize`). */ export class I18nInliner { + #cacheInitFailed = false; #workerPool: WorkerPool; - readonly #localizeFiles: ReadonlyMap; + #cache: LmbdCacheStore | undefined; + readonly #localizeFiles: ReadonlyMap; readonly #unmodifiedFiles: Array; - readonly #fileToType = new Map(); - constructor(options: I18nInlinerOptions, maxThreads?: number) { + constructor( + private readonly options: I18nInlinerOptions, + maxThreads?: number, + ) { this.#unmodifiedFiles = []; + const { outputFiles, shouldOptimize, missingTranslation } = options; + const files = new Map(); - const files = new Map(); const pendingMaps = []; - for (const file of options.outputFiles) { + for (const file of outputFiles) { if (file.type === BuildOutputFileType.Root || file.type === BuildOutputFileType.ServerRoot) { // Skip also the server entry-point. // Skip stats and similar files. continue; } - this.#fileToType.set(file.path, file.type); - - if (file.path.endsWith('.js') || file.path.endsWith('.mjs')) { + const fileExtension = extname(file.path); + if (fileExtension === '.js' || fileExtension === '.mjs') { // Check if localizations are present const contentBuffer = Buffer.isBuffer(file.contents) ? file.contents @@ -60,15 +68,11 @@ export class I18nInliner { const hasLocalize = contentBuffer.includes(LOCALIZE_KEYWORD); if (hasLocalize) { - // A Blob is an immutable data structure that allows sharing the data between workers - // without copying until the data is actually used within a Worker. This is useful here - // since each file may not actually be processed in each Worker and the Blob avoids - // unneeded repeat copying of potentially large JavaScript files. - files.set(file.path, new Blob([file.contents])); + files.set(file.path, file); continue; } - } else if (file.path.endsWith('.js.map')) { + } else if (fileExtension === '.map') { // The related JS file may not have been checked yet. To ensure that map files are not // missed, store any pending map files and check them after all output files. pendingMaps.push(file); @@ -81,7 +85,7 @@ export class I18nInliner { // Check if any pending map files should be processed by checking if the parent JS file is present for (const file of pendingMaps) { if (files.has(file.path.slice(0, -4))) { - files.set(file.path, new Blob([file.contents])); + files.set(file.path, file); } else { this.#unmodifiedFiles.push(file); } @@ -94,9 +98,15 @@ export class I18nInliner { maxThreads, // Extract options to ensure only the named options are serialized and sent to the worker workerData: { - missingTranslation: options.missingTranslation, - shouldOptimize: options.shouldOptimize, - files, + missingTranslation, + shouldOptimize, + // A Blob is an immutable data structure that allows sharing the data between workers + // without copying until the data is actually used within a Worker. This is useful here + // since each file may not actually be processed in each Worker and the Blob avoids + // unneeded repeat copying of potentially large JavaScript files. + files: new Map( + Array.from(files, ([name, file]) => [name, new Blob([file.contents])]), + ), }, }); } @@ -113,19 +123,54 @@ export class I18nInliner { locale: string, translation: Record | undefined, ): Promise<{ outputFiles: BuildOutputFile[]; errors: string[]; warnings: string[] }> { + await this.initCache(); + + const { shouldOptimize, missingTranslation } = this.options; // Request inlining for each file that contains localize calls const requests = []; - for (const filename of this.#localizeFiles.keys()) { + + let fileCacheKeyBase: Uint8Array | undefined; + + for (const [filename, file] of this.#localizeFiles) { + let cacheKey: string | undefined; if (filename.endsWith('.map')) { continue; } - const fileRequest = this.#workerPool.run({ - filename, - locale, - translation, + let cacheResultPromise = Promise.resolve(null); + if (this.#cache) { + fileCacheKeyBase ??= Buffer.from( + JSON.stringify({ locale, translation, missingTranslation, shouldOptimize }), + 'utf-8', + ); + + // NOTE: If additional options are added, this may need to be updated. + // TODO: Consider xxhash or similar instead of SHA256 + cacheKey = createHash('sha256') + .update(file.hash) + .update(filename) + .update(fileCacheKeyBase) + .digest('hex'); + + // Failure to get the value should not fail the transform + cacheResultPromise = this.#cache.get(cacheKey).catch(() => null); + } + + const fileResult = cacheResultPromise.then(async (cachedResult) => { + if (cachedResult) { + return cachedResult; + } + + const result = await this.#workerPool.run({ filename, locale, translation }); + if (this.#cache && cacheKey) { + // Failure to set the value should not fail the transform + await this.#cache.set(cacheKey, result).catch(() => {}); + } + + return result; }); - requests.push(fileRequest); + + requests.push(fileResult); } // Wait for all file requests to complete @@ -136,7 +181,7 @@ export class I18nInliner { const warnings: string[] = []; const outputFiles = [ ...rawResults.flatMap(({ file, code, map, messages }) => { - const type = this.#fileToType.get(file); + const type = this.#localizeFiles.get(file)?.type; assert(type !== undefined, 'localized file should always have a type' + file); const resultFiles = [createOutputFile(file, code, type)]; @@ -171,4 +216,37 @@ export class I18nInliner { close(): Promise { return this.#workerPool.destroy(); } + + /** + * Initializes the cache for storing translated bundles. + * If the cache is already initialized, it does nothing. + * + * @returns A promise that resolves once the cache initialization process is complete. + */ + private async initCache(): Promise { + if (this.#cache || this.#cacheInitFailed) { + return; + } + + const { persistentCachePath } = this.options; + // Webcontainers currently do not support this persistent cache store. + if (!persistentCachePath || process.versions.webcontainer) { + return; + } + + // Initialize a persistent cache for i18n transformations. + try { + const { LmbdCacheStore } = await import('./lmdb-cache-store'); + + this.#cache = new LmbdCacheStore(join(persistentCachePath, 'angular-i18n.db')); + } catch { + this.#cacheInitFailed = true; + + // eslint-disable-next-line no-console + console.warn( + 'Unable to initialize JavaScript cache storage.\n' + + 'This will not affect the build output content but may result in slower builds.', + ); + } + } } From df1d388465b6f0d3aab5fb4f011cbbe74d3058f4 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Wed, 12 Feb 2025 09:42:28 +0000 Subject: [PATCH 25/26] fix(@angular/build): configure Vite CORS option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vite's `allowedHosts` option does not enable CORS; instead, it allows the dev server to respond to requests with a matching hostname (e.g., http://example.com/main.js). It only verifies that the request’s hostname is on the allowed list. However, this does not consider the `origin` in the case of a CORS request. This commit updates Vite's configuration to enable CORS. Closes #29549 (cherry picked from commit be15b886c75d0ed9834aef38690d3169fcf16ef5) --- packages/angular/build/src/builders/dev-server/schema.json | 4 ++-- packages/angular/build/src/builders/dev-server/vite-server.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/angular/build/src/builders/dev-server/schema.json b/packages/angular/build/src/builders/dev-server/schema.json index 3d5cf155aae2..c36d8614e4ea 100644 --- a/packages/angular/build/src/builders/dev-server/schema.json +++ b/packages/angular/build/src/builders/dev-server/schema.json @@ -37,12 +37,12 @@ "description": "SSL certificate to use for serving HTTPS." }, "allowedHosts": { - "description": "The hosts that can access the development server. This option sets the Vite option of the same name. For further details: https://vite.dev/config/server-options.html#server-allowedhosts", + "description": "The hosts that the development server will respond to. This option sets the Vite option of the same name. For further details: https://vite.dev/config/server-options.html#server-allowedhosts", "default": [], "oneOf": [ { "type": "array", - "description": "List of hosts that are allowed to access the development server.", + "description": "A list of hosts that the development server will respond to.", "items": { "type": "string" } diff --git a/packages/angular/build/src/builders/dev-server/vite-server.ts b/packages/angular/build/src/builders/dev-server/vite-server.ts index fcdf5e42934a..6773e8945c21 100644 --- a/packages/angular/build/src/builders/dev-server/vite-server.ts +++ b/packages/angular/build/src/builders/dev-server/vite-server.ts @@ -841,6 +841,9 @@ export async function setupServer( ? (proxy ?? {}) : proxy, cors: { + // This will add the header `Access-Control-Allow-Origin: http://example.com`, + // where `http://example.com` is the requesting origin. + origin: true, // Allow preflight requests to be proxied. preflightContinue: true, }, From 90cf0f803cf166091fce39a6590330263c7b4db2 Mon Sep 17 00:00:00 2001 From: Doug Parker Date: Wed, 12 Feb 2025 12:01:14 -0800 Subject: [PATCH 26/26] release: cut the v19.1.7 release --- .../npm_translate_lock_MzA5NzUwNzMx | 2 +- CHANGELOG.md | 29 +++++++++++++++++++ package.json | 2 +- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/.aspect/rules/external_repository_action_cache/npm_translate_lock_MzA5NzUwNzMx b/.aspect/rules/external_repository_action_cache/npm_translate_lock_MzA5NzUwNzMx index 7830a5c17b0b..328e96b4f03d 100755 --- a/.aspect/rules/external_repository_action_cache/npm_translate_lock_MzA5NzUwNzMx +++ b/.aspect/rules/external_repository_action_cache/npm_translate_lock_MzA5NzUwNzMx @@ -3,7 +3,7 @@ # This file should be checked into version control along with the pnpm-lock.yaml file. .npmrc=-1406867100 modules/testing/builder/package.json=973445093 -package.json=-458933914 +package.json=2077448069 packages/angular/build/package.json=954937711 packages/angular/cli/package.json=349838588 packages/angular/pwa/package.json=-1352285148 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fdff0aaa725..bdf46cc8c665 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ + + +# 19.1.7 (2025-02-12) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------- | +| [de73b1c0c](https://github.com/angular/angular-cli/commit/de73b1c0c2d5748818d2e94f93f2640d4c6b949c) | fix | include default export for Express app | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------- | +| [8890a5f76](https://github.com/angular/angular-cli/commit/8890a5f76c252fe383a632880df476e5f63ef931) | fix | always provide Vite client helpers with development server | +| [df1d38846](https://github.com/angular/angular-cli/commit/df1d388465b6f0d3aab5fb4f011cbbe74d3058f4) | fix | configure Vite CORS option | +| [a13a49d95](https://github.com/angular/angular-cli/commit/a13a49d95be61d2a2458962d57318f301dede502) | fix | exclude unmodified files from logs with `--localize` | +| [0826315fa](https://github.com/angular/angular-cli/commit/0826315fac1c3fd2d22aa0ea544bd59ef9ed8781) | fix | handle unlocalizable files correctly in localized prerender | +| [d2e1c8e9f](https://github.com/angular/angular-cli/commit/d2e1c8e9f5c03a410d8204a5f9b11b4ad9cc9eaa) | perf | cache translated i18n bundles for faster builds | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------- | +| [f5d974576](https://github.com/angular/angular-cli/commit/f5d97457622897b41e73a859dd1f218fa962be15) | fix | accurately calculate content length for static pages with `\r\n` | +| [c26ea1619](https://github.com/angular/angular-cli/commit/c26ea1619095102b21176435af826cf53f0054b1) | fix | properly handle baseHref with protocol | + + + # 19.1.6 (2025-02-05) diff --git a/package.json b/package.json index 358107453b18..452b0d206f1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "19.1.6", + "version": "19.1.7", "private": true, "description": "Software Development Kit for Angular", "keywords": [