gitnexus 1.6.10-rc.201 → 1.6.10-rc.202
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/ingestion/import-resolvers/node-workspace-packages.d.ts +90 -0
- package/dist/core/ingestion/import-resolvers/node-workspace-packages.js +428 -0
- package/dist/core/ingestion/languages/javascript/import-target.d.ts +20 -48
- package/dist/core/ingestion/languages/javascript/import-target.js +27 -98
- package/dist/core/ingestion/languages/javascript/scope-resolver.js +10 -0
- package/dist/core/ingestion/languages/typescript/file-candidates.d.ts +29 -0
- package/dist/core/ingestion/languages/typescript/file-candidates.js +70 -0
- package/dist/core/ingestion/languages/typescript/import-target.d.ts +25 -37
- package/dist/core/ingestion/languages/typescript/import-target.js +24 -26
- package/dist/core/ingestion/languages/typescript/module-resolution.d.ts +48 -0
- package/dist/core/ingestion/languages/typescript/module-resolution.js +171 -0
- package/dist/core/ingestion/languages/typescript/scope-resolver.js +15 -34
- package/dist/core/ingestion/languages/typescript/tsconfig.d.ts +59 -0
- package/dist/core/ingestion/languages/typescript/tsconfig.js +291 -0
- package/dist/core/ingestion/languages/vue/import-target.d.ts +6 -22
- package/dist/core/ingestion/languages/vue/import-target.js +11 -55
- package/dist/core/ingestion/languages/vue/scope-resolver.js +4 -2
- package/package.json +1 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-repo `package.json` manifests, as module-resolution input (#2953).
|
|
3
|
+
*
|
|
4
|
+
* A bare specifier (`@acme/telemetry/nest`, `@repo/utils`, `lodash/fp`) names a
|
|
5
|
+
* PACKAGE, not a path, and the manifest is the only thing that says which
|
|
6
|
+
* packages exist and where their entry points are. Without it a resolver can do
|
|
7
|
+
* nothing but guess — which is what the old suffix matcher did, landing
|
|
8
|
+
* `@acme/telemetry/nest` on the repo's only path ending in `nest/index.ts`
|
|
9
|
+
* while `@repo/utils`, a real first-party package, resolved to nothing because
|
|
10
|
+
* its name appears in no file path at all.
|
|
11
|
+
*
|
|
12
|
+
* Both directions come from the same missing input, so both are fixed by
|
|
13
|
+
* reading it: every in-repo `package.json` contributes its `name`, its `exports`
|
|
14
|
+
* map (including subpath patterns), its legacy entry fields, and its `imports`
|
|
15
|
+
* map for `#`-prefixed specifiers.
|
|
16
|
+
*/
|
|
17
|
+
/** One in-repo package. */
|
|
18
|
+
export interface NodeWorkspacePackage {
|
|
19
|
+
/** Repo-relative directory holding the `package.json` (`''` for the root). */
|
|
20
|
+
readonly dir: string;
|
|
21
|
+
/**
|
|
22
|
+
* Repo-relative entry stems for the package root (`import '@repo/utils'`),
|
|
23
|
+
* best first: declared `exports["."]`, then `module` / `main` / `types`, then
|
|
24
|
+
* the conventional `src/index` and `index`.
|
|
25
|
+
*
|
|
26
|
+
* A published `dist/...` entry simply fails to match an indexed source file
|
|
27
|
+
* (build output is not indexed) and the next candidate is tried, which is why
|
|
28
|
+
* the conventional fallbacks stay at the end rather than being a guess: they
|
|
29
|
+
* are what the package resolves to when it is consumed from source, which in
|
|
30
|
+
* a workspace it always is.
|
|
31
|
+
*/
|
|
32
|
+
readonly entries: readonly string[];
|
|
33
|
+
/**
|
|
34
|
+
* Declared `exports` subpaths, specifier suffix -> repo-relative stems.
|
|
35
|
+
* Keys are as written minus the leading `./`, so `"./nest"` is stored `nest`;
|
|
36
|
+
* a pattern key keeps its `*` (`"./features/*"` -> `features/*`).
|
|
37
|
+
*/
|
|
38
|
+
readonly subpathExports: ReadonlyMap<string, readonly string[]>;
|
|
39
|
+
/** Declared `imports` map, `#name` -> repo-relative stems. */
|
|
40
|
+
readonly subpathImports: ReadonlyMap<string, readonly string[]>;
|
|
41
|
+
}
|
|
42
|
+
export interface NodeWorkspacePackages {
|
|
43
|
+
/** Package name (`@repo/utils`, `utils`) -> that package. */
|
|
44
|
+
readonly byName: ReadonlyMap<string, NodeWorkspacePackage>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The package name a bare specifier addresses, or `null` when the specifier
|
|
48
|
+
* names a path rather than a package.
|
|
49
|
+
*
|
|
50
|
+
* `@acme/telemetry/nest` -> `@acme/telemetry`, `lodash/fp` -> `lodash`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function nodePackageNameOf(specifier: string): string | null;
|
|
53
|
+
/** The in-repo package whose directory most closely contains `filePath`. */
|
|
54
|
+
export declare function owningPackage(filePath: string, packages: NodeWorkspacePackages | null | undefined): NodeWorkspacePackage | null;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a bare specifier that names an in-repo package.
|
|
57
|
+
*
|
|
58
|
+
* `null` means the specifier names no in-repo package — an external dependency,
|
|
59
|
+
* whose correct in-repo resolution is nothing — or names one that does not
|
|
60
|
+
* export the requested subpath.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveNodeWorkspaceImport(specifier: string, packages: NodeWorkspacePackages | null | undefined, allFiles: ReadonlySet<string>): string | null;
|
|
63
|
+
/**
|
|
64
|
+
* Look a specifier up in a subpath map — `exports` or `imports`, which share
|
|
65
|
+
* Node's matching rule exactly: an exact key wins, otherwise the pattern with
|
|
66
|
+
* the longest literal prefix does, and its `*` takes whatever the specifier put
|
|
67
|
+
* there.
|
|
68
|
+
*
|
|
69
|
+
* Shared because they diverged once: the `imports` side did an exact lookup
|
|
70
|
+
* only, so a declared `"#internal/*"` could never match `#internal/foo`.
|
|
71
|
+
*/
|
|
72
|
+
export declare function matchSubpathMap(map: ReadonlyMap<string, readonly string[]>, specifier: string): readonly string[] | null;
|
|
73
|
+
/**
|
|
74
|
+
* Substitute a subpath pattern's single `*`.
|
|
75
|
+
*
|
|
76
|
+
* Node's subpath patterns and TypeScript's `paths` both allow AT MOST one `*`,
|
|
77
|
+
* so replacing the first occurrence is the specified behaviour rather than a
|
|
78
|
+
* partial one — but `String.replace` with a string needle says that only by
|
|
79
|
+
* accident, and reads as a bug to anyone (CodeQL included) who has met the
|
|
80
|
+
* replace-all footgun. Slicing at the known index states the rule instead.
|
|
81
|
+
*/
|
|
82
|
+
export declare function substituteStar(target: string, stem: string): string;
|
|
83
|
+
/**
|
|
84
|
+
* Collect the `package.json` of every ADMITTED workspace package.
|
|
85
|
+
*
|
|
86
|
+
* Directory-only BFS: the sole files opened are manifests and the workspace
|
|
87
|
+
* declaration, so this is far cheaper than the C# namespace scan next door,
|
|
88
|
+
* which reads every `.cs` file.
|
|
89
|
+
*/
|
|
90
|
+
export declare function loadNodeWorkspacePackages(repoRoot: string): Promise<NodeWorkspacePackages | null>;
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-repo `package.json` manifests, as module-resolution input (#2953).
|
|
3
|
+
*
|
|
4
|
+
* A bare specifier (`@acme/telemetry/nest`, `@repo/utils`, `lodash/fp`) names a
|
|
5
|
+
* PACKAGE, not a path, and the manifest is the only thing that says which
|
|
6
|
+
* packages exist and where their entry points are. Without it a resolver can do
|
|
7
|
+
* nothing but guess — which is what the old suffix matcher did, landing
|
|
8
|
+
* `@acme/telemetry/nest` on the repo's only path ending in `nest/index.ts`
|
|
9
|
+
* while `@repo/utils`, a real first-party package, resolved to nothing because
|
|
10
|
+
* its name appears in no file path at all.
|
|
11
|
+
*
|
|
12
|
+
* Both directions come from the same missing input, so both are fixed by
|
|
13
|
+
* reading it: every in-repo `package.json` contributes its `name`, its `exports`
|
|
14
|
+
* map (including subpath patterns), its legacy entry fields, and its `imports`
|
|
15
|
+
* map for `#`-prefixed specifiers.
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'fs/promises';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
import { isHardcodedIgnoredDirectory } from '../../../config/ignore-service.js';
|
|
21
|
+
import { logger } from '../../logger.js';
|
|
22
|
+
import { resolveFile } from '../languages/typescript/file-candidates.js';
|
|
23
|
+
// `js-yaml` is CJS; the rest of this repository reaches it the same way
|
|
24
|
+
// (`core/group/config-parser.ts`, `cli/group.ts`).
|
|
25
|
+
const _require = createRequire(import.meta.url);
|
|
26
|
+
const yaml = _require('js-yaml');
|
|
27
|
+
const SCAN_MAX_DIRS = 20_000;
|
|
28
|
+
const SCAN_MAX_DEPTH = 24;
|
|
29
|
+
/**
|
|
30
|
+
* The package name a bare specifier addresses, or `null` when the specifier
|
|
31
|
+
* names a path rather than a package.
|
|
32
|
+
*
|
|
33
|
+
* `@acme/telemetry/nest` -> `@acme/telemetry`, `lodash/fp` -> `lodash`.
|
|
34
|
+
*/
|
|
35
|
+
export function nodePackageNameOf(specifier) {
|
|
36
|
+
if (specifier === '' || specifier.startsWith('.') || specifier.startsWith('/'))
|
|
37
|
+
return null;
|
|
38
|
+
if (specifier.startsWith('#'))
|
|
39
|
+
return null;
|
|
40
|
+
if (specifier.startsWith('@')) {
|
|
41
|
+
const parts = specifier.split('/');
|
|
42
|
+
return parts.length >= 2 && parts[0].length > 1 && parts[1] !== ''
|
|
43
|
+
? `${parts[0]}/${parts[1]}`
|
|
44
|
+
: null;
|
|
45
|
+
}
|
|
46
|
+
return specifier.split('/')[0] || null;
|
|
47
|
+
}
|
|
48
|
+
/** The in-repo package whose directory most closely contains `filePath`. */
|
|
49
|
+
export function owningPackage(filePath, packages) {
|
|
50
|
+
if (!packages)
|
|
51
|
+
return null;
|
|
52
|
+
let best = null;
|
|
53
|
+
for (const pkg of packages.byName.values()) {
|
|
54
|
+
const inside = pkg.dir === '' || filePath.startsWith(`${pkg.dir}/`);
|
|
55
|
+
if (inside && (best === null || pkg.dir.length > best.dir.length))
|
|
56
|
+
best = pkg;
|
|
57
|
+
}
|
|
58
|
+
return best;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve a bare specifier that names an in-repo package.
|
|
62
|
+
*
|
|
63
|
+
* `null` means the specifier names no in-repo package — an external dependency,
|
|
64
|
+
* whose correct in-repo resolution is nothing — or names one that does not
|
|
65
|
+
* export the requested subpath.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveNodeWorkspaceImport(specifier, packages, allFiles) {
|
|
68
|
+
if (!packages)
|
|
69
|
+
return null;
|
|
70
|
+
const packageName = nodePackageNameOf(specifier);
|
|
71
|
+
if (packageName === null)
|
|
72
|
+
return null;
|
|
73
|
+
const pkg = packages.byName.get(packageName);
|
|
74
|
+
if (pkg === undefined)
|
|
75
|
+
return null;
|
|
76
|
+
const subpath = specifier.slice(packageName.length).replace(/^\//, '');
|
|
77
|
+
for (const stem of entryStemsFor(pkg, subpath)) {
|
|
78
|
+
const hit = resolveFile(stem, allFiles);
|
|
79
|
+
if (hit !== null)
|
|
80
|
+
return hit;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Look a specifier up in a subpath map — `exports` or `imports`, which share
|
|
86
|
+
* Node's matching rule exactly: an exact key wins, otherwise the pattern with
|
|
87
|
+
* the longest literal prefix does, and its `*` takes whatever the specifier put
|
|
88
|
+
* there.
|
|
89
|
+
*
|
|
90
|
+
* Shared because they diverged once: the `imports` side did an exact lookup
|
|
91
|
+
* only, so a declared `"#internal/*"` could never match `#internal/foo`.
|
|
92
|
+
*/
|
|
93
|
+
export function matchSubpathMap(map, specifier) {
|
|
94
|
+
const exact = map.get(specifier);
|
|
95
|
+
if (exact !== undefined)
|
|
96
|
+
return exact;
|
|
97
|
+
const patterns = [...map.entries()]
|
|
98
|
+
.filter(([key]) => key.includes('*'))
|
|
99
|
+
.map(([key, stems]) => {
|
|
100
|
+
const star = key.indexOf('*');
|
|
101
|
+
return { prefix: key.slice(0, star), suffix: key.slice(star + 1), stems };
|
|
102
|
+
})
|
|
103
|
+
.filter(({ prefix, suffix }) => specifier.startsWith(prefix) &&
|
|
104
|
+
specifier.endsWith(suffix) &&
|
|
105
|
+
specifier.length >= prefix.length + suffix.length)
|
|
106
|
+
.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
107
|
+
for (const { prefix, suffix, stems } of patterns) {
|
|
108
|
+
const stem = specifier.slice(prefix.length, specifier.length - suffix.length);
|
|
109
|
+
return stems.map((target) => substituteStar(target, stem));
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Substitute a subpath pattern's single `*`.
|
|
115
|
+
*
|
|
116
|
+
* Node's subpath patterns and TypeScript's `paths` both allow AT MOST one `*`,
|
|
117
|
+
* so replacing the first occurrence is the specified behaviour rather than a
|
|
118
|
+
* partial one — but `String.replace` with a string needle says that only by
|
|
119
|
+
* accident, and reads as a bug to anyone (CodeQL included) who has met the
|
|
120
|
+
* replace-all footgun. Slicing at the known index states the rule instead.
|
|
121
|
+
*/
|
|
122
|
+
export function substituteStar(target, stem) {
|
|
123
|
+
const star = target.indexOf('*');
|
|
124
|
+
return star === -1 ? target : target.slice(0, star) + stem + target.slice(star + 1);
|
|
125
|
+
}
|
|
126
|
+
/** Candidate stems for one specifier into `pkg`, best first. */
|
|
127
|
+
function entryStemsFor(pkg, subpath) {
|
|
128
|
+
if (subpath === '')
|
|
129
|
+
return pkg.entries;
|
|
130
|
+
const declared = matchSubpathMap(pkg.subpathExports, subpath);
|
|
131
|
+
if (declared !== null)
|
|
132
|
+
return declared;
|
|
133
|
+
// A package with NO `exports` map is not restricted: Node resolves any
|
|
134
|
+
// subpath against the package DIRECTORY, and only against it. A package WITH
|
|
135
|
+
// one exposes only what it lists, so an unlisted subpath resolves to nothing.
|
|
136
|
+
//
|
|
137
|
+
// Both restrictions are real, and neither is softened here. An earlier draft
|
|
138
|
+
// also tried `<dir>/src/<subpath>`, on the theory that a workspace package is
|
|
139
|
+
// consumed from source — but nothing declares that mapping, so it is the same
|
|
140
|
+
// kind of guess this module exists to remove: it would resolve
|
|
141
|
+
// `@repo/utils/deep/thing` to `packages/utils/src/deep/thing.ts` for a
|
|
142
|
+
// package whose manifest never said `deep/thing` lives under `src/`, and the
|
|
143
|
+
// import would be broken in the real project too.
|
|
144
|
+
if (pkg.subpathExports.size > 0)
|
|
145
|
+
return [];
|
|
146
|
+
return [joinRepoPath(pkg.dir, subpath)];
|
|
147
|
+
}
|
|
148
|
+
/** Whether `dir` (repo-relative, `''` for the root) is an admitted package. */
|
|
149
|
+
function admits(scope, dir) {
|
|
150
|
+
// The root package is always itself, workspace or not.
|
|
151
|
+
if (dir === '')
|
|
152
|
+
return true;
|
|
153
|
+
if (scope === null)
|
|
154
|
+
return false;
|
|
155
|
+
if (scope.exclude.some((pattern) => globToRegExp(pattern).test(dir)))
|
|
156
|
+
return false;
|
|
157
|
+
return scope.include.some((pattern) => globToRegExp(pattern).test(dir));
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Match one workspace glob.
|
|
161
|
+
*
|
|
162
|
+
* The subset npm, pnpm, yarn and lerna actually use in `workspaces` /
|
|
163
|
+
* `packages`: `*` within a segment, `**` across segments, `?`, and a leading
|
|
164
|
+
* `!` for exclusion (handled by the caller). Deliberately not a general glob
|
|
165
|
+
* engine — the patterns are a documented, narrow dialect, and `minimatch` is
|
|
166
|
+
* only present here transitively through `glob`.
|
|
167
|
+
*/
|
|
168
|
+
function globToRegExp(pattern) {
|
|
169
|
+
const normalized = pattern.replace(/^\.\//, '').replace(/\/$/, '');
|
|
170
|
+
let out = '';
|
|
171
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
172
|
+
const ch = normalized[i];
|
|
173
|
+
if (ch === '*') {
|
|
174
|
+
if (normalized[i + 1] === '*') {
|
|
175
|
+
// `**/` may match nothing at all, so `packages/**/x` also matches
|
|
176
|
+
// `packages/x`; a trailing `**` matches any depth below.
|
|
177
|
+
if (normalized[i + 2] === '/') {
|
|
178
|
+
out += '(?:.*/)?';
|
|
179
|
+
i += 2;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
out += '.*';
|
|
183
|
+
i += 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
out += '[^/]*';
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (ch === '?') {
|
|
192
|
+
out += '[^/]';
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
out += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
196
|
+
}
|
|
197
|
+
return new RegExp(`^${out}$`);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Read the repository's workspace declaration.
|
|
201
|
+
*
|
|
202
|
+
* All three spellings are read and merged, because a repo may carry more than
|
|
203
|
+
* one (a pnpm workspace whose root `package.json` also lists `workspaces` for
|
|
204
|
+
* tooling that does not read pnpm's file).
|
|
205
|
+
*/
|
|
206
|
+
async function loadWorkspaceScope(repoRoot) {
|
|
207
|
+
const patterns = [];
|
|
208
|
+
const rootManifest = await readJsonFile(path.join(repoRoot, 'package.json'));
|
|
209
|
+
const workspaces = rootManifest?.workspaces;
|
|
210
|
+
if (Array.isArray(workspaces)) {
|
|
211
|
+
patterns.push(...workspaces.filter((w) => typeof w === 'string'));
|
|
212
|
+
}
|
|
213
|
+
else if (workspaces !== null && typeof workspaces === 'object') {
|
|
214
|
+
// Yarn's object form: `{ "packages": [...], "nohoist": [...] }`.
|
|
215
|
+
const nested = workspaces.packages;
|
|
216
|
+
if (Array.isArray(nested)) {
|
|
217
|
+
patterns.push(...nested.filter((w) => typeof w === 'string'));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
patterns.push(...(await readYamlPackages(path.join(repoRoot, 'pnpm-workspace.yaml'))));
|
|
221
|
+
patterns.push(...(await readYamlPackages(path.join(repoRoot, 'pnpm-workspace.yml'))));
|
|
222
|
+
const lerna = await readJsonFile(path.join(repoRoot, 'lerna.json'));
|
|
223
|
+
if (Array.isArray(lerna?.packages)) {
|
|
224
|
+
patterns.push(...lerna.packages.filter((w) => typeof w === 'string'));
|
|
225
|
+
}
|
|
226
|
+
if (patterns.length === 0)
|
|
227
|
+
return null;
|
|
228
|
+
return {
|
|
229
|
+
include: patterns.filter((p) => !p.startsWith('!')),
|
|
230
|
+
exclude: patterns.filter((p) => p.startsWith('!')).map((p) => p.slice(1)),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
async function readJsonFile(filePath) {
|
|
234
|
+
try {
|
|
235
|
+
return JSON.parse(await fs.readFile(filePath, 'utf-8'));
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function readYamlPackages(filePath) {
|
|
242
|
+
let raw;
|
|
243
|
+
try {
|
|
244
|
+
raw = await fs.readFile(filePath, 'utf-8');
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const parsed = yaml.load(raw);
|
|
251
|
+
const packages = parsed?.packages;
|
|
252
|
+
return Array.isArray(packages)
|
|
253
|
+
? packages.filter((p) => typeof p === 'string')
|
|
254
|
+
: [];
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Collect the `package.json` of every ADMITTED workspace package.
|
|
262
|
+
*
|
|
263
|
+
* Directory-only BFS: the sole files opened are manifests and the workspace
|
|
264
|
+
* declaration, so this is far cheaper than the C# namespace scan next door,
|
|
265
|
+
* which reads every `.cs` file.
|
|
266
|
+
*/
|
|
267
|
+
export async function loadNodeWorkspacePackages(repoRoot) {
|
|
268
|
+
const scope = await loadWorkspaceScope(repoRoot);
|
|
269
|
+
const byName = new Map();
|
|
270
|
+
const queue = [{ dir: repoRoot, depth: 0 }];
|
|
271
|
+
let dirsScanned = 0;
|
|
272
|
+
while (queue.length > 0) {
|
|
273
|
+
if (dirsScanned >= SCAN_MAX_DIRS) {
|
|
274
|
+
logger.warn(`[node] package.json scan of ${repoRoot} hit the ${SCAN_MAX_DIRS}-directory cap; workspace packages below it will not resolve`);
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
const { dir, depth } = queue.shift();
|
|
278
|
+
dirsScanned++;
|
|
279
|
+
let entries;
|
|
280
|
+
try {
|
|
281
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
for (const entry of entries) {
|
|
287
|
+
if (entry.isDirectory()) {
|
|
288
|
+
if (isHardcodedIgnoredDirectory(entry.name))
|
|
289
|
+
continue;
|
|
290
|
+
if (depth < SCAN_MAX_DEPTH) {
|
|
291
|
+
queue.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
|
|
292
|
+
}
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (!entry.isFile() || entry.name !== 'package.json')
|
|
296
|
+
continue;
|
|
297
|
+
const relDir = repoRelativeDir(repoRoot, dir);
|
|
298
|
+
// Found is not the same as admitted. A manifest outside the declared
|
|
299
|
+
// workspace belongs to something this repository does not build — a
|
|
300
|
+
// fixture, an example, a vendored copy — and its name is not addressable.
|
|
301
|
+
if (!admits(scope, relDir))
|
|
302
|
+
continue;
|
|
303
|
+
const pkg = await readManifest(path.join(dir, entry.name), repoRoot, dir);
|
|
304
|
+
// First declaration wins: BFS visits shallower directories first, so a
|
|
305
|
+
// top-level package outranks a nested one that reuses the name.
|
|
306
|
+
if (pkg !== null && !byName.has(pkg.name))
|
|
307
|
+
byName.set(pkg.name, pkg.package);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return byName.size === 0 ? null : { byName };
|
|
311
|
+
}
|
|
312
|
+
async function readManifest(manifestPath, repoRoot, dir) {
|
|
313
|
+
let parsed;
|
|
314
|
+
try {
|
|
315
|
+
parsed = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
const name = typeof parsed.name === 'string' ? parsed.name : '';
|
|
321
|
+
if (name === '')
|
|
322
|
+
return null;
|
|
323
|
+
const packageDir = repoRelativeDir(repoRoot, dir);
|
|
324
|
+
const rebase = (raw) => joinRepoPath(packageDir, stripEntryPrefixes(raw));
|
|
325
|
+
const subpathExports = new Map();
|
|
326
|
+
const rootExports = [];
|
|
327
|
+
collectExports(parsed.exports, subpathExports, rootExports, rebase);
|
|
328
|
+
// `exports`, when present, is the package's ENTIRE public interface: Node
|
|
329
|
+
// ignores `main` outright and refuses any subpath the map does not list. This
|
|
330
|
+
// resolver already honoured that restriction for subpaths (`entryStemsFor`)
|
|
331
|
+
// and not for the ROOT, which is the same rule — so a manifest exporting only
|
|
332
|
+
// `"./feature"` still answered a bare `@repo/pkg` with `src/index`, an edge
|
|
333
|
+
// for an import that does not resolve in the real project.
|
|
334
|
+
const declaresExports = parsed.exports !== undefined && parsed.exports !== null;
|
|
335
|
+
const entries = [...rootExports];
|
|
336
|
+
if (!declaresExports) {
|
|
337
|
+
for (const field of ['module', 'main', 'types', 'typings']) {
|
|
338
|
+
const value = parsed[field];
|
|
339
|
+
if (typeof value === 'string')
|
|
340
|
+
push(entries, rebase(value));
|
|
341
|
+
}
|
|
342
|
+
for (const conventional of ['src/index', 'index', 'lib/index']) {
|
|
343
|
+
push(entries, joinRepoPath(packageDir, conventional));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const subpathImports = new Map();
|
|
347
|
+
collectImports(parsed.imports, subpathImports, rebase);
|
|
348
|
+
return { name, package: { dir: packageDir, entries, subpathExports, subpathImports } };
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Walk an `exports` value into the root-entry list and the subpath map.
|
|
352
|
+
*
|
|
353
|
+
* `exports` nests three ways at once — a bare string, a subpath map, and
|
|
354
|
+
* condition maps (`import` / `require` / `types` / `default`) at any depth — so
|
|
355
|
+
* this collects string leaves per subpath rather than assuming a shape.
|
|
356
|
+
*/
|
|
357
|
+
function collectExports(node, subpaths, rootStems, rebase, currentSubpath = '') {
|
|
358
|
+
if (typeof node === 'string') {
|
|
359
|
+
if (currentSubpath === null)
|
|
360
|
+
return;
|
|
361
|
+
if (currentSubpath === '') {
|
|
362
|
+
push(rootStems, rebase(node));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
subpaths.set(currentSubpath, [...(subpaths.get(currentSubpath) ?? []), rebase(node)]);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
// An array is an ordered FALLBACK LIST, not an opaque value: Node tries each
|
|
369
|
+
// entry in turn. `{"./feature": ["./dist/feature.js", "./src/feature.ts"]}` is
|
|
370
|
+
// the shape a workspace package publishes to say "built output, or source" —
|
|
371
|
+
// and the source arm is the one that matters here, because `dist/` is build
|
|
372
|
+
// output and is not indexed. Skipping arrays dropped the declaration entirely
|
|
373
|
+
// and left the package looking as though it declared no subpath exports.
|
|
374
|
+
if (Array.isArray(node)) {
|
|
375
|
+
for (const element of node)
|
|
376
|
+
collectExports(element, subpaths, rootStems, rebase, currentSubpath);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (node === null || typeof node !== 'object')
|
|
380
|
+
return;
|
|
381
|
+
for (const [key, value] of Object.entries(node)) {
|
|
382
|
+
if (key.startsWith('.')) {
|
|
383
|
+
// A subpath key: `"."` is the package root, `"./nest"` the subpath `nest`.
|
|
384
|
+
collectExports(value, subpaths, rootStems, rebase, key === '.' ? '' : key.replace(/^\.\//, ''));
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
// A condition key — stays on whatever subpath we were already resolving.
|
|
388
|
+
collectExports(value, subpaths, rootStems, rebase, currentSubpath);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/** Walk an `imports` map (`"#env": "./src/env.node.ts"`) into stems. */
|
|
393
|
+
function collectImports(node, out, rebase, currentKey = null) {
|
|
394
|
+
if (typeof node === 'string') {
|
|
395
|
+
if (currentKey === null)
|
|
396
|
+
return;
|
|
397
|
+
out.set(currentKey, [...(out.get(currentKey) ?? []), rebase(node)]);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
// Same ordered-fallback rule as `exports` — see `collectExports`.
|
|
401
|
+
if (Array.isArray(node)) {
|
|
402
|
+
for (const element of node)
|
|
403
|
+
collectImports(element, out, rebase, currentKey);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (node === null || typeof node !== 'object')
|
|
407
|
+
return;
|
|
408
|
+
for (const [key, value] of Object.entries(node)) {
|
|
409
|
+
collectImports(value, out, rebase, key.startsWith('#') ? key : currentKey);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/** `"./src/index.ts"` -> `"src/index"`; leaves an extension-less path alone. */
|
|
413
|
+
function stripEntryPrefixes(entry) {
|
|
414
|
+
const withoutDot = entry.replace(/^\.\//, '').replace(/^\//, '');
|
|
415
|
+
return withoutDot.replace(/\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|vue)$/, '');
|
|
416
|
+
}
|
|
417
|
+
function push(list, value) {
|
|
418
|
+
if (value !== '' && !list.includes(value))
|
|
419
|
+
list.push(value);
|
|
420
|
+
}
|
|
421
|
+
/** `/repo/packages/utils` -> `packages/utils`; the root -> `''`. */
|
|
422
|
+
function repoRelativeDir(repoRoot, dir) {
|
|
423
|
+
const rel = path.relative(repoRoot, dir).split(path.sep).join('/');
|
|
424
|
+
return rel === '.' ? '' : rel;
|
|
425
|
+
}
|
|
426
|
+
function joinRepoPath(dir, rest) {
|
|
427
|
+
return dir === '' ? rest : `${dir}/${rest}`;
|
|
428
|
+
}
|
|
@@ -1,53 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Import-target resolver for JavaScript.
|
|
3
3
|
*
|
|
4
|
-
* Delegates to the TypeScript
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* `
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* `
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* Supplying `index` is not only a speed-up: `suffixResolve` answers a different
|
|
25
|
-
* question with one than without. Without an index it tests
|
|
26
|
-
* `filePath.endsWith('/' + suffix)`, so only a PROPER suffix can match; with
|
|
27
|
-
* one it reads `buildSuffixIndex`, which indexes `j = 0` and therefore matches
|
|
28
|
-
* WHOLE paths too. Two classes of answer move, both only on the bare/absolute
|
|
29
|
-
* specifier leg (relative imports resolve by exact `Set.has` and never reach
|
|
30
|
-
* it), and both toward what TypeScript and Vue have always answered:
|
|
31
|
-
*
|
|
32
|
-
* 1. a repo-root file becomes reachable at all — `require('config')` now
|
|
33
|
-
* finds `config.js`, where before no proper suffix existed and the answer
|
|
34
|
-
* was null;
|
|
35
|
-
* 2. a whole-path candidate outranks a proper-suffix candidate found at a
|
|
36
|
-
* SHORTER path suffix or a later extension — `import 'app/main'` resolved
|
|
37
|
-
* to `node_modules/dep/lib/main.js` (the first `/main.js` in file order)
|
|
38
|
-
* and now resolves to `app/main.js`.
|
|
39
|
-
*
|
|
40
|
-
* Measured over 211 200 old-vs-new pairs there is no third class: the index
|
|
41
|
-
* never loses a match the scan found, and its answer is never matched at a less
|
|
42
|
-
* specific (path-part, extension) position. `test/unit/scope-resolution/
|
|
43
|
-
* javascript-import-target-parity.test.ts` is that differential, and pins both
|
|
44
|
-
* classes by witness.
|
|
45
|
-
*/
|
|
46
|
-
import { type TsResolveContext } from '../typescript/import-target.js';
|
|
47
|
-
export type JsResolveContext = TsResolveContext;
|
|
48
|
-
/**
|
|
49
|
-
* Build a memoized `resolveImportTarget` adapter for JavaScript.
|
|
50
|
-
* Caches the derived arrays, the suffix index and the per-pass resolve cache
|
|
51
|
-
* across `resolveImportTarget` calls over one workspace file set.
|
|
4
|
+
* Delegates to the TypeScript resolver, which is correct rather than merely
|
|
5
|
+
* convenient: `jsconfig.json` is a tsconfig by another name, `package.json`
|
|
6
|
+
* governs both languages identically, and Node's algorithm does not branch on
|
|
7
|
+
* which of the two wrote the file. The extension list already carries the JS
|
|
8
|
+
* family, so a `.js`/`.jsx`/`.mjs`/`.cjs` source resolves the same way.
|
|
9
|
+
*
|
|
10
|
+
* CJS `require()` calls reference the same module-path strings as ESM `import`
|
|
11
|
+
* statements, so the resolver handles them uniformly with no CJS-specific
|
|
12
|
+
* logic here.
|
|
13
|
+
*
|
|
14
|
+
* ## What #2953 removed
|
|
15
|
+
*
|
|
16
|
+
* This adapter used to reach `resolveImportPath`, whose last step was
|
|
17
|
+
* `suffixResolve` — a search for any repo file whose path ends in the
|
|
18
|
+
* specifier, retried with each leading segment dropped. The header this
|
|
19
|
+
* replaces recorded the symptom without naming it a defect: `import 'app/main'`
|
|
20
|
+
* resolving to `node_modules/dep/lib/main.js`, "the first `/main.js` in file
|
|
21
|
+
* order". A bare specifier now resolves only through a declared tsconfig
|
|
22
|
+
* mapping or a package manifest, and otherwise not at all.
|
|
52
23
|
*/
|
|
24
|
+
/** Build the JavaScript `resolveImportTarget` adapter. */
|
|
53
25
|
export declare function makeJsResolveImportTarget(): (targetRaw: string, fromFile: string, allFilePaths: ReadonlySet<string>, resolutionConfig?: unknown) => string | readonly string[] | null;
|