From 91b93d852d04ca01488572903826a2793cc06b8a Mon Sep 17 00:00:00 2001 From: "Houston (Jeb's AI)" Date: Mon, 27 Apr 2026 06:49:55 +0000 Subject: [PATCH 1/2] fix(jest): default isolatedModules to true for faster compilation (fixes #1899) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Angular 19 introduced signals, new control flow syntax, and standalone-by-default components. These produce significantly more complex TypeScript that causes the ts-jest language service (used when isolatedModules: false) to build a full cross-file Program per test file, resulting in severe slowdowns (reports of 2 min → 15 min test runs). Root cause: when isolatedModules is false (previously the implicit default), ts-jest instantiates a TypeScript LanguageService and rebuilds a full Program for each file. Angular 19+'s richer type surface makes this path prohibitively slow. Fix: set isolatedModules: true in the builder's default transformer options. This switches ts-jest to its fast per-file transpile path, matching the recommendation from jest-preset-angular's own example apps since v14.4.0. Cross-file type checking is better served by tsc --noEmit or ng build. Users who need the previous behaviour can opt out in their jest.config.ts: transform: { '^.+\.(ts|js|mjs|html|svg)$': ['jest-preset-angular', { isolatedModules: false }] } BREAKING CHANGE: isolatedModules now defaults to true. This disables cross-file TypeScript type checking during jest runs. Targeted for the next major version. --- packages/jest/src/default-config.resolver.spec.ts | 2 ++ packages/jest/src/default-config.resolver.ts | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/jest/src/default-config.resolver.spec.ts b/packages/jest/src/default-config.resolver.spec.ts index 2235f8f3db..26ff41ec69 100644 --- a/packages/jest/src/default-config.resolver.spec.ts +++ b/packages/jest/src/default-config.resolver.spec.ts @@ -14,6 +14,7 @@ describe('Resolve project default configuration', () => { { stringifyContentPathRegex: '\\.(html|svg)$', tsconfig: getSystemPath(normalize(`/some/cool/directory/${tsConfigName}`)), + isolatedModules: true, }, ]); }); @@ -28,6 +29,7 @@ describe('Resolve project default configuration', () => { { stringifyContentPathRegex: '\\.(html|svg)$', tsconfig: getSystemPath(normalize(`/some/cool/project/ts-configs/tsconfig.spec.json`)), + isolatedModules: true, }, ]); }); diff --git a/packages/jest/src/default-config.resolver.ts b/packages/jest/src/default-config.resolver.ts index f9d950c19e..022bce8234 100644 --- a/packages/jest/src/default-config.resolver.ts +++ b/packages/jest/src/default-config.resolver.ts @@ -47,6 +47,16 @@ export class DefaultConfigResolver { stringifyContentPathRegex: '\\.(html|svg)$', // Join with the default `tsConfigName` if the `tsConfig` option is not provided tsconfig: getTsConfigPath(projectRoot, this.options), + // Default to isolatedModules: true for significantly faster compilation. + // With isolatedModules: false (the previous implicit default), ts-jest uses the + // TypeScript language service to build a full cross-file Program for every test + // file, which becomes extremely slow with Angular 19+ code (signals, new control + // flow, standalone-by-default). Angular 19+ users report 2min → 15min regressions. + // Cross-file type checking is better handled by `tsc --noEmit` or `ng build`. + // Users who need the old behaviour can opt out via their jest.config.ts: + // transform: { '...': ['jest-preset-angular', { isolatedModules: false }] } + // BREAKING CHANGE: targeted for the next major version. + isolatedModules: true, }, ], }, From 977cbb1596038decf30975dd16f181005fc5ec3e Mon Sep 17 00:00:00 2001 From: "Houston (Jeb's AI)" Date: Mon, 27 Apr 2026 08:08:45 +0000 Subject: [PATCH 2/2] docs(custom-esbuild): document target.configuration in plugin and indexHtmlTransformer (fixes #1899) - Add target.configuration to factory plugin example in README (alongside existing target.project) - Update indexHtmlTransformer examples (JS and TS) to include the target param with comments - Add integration test 'target-options-in-plugin-and-transformer' that builds with a real factory plugin and indexHtmlTransformer, both using target.configuration, then verifies the value appears in the JS bundle and index.html meta tag - Add esbuild/define-configuration-plugin.js and esbuild/configuration-transformer.js to sanity-esbuild-app as the test fixtures --- .../sanity-esbuild-app/angular.json | 10 ++++++++- .../esbuild/configuration-transformer.js | 10 +++++++++ .../esbuild/define-configuration-plugin.js | 21 +++++++++++++++++++ .../sanity-esbuild-app/package.json | 5 +++-- .../src/app/app.component.html | 1 + .../src/app/app.component.ts | 3 +++ packages/custom-esbuild/README.md | 13 ++++++++++-- packages/custom-esbuild/tests/integration.js | 10 +++++++++ 8 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 examples/custom-esbuild/sanity-esbuild-app/esbuild/configuration-transformer.js create mode 100644 examples/custom-esbuild/sanity-esbuild-app/esbuild/define-configuration-plugin.js diff --git a/examples/custom-esbuild/sanity-esbuild-app/angular.json b/examples/custom-esbuild/sanity-esbuild-app/angular.json index bf46ee4396..068af97671 100644 --- a/examples/custom-esbuild/sanity-esbuild-app/angular.json +++ b/examples/custom-esbuild/sanity-esbuild-app/angular.json @@ -80,6 +80,14 @@ } } ] + }, + "target-options-test": { + "plugins": [ + "esbuild/define-configuration-plugin.js" + ], + "indexHtmlTransformer": "esbuild/configuration-transformer.js", + "optimization": false, + "outputHashing": "none" } }, "defaultConfiguration": "production" @@ -206,4 +214,4 @@ "typeSeparator": "." } } -} +} \ No newline at end of file diff --git a/examples/custom-esbuild/sanity-esbuild-app/esbuild/configuration-transformer.js b/examples/custom-esbuild/sanity-esbuild-app/esbuild/configuration-transformer.js new file mode 100644 index 0000000000..63bb308295 --- /dev/null +++ b/examples/custom-esbuild/sanity-esbuild-app/esbuild/configuration-transformer.js @@ -0,0 +1,10 @@ +/** + * Index HTML transformer for issues #1690 / #1710 test. + * Signature: (indexHtml: string, target: Target) => string + * Injects a tag with the configuration name so we can verify it in the output file. + */ +module.exports = function configurationTransformer(indexHtml, target) { + const configuration = target.configuration ?? 'default'; + const metaTag = ``; + return indexHtml.replace('', ` ${metaTag}\n`); +}; diff --git a/examples/custom-esbuild/sanity-esbuild-app/esbuild/define-configuration-plugin.js b/examples/custom-esbuild/sanity-esbuild-app/esbuild/define-configuration-plugin.js new file mode 100644 index 0000000000..8a5bb7e45a --- /dev/null +++ b/examples/custom-esbuild/sanity-esbuild-app/esbuild/define-configuration-plugin.js @@ -0,0 +1,21 @@ +/** + * Factory plugin (Pattern 1: string path in angular.json). + * Receives (builderOptions, target) from the builder. + * Injects `target.configuration` as a global define constant `buildConfiguration`. + * + * This is the test for issues #1710 and #1690: + * verifying that target.configuration is accessible inside a factory plugin. + */ +function defineConfigurationPlugin(builderOptions, target) { + const configuration = target.configuration ?? 'default'; + return { + name: 'define-configuration', + setup(build) { + const options = build.initialOptions; + options.define = options.define || {}; + options.define.buildConfiguration = JSON.stringify(configuration); + }, + }; +} + +module.exports = defineConfigurationPlugin; diff --git a/examples/custom-esbuild/sanity-esbuild-app/package.json b/examples/custom-esbuild/sanity-esbuild-app/package.json index fd76748c4a..ff4c824081 100644 --- a/examples/custom-esbuild/sanity-esbuild-app/package.json +++ b/examples/custom-esbuild/sanity-esbuild-app/package.json @@ -8,7 +8,8 @@ "lint": "ng lint", "e2e": "ng e2e", "cypress:open": "cypress open", - "cypress:run": "cypress run" + "cypress:run": "cypress run", + "build-target-options": "ng build --configuration target-options-test && node -e \"const fs = require('fs');const dist = 'dist/sanity-esbuild-app/browser';const indexHtml = fs.readFileSync(dist + '/index.html', 'utf8');const jsFiles = fs.readdirSync(dist).filter(f => f.endsWith('.js'));const mainJs = jsFiles.map(f => fs.readFileSync(dist + '/' + f, 'utf8')).join('');if (!indexHtml.includes('content=\\\"target-options-test\\\"')) { console.error('FAIL: indexHtmlTransformer did not inject configuration into index.html'); process.exit(1); }if (!mainJs.includes('target-options-test')) { console.error('FAIL: plugin did not inject buildConfiguration into JS bundle'); process.exit(1); }console.log('PASS: target.configuration is accessible in both plugin and indexHtmlTransformer');\"" }, "private": true, "dependencies": { @@ -42,4 +43,4 @@ "typescript-eslint": "8.59.0", "vitest": "4.1.5" } -} +} \ No newline at end of file diff --git a/examples/custom-esbuild/sanity-esbuild-app/src/app/app.component.html b/examples/custom-esbuild/sanity-esbuild-app/src/app/app.component.html index 7034c8f302..7e7a14d708 100644 --- a/examples/custom-esbuild/sanity-esbuild-app/src/app/app.component.html +++ b/examples/custom-esbuild/sanity-esbuild-app/src/app/app.component.html @@ -1,3 +1,4 @@

{{ title }}

{{ subtitle }}

{{ titleByOption }}

+

{{ buildConfiguration }}

diff --git a/examples/custom-esbuild/sanity-esbuild-app/src/app/app.component.ts b/examples/custom-esbuild/sanity-esbuild-app/src/app/app.component.ts index b878467344..ca0d751028 100644 --- a/examples/custom-esbuild/sanity-esbuild-app/src/app/app.component.ts +++ b/examples/custom-esbuild/sanity-esbuild-app/src/app/app.component.ts @@ -3,6 +3,7 @@ import { Component } from '@angular/core'; declare const title: string; declare const subtitle: string; declare const titleByOption: string; +declare const buildConfiguration: string; @Component({ selector: 'app-root', @@ -14,10 +15,12 @@ export class AppComponent { title: string; subtitle: string; titleByOption: string; + buildConfiguration: string; constructor() { this.title = typeof title !== 'undefined' ? title : 'sanity-esbuild-app'; this.subtitle = typeof subtitle !== 'undefined' ? subtitle : 'sanity-esbuild-app subtitle'; this.titleByOption = typeof titleByOption !== 'undefined' ? titleByOption : 'sanity-esbuild-app optionTitle'; + this.buildConfiguration = typeof buildConfiguration !== 'undefined' ? buildConfiguration : 'no-config'; } } diff --git a/packages/custom-esbuild/README.md b/packages/custom-esbuild/README.md index 7fcaf14728..931877908c 100644 --- a/packages/custom-esbuild/README.md +++ b/packages/custom-esbuild/README.md @@ -202,7 +202,10 @@ export default (builderOptions: ApplicationBuilderOptions, target: Target): Plug name: 'define-text', setup(build: PluginBuild) { const options = build.initialOptions; + // target.project is the Angular project name (e.g. "my-app") options.define.currentProject = JSON.stringify(target.project); + // target.configuration is the active build configuration (e.g. "production", "staging") + options.define.currentConfiguration = JSON.stringify(target.configuration ?? 'default'); }, }; }; @@ -296,7 +299,9 @@ It is useful when you want to transform your `index.html` according to the build `index-html-transformer.js`: ```js -module.exports = indexHtml => { +module.exports = (indexHtml, target) => { + // target.configuration is the active build configuration (e.g. "production", "staging") + // target.project is the Angular project name const i = indexHtml.indexOf(''); const content = `

Dynamically inserted content

`; return `${indexHtml.slice(0, i)} @@ -308,7 +313,11 @@ module.exports = indexHtml => { Alternatively, using TypeScript: ```ts -export default (indexHtml: string) => { +import type { Target } from '@angular-devkit/architect'; + +export default (indexHtml: string, target: Target) => { + // target.configuration is the active build configuration (e.g. "production", "staging") + // target.project is the Angular project name const i = indexHtml.indexOf(''); const content = `

Dynamically inserted content

`; return `${indexHtml.slice(0, i)} diff --git a/packages/custom-esbuild/tests/integration.js b/packages/custom-esbuild/tests/integration.js index 4932b6ed73..ea0acdbce3 100644 --- a/packages/custom-esbuild/tests/integration.js +++ b/packages/custom-esbuild/tests/integration.js @@ -88,4 +88,14 @@ module.exports = [ app: 'examples/custom-esbuild/sanity-esbuild-app-esm', command: 'yarn build-ts -c tsEsm', }, + + // Target options accessibility (#1690, #1710) + { + id: 'target-options-in-plugin-and-transformer', + name: 'custom-esbuild: target.configuration accessible in plugin and indexHtmlTransformer', + purpose: + 'Factory plugin and indexHtmlTransformer both receive target.configuration at runtime', + app: 'examples/custom-esbuild/sanity-esbuild-app', + command: 'yarn build-target-options', + }, ];