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.
@@ -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 `resolveTsTarget` standard-strategy resolver
5
- * with `language: SupportedLanguages.JavaScript` so the resolver tries
6
- * `.js` / `.jsx` extensions in addition to (or instead of) `.ts` / `.tsx`.
7
- *
8
- * The `TsResolveContext.language` flag already exists in `import-target.ts`
9
- * and the resolver (`resolveImportPath`) already branches on it — this
10
- * adapter just wires the right value in.
11
- *
12
- * CJS `require()` calls reference the same module-path strings as ESM
13
- * `import` statements, so the resolver handles them uniformly without any
14
- * CJS-specific logic here.
15
- *
16
- * No `tsconfig.json` path-alias support (JavaScript projects don't use
17
- * `tsconfig.json` compilerOptions.paths in general). Projects that DO use
18
- * tsconfig-based aliases alongside JavaScript can still resolve via the
19
- * standard extension-suffix fallback; the alias branch is a no-op when
20
- * `tsconfigPaths` is null.
21
- *
22
- * ## The suffix index changes bare-specifier answers (PR #2911)
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;