Skip to content

Commit 2311352

Browse files
committed
fix: resolve migration failure when tsconfig specifies rootDir
When `rootDir` was set in a project's tsconfig (e.g. `rootDir: "src"`), tsurge-based migrations would fail because `projectRoot` was derived from `rootDir`, causing `rootRelativePath` to be computed relative to `src/` instead of the workspace root. This produced paths like `app/app.ts` instead of `src/app/app.ts`, which the DevKit tree could not resolve. Fix by overriding `info.projectRoot` to `fs.pwd()` (always `/` in the DevKit filesystem) immediately after program creation, ensuring workspace-relative paths are used for all tree updates.
1 parent af04e26 commit 2311352

3 files changed

Lines changed: 181 additions & 2 deletions

File tree

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
load("//tools:defaults.bzl", "ts_project")
1+
load("//tools:defaults.bzl", "jasmine_test", "ts_project")
22

33
package(default_visibility = ["//packages/core/schematics:__subpackages__"])
44

55
ts_project(
66
name = "angular_devkit",
7-
srcs = glob(["**/*.ts"]),
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["*.spec.ts"],
10+
),
811
deps = [
912
"//:node_modules/@angular-devkit/core",
1013
"//:node_modules/@angular-devkit/schematics",
@@ -14,3 +17,19 @@ ts_project(
1417
"//packages/core/schematics/utils/tsurge",
1518
],
1619
)
20+
21+
ts_project(
22+
name = "test_lib",
23+
testonly = True,
24+
srcs = glob(["*.spec.ts"]),
25+
deps = [
26+
":angular_devkit",
27+
"//:node_modules/@angular-devkit/schematics",
28+
"//packages/core/schematics/utils/tsurge",
29+
],
30+
)
31+
32+
jasmine_test(
33+
name = "test",
34+
data = [":test_lib"],
35+
)
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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 {HostTree} from '@angular-devkit/schematics';
10+
import {UnitTestTree} from '@angular-devkit/schematics/testing/index.js';
11+
import {
12+
confirmAsSerializable,
13+
ProgramInfo,
14+
projectFile,
15+
Replacement,
16+
Serializable,
17+
TextUpdate,
18+
TsurgeFunnelMigration,
19+
} from '../../index';
20+
import {runMigrationInDevkit} from './run_in_devkit';
21+
22+
interface TestMigrationData {
23+
replacements: Replacement[];
24+
}
25+
26+
class TestMigration extends TsurgeFunnelMigration<TestMigrationData, TestMigrationData> {
27+
override async analyze(info: ProgramInfo): Promise<Serializable<TestMigrationData>> {
28+
const replacements: Replacement[] = [];
29+
30+
for (const sf of info.sourceFiles) {
31+
const start = sf.text.indexOf('before');
32+
if (start === -1) {
33+
continue;
34+
}
35+
36+
replacements.push(
37+
new Replacement(
38+
projectFile(sf, info),
39+
new TextUpdate({position: start, end: start + 'before'.length, toInsert: 'after'}),
40+
),
41+
);
42+
}
43+
44+
return confirmAsSerializable({replacements});
45+
}
46+
47+
override async combine(
48+
unitA: TestMigrationData,
49+
unitB: TestMigrationData,
50+
): Promise<Serializable<TestMigrationData>> {
51+
return confirmAsSerializable({
52+
replacements: [...unitA.replacements, ...unitB.replacements],
53+
});
54+
}
55+
56+
override async globalMeta(data: TestMigrationData): Promise<Serializable<TestMigrationData>> {
57+
return confirmAsSerializable(data);
58+
}
59+
60+
override async stats(): Promise<Serializable<unknown>> {
61+
return confirmAsSerializable({});
62+
}
63+
64+
override async migrate(data: TestMigrationData): Promise<{replacements: Replacement[]}> {
65+
return {replacements: data.replacements};
66+
}
67+
}
68+
69+
describe('runMigrationInDevkit', () => {
70+
let tree: UnitTestTree;
71+
72+
beforeEach(() => {
73+
tree = new UnitTestTree(new HostTree());
74+
});
75+
76+
it('applies replacements when no rootDir is specified', async () => {
77+
tree.create(
78+
'/angular.json',
79+
JSON.stringify({
80+
version: 1,
81+
projects: {
82+
app: {
83+
root: '',
84+
architect: {
85+
build: {
86+
options: {
87+
tsConfig: './tsconfig.app.json',
88+
},
89+
},
90+
},
91+
},
92+
},
93+
}),
94+
);
95+
tree.create(
96+
'/tsconfig.app.json',
97+
JSON.stringify({
98+
compilerOptions: {
99+
module: 'preserve',
100+
target: 'ES2022',
101+
noLib: true,
102+
},
103+
include: ['src/**/*.ts'],
104+
}),
105+
);
106+
tree.create('/src/app/app.ts', `export const value = 'before';\n`);
107+
108+
await runMigrationInDevkit({
109+
tree,
110+
getMigration: () => new TestMigration(),
111+
});
112+
113+
expect(tree.readContent('/src/app/app.ts')).toContain(`'after'`);
114+
});
115+
116+
it('applies replacements to workspace-relative paths when tsconfig rootDir is narrower', async () => {
117+
tree.create(
118+
'/angular.json',
119+
JSON.stringify({
120+
version: 1,
121+
projects: {
122+
app: {
123+
root: '',
124+
architect: {
125+
build: {
126+
options: {
127+
tsConfig: './tsconfig.app.json',
128+
},
129+
},
130+
},
131+
},
132+
},
133+
}),
134+
);
135+
tree.create(
136+
'/tsconfig.app.json',
137+
JSON.stringify({
138+
compilerOptions: {
139+
rootDir: './src',
140+
module: 'preserve',
141+
target: 'ES2022',
142+
noLib: true,
143+
},
144+
include: ['src/**/*.ts'],
145+
}),
146+
);
147+
tree.create('/src/app/app.ts', `export const value = 'before';\n`);
148+
149+
await runMigrationInDevkit({
150+
tree,
151+
getMigration: () => new TestMigration(),
152+
});
153+
154+
expect(tree.readContent('/src/app/app.ts')).toContain(`'after'`);
155+
});
156+
});

packages/core/schematics/utils/tsurge/helpers/angular_devkit/run_in_devkit.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ export async function runMigrationInDevkit<Stats>(
7979
for (const tsconfigPath of tsconfigPaths) {
8080
config.beforeProgramCreation?.(tsconfigPath, MigrationStage.Analysis);
8181
const info = migration.createProgram(tsconfigPath, fs);
82+
// Devkit tree updates need workspace-relative paths. Keep `sortedRootDirs`
83+
// intact for logical file IDs, but make `rootRelativePath` resolve from `/`.
84+
info.projectRoot = fs.pwd();
8285

8386
modifyProgramInfoToEnsureNonOverlappingFiles(tsconfigPath, info, compilationUnitAssignments);
8487

@@ -107,6 +110,7 @@ export async function runMigrationInDevkit<Stats>(
107110
for (const tsconfigPath of tsconfigPaths) {
108111
config.beforeProgramCreation?.(tsconfigPath, MigrationStage.Migrate);
109112
const info = migration.createProgram(tsconfigPath, fs);
113+
info.projectRoot = fs.pwd();
110114
modifyProgramInfoToEnsureNonOverlappingFiles(tsconfigPath, info, compilationUnitAssignments);
111115

112116
config.afterProgramCreation?.(info, fs, MigrationStage.Migrate);

0 commit comments

Comments
 (0)