Skip to content

Commit 19f5ba6

Browse files
committed
build: refactor ng_package substitutions
This refactoring moves the substitution for `@angular/ssr` and `beasties` from a global configuration to a local one within the `@angular/ssr` package definition. This is achieved by introducing an `extra_substitutions` parameter to the `ng_package` function, allowing package-specific substitutions to be defined. This change improves modularity and removes hardcoded paths from the global substitution rules, making the build system cleaner and easier to maintain. Additionally, this fixes an issue where the generated TypeScript definition files (.d.ts) for `@angular/ssr` had incorrect relative paths for the `beasties` dependency, causing type resolution failures in consuming projects. The new local substitution corrects these paths.
1 parent 7134dfe commit 19f5ba6

File tree

3 files changed

+183
-0
lines changed

3 files changed

+183
-0
lines changed

packages/schematics/angular/migrations/migration-collection.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
"version": "21.0.0",
1919
"factory": "./update-typescript-lib/migration",
2020
"description": "Updates the 'lib' property in tsconfig files to use 'es2022' or a more modern version."
21+
},
22+
"update-module-resolution": {
23+
"version": "21.0.0",
24+
"factory": "./update-module-resolution/migration",
25+
"description": "Update 'moduleResolution' to 'bundler' in TypeScript configurations. You can read more about this, here: https://www.typescriptlang.org/tsconfig/#moduleResolution"
2126
}
2227
}
2328
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { JsonObject } from '@angular-devkit/core';
10+
import { Rule, Tree } from '@angular-devkit/schematics';
11+
import { JSONFile } from '../../utility/json-file';
12+
import { allTargetOptions, allWorkspaceTargets, getWorkspace } from '../../utility/workspace';
13+
14+
export default function (): Rule {
15+
return async (host) => {
16+
const uniqueTsConfigs = new Set<string>();
17+
18+
if (host.exists('tsconfig.json')) {
19+
// Workspace level tsconfig
20+
uniqueTsConfigs.add('tsconfig.json');
21+
}
22+
23+
const workspace = await getWorkspace(host);
24+
for (const [, target] of allWorkspaceTargets(workspace)) {
25+
for (const [, opt] of allTargetOptions(target)) {
26+
if (typeof opt?.tsConfig === 'string') {
27+
uniqueTsConfigs.add(opt.tsConfig);
28+
}
29+
}
30+
}
31+
32+
for (const tsConfig of uniqueTsConfigs) {
33+
if (host.exists(tsConfig)) {
34+
updateModuleResolution(host, tsConfig);
35+
}
36+
}
37+
};
38+
}
39+
40+
function updateModuleResolution(host: Tree, tsConfigPath: string): void {
41+
const json = new JSONFile(host, tsConfigPath);
42+
const jsonPath = ['compilerOptions'];
43+
const compilerOptions = json.get(jsonPath);
44+
45+
if (compilerOptions && typeof compilerOptions === 'object') {
46+
const { moduleResolution, module } = compilerOptions as JsonObject;
47+
if (typeof moduleResolution !== 'string' || moduleResolution.toLowerCase() === 'bundler') {
48+
return;
49+
}
50+
51+
if (typeof module === 'string' && module.toLowerCase() === 'preserve') {
52+
return;
53+
}
54+
55+
json.modify(jsonPath, {
56+
...compilerOptions,
57+
'moduleResolution': 'bundler',
58+
});
59+
}
60+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { isJsonObject } from '@angular-devkit/core';
10+
import { EmptyTree } from '@angular-devkit/schematics';
11+
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
12+
import { Builders, ProjectType, WorkspaceSchema } from '../../utility/workspace-models';
13+
14+
describe('Migration to update moduleResolution', () => {
15+
const schematicName = 'update-module-resolution';
16+
const schematicRunner = new SchematicTestRunner(
17+
'migrations',
18+
require.resolve('../migration-collection.json'),
19+
);
20+
21+
function createJsonFile(tree: UnitTestTree, filePath: string, content: {}): void {
22+
const stringifiedContent = JSON.stringify(content, undefined, 2);
23+
if (tree.exists(filePath)) {
24+
tree.overwrite(filePath, stringifiedContent);
25+
} else {
26+
tree.create(filePath, stringifiedContent);
27+
}
28+
}
29+
30+
function getCompilerOptionsValue(tree: UnitTestTree, filePath: string): Record<string, unknown> {
31+
const json = tree.readJson(filePath);
32+
if (isJsonObject(json) && isJsonObject(json.compilerOptions)) {
33+
return json.compilerOptions;
34+
}
35+
36+
throw new Error(`Cannot retrieve 'compilerOptions'.`);
37+
}
38+
39+
const angularConfig: WorkspaceSchema = {
40+
version: 1,
41+
projects: {
42+
app: {
43+
root: '',
44+
sourceRoot: 'src',
45+
projectType: ProjectType.Application,
46+
prefix: 'app',
47+
architect: {
48+
build: {
49+
builder: Builders.Browser,
50+
options: {
51+
tsConfig: 'src/tsconfig.app.json',
52+
main: '',
53+
polyfills: '',
54+
},
55+
configurations: {
56+
production: {
57+
tsConfig: 'src/tsconfig.app.prod.json',
58+
},
59+
},
60+
},
61+
test: {
62+
builder: Builders.Karma,
63+
options: {
64+
karmaConfig: '',
65+
tsConfig: 'src/tsconfig.spec.json',
66+
},
67+
},
68+
},
69+
},
70+
},
71+
};
72+
73+
let tree: UnitTestTree;
74+
beforeEach(() => {
75+
tree = new UnitTestTree(new EmptyTree());
76+
const compilerOptions = { module: 'es2020', moduleResolution: 'node' };
77+
const configWithExtends = { extends: './tsconfig.json', compilerOptions };
78+
79+
// Workspace
80+
createJsonFile(tree, 'angular.json', angularConfig);
81+
createJsonFile(tree, 'tsconfig.json', { compilerOptions });
82+
83+
// Application
84+
createJsonFile(tree, 'src/tsconfig.app.json', configWithExtends);
85+
createJsonFile(tree, 'src/tsconfig.app.prod.json', configWithExtends);
86+
createJsonFile(tree, 'src/tsconfig.spec.json', { compilerOptions });
87+
});
88+
89+
it(`should update moduleResolution to 'bundler' in workspace 'tsconfig.json'`, async () => {
90+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
91+
const compilerOptions = getCompilerOptionsValue(newTree, 'tsconfig.json');
92+
expect(compilerOptions).toEqual(
93+
jasmine.objectContaining({
94+
moduleResolution: 'bundler',
95+
}),
96+
);
97+
});
98+
99+
it(`should update moduleResolution to 'bundler' in builder tsconfig`, async () => {
100+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
101+
const compilerOptions = getCompilerOptionsValue(newTree, 'src/tsconfig.spec.json');
102+
expect(compilerOptions).toEqual(
103+
jasmine.objectContaining({
104+
moduleResolution: 'bundler',
105+
}),
106+
);
107+
});
108+
109+
it('should not update moduleResolution when module is preserve', async () => {
110+
createJsonFile(tree, 'tsconfig.json', {
111+
compilerOptions: { module: 'preserve', moduleResolution: 'node' },
112+
});
113+
114+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
115+
const compilerOptions = getCompilerOptionsValue(newTree, 'tsconfig.json');
116+
expect(compilerOptions).toEqual({ module: 'preserve', moduleResolution: 'node' });
117+
});
118+
});

0 commit comments

Comments
 (0)