carrick 0.3.58 → 0.3.59
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/README.md +94 -26
- package/bin/carrick.mjs +13 -2
- package/dist/auth/credentials.d.ts +14 -0
- package/dist/auth/credentials.js +86 -0
- package/dist/auth/credentials.js.map +1 -0
- package/dist/auth/oauth.d.ts +11 -0
- package/dist/auth/oauth.js +119 -0
- package/dist/auth/oauth.js.map +1 -0
- package/dist/auth/read.d.ts +139 -0
- package/dist/auth/read.js +44 -0
- package/dist/auth/read.js.map +1 -0
- package/dist/auth/run.d.ts +12 -0
- package/dist/auth/run.js +68 -0
- package/dist/auth/run.js.map +1 -0
- package/dist/contract.d.ts +10 -0
- package/dist/contract.js +3 -0
- package/dist/contract.js.map +1 -1
- package/dist/definition.js +2 -2
- package/dist/definition.js.map +1 -1
- package/dist/diagnostics.d.ts +35 -1
- package/dist/diagnostics.js +52 -11
- package/dist/diagnostics.js.map +1 -1
- package/dist/hook/refresh.d.ts +32 -0
- package/dist/hook/refresh.js +138 -0
- package/dist/hook/refresh.js.map +1 -0
- package/dist/hook/session-start.js +13 -0
- package/dist/hook/session-start.js.map +1 -1
- package/dist/init/connect.d.ts +17 -0
- package/dist/init/connect.js +120 -0
- package/dist/init/connect.js.map +1 -0
- package/dist/init/mcp.d.ts +50 -0
- package/dist/init/mcp.js +235 -0
- package/dist/init/mcp.js.map +1 -0
- package/dist/init/projects.d.ts +46 -0
- package/dist/init/projects.js +137 -0
- package/dist/init/projects.js.map +1 -0
- package/dist/init/repos.d.ts +113 -19
- package/dist/init/repos.js +60 -39
- package/dist/init/repos.js.map +1 -1
- package/dist/init/run.d.ts +16 -0
- package/dist/init/run.js +243 -61
- package/dist/init/run.js.map +1 -1
- package/dist/init/settings.d.ts +1 -1
- package/dist/init/settings.js +1 -1
- package/dist/init/settings.js.map +1 -1
- package/dist/server.js +173 -16
- package/dist/server.js.map +1 -1
- package/package.json +6 -6
- package/sidecar/dist/src/capture/deno-project.d.ts +41 -0
- package/sidecar/dist/src/capture/deno-project.js +513 -0
- package/sidecar/dist/src/capture/index.d.ts +1 -0
- package/sidecar/dist/src/capture/index.js +60 -19
- package/sidecar/dist/src/capture/self-check.d.ts +2 -0
- package/sidecar/dist/src/capture/self-check.js +3 -2
- package/sidecar/dist/src/project-loader.js +22 -1
- package/dist/init/identity.d.ts +0 -20
- package/dist/init/identity.js +0 -60
- package/dist/init/identity.js.map +0 -1
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/** Deno owns module resolution; both compiler frontends consume its graph. */
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import { rewriteSpecifiers } from './specifiers.js';
|
|
9
|
+
function readConfig(file) {
|
|
10
|
+
const parsed = ts.parseConfigFileTextToJson(file, fs.readFileSync(file, 'utf8'));
|
|
11
|
+
if (parsed.error)
|
|
12
|
+
throw new Error(ts.flattenDiagnosticMessageText(parsed.error.messageText, '\n'));
|
|
13
|
+
return parsed.config;
|
|
14
|
+
}
|
|
15
|
+
/** Explicit TS configs keep their existing behaviour, including mixed repos. */
|
|
16
|
+
export function findDenoConfig(repoRoot, explicit) {
|
|
17
|
+
if (explicit && !/^deno\.jsonc?$/.test(path.basename(explicit)))
|
|
18
|
+
return undefined;
|
|
19
|
+
let dir = path.resolve(repoRoot);
|
|
20
|
+
const configs = [];
|
|
21
|
+
while (true) {
|
|
22
|
+
const file = ['deno.json', 'deno.jsonc'].map(n => path.join(dir, n)).find(f => fs.existsSync(f));
|
|
23
|
+
if (file)
|
|
24
|
+
configs.push({ file, config: readConfig(file) });
|
|
25
|
+
if (fs.existsSync(path.join(dir, '.git')) || path.dirname(dir) === dir)
|
|
26
|
+
break;
|
|
27
|
+
dir = path.dirname(dir);
|
|
28
|
+
}
|
|
29
|
+
if (!configs.length)
|
|
30
|
+
return undefined;
|
|
31
|
+
const nearest = configs[0];
|
|
32
|
+
if (explicit && path.resolve(repoRoot, explicit) !== nearest.file) {
|
|
33
|
+
throw new Error('Explicit Deno config must be the nearest deno.json or deno.jsonc for the service directory.');
|
|
34
|
+
}
|
|
35
|
+
const declaredWorkspace = configs.find(c => c.config.workspace?.some(member => {
|
|
36
|
+
const memberRoot = path.resolve(path.dirname(c.file), member);
|
|
37
|
+
const relative = path.relative(memberRoot, path.resolve(repoRoot));
|
|
38
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
39
|
+
}));
|
|
40
|
+
if (!explicit && path.dirname(nearest.file) !== path.resolve(repoRoot) &&
|
|
41
|
+
fs.existsSync(path.join(repoRoot, 'package.json')) && !declaredWorkspace)
|
|
42
|
+
return undefined;
|
|
43
|
+
const workspace = declaredWorkspace ?? nearest;
|
|
44
|
+
const opts = { ...workspace.config.compilerOptions, ...nearest.config.compilerOptions };
|
|
45
|
+
const typesOwner = nearest.config.compilerOptions?.types !== undefined ? nearest.file : workspace.file;
|
|
46
|
+
if (Array.isArray(opts.types)) {
|
|
47
|
+
opts.types = opts.types.map((spec) => typeof spec === 'string' && (spec.startsWith('.') || path.isAbsolute(spec))
|
|
48
|
+
? pathToFileURL(path.resolve(path.dirname(typesOwner), spec)).href : spec);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
configPath: explicit ? path.resolve(repoRoot, explicit) : nearest.file,
|
|
52
|
+
workspaceRoot: path.dirname(workspace.file),
|
|
53
|
+
compilerOptions: opts,
|
|
54
|
+
exclude: [...(workspace.config.exclude ?? []).map(p => path.resolve(path.dirname(workspace.file), p)),
|
|
55
|
+
...(nearest.config.exclude ?? []).map(p => path.resolve(path.dirname(nearest.file), p))],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function runDeno(args, cwd) {
|
|
59
|
+
try {
|
|
60
|
+
return execFileSync('deno', args, {
|
|
61
|
+
cwd, encoding: 'utf8', timeout: 120_000, maxBuffer: 64 * 1024 * 1024,
|
|
62
|
+
stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, DENO_NO_UPDATE_CHECK: '1' },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
const detail = err;
|
|
67
|
+
throw new Error(`Deno type preparation failed. Install Deno on PATH and prepare the project's dependencies without lifecycle scripts before scanning. ${detail.stderr || detail.message}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** One request's immutable graph, shared by analysis, emit and relocation. */
|
|
71
|
+
export class DenoProject {
|
|
72
|
+
config;
|
|
73
|
+
repoRoot;
|
|
74
|
+
cacheDir;
|
|
75
|
+
parsed;
|
|
76
|
+
globals;
|
|
77
|
+
diagnostics = [];
|
|
78
|
+
pinned = {};
|
|
79
|
+
modules = new Map();
|
|
80
|
+
localPaths = new Map();
|
|
81
|
+
edges = new Map();
|
|
82
|
+
redirects;
|
|
83
|
+
npmPackages;
|
|
84
|
+
externalNames = new Map();
|
|
85
|
+
constructor(config, repoRoot) {
|
|
86
|
+
this.config = config;
|
|
87
|
+
this.repoRoot = repoRoot;
|
|
88
|
+
const cache = path.join(config.workspaceRoot, '.carrick', 'deno', createHash('sha256').update(path.resolve(repoRoot)).digest('hex').slice(0, 16));
|
|
89
|
+
this.cacheDir = cache;
|
|
90
|
+
fs.mkdirSync(cache, { recursive: true });
|
|
91
|
+
const raw = { ...config.compilerOptions };
|
|
92
|
+
const libs = Array.isArray(raw.lib) ? raw.lib : ['deno.window'];
|
|
93
|
+
const denoLibs = libs.filter(l => l.startsWith('deno.'));
|
|
94
|
+
if (denoLibs.some(l => !['deno.window', 'deno.ns', 'deno.unstable'].includes(l))) {
|
|
95
|
+
throw new Error(`Unsupported Deno type libraries: ${denoLibs.join(', ')}. Supported runtime scopes are deno.window and deno.ns.`);
|
|
96
|
+
}
|
|
97
|
+
delete raw.types;
|
|
98
|
+
delete raw.jsxImportSourceTypes;
|
|
99
|
+
const runtime = denoLibs.includes('deno.window');
|
|
100
|
+
this.parsed = ts.parseJsonConfigFileContent({
|
|
101
|
+
compilerOptions: {
|
|
102
|
+
target: 'ESNext', module: 'ESNext', moduleResolution: 'Bundler', strict: true,
|
|
103
|
+
allowJs: true, checkJs: false, skipLibCheck: true, resolveJsonModule: true,
|
|
104
|
+
...raw, lib: ['esnext', ...libs.filter(l => !l.startsWith('deno.'))],
|
|
105
|
+
allowImportingTsExtensions: true, noEmit: true,
|
|
106
|
+
},
|
|
107
|
+
include: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts', '**/*.js', '**/*.jsx'],
|
|
108
|
+
exclude: ['**/node_modules/**', '**/.git/**', '**/.carrick/**', '**/dist/**', '**/.vite/**', ...config.exclude],
|
|
109
|
+
}, ts.sys, repoRoot);
|
|
110
|
+
this.parsed.options.rootDir = config.workspaceRoot;
|
|
111
|
+
this.globals = [];
|
|
112
|
+
if (denoLibs.length) {
|
|
113
|
+
const declarations = runDeno(['types'], repoRoot);
|
|
114
|
+
// deno types concatenates its libs, leaving references to Deno-only lib
|
|
115
|
+
// names that stock TypeScript cannot load. The declarations themselves
|
|
116
|
+
// come unchanged from the installed compiler; no ambient stand-ins.
|
|
117
|
+
let text = declarations.replace(/^\/\/\/\s*<reference\s+(?:no-default-lib="true"|lib="deno\.[^"]+")\s*\/>\s*$/gm, '');
|
|
118
|
+
if (!runtime) {
|
|
119
|
+
const source = ts.createSourceFile('runtime.d.ts', text, ts.ScriptTarget.Latest, true);
|
|
120
|
+
text = source.statements.filter(s => (ts.isModuleDeclaration(s) && (s.name.text === 'Deno' || denoLibs.includes('deno.unstable'))) ||
|
|
121
|
+
(ts.isInterfaceDeclaration(s) && s.name.text === 'ImportMeta')).map(s => s.getFullText(source)).join('\n');
|
|
122
|
+
}
|
|
123
|
+
const globalPath = path.join(cache, 'runtime.d.ts');
|
|
124
|
+
fs.writeFileSync(globalPath, text);
|
|
125
|
+
this.globals.push(globalPath);
|
|
126
|
+
}
|
|
127
|
+
// The graph root lives within the service so compilerOptions.types uses
|
|
128
|
+
// that member's import-map scope, just like the real source files do.
|
|
129
|
+
const entry = path.join(repoRoot, '.carrick', 'deno', 'graph.ts');
|
|
130
|
+
fs.mkdirSync(path.dirname(entry), { recursive: true });
|
|
131
|
+
const roots = this.parsed.fileNames.map(f => pathToFileURL(f).href);
|
|
132
|
+
const extraTypes = config.compilerOptions.types;
|
|
133
|
+
if (Array.isArray(extraTypes))
|
|
134
|
+
roots.push(...extraTypes.filter((t) => typeof t === 'string'));
|
|
135
|
+
fs.writeFileSync(entry, roots.map(s => `import ${JSON.stringify(s)};`).join('\n'));
|
|
136
|
+
let graph;
|
|
137
|
+
try {
|
|
138
|
+
graph = JSON.parse(runDeno(['info', '--json', '--frozen', '--node-modules-dir=none', '--config', config.configPath, entry], repoRoot));
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
fs.rmSync(entry, { force: true });
|
|
142
|
+
}
|
|
143
|
+
if (!Array.isArray(graph.modules))
|
|
144
|
+
throw new Error('Unsupported deno info JSON: missing modules array');
|
|
145
|
+
this.redirects = graph.redirects ?? {};
|
|
146
|
+
this.npmPackages = graph.npmPackages ?? {};
|
|
147
|
+
for (const [id, pkg] of Object.entries(this.npmPackages)) {
|
|
148
|
+
if (!pkg.localPath) {
|
|
149
|
+
throw new Error(`Deno npm graph lacks localPath for ${id}. Use Deno 2.9.4 and run deno install --frozen --node-modules-dir=none before scanning.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const module of graph.modules) {
|
|
153
|
+
this.modules.set(module.specifier, module);
|
|
154
|
+
if (module.error)
|
|
155
|
+
this.diagnostics.push(`${module.specifier}: ${module.error}`);
|
|
156
|
+
if (!module.local)
|
|
157
|
+
continue;
|
|
158
|
+
let local = module.local;
|
|
159
|
+
if (!module.specifier.startsWith('file:')) {
|
|
160
|
+
const extensions = { TypeScript: '.ts', Tsx: '.tsx', JavaScript: '.js', Jsx: '.jsx', Dts: '.d.ts', Json: '.json', Mts: '.mts', Cts: '.cts' };
|
|
161
|
+
const extension = extensions[module.mediaType ?? ''];
|
|
162
|
+
if (!extension) {
|
|
163
|
+
this.diagnostics.push(`${module.specifier}: unsupported media type ${module.mediaType}`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
local = path.join(cache, 'remote', createHash('sha256').update(module.specifier).digest('hex') + extension);
|
|
167
|
+
fs.mkdirSync(path.dirname(local), { recursive: true });
|
|
168
|
+
fs.copyFileSync(module.local, local);
|
|
169
|
+
}
|
|
170
|
+
this.localPaths.set(module.specifier, path.resolve(local));
|
|
171
|
+
}
|
|
172
|
+
for (const module of graph.modules) {
|
|
173
|
+
const local = this.localPaths.get(module.specifier);
|
|
174
|
+
if (!local)
|
|
175
|
+
continue;
|
|
176
|
+
const edges = new Map();
|
|
177
|
+
for (const dep of module.dependencies ?? []) {
|
|
178
|
+
const target = dep.type ?? dep.code;
|
|
179
|
+
if (target)
|
|
180
|
+
edges.set(dep.specifier, target);
|
|
181
|
+
if (target?.error)
|
|
182
|
+
this.diagnostics.push(`${module.specifier}: ${dep.specifier}: ${target.error}`);
|
|
183
|
+
}
|
|
184
|
+
this.edges.set(local, edges);
|
|
185
|
+
}
|
|
186
|
+
// Deno's extra type roots must participate in both compiler programs.
|
|
187
|
+
const entryModule = this.modules.get(pathToFileURL(entry).href);
|
|
188
|
+
for (const dep of entryModule?.dependencies ?? []) {
|
|
189
|
+
if (Array.isArray(extraTypes) && extraTypes.includes(dep.specifier)) {
|
|
190
|
+
const local = this.targetPath(dep.type ?? dep.code);
|
|
191
|
+
if (local)
|
|
192
|
+
this.globals.push(local);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
fs.writeFileSync(path.join(cache, 'resolution-diagnostics.json'), JSON.stringify(this.diagnostics, null, 2));
|
|
196
|
+
this.parsed.fileNames.push(...this.globals);
|
|
197
|
+
}
|
|
198
|
+
targetPath(resolution, from = this.repoRoot) {
|
|
199
|
+
if (!resolution?.specifier || resolution.error)
|
|
200
|
+
return undefined;
|
|
201
|
+
let spec = resolution.specifier;
|
|
202
|
+
const seen = new Set();
|
|
203
|
+
while (!seen.has(spec)) {
|
|
204
|
+
seen.add(spec);
|
|
205
|
+
const next = this.redirects[spec] ?? this.modules.get(spec)?.typesDependency?.dependency.specifier;
|
|
206
|
+
if (!next)
|
|
207
|
+
break;
|
|
208
|
+
spec = next;
|
|
209
|
+
}
|
|
210
|
+
const npm = this.modules.get(spec)?.npmPackage;
|
|
211
|
+
if (npm) {
|
|
212
|
+
const pkg = this.npmPackages[npm];
|
|
213
|
+
const suffix = /^npm:\/?(?:@[^/]+\/[^/@]+|[^/@]+)@[^/]+(.*)$/.exec(spec)?.[1] ?? '';
|
|
214
|
+
if (pkg?.localPath && !fs.existsSync(path.join(pkg.localPath, 'package.json'))) {
|
|
215
|
+
const diagnostic = `Deno npm dependency ${npm} is not cached; run deno install --frozen --node-modules-dir=none.`;
|
|
216
|
+
if (!this.diagnostics.includes(diagnostic))
|
|
217
|
+
this.diagnostics.push(diagnostic);
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
return pkg && this.resolveNpm(pkg.name + suffix, from, this.parsed.options, pkg)?.resolvedFileName;
|
|
221
|
+
}
|
|
222
|
+
return this.localPaths.get(spec) ?? (spec.startsWith('file:') ? fileURLToPath(spec) : undefined);
|
|
223
|
+
}
|
|
224
|
+
resolve(spec, from, options, host = ts.sys) {
|
|
225
|
+
const edge = this.edges.get(path.resolve(from))?.get(spec);
|
|
226
|
+
if (edge) {
|
|
227
|
+
const target = this.targetPath(edge, from);
|
|
228
|
+
if (!target || !fs.existsSync(target))
|
|
229
|
+
return undefined;
|
|
230
|
+
if (!/\.(?:[cm]?tsx?|jsx?|json)$/i.test(target))
|
|
231
|
+
return undefined;
|
|
232
|
+
return { resolvedFileName: target, isExternalLibraryImport: this.isNpmFile(target) };
|
|
233
|
+
}
|
|
234
|
+
// Generated capture imports and the internals of installed npm packages
|
|
235
|
+
// use TypeScript resolution. Recorded failed Deno edges never fall back.
|
|
236
|
+
return this.resolveNpm(spec, from, options) ?? ts.resolveModuleName(spec, from, options, host).resolvedModule;
|
|
237
|
+
}
|
|
238
|
+
npmOwner(file) {
|
|
239
|
+
return Object.values(this.npmPackages).find(pkg => pkg.localPath &&
|
|
240
|
+
(file === pkg.localPath || file.startsWith(pkg.localPath + path.sep)));
|
|
241
|
+
}
|
|
242
|
+
isNpmFile(file) {
|
|
243
|
+
return !!this.npmOwner(file) || file.split(path.sep).includes('node_modules');
|
|
244
|
+
}
|
|
245
|
+
/** Let TypeScript interpret real package exports/types using Deno's exact
|
|
246
|
+
* dependency graph. Only the resolution host sees virtual node_modules;
|
|
247
|
+
* every returned source path points at the existing Deno cache. */
|
|
248
|
+
resolveNpm(spec, from, options, exact) {
|
|
249
|
+
if (spec.startsWith('.') || path.isAbsolute(spec))
|
|
250
|
+
return undefined;
|
|
251
|
+
const name = spec.startsWith('@') ? spec.split('/').slice(0, 2).join('/') : spec.split('/')[0];
|
|
252
|
+
const owner = this.npmOwner(from);
|
|
253
|
+
const candidates = exact ? [exact] : owner
|
|
254
|
+
? [owner, ...owner.dependencies.map(id => this.npmPackages[id]).filter((pkg) => !!pkg)]
|
|
255
|
+
: Object.values(this.npmPackages);
|
|
256
|
+
const matches = candidates.filter(pkg => pkg.name === name);
|
|
257
|
+
// Never select an arbitrary version outside a recorded graph edge.
|
|
258
|
+
const pkg = matches.length === 1 ? matches[0] : matches.find(p => p.version === this.pinned[name]);
|
|
259
|
+
if (!pkg?.localPath)
|
|
260
|
+
return undefined;
|
|
261
|
+
const virtualRoot = path.join(this.cacheDir, 'npm-resolution');
|
|
262
|
+
const scopedTypes = [...(this.edges.get(path.resolve(from))?.values() ?? [])].flatMap(edge => {
|
|
263
|
+
let specifier = edge.specifier;
|
|
264
|
+
const seen = new Set();
|
|
265
|
+
while (specifier && this.redirects[specifier] && !seen.has(specifier)) {
|
|
266
|
+
seen.add(specifier);
|
|
267
|
+
specifier = this.redirects[specifier];
|
|
268
|
+
}
|
|
269
|
+
const id = specifier && this.modules.get(specifier)?.npmPackage;
|
|
270
|
+
return id && this.npmPackages[id] ? [this.npmPackages[id]] : [];
|
|
271
|
+
});
|
|
272
|
+
const virtualModules = path.join(virtualRoot, 'node_modules');
|
|
273
|
+
const actual = (file) => {
|
|
274
|
+
if (!file.startsWith(virtualModules + path.sep))
|
|
275
|
+
return file;
|
|
276
|
+
const relative = file.slice(virtualModules.length + 1).split(path.sep);
|
|
277
|
+
const packageName = relative.slice(0, relative[0].startsWith('@') ? 2 : 1).join('/');
|
|
278
|
+
const alternatives = Object.values(this.npmPackages).filter(p => p.name === packageName);
|
|
279
|
+
const selected = packageName === name ? pkg : scopedTypes.find(p => p.name === packageName)
|
|
280
|
+
?? (owner ? candidates.find(p => p.name === packageName) : undefined)
|
|
281
|
+
?? alternatives.find(p => p.version === this.pinned[packageName])
|
|
282
|
+
?? (alternatives.length === 1 ? alternatives[0] : undefined);
|
|
283
|
+
return selected?.localPath ? path.join(selected.localPath, ...relative.slice(packageName.startsWith('@') ? 2 : 1)) : file;
|
|
284
|
+
};
|
|
285
|
+
const host = {
|
|
286
|
+
fileExists: file => ts.sys.fileExists(actual(file)),
|
|
287
|
+
readFile: file => ts.sys.readFile(actual(file)),
|
|
288
|
+
directoryExists: dir => dir === virtualRoot || dir === virtualModules || dir === path.join(virtualModules, '@types') || ts.sys.directoryExists(actual(dir)),
|
|
289
|
+
realpath: file => actual(file),
|
|
290
|
+
getCurrentDirectory: () => virtualRoot,
|
|
291
|
+
};
|
|
292
|
+
const result = ts.resolveModuleName(spec, path.join(virtualRoot, 'entry.ts'), options, host).resolvedModule;
|
|
293
|
+
return result && { ...result, resolvedFileName: actual(result.resolvedFileName), isExternalLibraryImport: true };
|
|
294
|
+
}
|
|
295
|
+
host(options) {
|
|
296
|
+
const host = ts.createCompilerHost(options);
|
|
297
|
+
host.resolveModuleNames = (names, from) => names.map(name => this.resolve(name, from, options, host));
|
|
298
|
+
host.resolveTypeReferenceDirectives = (names, from) => names.map(name => this.resolveTypeReference(typeof name === 'string' ? name : name.fileName, from, options, host));
|
|
299
|
+
return host;
|
|
300
|
+
}
|
|
301
|
+
resolveTypeReference(spec, from, options, host = ts.sys) {
|
|
302
|
+
if (this.edges.get(path.resolve(from))?.has(spec)) {
|
|
303
|
+
const resolved = this.resolve(spec, from, options, host);
|
|
304
|
+
return resolved ? { resolvedFileName: resolved.resolvedFileName, primary: true } : undefined;
|
|
305
|
+
}
|
|
306
|
+
return ts.resolveTypeReferenceDirective(spec, from, options, host).resolvedTypeReferenceDirective;
|
|
307
|
+
}
|
|
308
|
+
/** Turn a Deno npm alias into its real package export, with the exact pin. */
|
|
309
|
+
externalName(spec, target, from) {
|
|
310
|
+
const key = `${spec}\0${target}`;
|
|
311
|
+
if (this.externalNames.has(key))
|
|
312
|
+
return this.externalNames.get(key);
|
|
313
|
+
let dir = path.dirname(target);
|
|
314
|
+
while (this.isNpmFile(dir)) {
|
|
315
|
+
const file = path.join(dir, 'package.json');
|
|
316
|
+
if (fs.existsSync(file)) {
|
|
317
|
+
const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
318
|
+
if (pkg.name && pkg.version) {
|
|
319
|
+
if (this.pinned[pkg.name] && this.pinned[pkg.name] !== pkg.version) {
|
|
320
|
+
throw new Error(`Deno capture references multiple versions of ${pkg.name}; a single stub dependency cannot preserve both ${this.pinned[pkg.name]} and ${pkg.version}.`);
|
|
321
|
+
}
|
|
322
|
+
this.pinned[pkg.name] = pkg.version;
|
|
323
|
+
// DefinitelyTyped declarations keep their public runtime specifier;
|
|
324
|
+
// importing @types/foo directly is rejected by TypeScript (TS6137).
|
|
325
|
+
if (pkg.name.startsWith('@types/')) {
|
|
326
|
+
const publicName = pkg.name.slice('@types/'.length).replace(/^([^_]+)__/, '@$1/');
|
|
327
|
+
let code = this.edges.get(path.resolve(from))?.get(spec)?.specifier;
|
|
328
|
+
const seen = new Set();
|
|
329
|
+
while (code && this.redirects[code] && !seen.has(code)) {
|
|
330
|
+
seen.add(code);
|
|
331
|
+
code = this.redirects[code];
|
|
332
|
+
}
|
|
333
|
+
const id = code && this.modules.get(code)?.npmPackage;
|
|
334
|
+
const runtimePackage = id && this.npmPackages[id];
|
|
335
|
+
if (runtimePackage && runtimePackage.name === publicName) {
|
|
336
|
+
if (this.pinned[publicName] && this.pinned[publicName] !== runtimePackage.version) {
|
|
337
|
+
throw new Error(`Deno capture references multiple versions of ${publicName}.`);
|
|
338
|
+
}
|
|
339
|
+
this.pinned[publicName] = runtimePackage.version;
|
|
340
|
+
}
|
|
341
|
+
const suffix = (code && /^npm:\/?(?:@[^/]+\/[^/@]+|[^/@]+)@[^/]+(.*)$/.exec(code)?.[1])
|
|
342
|
+
|| (spec.startsWith(publicName + '/') ? spec.slice(publicName.length) : '');
|
|
343
|
+
return publicName + suffix;
|
|
344
|
+
}
|
|
345
|
+
const plain = spec.replace(/^npm:/, '').replace(/(@[^/]+\/[^/@]+|^[^/@]+)@[^/]+/, '$1');
|
|
346
|
+
let result;
|
|
347
|
+
if (plain === pkg.name || plain.startsWith(`${pkg.name}/`))
|
|
348
|
+
result = plain;
|
|
349
|
+
else {
|
|
350
|
+
const relative = './' + path.relative(dir, target).split(path.sep).join('/');
|
|
351
|
+
const contains = (value) => typeof value === 'string' ? value === relative : !!value && typeof value === 'object' && Object.values(value).some(contains);
|
|
352
|
+
if (pkg.exports && typeof pkg.exports === 'object') {
|
|
353
|
+
for (const [entry, value] of Object.entries(pkg.exports)) {
|
|
354
|
+
if (entry.startsWith('.') && contains(value))
|
|
355
|
+
result = pkg.name + (entry === '.' ? '' : entry.slice(1));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (!result && contains(pkg.exports))
|
|
359
|
+
result = pkg.name;
|
|
360
|
+
}
|
|
361
|
+
if (result)
|
|
362
|
+
this.externalNames.set(key, result);
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
dir = path.dirname(dir);
|
|
367
|
+
}
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
/** Relocate Deno's per-file resolutions into the portable declaration tree. */
|
|
371
|
+
rewrite(typesDir, files, sourceByEmitted) {
|
|
372
|
+
let count = 0;
|
|
373
|
+
const emittedBySource = new Map([...sourceByEmitted].map(([rel, source]) => [path.resolve(source), rel]));
|
|
374
|
+
for (const rel of files) {
|
|
375
|
+
const from = sourceByEmitted.get(rel);
|
|
376
|
+
if (!from)
|
|
377
|
+
continue;
|
|
378
|
+
const file = path.join(typesDir, rel);
|
|
379
|
+
const result = rewriteSpecifiers(fs.readFileSync(file, 'utf8'), spec => {
|
|
380
|
+
const target = this.resolve(spec, from, this.parsed.options)?.resolvedFileName;
|
|
381
|
+
if (!target)
|
|
382
|
+
return undefined;
|
|
383
|
+
if (this.isNpmFile(target))
|
|
384
|
+
return this.externalName(spec, target, from);
|
|
385
|
+
const dest = emittedBySource.get(path.resolve(target));
|
|
386
|
+
if (!dest)
|
|
387
|
+
return undefined;
|
|
388
|
+
let relative = path.posix.relative(path.posix.dirname(rel), dest).replace(/\.d\.(ts|mts|cts)$/, '');
|
|
389
|
+
if (!relative.startsWith('.'))
|
|
390
|
+
relative = './' + relative;
|
|
391
|
+
return relative;
|
|
392
|
+
});
|
|
393
|
+
if (result.rewrites)
|
|
394
|
+
fs.writeFileSync(file, result.text);
|
|
395
|
+
count += result.rewrites;
|
|
396
|
+
}
|
|
397
|
+
const runtime = this.globals.find(file => path.basename(file) === 'runtime.d.ts');
|
|
398
|
+
const runtimeRel = runtime && emittedBySource.get(path.resolve(runtime));
|
|
399
|
+
if (runtimeRel)
|
|
400
|
+
isolateRuntime(typesDir, files, runtimeRel);
|
|
401
|
+
const references = this.globals.filter(file => file !== runtime)
|
|
402
|
+
.map(file => emittedBySource.get(path.resolve(file)))
|
|
403
|
+
.filter((file) => file !== undefined)
|
|
404
|
+
.map(file => `/// <reference path=${JSON.stringify('./' + file)} />`);
|
|
405
|
+
if (references.length) {
|
|
406
|
+
const surface = path.join(typesDir, 'surface.d.ts');
|
|
407
|
+
fs.writeFileSync(surface, references.join('\n') + '\n' + fs.readFileSync(surface, 'utf8'));
|
|
408
|
+
}
|
|
409
|
+
return count;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/** Runtime declarations belong to their producer, not the checker's globals. */
|
|
413
|
+
function isolateRuntime(typesDir, files, runtimeRel) {
|
|
414
|
+
const runtimePath = path.join(typesDir, runtimeRel);
|
|
415
|
+
const program = ts.createProgram(files.map(file => path.join(typesDir, file)), {
|
|
416
|
+
strict: true, skipLibCheck: true, target: ts.ScriptTarget.ESNext,
|
|
417
|
+
module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
418
|
+
types: [],
|
|
419
|
+
});
|
|
420
|
+
const checker = program.getTypeChecker();
|
|
421
|
+
const runtime = program.getSourceFile(runtimePath);
|
|
422
|
+
const retained = new Set();
|
|
423
|
+
const pending = [];
|
|
424
|
+
const retainSymbol = (node) => {
|
|
425
|
+
if (ts.isIdentifier(node) && ts.isQualifiedName(node.parent) && node.parent.left === node)
|
|
426
|
+
return;
|
|
427
|
+
const symbol = checker.getSymbolAtLocation(node);
|
|
428
|
+
// Runtime additions to standard library interfaces retain the compiler's
|
|
429
|
+
// merged standard type; they cannot become a standalone shadow interface.
|
|
430
|
+
if (symbol?.declarations?.some(d => program.isSourceFileDefaultLibrary(d.getSourceFile())))
|
|
431
|
+
return;
|
|
432
|
+
for (const declaration of symbol?.declarations ?? []) {
|
|
433
|
+
if (declaration.getSourceFile() !== runtime)
|
|
434
|
+
continue;
|
|
435
|
+
let statement = declaration;
|
|
436
|
+
while (statement.parent && !ts.isSourceFile(statement.parent) && !ts.isModuleBlock(statement.parent))
|
|
437
|
+
statement = statement.parent;
|
|
438
|
+
if (ts.isStatement(statement) && !retained.has(statement)) {
|
|
439
|
+
retained.add(statement);
|
|
440
|
+
pending.push(statement);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
for (const rel of files) {
|
|
445
|
+
if (rel === runtimeRel)
|
|
446
|
+
continue;
|
|
447
|
+
const file = path.join(typesDir, rel);
|
|
448
|
+
const source = program.getSourceFile(file);
|
|
449
|
+
if (!source)
|
|
450
|
+
continue;
|
|
451
|
+
let specifier = path.posix.relative(path.posix.dirname(rel), runtimeRel).replace(/\.d\.ts$/, '');
|
|
452
|
+
if (!specifier.startsWith('.'))
|
|
453
|
+
specifier = './' + specifier;
|
|
454
|
+
const edits = [];
|
|
455
|
+
const visit = (node) => {
|
|
456
|
+
if (ts.isIdentifier(node) || ts.isQualifiedName(node))
|
|
457
|
+
retainSymbol(node);
|
|
458
|
+
if (ts.isIdentifier(node)) {
|
|
459
|
+
const symbol = checker.getSymbolAtLocation(node);
|
|
460
|
+
const declarations = symbol?.declarations;
|
|
461
|
+
if (declarations?.some(d => d.getSourceFile() === runtime) &&
|
|
462
|
+
!declarations.some(d => program.isSourceFileDefaultLibrary(d.getSourceFile())) &&
|
|
463
|
+
// Only a type's root identifier, never a property or a declaration.
|
|
464
|
+
((ts.isTypeReferenceNode(node.parent) && node.parent.typeName === node) ||
|
|
465
|
+
(ts.isQualifiedName(node.parent) && node.parent.left === node) ||
|
|
466
|
+
(ts.isTypeQueryNode(node.parent) && node.parent.exprName === node))) {
|
|
467
|
+
edits.push({ start: node.getStart(source), end: node.getEnd(), text: `import(${JSON.stringify(specifier)}).${node.text}` });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
ts.forEachChild(node, visit);
|
|
471
|
+
};
|
|
472
|
+
visit(source);
|
|
473
|
+
let text = source.text;
|
|
474
|
+
for (const edit of edits.sort((a, b) => b.start - a.start))
|
|
475
|
+
text = text.slice(0, edit.start) + edit.text + text.slice(edit.end);
|
|
476
|
+
if (edits.length)
|
|
477
|
+
fs.writeFileSync(file, text);
|
|
478
|
+
}
|
|
479
|
+
while (pending.length) {
|
|
480
|
+
const visit = (node) => {
|
|
481
|
+
if (ts.isIdentifier(node) || ts.isQualifiedName(node))
|
|
482
|
+
retainSymbol(node);
|
|
483
|
+
ts.forEachChild(node, visit);
|
|
484
|
+
};
|
|
485
|
+
visit(pending.pop());
|
|
486
|
+
}
|
|
487
|
+
const prune = (statement) => {
|
|
488
|
+
if (ts.isModuleDeclaration(statement) && statement.body && ts.isModuleBlock(statement.body)) {
|
|
489
|
+
const children = statement.body.statements.map(prune).filter((s) => s !== undefined);
|
|
490
|
+
if (!children.length && !retained.has(statement))
|
|
491
|
+
return undefined;
|
|
492
|
+
return ts.factory.updateModuleDeclaration(statement, statement.modifiers, statement.name, ts.factory.updateModuleBlock(statement.body, children));
|
|
493
|
+
}
|
|
494
|
+
return retained.has(statement) ? statement : undefined;
|
|
495
|
+
};
|
|
496
|
+
const statements = runtime.statements.map(prune).filter((s) => s !== undefined);
|
|
497
|
+
const names = new Set();
|
|
498
|
+
for (const statement of statements) {
|
|
499
|
+
if (ts.isVariableStatement(statement)) {
|
|
500
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
501
|
+
if (ts.isIdentifier(declaration.name))
|
|
502
|
+
names.add(declaration.name.text);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
else if ((ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) ||
|
|
506
|
+
ts.isModuleDeclaration(statement) || ts.isClassDeclaration(statement) ||
|
|
507
|
+
ts.isFunctionDeclaration(statement) || ts.isEnumDeclaration(statement)) && statement.name && ts.isIdentifier(statement.name)) {
|
|
508
|
+
names.add(statement.name.text);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const text = ts.createPrinter().printFile(ts.factory.updateSourceFile(runtime, statements));
|
|
512
|
+
fs.writeFileSync(runtimePath, '// Runtime declarations from deno types. Copyright the Deno authors. MIT license.\n/// <reference lib="esnext" />\n' + text + `\nexport { ${[...names].sort().join(', ')} };\n`);
|
|
513
|
+
}
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import type { CaptureStubOptions, CaptureStubResult } from './api.js';
|
|
31
31
|
export type { CaptureStubOptions, CaptureStubResult } from './api.js';
|
|
32
|
+
export { DenoProject, findDenoConfig } from './deno-project.js';
|
|
32
33
|
export { runCheck } from './check.js';
|
|
33
34
|
export type { CheckProgress } from './check.js';
|
|
34
35
|
/** Same normalization intent as bundle_file_stems on the Rust side. */
|