carrick 0.3.72 → 0.3.73
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/package.json +6 -6
- package/sidecar/dist/src/capture/anchors.d.ts +15 -0
- package/sidecar/dist/src/capture/anchors.js +70 -5
- package/sidecar/dist/src/capture/api.d.ts +39 -4
- package/sidecar/dist/src/capture/check-classify.d.ts +2 -0
- package/sidecar/dist/src/capture/check-classify.js +28 -0
- package/sidecar/dist/src/capture/check-deep.js +1 -1
- package/sidecar/dist/src/capture/check-probe.d.ts +1 -1
- package/sidecar/dist/src/capture/check-probe.js +16 -0
- package/sidecar/dist/src/capture/deep-walk.d.ts +33 -6
- package/sidecar/dist/src/capture/deep-walk.js +81 -28
- package/sidecar/dist/src/capture/index.js +16 -6
- package/sidecar/dist/src/capture/installed-package.d.ts +58 -0
- package/sidecar/dist/src/capture/installed-package.js +311 -0
- package/sidecar/dist/src/capture/lockfile.d.ts +8 -0
- package/sidecar/dist/src/capture/lockfile.js +1 -1
- package/sidecar/dist/src/capture/node-builder.d.ts +27 -0
- package/sidecar/dist/src/capture/node-builder.js +107 -1
- package/sidecar/dist/src/capture/paths-rewrite.d.ts +23 -6
- package/sidecar/dist/src/capture/paths-rewrite.js +43 -11
- package/sidecar/dist/src/capture/self-check.js +12 -3
- package/sidecar/dist/src/capture/unresolved.d.ts +28 -0
- package/sidecar/dist/src/capture/unresolved.js +107 -0
- package/sidecar/dist/src/type-inferrer.d.ts +108 -11
- package/sidecar/dist/src/type-inferrer.js +513 -90
- package/sidecar/dist/src/type-structural-expander.d.ts +23 -2
- package/sidecar/dist/src/type-structural-expander.js +55 -17
- package/sidecar/dist/src/type-text-canonicalizer.d.ts +14 -0
- package/sidecar/dist/src/type-text-canonicalizer.js +23 -2
- package/sidecar/dist/src/validators.d.ts +10 -0
- package/sidecar/dist/src/validators.js +1 -0
|
@@ -35,6 +35,7 @@ import { entryRelativeSpecifier, resolveAnchor } from './anchors.js';
|
|
|
35
35
|
import { findAugmentationFiles } from './augmentations.js';
|
|
36
36
|
import { installedVersions, lockfileVersions } from './lockfile.js';
|
|
37
37
|
import { rewriteEmittedSpecifiers } from './paths-rewrite.js';
|
|
38
|
+
import { typesPackageOf, withInstalledPackages } from './installed-package.js';
|
|
38
39
|
import { selfCheckStub } from './self-check.js';
|
|
39
40
|
import { collectSpecifiers, isRelative, packageNameOf } from './specifiers.js';
|
|
40
41
|
import { DenoProject, findDenoConfig } from './deno-project.js';
|
|
@@ -304,13 +305,14 @@ export function captureStub(opts) {
|
|
|
304
305
|
catch (err) {
|
|
305
306
|
return fail(stubDir, packageName, [err instanceof Error ? err.message : String(err)]);
|
|
306
307
|
}
|
|
307
|
-
const
|
|
308
|
+
const rewritten = rewriteEmittedSpecifiers({
|
|
308
309
|
typesDir,
|
|
309
310
|
files: emittedFiles,
|
|
310
311
|
options: parsed.options,
|
|
311
312
|
configPath,
|
|
312
313
|
entryDir,
|
|
313
314
|
});
|
|
315
|
+
const specifierRewrites = denoRewrites + rewritten.rewrites;
|
|
314
316
|
// ---- Pin external deps: installed node_modules first, lockfile fallback ----
|
|
315
317
|
// Externals are collected AFTER the rewrite pass: a rewritten paths
|
|
316
318
|
// specifier is internal, not a dependency.
|
|
@@ -335,13 +337,20 @@ export function captureStub(opts) {
|
|
|
335
337
|
const lockVersions = lockfileVersions(repoRoot);
|
|
336
338
|
for (const name of Object.keys(deno?.pinned ?? {}))
|
|
337
339
|
externalSpecs.add(name);
|
|
340
|
+
// A package an absolute specifier was rewritten into carries the version
|
|
341
|
+
// installed at that path (#1174); it may be a transitive the repo root
|
|
342
|
+
// neither installs by name nor locks.
|
|
343
|
+
for (const name of Object.keys(rewritten.pins))
|
|
344
|
+
externalSpecs.add(name);
|
|
338
345
|
const pinned = {};
|
|
339
346
|
const unpinned = [];
|
|
340
347
|
for (const name of [...externalSpecs].sort()) {
|
|
341
|
-
const version = deno?.pinned[name] ?? installed.get(name) ?? lockVersions.get(name);
|
|
348
|
+
const version = deno?.pinned[name] ?? rewritten.pins[name] ?? installed.get(name) ?? lockVersions.get(name);
|
|
342
349
|
if (version)
|
|
343
350
|
pinned[name] = version;
|
|
344
|
-
|
|
351
|
+
// A runtime name whose declarations come from a rewritten `@types/*`
|
|
352
|
+
// package resolves through that pin.
|
|
353
|
+
else if (!rewritten.pins[typesPackageOf(name)])
|
|
345
354
|
unpinned.push(name);
|
|
346
355
|
}
|
|
347
356
|
const dependencyRoot = deno?.config.workspaceRoot ?? repoRoot;
|
|
@@ -369,7 +378,9 @@ export function captureStub(opts) {
|
|
|
369
378
|
pinned,
|
|
370
379
|
bareCheckout,
|
|
371
380
|
repoRoot: dependencyRoot,
|
|
372
|
-
compilerHost: deno
|
|
381
|
+
compilerHost: deno || Object.keys(rewritten.installs).length > 0
|
|
382
|
+
? (options) => withInstalledPackages(options, rewritten.installs, deno ? deno.host(options) : undefined)
|
|
383
|
+
: undefined,
|
|
373
384
|
});
|
|
374
385
|
const fidelity = computeFidelity(aliases);
|
|
375
386
|
fs.writeFileSync(path.join(stubDir, 'carrick-manifest.json'), JSON.stringify({
|
|
@@ -478,8 +489,7 @@ function resolveAnchors(opts, parsed, ctx, deno) {
|
|
|
478
489
|
try {
|
|
479
490
|
const anchorSources = [
|
|
480
491
|
...new Set(opts.anchors
|
|
481
|
-
.
|
|
482
|
-
.map((a) => path.join(ctx.repoRoot, a.source_file))),
|
|
492
|
+
.flatMap((a) => (a.source_file ? [path.join(ctx.repoRoot, a.source_file)] : []))),
|
|
483
493
|
].filter((f) => fs.existsSync(f));
|
|
484
494
|
const options = {
|
|
485
495
|
...parsed.options,
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
export interface InstalledPackageSpecifier {
|
|
26
|
+
/** Bare specifier: public package name plus subpath, e.g. `pkg/sub`. */
|
|
27
|
+
specifier: string;
|
|
28
|
+
/** The package's own name and real install directory (never written out). */
|
|
29
|
+
install: {
|
|
30
|
+
name: string;
|
|
31
|
+
root: string;
|
|
32
|
+
};
|
|
33
|
+
/** The package the path landed in and its installed version, when published. */
|
|
34
|
+
pin?: {
|
|
35
|
+
name: string;
|
|
36
|
+
version: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The `@types/*` package that serves a bare name's declarations, for callers
|
|
41
|
+
* deciding whether a runtime name is covered by a pinned types package.
|
|
42
|
+
*/
|
|
43
|
+
export declare function typesPackageOf(name: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* A compiler host that also resolves the packages an absolute specifier was
|
|
46
|
+
* rewritten into, each from its own install directory. The capture's
|
|
47
|
+
* self-check compiles the stub against the producer's `node_modules`, which
|
|
48
|
+
* does not name a transitive package at its root; the check workspace does,
|
|
49
|
+
* because the stub pins it. Without this the self-check would fail a
|
|
50
|
+
* specifier the check resolves.
|
|
51
|
+
*/
|
|
52
|
+
export declare function withInstalledPackages(options: ts.CompilerOptions, installs: Record<string, string>, base?: ts.CompilerHost): ts.CompilerHost;
|
|
53
|
+
/**
|
|
54
|
+
* The bare specifier for an absolute path into an installed package, or
|
|
55
|
+
* undefined when the path does not land in one. `importedName` is the first
|
|
56
|
+
* name read off the import (`import("...").Name`), when the text has one.
|
|
57
|
+
*/
|
|
58
|
+
export declare function installedPackageSpecifier(absoluteSpecifier: string, importedName?: string): InstalledPackageSpecifier | undefined;
|
|
@@ -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
|
-
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
32
|
-
* file
|
|
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):
|
|
51
|
+
export declare function rewriteEmittedSpecifiers(args: RewriteArgs): RewriteResult;
|