Skip to content

Commit d8d5951

Browse files
committed
Merge remote-tracking branch 'origin/main' into blove/home-stage-subagents
2 parents a369d42 + 41900ad commit d8d5951

5 files changed

Lines changed: 110 additions & 7 deletions

File tree

libs/cockpit-registry/tsconfig.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
"extends": "../../tsconfig.base.json",
33
"compilerOptions": {
44
"composite": false,
5-
"emitDeclarationOnly": false,
6-
"baseUrl": "."
5+
"emitDeclarationOnly": false
76
},
87
"files": [],
98
"include": [],

libs/cockpit-runtime-bridge/tsconfig.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
"extends": "../../tsconfig.base.json",
33
"compilerOptions": {
44
"composite": false,
5-
"emitDeclarationOnly": false,
6-
"baseUrl": "."
5+
"emitDeclarationOnly": false
76
},
87
"files": [],
98
"include": [],

libs/example-layouts/tsconfig.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
"noPropertyAccessFromIndexSignature": true,
66
"module": "preserve",
77
"emitDeclarationOnly": false,
8-
"composite": false,
9-
"baseUrl": "."
8+
"composite": false
109
},
1110
"angularCompilerOptions": {
1211
"enableI18nLegacyMessageIdFormat": false,

libs/growth/tsconfig.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
{
22
"extends": "../../tsconfig.base.json",
33
"compilerOptions": {
4-
"baseUrl": ".",
54
"composite": false,
65
"emitDeclarationOnly": false
76
},
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
2+
import { dirname, join, relative, resolve } from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
5+
import ts from 'typescript';
6+
import { describe, expect, it } from 'vitest';
7+
8+
// Guard for a latent module-resolution defect.
9+
//
10+
// `tsconfig.base.json` declares the workspace `paths` map with entries that are
11+
// relative to the workspace root (`libs/design-tokens/src/index.ts`). A project
12+
// tsconfig that extends the base and re-declares `"baseUrl": "."` overrides the
13+
// inherited base directory, because `baseUrl` outranks the implicit
14+
// `pathsBasePath`. Every inherited substitution is then probed under the
15+
// *library* folder (`libs/<lib>/libs/design-tokens/src/index.ts`), misses, and
16+
// only resolves because the npm workspace symlink in `node_modules` happens to
17+
// land on the same source file. Builds stay green, so nothing catches it.
18+
//
19+
// A project that declares its own `paths` (including an empty `{}`) is exempt:
20+
// there `baseUrl` does real directory-resolution work and shadows nothing.
21+
22+
const workspaceRoot = dirname(dirname(fileURLToPath(import.meta.url)));
23+
const baseConfigPath = join(workspaceRoot, 'tsconfig.base.json');
24+
25+
function readConfig(configPath) {
26+
const parsed = ts.parseConfigFileTextToJson(configPath, readFileSync(configPath, 'utf8'));
27+
expect(parsed.error, `failed to parse ${relative(workspaceRoot, configPath)}`).toBeUndefined();
28+
return parsed.config ?? {};
29+
}
30+
31+
/**
32+
* Walk the `extends` chain from `configPath` upwards, stopping at (and
33+
* excluding) `tsconfig.base.json`. Returns null when the chain never reaches
34+
* the base config — such a project does not inherit the workspace `paths`.
35+
*/
36+
function chainBelowBase(configPath) {
37+
const chain = [];
38+
let current = configPath;
39+
for (let hop = 0; hop < 10; hop += 1) {
40+
if (current === baseConfigPath) return chain;
41+
const config = readConfig(current);
42+
chain.push({ path: current, config });
43+
if (typeof config.extends !== 'string') return null;
44+
const next = resolve(dirname(current), config.extends);
45+
current = existsSync(next) ? next : `${next}.json`;
46+
if (!existsSync(current)) return null;
47+
}
48+
return null;
49+
}
50+
51+
function libraryTsconfigs() {
52+
const found = [];
53+
for (const entry of readdirSync(join(workspaceRoot, 'libs'), { withFileTypes: true })) {
54+
if (!entry.isDirectory()) continue;
55+
const libDir = join(workspaceRoot, 'libs', entry.name);
56+
for (const file of readdirSync(libDir)) {
57+
if (file.startsWith('tsconfig') && file.endsWith('.json')) found.push(join(libDir, file));
58+
}
59+
}
60+
return found.sort();
61+
}
62+
63+
describe('library tsconfig path inheritance', () => {
64+
it('never shadows the workspace baseUrl the inherited paths map resolves against', () => {
65+
const offenders = [];
66+
for (const configPath of libraryTsconfigs()) {
67+
const chain = chainBelowBase(configPath);
68+
if (chain === null) continue;
69+
const declaresOwnPaths = chain.some((link) => link.config.compilerOptions?.paths !== undefined);
70+
if (declaresOwnPaths) continue;
71+
const shadowing = chain.find((link) => link.config.compilerOptions?.baseUrl !== undefined);
72+
if (shadowing) offenders.push(relative(workspaceRoot, shadowing.path));
73+
}
74+
expect([...new Set(offenders)]).toEqual([]);
75+
});
76+
77+
it.each([
78+
'libs/cockpit-runtime-bridge',
79+
'libs/growth',
80+
'libs/example-layouts',
81+
'libs/cockpit-registry',
82+
])('resolves @threadplane/design-tokens through the paths map from %s', (libDir) => {
83+
const configPath = join(workspaceRoot, libDir, 'tsconfig.json');
84+
const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, {
85+
...ts.sys,
86+
onUnRecoverableConfigFileDiagnostic: (diagnostic) => {
87+
throw new Error(ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'));
88+
},
89+
getCurrentDirectory: () => workspaceRoot,
90+
useCaseSensitiveFileNames: true,
91+
});
92+
expect(parsed, `could not parse ${libDir}/tsconfig.json`).toBeDefined();
93+
94+
const containingFile = join(workspaceRoot, libDir, 'src', 'index.ts');
95+
const resolved = ts.resolveModuleName(
96+
'@threadplane/design-tokens',
97+
containingFile,
98+
parsed.options,
99+
ts.sys,
100+
).resolvedModule;
101+
102+
expect(resolved?.resolvedFileName).toBe(join(workspaceRoot, 'libs/design-tokens/src/index.ts'));
103+
// A `packageId` means TypeScript fell through the `paths` substitution and
104+
// found the source only via the npm workspace symlink under node_modules.
105+
expect(resolved?.packageId, 'resolved via node_modules symlink, not the paths map').toBeUndefined();
106+
});
107+
});

0 commit comments

Comments
 (0)