carrick 0.3.72 → 0.3.75

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.
Files changed (38) hide show
  1. package/dist/contract.d.ts +11 -2
  2. package/dist/contract.js +5 -0
  3. package/dist/contract.js.map +1 -1
  4. package/dist/init/run.js +1 -1
  5. package/dist/init/run.js.map +1 -1
  6. package/dist/render.js +11 -3
  7. package/dist/render.js.map +1 -1
  8. package/package.json +6 -6
  9. package/sidecar/dist/src/capture/anchors.d.ts +15 -0
  10. package/sidecar/dist/src/capture/anchors.js +70 -5
  11. package/sidecar/dist/src/capture/api.d.ts +39 -4
  12. package/sidecar/dist/src/capture/check-classify.d.ts +2 -0
  13. package/sidecar/dist/src/capture/check-classify.js +28 -0
  14. package/sidecar/dist/src/capture/check-deep.js +1 -1
  15. package/sidecar/dist/src/capture/check-probe.d.ts +1 -1
  16. package/sidecar/dist/src/capture/check-probe.js +16 -0
  17. package/sidecar/dist/src/capture/deep-walk.d.ts +33 -6
  18. package/sidecar/dist/src/capture/deep-walk.js +81 -28
  19. package/sidecar/dist/src/capture/index.js +16 -6
  20. package/sidecar/dist/src/capture/installed-package.d.ts +58 -0
  21. package/sidecar/dist/src/capture/installed-package.js +311 -0
  22. package/sidecar/dist/src/capture/lockfile.d.ts +8 -0
  23. package/sidecar/dist/src/capture/lockfile.js +1 -1
  24. package/sidecar/dist/src/capture/node-builder.d.ts +27 -0
  25. package/sidecar/dist/src/capture/node-builder.js +107 -1
  26. package/sidecar/dist/src/capture/paths-rewrite.d.ts +23 -6
  27. package/sidecar/dist/src/capture/paths-rewrite.js +43 -11
  28. package/sidecar/dist/src/capture/self-check.js +12 -3
  29. package/sidecar/dist/src/capture/unresolved.d.ts +28 -0
  30. package/sidecar/dist/src/capture/unresolved.js +107 -0
  31. package/sidecar/dist/src/type-inferrer.d.ts +108 -11
  32. package/sidecar/dist/src/type-inferrer.js +513 -90
  33. package/sidecar/dist/src/type-structural-expander.d.ts +23 -2
  34. package/sidecar/dist/src/type-structural-expander.js +55 -17
  35. package/sidecar/dist/src/type-text-canonicalizer.d.ts +14 -0
  36. package/sidecar/dist/src/type-text-canonicalizer.js +23 -2
  37. package/sidecar/dist/src/validators.d.ts +10 -0
  38. package/sidecar/dist/src/validators.js +1 -0
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Map an absolute path into an installed package onto the specifier a
3
+ * consumer of that package would write (carrick#1174).
4
+ *
5
+ * The v1 printer and the node builder name an out-of-scope type by the
6
+ * absolute path of its declaration file when no bare specifier reaches it
7
+ * from where they print. Inside a capture stub that path is both private (the
8
+ * checkout root, or the home directory of a runtime's npm cache) and dead:
9
+ * the check workspace resolves packages from the stub's pinned dependencies,
10
+ * never from the scanning machine's disk.
11
+ *
12
+ * The replacement is decided against the package itself, installed alone at
13
+ * `node_modules/<name>`, which is exactly what the check workspace sees:
14
+ * 1. an `exports` entry (or, without `exports`, the types entry or the file's
15
+ * subpath) that resolves to the same file under bundler resolution;
16
+ * 2. failing that, an exported entry whose module exports the imported name
17
+ * from that same file (a type in an unexported file, re-exported by the
18
+ * package root);
19
+ * 3. failing both, the subpath as written, so the self-check reports the
20
+ * specifier as unresolvable rather than the stub leaking the path.
21
+ *
22
+ * Seam: node builtins, `typescript`, and this bundle only.
23
+ */
24
+ import ts from 'typescript';
25
+ import * as fs from 'node:fs';
26
+ import * as path from 'node:path';
27
+ import { isPublishedSemver } from './lockfile.js';
28
+ const DECLARATION_SUFFIXES = ['', '.d.ts', '.d.mts', '.d.cts', '.ts', '.mts', '.cts', '/index.d.ts'];
29
+ const RESOLUTION_OPTIONS = {
30
+ module: ts.ModuleKind.ESNext,
31
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
32
+ target: ts.ScriptTarget.ESNext,
33
+ noEmit: true,
34
+ skipLibCheck: true,
35
+ types: [],
36
+ };
37
+ /** DefinitelyTyped packages are imported by the runtime name (TS6137). */
38
+ function publicNameOf(name) {
39
+ if (!name.startsWith('@types/'))
40
+ return name;
41
+ const bare = name.slice('@types/'.length);
42
+ const scoped = /^([^_]+)__(.+)$/.exec(bare);
43
+ return scoped ? `@${scoped[1]}/${scoped[2]}` : bare;
44
+ }
45
+ /**
46
+ * The `@types/*` package that serves a bare name's declarations, for callers
47
+ * deciding whether a runtime name is covered by a pinned types package.
48
+ */
49
+ export function typesPackageOf(name) {
50
+ const scoped = /^@([^/]+)\/(.+)$/.exec(name);
51
+ return scoped ? `@types/${scoped[1]}__${scoped[2]}` : `@types/${name}`;
52
+ }
53
+ function toPosix(p) {
54
+ return p.split(path.sep).join('/');
55
+ }
56
+ function readManifest(dir) {
57
+ try {
58
+ const parsed = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
59
+ if (typeof parsed?.name === 'string' && typeof parsed?.version === 'string') {
60
+ return parsed;
61
+ }
62
+ }
63
+ catch {
64
+ // Absent or unreadable: not a package root.
65
+ }
66
+ return undefined;
67
+ }
68
+ /**
69
+ * An installed package root is a directory named by its package, either at
70
+ * `node_modules/<name>` (flat and isolated stores alike) or at
71
+ * `<name>/<version>` (a runtime's npm cache). A nested `package.json` that only
72
+ * sets a module type, or a workspace member, is neither.
73
+ */
74
+ function isInstalledRoot(dir, manifest) {
75
+ const posix = toPosix(dir);
76
+ return (posix.endsWith(`/node_modules/${manifest.name}`) ||
77
+ posix.endsWith(`/${manifest.name}/${manifest.version}`));
78
+ }
79
+ function packageRootOf(file) {
80
+ let dir = path.dirname(file);
81
+ while (true) {
82
+ const manifest = readManifest(dir);
83
+ if (manifest && isInstalledRoot(dir, manifest))
84
+ return { root: dir, manifest };
85
+ const parent = path.dirname(dir);
86
+ if (parent === dir)
87
+ return undefined;
88
+ dir = parent;
89
+ }
90
+ }
91
+ function existingFile(spec) {
92
+ for (const suffix of DECLARATION_SUFFIXES) {
93
+ const candidate = spec + suffix;
94
+ try {
95
+ if (fs.statSync(candidate).isFile())
96
+ return fs.realpathSync(candidate);
97
+ }
98
+ catch {
99
+ // try the next suffix
100
+ }
101
+ }
102
+ return undefined;
103
+ }
104
+ function withoutExtension(subpath) {
105
+ return subpath.replace(/(?:\.d)?\.(?:ts|mts|cts|js|mjs|cjs|tsx|jsx)$/, '');
106
+ }
107
+ /** Every string target an exports value names, whatever its conditions. */
108
+ function exportTargets(value) {
109
+ if (typeof value === 'string')
110
+ return [value];
111
+ if (Array.isArray(value))
112
+ return value.flatMap(exportTargets);
113
+ if (value && typeof value === 'object')
114
+ return Object.values(value).flatMap(exportTargets);
115
+ return [];
116
+ }
117
+ /** Candidate subpaths (`''` or `/sub`) that could name `subpath` of the package. */
118
+ function candidateSubpaths(manifest, subpath) {
119
+ const target = withoutExtension(subpath);
120
+ const candidates = [];
121
+ const exportsField = manifest.exports;
122
+ if (exportsField !== undefined && exportsField !== null) {
123
+ const entries = typeof exportsField === 'object' &&
124
+ !Array.isArray(exportsField) &&
125
+ Object.keys(exportsField).some((key) => key.startsWith('.'))
126
+ ? Object.entries(exportsField)
127
+ : [['.', exportsField]];
128
+ for (const [key, value] of entries) {
129
+ if (!key.startsWith('.'))
130
+ continue;
131
+ for (const raw of exportTargets(value)) {
132
+ const exported = withoutExtension(raw.replace(/^\.\//, ''));
133
+ if (!key.includes('*')) {
134
+ if (exported === target)
135
+ candidates.push(key === '.' ? '' : key.slice(1));
136
+ continue;
137
+ }
138
+ const [before, after] = exported.split('*');
139
+ if (after === undefined || !target.startsWith(before) || !target.endsWith(after))
140
+ continue;
141
+ const star = target.slice(before.length, target.length - after.length);
142
+ candidates.push(key.slice(1).replace('*', star));
143
+ }
144
+ }
145
+ return [...new Set(candidates)];
146
+ }
147
+ const entry = manifest.types ?? manifest.typings ?? manifest.main ?? 'index';
148
+ if (withoutExtension(entry.replace(/^\.\//, '')) === target)
149
+ candidates.push('');
150
+ candidates.push(`/${target.replace(/\/index$/, '')}`);
151
+ return [...new Set(candidates)];
152
+ }
153
+ /**
154
+ * Resolve `specifier` as the check workspace would: the package installed
155
+ * alone at `node_modules/<name>` beside the importing file. Returns the real
156
+ * path of the file it lands on.
157
+ */
158
+ function resolveAgainstInstall(specifier, root, packageName) {
159
+ const virtualRoot = path.join(path.parse(root).root, '__carrick_specifier_probe__');
160
+ const virtualPackage = path.join(virtualRoot, 'node_modules', ...packageName.split('/'));
161
+ const actual = (file) => {
162
+ if (file === virtualPackage || file.startsWith(virtualPackage + path.sep)) {
163
+ return path.join(root, file.slice(virtualPackage.length));
164
+ }
165
+ return undefined;
166
+ };
167
+ const isVirtualAncestor = (dir) => dir === virtualRoot || (virtualPackage + path.sep).startsWith(dir + path.sep);
168
+ const host = {
169
+ fileExists: (file) => {
170
+ const real = actual(file);
171
+ return real !== undefined && ts.sys.fileExists(real);
172
+ },
173
+ readFile: (file) => {
174
+ const real = actual(file);
175
+ return real === undefined ? undefined : ts.sys.readFile(real);
176
+ },
177
+ directoryExists: (dir) => {
178
+ if (isVirtualAncestor(dir))
179
+ return true;
180
+ const real = actual(dir);
181
+ return real !== undefined && ts.sys.directoryExists(real);
182
+ },
183
+ realpath: (file) => actual(file) ?? file,
184
+ getCurrentDirectory: () => virtualRoot,
185
+ };
186
+ const resolved = ts.resolveModuleName(specifier, path.join(virtualRoot, 'probe.ts'), RESOLUTION_OPTIONS, host).resolvedModule;
187
+ if (!resolved)
188
+ return undefined;
189
+ try {
190
+ const resolvedFileName = fs.realpathSync(actual(resolved.resolvedFileName) ?? resolved.resolvedFileName);
191
+ return { ...resolved, resolvedFileName, isExternalLibraryImport: true };
192
+ }
193
+ catch {
194
+ return undefined;
195
+ }
196
+ }
197
+ /**
198
+ * A compiler host that also resolves the packages an absolute specifier was
199
+ * rewritten into, each from its own install directory. The capture's
200
+ * self-check compiles the stub against the producer's `node_modules`, which
201
+ * does not name a transitive package at its root; the check workspace does,
202
+ * because the stub pins it. Without this the self-check would fail a
203
+ * specifier the check resolves.
204
+ */
205
+ export function withInstalledPackages(options, installs, base = ts.createCompilerHost(options)) {
206
+ if (Object.keys(installs).length === 0)
207
+ return base;
208
+ const fallback = base.resolveModuleNames?.bind(base);
209
+ base.resolveModuleNames = (names, containingFile, reused, redirected, compilerOptions, containingSourceFile) => names.map((name, index) => {
210
+ const resolved = fallback
211
+ ? fallback([name], containingFile, reused?.slice(index, index + 1), redirected, compilerOptions, containingSourceFile)[0]
212
+ : ts.resolveModuleName(name, containingFile, compilerOptions, base).resolvedModule;
213
+ if (resolved || name.startsWith('.') || name.startsWith('/'))
214
+ return resolved;
215
+ const packageName = packageNameOfSpecifier(name);
216
+ for (const owner of [packageName, typesPackageOf(packageName)]) {
217
+ const root = installs[owner];
218
+ if (root) {
219
+ const fromInstall = resolveAgainstInstall(name, root, owner);
220
+ if (fromInstall)
221
+ return fromInstall;
222
+ }
223
+ }
224
+ return undefined;
225
+ });
226
+ return base;
227
+ }
228
+ function packageNameOfSpecifier(spec) {
229
+ const parts = spec.split('/');
230
+ return spec.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
231
+ }
232
+ /** Entry subpaths of a package that a consumer can import. */
233
+ function entrySubpaths(manifest) {
234
+ const exportsField = manifest.exports;
235
+ if (exportsField === undefined || exportsField === null)
236
+ return [''];
237
+ if (typeof exportsField === 'object' &&
238
+ !Array.isArray(exportsField) &&
239
+ Object.keys(exportsField).some((key) => key.startsWith('.'))) {
240
+ return Object.keys(exportsField)
241
+ .filter((key) => key.startsWith('.') && !key.includes('*'))
242
+ .map((key) => (key === '.' ? '' : key.slice(1)));
243
+ }
244
+ return [''];
245
+ }
246
+ const moduleExportsCache = new Map();
247
+ /** Exported names of `entryFile`, each with the real files declaring it. */
248
+ function exportsOf(entryFile) {
249
+ const cached = moduleExportsCache.get(entryFile);
250
+ if (cached)
251
+ return cached;
252
+ const exported = new Map();
253
+ const program = ts.createProgram([entryFile], RESOLUTION_OPTIONS);
254
+ const checker = program.getTypeChecker();
255
+ const source = program.getSourceFile(entryFile);
256
+ const moduleSymbol = source && checker.getSymbolAtLocation(source);
257
+ for (const symbol of moduleSymbol ? checker.getExportsOfModule(moduleSymbol) : []) {
258
+ const target = symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
259
+ const files = new Set();
260
+ for (const declaration of target.declarations ?? []) {
261
+ try {
262
+ files.add(fs.realpathSync(declaration.getSourceFile().fileName));
263
+ }
264
+ catch {
265
+ // declaration outside the real file system: ignore
266
+ }
267
+ }
268
+ exported.set(symbol.getName(), files);
269
+ }
270
+ moduleExportsCache.set(entryFile, exported);
271
+ return exported;
272
+ }
273
+ /**
274
+ * The bare specifier for an absolute path into an installed package, or
275
+ * undefined when the path does not land in one. `importedName` is the first
276
+ * name read off the import (`import("...").Name`), when the text has one.
277
+ */
278
+ export function installedPackageSpecifier(absoluteSpecifier, importedName) {
279
+ const file = existingFile(absoluteSpecifier);
280
+ if (!file)
281
+ return undefined;
282
+ const owner = packageRootOf(file);
283
+ if (!owner)
284
+ return undefined;
285
+ const root = fs.realpathSync(owner.root);
286
+ const { manifest } = owner;
287
+ const publicName = publicNameOf(manifest.name);
288
+ const pin = isPublishedSemver(manifest.version)
289
+ ? { name: manifest.name, version: manifest.version }
290
+ : undefined;
291
+ const subpath = toPosix(path.relative(root, file));
292
+ const install = { name: manifest.name, root };
293
+ const withPin = (specifier) => pin ? { specifier, install, pin } : { specifier, install };
294
+ const candidates = candidateSubpaths(manifest, subpath);
295
+ for (const candidate of candidates) {
296
+ if (resolveAgainstInstall(publicName + candidate, root, manifest.name)?.resolvedFileName === file) {
297
+ return withPin(publicName + candidate);
298
+ }
299
+ }
300
+ if (importedName) {
301
+ for (const entry of entrySubpaths(manifest)) {
302
+ const entryFile = resolveAgainstInstall(publicName + entry, root, manifest.name)?.resolvedFileName;
303
+ if (!entryFile)
304
+ continue;
305
+ if (exportsOf(entryFile).get(importedName)?.has(file)) {
306
+ return withPin(publicName + entry);
307
+ }
308
+ }
309
+ }
310
+ return withPin(publicName + (candidates[0] ?? `/${withoutExtension(subpath)}`));
311
+ }
@@ -19,6 +19,14 @@
19
19
  * stays UNPINNED (fail-closed abstain): a wrong pin fails or corrupts the
20
20
  * synthetic-workspace typecheck, no pin merely abstains.
21
21
  */
22
+ /**
23
+ * Published-semver gate for node_modules pins. Rejects anything a registry
24
+ * install could not satisfy: workspace/link protocol leakage
25
+ * (`workspace:*`, `link:...`) and yarn-berry's `0.0.0-use.local` local
26
+ * sentinel. A rejected version means "unpinned" (fail-closed abstain
27
+ * downstream), never a pin that would fail the synthetic-workspace install.
28
+ */
29
+ export declare function isPublishedSemver(version: string): boolean;
22
30
  /**
23
31
  * Resolve exact versions from the repo's installed node_modules by reading
24
32
  * `node_modules/<pkg>/package.json` for each referenced external. On an
@@ -155,7 +155,7 @@ function pnpmLockfileVersions(lockPath) {
155
155
  * sentinel. A rejected version means "unpinned" (fail-closed abstain
156
156
  * downstream), never a pin that would fail the synthetic-workspace install.
157
157
  */
158
- function isPublishedSemver(version) {
158
+ export function isPublishedSemver(version) {
159
159
  return (/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version) &&
160
160
  !version.startsWith('0.0.0-use.'));
161
161
  }
@@ -28,6 +28,11 @@ export interface NodeBuilderPrintResult {
28
28
  inaccessible: string[];
29
29
  /** Failure description when text is absent. */
30
30
  failure?: string;
31
+ /**
32
+ * Names the print refers to that do not resolve at the destination in the
33
+ * producer's program (carrick#1165). Present only when there are some.
34
+ */
35
+ undeclaredNames?: string[];
31
36
  }
32
37
  /**
33
38
  * Print `type` as a type node anchored at `destination` (a declaration inside
@@ -35,3 +40,25 @@ export interface NodeBuilderPrintResult {
35
40
  * any referenced symbol is not plainly accessible from the destination.
36
41
  */
37
42
  export declare function printTypeForDestination(program: ts.Program, type: ts.Type, destination: ts.Node): NodeBuilderPrintResult;
43
+ /**
44
+ * Bare names in a printed type node that nothing in the producer's program
45
+ * declares (carrick#1165).
46
+ *
47
+ * The builder names an out-of-scope declaration through `import("...")`, and
48
+ * the tracker demotes a symbol it cannot reach, so a bare reference is meant
49
+ * to be in scope where the alias is declared. The one way it is not: the
50
+ * builder REUSES a source annotation as written (`parcel: Row`) when the
51
+ * annotation's import did not resolve. There is then no symbol to track, and
52
+ * the surface names an identifier nothing declares. Literal anchor text, which
53
+ * the v1 walk printed, can name a type the same way.
54
+ *
55
+ * A name is listed only when it neither resolves at the destination (every
56
+ * lib, runtime and `@types` global does) nor has a declaration anywhere in the
57
+ * program. The second test matters on a healthy checkout: the v1 walk prints
58
+ * an enum member (`Status.Open`) or a recursive reference by name, and that
59
+ * name is declared in the project though not in scope at the surface. An
60
+ * import binding is not a declaration, so a name whose only source is an
61
+ * import that did not resolve is still listed. Type parameters the print
62
+ * itself declares (generic signatures, mapped and `infer` types) are excluded.
63
+ */
64
+ export declare function undeclaredNamesIn(node: ts.TypeNode, program: ts.Program, destination: ts.Node): string[];
@@ -119,5 +119,111 @@ export function printTypeForDestination(program, type, destination) {
119
119
  }
120
120
  const printer = ts.createPrinter({ removeComments: true });
121
121
  const text = printer.printNode(ts.EmitHint.Unspecified, node, destination.getSourceFile());
122
- return { text, inaccessible };
122
+ const undeclaredNames = undeclaredNamesIn(node, program, destination);
123
+ return { text, inaccessible, ...(undeclaredNames.length > 0 ? { undeclaredNames } : {}) };
124
+ }
125
+ /**
126
+ * Bare names in a printed type node that nothing in the producer's program
127
+ * declares (carrick#1165).
128
+ *
129
+ * The builder names an out-of-scope declaration through `import("...")`, and
130
+ * the tracker demotes a symbol it cannot reach, so a bare reference is meant
131
+ * to be in scope where the alias is declared. The one way it is not: the
132
+ * builder REUSES a source annotation as written (`parcel: Row`) when the
133
+ * annotation's import did not resolve. There is then no symbol to track, and
134
+ * the surface names an identifier nothing declares. Literal anchor text, which
135
+ * the v1 walk printed, can name a type the same way.
136
+ *
137
+ * A name is listed only when it neither resolves at the destination (every
138
+ * lib, runtime and `@types` global does) nor has a declaration anywhere in the
139
+ * program. The second test matters on a healthy checkout: the v1 walk prints
140
+ * an enum member (`Status.Open`) or a recursive reference by name, and that
141
+ * name is declared in the project though not in scope at the surface. An
142
+ * import binding is not a declaration, so a name whose only source is an
143
+ * import that did not resolve is still listed. Type parameters the print
144
+ * itself declares (generic signatures, mapped and `infer` types) are excluded.
145
+ */
146
+ export function undeclaredNamesIn(node, program, destination) {
147
+ const checker = program.getTypeChecker();
148
+ const typeParameters = new Set();
149
+ const leftmost = (name) => ts.isIdentifier(name) ? name : leftmost(name.left);
150
+ const references = [];
151
+ const visit = (current) => {
152
+ if (ts.isTypeParameterDeclaration(current)) {
153
+ typeParameters.add(current.name.text);
154
+ }
155
+ else if (ts.isTypeReferenceNode(current)) {
156
+ references.push({ name: leftmost(current.typeName).text, space: 'types' });
157
+ }
158
+ else if (ts.isTypeQueryNode(current)) {
159
+ references.push({ name: leftmost(current.exprName).text, space: 'values' });
160
+ }
161
+ ts.forEachChild(current, visit);
162
+ };
163
+ visit(node);
164
+ const undeclared = new Set();
165
+ for (const { name, space } of references) {
166
+ if (typeParameters.has(name) || undeclared.has(name))
167
+ continue;
168
+ const meaning = (space === 'types' ? ts.SymbolFlags.Type : ts.SymbolFlags.Value) |
169
+ ts.SymbolFlags.Namespace |
170
+ ts.SymbolFlags.Alias;
171
+ if (checker.resolveName(name, destination, meaning, false))
172
+ continue;
173
+ if (declaredNamesOf(program)[space].has(name))
174
+ continue;
175
+ undeclared.add(name);
176
+ }
177
+ return [...undeclared].sort();
178
+ }
179
+ const declaredNamesCache = new WeakMap();
180
+ /**
181
+ * Every name a declaration in the program introduces, split by the space it
182
+ * can be referenced from: at any depth of a source file and at any namespace
183
+ * depth of a declaration file. Import bindings are left out on purpose (see
184
+ * `undeclaredNamesIn`). Built once per program.
185
+ */
186
+ function declaredNamesOf(program) {
187
+ const cached = declaredNamesCache.get(program);
188
+ if (cached)
189
+ return cached;
190
+ const names = { types: new Set(), values: new Set() };
191
+ const record = (name, types, values) => {
192
+ if (!name || !ts.isIdentifier(name))
193
+ return;
194
+ if (types)
195
+ names.types.add(name.text);
196
+ if (values)
197
+ names.values.add(name.text);
198
+ };
199
+ const visit = (current, deep) => {
200
+ if (ts.isInterfaceDeclaration(current) || ts.isTypeAliasDeclaration(current)) {
201
+ record(current.name, true, false);
202
+ }
203
+ else if (ts.isClassDeclaration(current) ||
204
+ ts.isEnumDeclaration(current) ||
205
+ ts.isModuleDeclaration(current)) {
206
+ // A namespace can qualify a type reference (`Ns.Row`) and a query.
207
+ record(current.name, true, true);
208
+ }
209
+ else if (ts.isFunctionDeclaration(current) || ts.isVariableDeclaration(current)) {
210
+ record(current.name, false, true);
211
+ }
212
+ // A declaration file has no bodies to hide a declaration in, so its
213
+ // statement lists (and namespace blocks) are the whole story; a lib or
214
+ // package file is walked no deeper than that.
215
+ if (deep ||
216
+ ts.isSourceFile(current) ||
217
+ ts.isModuleDeclaration(current) ||
218
+ ts.isModuleBlock(current) ||
219
+ ts.isVariableStatement(current) ||
220
+ ts.isVariableDeclarationList(current)) {
221
+ ts.forEachChild(current, (child) => visit(child, deep));
222
+ }
223
+ };
224
+ for (const sourceFile of program.getSourceFiles()) {
225
+ visit(sourceFile, !sourceFile.isDeclarationFile);
226
+ }
227
+ declaredNamesCache.set(program, names);
228
+ return names;
123
229
  }
@@ -7,9 +7,10 @@
7
7
  * producer's own resolution context -- `paths` cannot be fixed at check time
8
8
  * (program-global, and namespaces collide across stubs).
9
9
  *
10
- * Specifiers that map outside the emitted tree are left untouched: the
11
- * per-alias self-check classifies them as dangling internals with a recorded
12
- * reason, which is the honest outcome.
10
+ * An absolute path into an installed package becomes that package's bare
11
+ * specifier (installed-package.ts). Other specifiers that map outside the
12
+ * emitted tree are left untouched: the per-alias self-check classifies them
13
+ * as dangling internals with a recorded reason, which is the honest outcome.
13
14
  */
14
15
  import ts from 'typescript';
15
16
  import { type PathsPattern } from './specifiers.js';
@@ -27,8 +28,24 @@ export interface RewriteArgs {
27
28
  entryDir: string;
28
29
  }
29
30
  export declare function parsePathsPatterns(options: ts.CompilerOptions, configPath: string): PathsPattern[];
31
+ export interface RewriteResult {
32
+ /** Specifiers rewritten across every emitted file. */
33
+ rewrites: number;
34
+ /**
35
+ * Installed packages an absolute specifier was rewritten into, with their
36
+ * installed versions: the stub must pin them for the bare specifier to
37
+ * resolve at check time.
38
+ */
39
+ pins: Record<string, string>;
40
+ /**
41
+ * The same packages' real install directories, for the self-check's
42
+ * resolution only. Absolute paths: never written into the stub.
43
+ */
44
+ installs: Record<string, string>;
45
+ }
30
46
  /**
31
- * Rewrite paths-mapped and absolute-internal specifiers in every emitted
32
- * file. Returns the number of specifiers rewritten.
47
+ * Rewrite paths-mapped, absolute-internal and absolute installed-package
48
+ * specifiers in every emitted file (carrick#1174 for the last: a path into
49
+ * `node_modules` or a runtime npm cache becomes the package's bare specifier).
33
50
  */
34
- export declare function rewriteEmittedSpecifiers(args: RewriteArgs): number;
51
+ export declare function rewriteEmittedSpecifiers(args: RewriteArgs): RewriteResult;
@@ -7,12 +7,14 @@
7
7
  * producer's own resolution context -- `paths` cannot be fixed at check time
8
8
  * (program-global, and namespaces collide across stubs).
9
9
  *
10
- * Specifiers that map outside the emitted tree are left untouched: the
11
- * per-alias self-check classifies them as dangling internals with a recorded
12
- * reason, which is the honest outcome.
10
+ * An absolute path into an installed package becomes that package's bare
11
+ * specifier (installed-package.ts). Other specifiers that map outside the
12
+ * emitted tree are left untouched: the per-alias self-check classifies them
13
+ * as dangling internals with a recorded reason, which is the honest outcome.
13
14
  */
14
15
  import * as fs from 'node:fs';
15
16
  import * as path from 'node:path';
17
+ import { installedPackageSpecifier } from './installed-package.js';
16
18
  import { isRelative, matchPathsPattern, rewriteSpecifiers, } from './specifiers.js';
17
19
  export function parsePathsPatterns(options, configPath) {
18
20
  const paths = options.paths;
@@ -55,21 +57,51 @@ function relativeSpecifier(fromFile, toFile) {
55
57
  return rel;
56
58
  }
57
59
  /**
58
- * Rewrite paths-mapped and absolute-internal specifiers in every emitted
59
- * file. Returns the number of specifiers rewritten.
60
+ * `import("<absolute>").Name` occurrences: the absolute specifier and the
61
+ * first name read off it, so an installed-package rewrite can find the entry
62
+ * that exports that name.
63
+ */
64
+ const ABSOLUTE_IMPORT_TYPE = /(import\s*\(\s*)(["'])(\/[^"']+)\2(\s*\))(\s*\.\s*)([A-Za-z_$][\w$]*)/g;
65
+ /**
66
+ * Rewrite paths-mapped, absolute-internal and absolute installed-package
67
+ * specifiers in every emitted file (carrick#1174 for the last: a path into
68
+ * `node_modules` or a runtime npm cache becomes the package's bare specifier).
60
69
  */
61
70
  export function rewriteEmittedSpecifiers(args) {
62
71
  const patterns = parsePathsPatterns(args.options, args.configPath);
63
72
  const emitted = new Set(args.files);
73
+ const pins = {};
74
+ const installs = {};
64
75
  let total = 0;
76
+ const installed = (spec, importedName) => {
77
+ const mapped = installedPackageSpecifier(spec, importedName);
78
+ if (!mapped)
79
+ return undefined;
80
+ installs[mapped.install.name] = mapped.install.root;
81
+ if (mapped.pin)
82
+ pins[mapped.pin.name] = mapped.pin.version;
83
+ return mapped.specifier;
84
+ };
65
85
  for (const file of args.files) {
66
86
  const absFile = path.join(args.typesDir, file);
67
- const text = fs.readFileSync(absFile, 'utf8');
87
+ const original = fs.readFileSync(absFile, 'utf8');
88
+ let importTypeRewrites = 0;
89
+ // Import types first, while the member name is still attached to its
90
+ // specifier; an in-tree target is left for the general pass below.
91
+ const text = original.replace(ABSOLUTE_IMPORT_TYPE, (whole, open, quote, spec, close, dot, name) => {
92
+ if (treeFileFor(spec, args.entryDir, emitted))
93
+ return whole;
94
+ const replacement = installed(spec, name);
95
+ if (replacement === undefined)
96
+ return whole;
97
+ importTypeRewrites++;
98
+ return `${open}${quote}${replacement}${quote}${close}${dot}${name}`;
99
+ });
68
100
  const { text: rewritten, rewrites } = rewriteSpecifiers(text, (spec) => {
69
- // Absolute internal paths (node-builder import types).
101
+ // Absolute paths: an emitted tree file, else an installed package.
70
102
  if (spec.startsWith('/')) {
71
103
  const target = treeFileFor(spec, args.entryDir, emitted);
72
- return target ? relativeSpecifier(file, target) : undefined;
104
+ return target ? relativeSpecifier(file, target) : installed(spec);
73
105
  }
74
106
  if (isRelative(spec))
75
107
  return undefined;
@@ -91,10 +123,10 @@ export function rewriteEmittedSpecifiers(args) {
91
123
  }
92
124
  return undefined;
93
125
  });
94
- if (rewrites > 0) {
126
+ if (rewrites + importTypeRewrites > 0) {
95
127
  fs.writeFileSync(absFile, rewritten);
96
- total += rewrites;
128
+ total += rewrites + importTypeRewrites;
97
129
  }
98
130
  }
99
- return total;
131
+ return { rewrites: total, pins, installs };
100
132
  }
@@ -142,7 +142,7 @@ function demotedRecord(anchor) {
142
142
  alias: anchor.request.alias,
143
143
  anchor_kind: anchor.request.kind,
144
144
  symbol_name: 'symbol_name' in anchor.request ? anchor.request.symbol_name : undefined,
145
- source_file: 'source_file' in anchor.request ? anchor.request.source_file : '<inline>',
145
+ source_file: anchor.request.kind === 'literal' ? '<inline>' : anchor.request.source_file,
146
146
  anchor_origin: anchor.request.anchor_origin,
147
147
  serialization: 'structural_fallback',
148
148
  self_check: 'decayed_internal',
@@ -194,6 +194,7 @@ function checkedRecord(anchor, ctx) {
194
194
  }
195
195
  let blamedExternal;
196
196
  let internalFailure;
197
+ const danglingSpecifiers = new Set();
197
198
  for (const file of closure) {
198
199
  const failures = ctx.failuresByFile.get(file);
199
200
  if (!failures)
@@ -202,6 +203,8 @@ function checkedRecord(anchor, ctx) {
202
203
  blamedExternal = [...failures.externalPinned][0];
203
204
  if (!internalFailure)
204
205
  internalFailure = [...failures.internal][0];
206
+ for (const specifier of failures.internal)
207
+ danglingSpecifiers.add(specifier);
205
208
  }
206
209
  // Classification consults the closure failures REGARDLESS of the root
207
210
  // type: a dangling internal specifier means part of this alias's closure
@@ -283,7 +286,7 @@ function checkedRecord(anchor, ctx) {
283
286
  alias,
284
287
  anchor_kind: anchor.request.kind,
285
288
  symbol_name: 'symbol_name' in anchor.request ? anchor.request.symbol_name : undefined,
286
- source_file: 'source_file' in anchor.request ? anchor.request.source_file : '<inline>',
289
+ source_file: anchor.request.kind === 'literal' ? '<inline>' : anchor.request.source_file,
287
290
  anchor_origin: anchor.request.anchor_origin,
288
291
  serialization: anchor.serialization,
289
292
  self_check: outcome,
@@ -295,8 +298,14 @@ function checkedRecord(anchor, ctx) {
295
298
  self_check_detail: anchor.abstainReason ?? detail ?? anchor.reaimNote,
296
299
  top_type_at_self_check: topType,
297
300
  ...(unexplainedDeep.length > 0
298
- ? { any_provenance: unexplainedDeep.map(provenanceOf) }
301
+ ? {
302
+ any_provenance: unexplainedDeep.map((finding) => provenanceOf(finding, anchor.unresolved)),
303
+ }
304
+ : {}),
305
+ ...(danglingSpecifiers.size > 0
306
+ ? { dangling_specifiers: [...danglingSpecifiers].sort() }
299
307
  : {}),
308
+ ...(anchor.undeclaredNames ? { undeclared_names: anchor.undeclaredNames } : {}),
300
309
  };
301
310
  }
302
311
  /** Resolve a relative specifier from `fromAbs` to a tree file, if present. */