@openrewrite/rewrite 8.93.0 → 8.93.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/javascript/index.d.ts +1 -0
- package/dist/javascript/index.d.ts.map +1 -1
- package/dist/javascript/index.js +1 -0
- package/dist/javascript/index.js.map +1 -1
- package/dist/javascript/node-runtime.d.ts +63 -0
- package/dist/javascript/node-runtime.d.ts.map +1 -0
- package/dist/javascript/node-runtime.js +196 -0
- package/dist/javascript/node-runtime.js.map +1 -0
- package/dist/javascript/package-json-parser.d.ts +13 -0
- package/dist/javascript/package-json-parser.d.ts.map +1 -1
- package/dist/javascript/package-json-parser.js +62 -22
- package/dist/javascript/package-json-parser.js.map +1 -1
- package/dist/javascript/package-manager.d.ts +15 -0
- package/dist/javascript/package-manager.d.ts.map +1 -1
- package/dist/javascript/package-manager.js +38 -0
- package/dist/javascript/package-manager.js.map +1 -1
- package/dist/javascript/parser.d.ts +2 -0
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +57 -40
- package/dist/javascript/parser.js.map +1 -1
- package/dist/rewrite-javascript-version.txt +1 -1
- package/dist/rpc/message-encoder.d.ts +18 -7
- package/dist/rpc/message-encoder.d.ts.map +1 -1
- package/dist/rpc/message-encoder.js +42 -18
- package/dist/rpc/message-encoder.js.map +1 -1
- package/dist/rpc/request/dependency-types.d.ts +6 -5
- package/dist/rpc/request/dependency-types.d.ts.map +1 -1
- package/dist/rpc/request/dependency-types.js +13 -12
- package/dist/rpc/request/dependency-types.js.map +1 -1
- package/package.json +1 -1
- package/src/javascript/index.ts +1 -0
- package/src/javascript/node-runtime.ts +213 -0
- package/src/javascript/package-json-parser.ts +75 -32
- package/src/javascript/package-manager.ts +48 -0
- package/src/javascript/parser.ts +86 -45
- package/src/rpc/message-encoder.ts +41 -18
- package/src/rpc/request/dependency-types.ts +13 -11
|
@@ -24,11 +24,12 @@ import {
|
|
|
24
24
|
PackageManager,
|
|
25
25
|
readNpmrcConfigs
|
|
26
26
|
} from "./node-resolution-result";
|
|
27
|
+
import {createNodeRuntimeMarker, NodeRuntime} from "./node-runtime";
|
|
27
28
|
import * as fs from "fs";
|
|
28
29
|
import * as fsp from "fs/promises";
|
|
29
30
|
import * as path from "path";
|
|
30
31
|
import * as YAML from "yaml";
|
|
31
|
-
import {getLockFileDetectionConfig, runList} from "./package-manager";
|
|
32
|
+
import {getLockFileDetectionConfig, getLockFileName, runList, runVersion} from "./package-manager";
|
|
32
33
|
import picomatch from "picomatch";
|
|
33
34
|
import {DEFAULT_DIR_EXCLUSIONS, walkDirs} from "../path-utils";
|
|
34
35
|
|
|
@@ -76,6 +77,9 @@ export class PackageJsonParser extends Parser {
|
|
|
76
77
|
private readonly jsonParser: JsonParser;
|
|
77
78
|
private readonly skipDependencyResolution: boolean;
|
|
78
79
|
|
|
80
|
+
/** The NodeRuntime of each project root, by package manager and root directory, for the parser's lifetime */
|
|
81
|
+
private readonly runtimes = new Map<string, NodeRuntime>();
|
|
82
|
+
|
|
79
83
|
/** Fields to copy from package.json that contain dependency maps */
|
|
80
84
|
private static readonly DEPENDENCY_FIELDS = [
|
|
81
85
|
'dependencies',
|
|
@@ -166,18 +170,50 @@ export class PackageJsonParser extends Parser {
|
|
|
166
170
|
markersByDir.set(dir, await this.createMarker(input, dir));
|
|
167
171
|
}
|
|
168
172
|
const marker = markersByDir.get(dir)!;
|
|
173
|
+
const runtime = this.nodeRuntime(dir, marker?.packageManager);
|
|
174
|
+
|
|
175
|
+
// Attach the markers to the JSON document: every one gets the NodeRuntime of its project
|
|
176
|
+
yield {
|
|
177
|
+
...jsonDoc,
|
|
178
|
+
markers: {
|
|
179
|
+
...jsonDoc.markers,
|
|
180
|
+
markers: [...jsonDoc.markers.markers, ...(marker ? [marker] : []), runtime]
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
169
185
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
186
|
+
/**
|
|
187
|
+
* The NodeRuntime of the project whose package.json is in `dir`, one instance per project root: the directory of
|
|
188
|
+
* the lock the parser found `packageManager` by, else `dir`. That package manager's `--version` is asked there,
|
|
189
|
+
* once per package manager and root directory; without one, as when dependency resolution is skipped, none is.
|
|
190
|
+
*/
|
|
191
|
+
private nodeRuntime(dir: string, packageManager?: PackageManager): NodeRuntime {
|
|
192
|
+
const absoluteDir = path.resolve(this.relativeTo ?? '', dir);
|
|
193
|
+
const root = packageManager === undefined ? absoluteDir : this.projectRoot(absoluteDir, packageManager);
|
|
194
|
+
const key = `${packageManager ?? ''}:${root}`;
|
|
195
|
+
let runtime = this.runtimes.get(key);
|
|
196
|
+
if (!runtime) {
|
|
197
|
+
runtime = createNodeRuntimeMarker(packageManager,
|
|
198
|
+
packageManager === undefined ? undefined : runVersion(packageManager, root));
|
|
199
|
+
this.runtimes.set(key, runtime);
|
|
200
|
+
}
|
|
201
|
+
return runtime;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The nearest directory from `dir` up to relativeTo that holds `packageManager`'s lock, as the lock file walk
|
|
206
|
+
* finds it; `dir` where none does.
|
|
207
|
+
*/
|
|
208
|
+
private projectRoot(dir: string, packageManager: PackageManager): string {
|
|
209
|
+
const lockFile = getLockFileName(packageManager);
|
|
210
|
+
const stop = this.relativeTo ? path.resolve(this.relativeTo) : undefined;
|
|
211
|
+
for (let current = dir; ; current = path.dirname(current)) {
|
|
212
|
+
if (fs.existsSync(path.join(current, lockFile))) {
|
|
213
|
+
return current;
|
|
214
|
+
}
|
|
215
|
+
if (current === stop || path.dirname(current) === current) {
|
|
216
|
+
return dir;
|
|
181
217
|
}
|
|
182
218
|
}
|
|
183
219
|
}
|
|
@@ -343,6 +379,9 @@ export class PackageJsonParser extends Parser {
|
|
|
343
379
|
continue;
|
|
344
380
|
}
|
|
345
381
|
|
|
382
|
+
// a pattern without wildcards names the directory itself
|
|
383
|
+
candidates.push(absoluteBaseDir);
|
|
384
|
+
|
|
346
385
|
// Use walkDirs with appropriate depth limit
|
|
347
386
|
const dirs = await walkDirs(absoluteBaseDir, {
|
|
348
387
|
maxDepth: hasRecursive ? undefined : 0,
|
|
@@ -353,7 +392,7 @@ export class PackageJsonParser extends Parser {
|
|
|
353
392
|
}
|
|
354
393
|
}
|
|
355
394
|
|
|
356
|
-
return candidates;
|
|
395
|
+
return [...new Set(candidates)];
|
|
357
396
|
}
|
|
358
397
|
|
|
359
398
|
/**
|
|
@@ -429,29 +468,41 @@ export class PackageJsonParser extends Parser {
|
|
|
429
468
|
): Promise<{ content: PackageLockContent; packageManager: PackageManager } | undefined> {
|
|
430
469
|
// Normalize paths for comparison
|
|
431
470
|
const normalizedRoot = rootDir ? path.resolve(rootDir) : undefined;
|
|
432
|
-
let currentDir = path.resolve(startDir);
|
|
433
471
|
|
|
434
|
-
//
|
|
472
|
+
// Each directory from startDir up, stopping at rootDir (or the filesystem root)
|
|
473
|
+
const dirs: string[] = [];
|
|
474
|
+
let currentDir = path.resolve(startDir);
|
|
435
475
|
while (true) {
|
|
436
|
-
|
|
437
|
-
if (result) {
|
|
438
|
-
return result;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// If we've reached rootDir, stop walking (don't go above it)
|
|
476
|
+
dirs.push(currentDir);
|
|
442
477
|
if (normalizedRoot && currentDir === normalizedRoot) {
|
|
443
478
|
break;
|
|
444
479
|
}
|
|
445
|
-
|
|
446
|
-
// Check if we've reached the filesystem root
|
|
447
480
|
const parentDir = path.dirname(currentDir);
|
|
448
481
|
if (parentDir === currentDir) {
|
|
449
482
|
break;
|
|
450
483
|
}
|
|
451
|
-
|
|
452
484
|
currentDir = parentDir;
|
|
453
485
|
}
|
|
454
486
|
|
|
487
|
+
// A lock file names the package manager, wherever it is on the way up. An installed
|
|
488
|
+
// workspace member has node_modules of its own and no lock: its workspace's lock, above it,
|
|
489
|
+
// says which package manager installed them.
|
|
490
|
+
for (const dir of dirs) {
|
|
491
|
+
const result = await this.tryReadLockFile(dir);
|
|
492
|
+
if (result) {
|
|
493
|
+
return result;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Fallback: node_modules without a lock file anywhere above (e.g., symlinked from another
|
|
498
|
+
// workspace). Walk them for resolved dependency information, and assume npm.
|
|
499
|
+
for (const dir of dirs) {
|
|
500
|
+
const parsed = await this.walkNodeModules(dir);
|
|
501
|
+
if (parsed) {
|
|
502
|
+
return { content: parsed, packageManager: PackageManager.Npm };
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
455
506
|
return undefined;
|
|
456
507
|
}
|
|
457
508
|
|
|
@@ -504,14 +555,6 @@ export class PackageJsonParser extends Parser {
|
|
|
504
555
|
}
|
|
505
556
|
}
|
|
506
557
|
|
|
507
|
-
// Fallback: if node_modules exists but no lock file was found (e.g., symlinked from another workspace),
|
|
508
|
-
// walk node_modules to get resolved dependency information
|
|
509
|
-
const parsed = await this.walkNodeModules(dir);
|
|
510
|
-
if (parsed) {
|
|
511
|
-
// Assume npm as the default package manager when only node_modules exists
|
|
512
|
-
return { content: parsed, packageManager: PackageManager.Npm };
|
|
513
|
-
}
|
|
514
|
-
|
|
515
558
|
return undefined;
|
|
516
559
|
}
|
|
517
560
|
|
|
@@ -52,6 +52,9 @@ interface PackageManagerConfig {
|
|
|
52
52
|
|
|
53
53
|
/** Command to list dependencies in JSON format (if supported) */
|
|
54
54
|
listCommand?: string[];
|
|
55
|
+
|
|
56
|
+
/** Command that prints the release of the package manager that runs where it is run */
|
|
57
|
+
versionCommand: string[];
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
/**
|
|
@@ -69,6 +72,7 @@ const PACKAGE_MANAGER_CONFIGS: Record<PackageManager, PackageManagerConfig> = {
|
|
|
69
72
|
installLockOnlyCommand: ['npm', 'install', '--package-lock-only', '--ignore-scripts', '--legacy-peer-deps'],
|
|
70
73
|
installCommand: ['npm', 'install', '--ignore-scripts', '--legacy-peer-deps'],
|
|
71
74
|
listCommand: ['npm', 'list', '--json', '--all'],
|
|
75
|
+
versionCommand: ['npm', '--version'],
|
|
72
76
|
},
|
|
73
77
|
[PackageManager.YarnClassic]: {
|
|
74
78
|
lockFile: 'yarn.lock',
|
|
@@ -76,6 +80,7 @@ const PACKAGE_MANAGER_CONFIGS: Record<PackageManager, PackageManagerConfig> = {
|
|
|
76
80
|
installLockOnlyCommand: ['yarn', 'install', '--ignore-scripts'],
|
|
77
81
|
installCommand: ['yarn', 'install', '--ignore-scripts'],
|
|
78
82
|
listCommand: ['yarn', 'list', '--json'],
|
|
83
|
+
versionCommand: ['yarn', '--version'],
|
|
79
84
|
},
|
|
80
85
|
[PackageManager.YarnBerry]: {
|
|
81
86
|
lockFile: 'yarn.lock',
|
|
@@ -83,6 +88,7 @@ const PACKAGE_MANAGER_CONFIGS: Record<PackageManager, PackageManagerConfig> = {
|
|
|
83
88
|
installLockOnlyCommand: ['yarn', 'install', '--mode', 'skip-build'],
|
|
84
89
|
installCommand: ['yarn', 'install', '--mode', 'skip-build'],
|
|
85
90
|
listCommand: ['yarn', 'info', '--all', '--json'],
|
|
91
|
+
versionCommand: ['yarn', '--version'],
|
|
86
92
|
},
|
|
87
93
|
[PackageManager.Pnpm]: {
|
|
88
94
|
lockFile: 'pnpm-lock.yaml',
|
|
@@ -92,12 +98,14 @@ const PACKAGE_MANAGER_CONFIGS: Record<PackageManager, PackageManagerConfig> = {
|
|
|
92
98
|
installLockOnlyCommand: ['pnpm', 'install', '--lockfile-only', '--ignore-scripts', '--no-strict-peer-dependencies'],
|
|
93
99
|
installCommand: ['pnpm', 'install', '--ignore-scripts', '--no-strict-peer-dependencies'],
|
|
94
100
|
listCommand: ['pnpm', 'list', '--json', '--depth=Infinity'],
|
|
101
|
+
versionCommand: ['pnpm', '--version'],
|
|
95
102
|
},
|
|
96
103
|
[PackageManager.Bun]: {
|
|
97
104
|
lockFile: 'bun.lock',
|
|
98
105
|
// Bun doesn't have a lock-only mode; --ignore-scripts prevents lifecycle scripts
|
|
99
106
|
installLockOnlyCommand: ['bun', 'install', '--ignore-scripts'],
|
|
100
107
|
installCommand: ['bun', 'install', '--ignore-scripts'],
|
|
108
|
+
versionCommand: ['bun', '--version'],
|
|
101
109
|
},
|
|
102
110
|
};
|
|
103
111
|
|
|
@@ -310,6 +318,46 @@ export function runList(pm: PackageManager, cwd: string, timeout: number = 30000
|
|
|
310
318
|
return result.stdout;
|
|
311
319
|
}
|
|
312
320
|
|
|
321
|
+
/**
|
|
322
|
+
* A release as a package manager's `--version` prints it: `major.minor.patch`, with a prerelease or build or both.
|
|
323
|
+
*/
|
|
324
|
+
const RELEASE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* What the package manager's `--version` answers, run in `cwd`: in the project's root directory, the release that
|
|
328
|
+
* would install the project on this machine, since Yarn and pnpm decide there which release runs (`yarnPath`,
|
|
329
|
+
* corepack, pnpm's own switching).
|
|
330
|
+
*
|
|
331
|
+
* It runs as the parser's other package manager commands do, with `COREPACK_ENABLE_DOWNLOAD_PROMPT=0`, no input and
|
|
332
|
+
* a short timeout, so it never prompts and never throws.
|
|
333
|
+
*
|
|
334
|
+
* @param pm The package manager to ask
|
|
335
|
+
* @param cwd The project's root directory
|
|
336
|
+
* @param timeout Timeout in milliseconds
|
|
337
|
+
* @returns The trimmed answer, or undefined when the package manager is not installed, fails, or answers something
|
|
338
|
+
* that is no version
|
|
339
|
+
*/
|
|
340
|
+
export function runVersion(pm: PackageManager, cwd: string, timeout: number = 10000): string | undefined {
|
|
341
|
+
const [cmd, ...args] = PACKAGE_MANAGER_CONFIGS[pm].versionCommand;
|
|
342
|
+
try {
|
|
343
|
+
const result = spawnSync(getPlatformCommand(cmd), args, {
|
|
344
|
+
cwd,
|
|
345
|
+
encoding: 'utf-8',
|
|
346
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
347
|
+
timeout,
|
|
348
|
+
env: {...process.env, COREPACK_ENABLE_DOWNLOAD_PROMPT: '0'},
|
|
349
|
+
...(os.platform() === 'win32' ? { shell: true } : {})
|
|
350
|
+
});
|
|
351
|
+
if (result.error || result.status !== 0 || typeof result.stdout !== 'string') {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
const answer = result.stdout.trim();
|
|
355
|
+
return RELEASE.test(answer) ? answer : undefined;
|
|
356
|
+
} catch {
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
313
361
|
/**
|
|
314
362
|
* Result of running install in a temporary directory.
|
|
315
363
|
*/
|
package/src/javascript/parser.ts
CHANGED
|
@@ -89,6 +89,25 @@ type WithInvalidSyntaxSlots<T> = T & {
|
|
|
89
89
|
modifiers?: ts.NodeArray<ts.ModifierLike>;
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
+
interface CompiledProject {
|
|
93
|
+
readonly program: ts.Program;
|
|
94
|
+
readonly typeMapping: JavaScriptTypeMapping;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The inputs of one parse that a single tsconfig governs, or that none does.
|
|
99
|
+
*/
|
|
100
|
+
interface Project {
|
|
101
|
+
/** Path of the governing config file, empty where there is none. */
|
|
102
|
+
readonly key: string;
|
|
103
|
+
readonly options: ts.CompilerOptions;
|
|
104
|
+
readonly rootNames: SourcePath[];
|
|
105
|
+
/** How many of the inputs are still to be mapped. */
|
|
106
|
+
unmapped: number;
|
|
107
|
+
/** Present from the first input mapped until the last. */
|
|
108
|
+
compiled?: CompiledProject;
|
|
109
|
+
}
|
|
110
|
+
|
|
92
111
|
export class JavaScriptParser extends Parser {
|
|
93
112
|
|
|
94
113
|
private readonly tsConfig: TsConfigResolver;
|
|
@@ -216,8 +235,8 @@ export class JavaScriptParser extends Parser {
|
|
|
216
235
|
|
|
217
236
|
override async* parse(...inputs: ParserInput[]): AsyncGenerator<SourceFile> {
|
|
218
237
|
const inputFiles = new Map<SourcePath, ParserInput>();
|
|
219
|
-
const projectOf = new Map<SourcePath,
|
|
220
|
-
const projects = new Map<string,
|
|
238
|
+
const projectOf = new Map<SourcePath, Project>();
|
|
239
|
+
const projects = new Map<string, Project>();
|
|
221
240
|
|
|
222
241
|
// Populate inputFiles map and remove from cache if necessary
|
|
223
242
|
for (const input of inputs) {
|
|
@@ -236,12 +255,13 @@ export class JavaScriptParser extends Parser {
|
|
|
236
255
|
for (const normalizedSourcePath of inputFiles.keys()) {
|
|
237
256
|
const {configFilePath, options} = this.tsConfig.forFile(normalizedSourcePath);
|
|
238
257
|
const projectKey = configFilePath ?? "";
|
|
239
|
-
projectOf.set(normalizedSourcePath, projectKey);
|
|
240
258
|
let project = projects.get(projectKey);
|
|
241
259
|
if (!project) {
|
|
242
|
-
projects.set(projectKey, project = {options, rootNames: []});
|
|
260
|
+
projects.set(projectKey, project = {key: projectKey, options, rootNames: [], unmapped: 0});
|
|
243
261
|
}
|
|
244
262
|
project.rootNames.push(normalizedSourcePath);
|
|
263
|
+
project.unmapped++;
|
|
264
|
+
projectOf.set(normalizedSourcePath, project);
|
|
245
265
|
}
|
|
246
266
|
|
|
247
267
|
// Module resolution probes a directory before the files in it, so a directory holding
|
|
@@ -253,53 +273,74 @@ export class JavaScriptParser extends Parser {
|
|
|
253
273
|
}
|
|
254
274
|
}
|
|
255
275
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const program = ts.createProgram(rootNames, options,
|
|
261
|
-
this.createCompilerHost(options, inputFiles, inputDirectories), this.oldPrograms.get(projectKey));
|
|
262
|
-
this.oldPrograms.set(projectKey, program);
|
|
263
|
-
|
|
264
|
-
// A JavaScriptTypeMapping deduplicates by `ts.Type.id`, which is only unique within
|
|
265
|
-
// the TypeChecker that issued it, so each program needs its own.
|
|
266
|
-
compiled.set(projectKey, {program, typeMapping: new JavaScriptTypeMapping(program.getTypeChecker(), this.relativeTo)});
|
|
267
|
-
}
|
|
276
|
+
// Each project's program holds its own copy of the standard library and of every dependency
|
|
277
|
+
// it reaches, a multiple of the sources it is built for. A repository of many projects cannot
|
|
278
|
+
// afford them all at once, so only one that is alone in its parse is kept for the next.
|
|
279
|
+
const carryForward = projects.size === 1;
|
|
268
280
|
|
|
269
281
|
for (const [sourcePath, input] of inputFiles) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
282
|
+
// Mapped by a call rather than inline: what a generator holds at a `yield` stays reachable
|
|
283
|
+
// until it is resumed, and here that would be the program of a project already finished.
|
|
284
|
+
yield await this.parseInput(input, projectOf.get(sourcePath)!, carryForward, inputFiles, inputDirectories);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
277
287
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
288
|
+
private async parseInput(input: ParserInput, project: Project, carryForward: boolean,
|
|
289
|
+
inputFiles: Map<SourcePath, ParserInput>,
|
|
290
|
+
inputDirectories: Set<string>): Promise<SourceFile> {
|
|
291
|
+
// Built when the first of a project's inputs comes up and let go with its last, so that the
|
|
292
|
+
// programs alive at any one time are those of the projects still being worked through.
|
|
293
|
+
const {program, typeMapping} = project.compiled ??=
|
|
294
|
+
this.compile(project, carryForward, inputFiles, inputDirectories);
|
|
295
|
+
if (--project.unmapped === 0) {
|
|
296
|
+
project.compiled = undefined;
|
|
297
|
+
}
|
|
282
298
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
299
|
+
const filePath = parserInputFile(input);
|
|
300
|
+
const sourceFile = program.getSourceFile(filePath);
|
|
301
|
+
if (!sourceFile) {
|
|
302
|
+
return this.error(input, new Error('Parser returned undefined'));
|
|
303
|
+
}
|
|
289
304
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
305
|
+
if (hasFlowAnnotation(sourceFile)) {
|
|
306
|
+
return this.error(input, new FlowSyntaxNotSupportedError("Flow syntax not supported"));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const syntaxErrors = checkSyntaxErrors(program, sourceFile);
|
|
310
|
+
if (syntaxErrors.length > 0) {
|
|
311
|
+
let errors = syntaxErrors.map(e => `${e[0]} [${e[1]}]`).join('; ');
|
|
312
|
+
return this.error(input, new SyntaxError(`Compiler error(s): ${errors}`));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
return await this.requirePrintEqualsInput(produce(
|
|
317
|
+
new JavaScriptParserVisitor(sourceFile, this.relativePath(input), typeMapping)
|
|
318
|
+
.visit(sourceFile) as SourceFile,
|
|
319
|
+
draft => {
|
|
320
|
+
if (this.styles) {
|
|
321
|
+
draft.markers = this.styles.reduce((m, s) => replaceMarkerByKind(m, s), draft.markers);
|
|
322
|
+
}
|
|
323
|
+
}), input, escapeLoneSurrogates(sourceFile.getFullText()));
|
|
324
|
+
} catch (error) {
|
|
325
|
+
return this.error(input, error instanceof Error ? error : new Error('Parser threw unknown error: ' + error));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private compile(project: Project, carryForward: boolean, inputFiles: Map<SourcePath, ParserInput>,
|
|
330
|
+
inputDirectories: Set<string>): CompiledProject {
|
|
331
|
+
// TypeScript carries the previous program's bound files forward when the root paths match,
|
|
332
|
+
// so a parser held across parses of one path costs a fraction of a freshly built one.
|
|
333
|
+
const program = ts.createProgram(project.rootNames, project.options,
|
|
334
|
+
this.createCompilerHost(project.options, inputFiles, inputDirectories), this.oldPrograms.get(project.key));
|
|
335
|
+
if (carryForward) {
|
|
336
|
+
this.oldPrograms.set(project.key, program);
|
|
337
|
+
} else {
|
|
338
|
+
this.oldPrograms.delete(project.key);
|
|
302
339
|
}
|
|
340
|
+
|
|
341
|
+
// A JavaScriptTypeMapping deduplicates by `ts.Type.id`, which is only unique within
|
|
342
|
+
// the TypeChecker that issued it, so each program needs its own.
|
|
343
|
+
return {program, typeMapping: new JavaScriptTypeMapping(program.getTypeChecker(), this.relativeTo)};
|
|
303
344
|
}
|
|
304
345
|
|
|
305
346
|
private createCompilerHost(compilerOptions: ts.CompilerOptions, inputFiles: Map<SourcePath, ParserInput>,
|
|
@@ -91,31 +91,54 @@ export function* jsonFragments(value: unknown): Generator<string> {
|
|
|
91
91
|
yield JSON.stringify(value) as string;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Streams {@link jsonFragments} into Buffers so that no more than one flush window is a JS string
|
|
96
|
+
* at a time, raising the outbound ceiling from V8's String.kMaxLength to Node's Buffer.MAX_LENGTH.
|
|
97
|
+
* Content-Length still needs the whole body up front, so this is a ceiling raise and not true
|
|
98
|
+
* streaming.
|
|
99
|
+
*
|
|
100
|
+
* Exported because {@link chunkedJsonEncoder} reaches it only above the cap: a test of the walk has
|
|
101
|
+
* to call it directly.
|
|
102
|
+
*/
|
|
103
|
+
export function encodeFragmented(msg: unknown, charset: BufferEncoding): Buffer {
|
|
104
|
+
const buffers: Buffer[] = [];
|
|
105
|
+
let pending = "";
|
|
106
|
+
for (const fragment of jsonFragments(msg)) {
|
|
107
|
+
pending += fragment;
|
|
108
|
+
if (pending.length >= FLUSH_THRESHOLD) {
|
|
109
|
+
buffers.push(Buffer.from(pending, charset));
|
|
110
|
+
pending = "";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (pending.length > 0) {
|
|
114
|
+
buffers.push(Buffer.from(pending, charset));
|
|
115
|
+
}
|
|
116
|
+
return Buffer.concat(buffers);
|
|
117
|
+
}
|
|
118
|
+
|
|
94
119
|
/**
|
|
95
120
|
* A drop-in replacement for vscode-jsonrpc's default `application/json` content-type encoder that
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
121
|
+
* keeps a message serializable past V8's single-string limit. The default encoder does {@code
|
|
122
|
+
* Buffer.from(JSON.stringify(msg))}; a large enough PrepareRecipe response (a deep recipe tree)
|
|
123
|
+
* overflows that limit and throws "RangeError: Invalid string length", hanging the RPC call.
|
|
124
|
+
*
|
|
125
|
+
* Below the limit it takes the default's own single-string path: {@link encodeFragmented} would
|
|
126
|
+
* assemble exactly that string anyway, at a yield per scalar, key and structural token to get
|
|
127
|
+
* there — ~5x slower on a thousand-element batch. Only a message that cannot be one string pays
|
|
128
|
+
* for the walk, as {@link chunkedJsonDecoder} does inbound.
|
|
103
129
|
*/
|
|
104
130
|
export const chunkedJsonEncoder: JsonRpcContentTypeEncoder = {
|
|
105
131
|
name: "application/json",
|
|
106
132
|
encode(msg: Message, options: { charset: BufferEncoding }): Promise<Uint8Array> {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
133
|
+
try {
|
|
134
|
+
return Promise.resolve(Buffer.from(JSON.stringify(msg), options.charset));
|
|
135
|
+
} catch (err) {
|
|
136
|
+
// A cycle or a bigint is a TypeError the walk cannot serialize either — and would meet
|
|
137
|
+
// by recursing until the stack ended. Only the string limit is worth a second attempt.
|
|
138
|
+
if (!(err instanceof RangeError)) {
|
|
139
|
+
return Promise.reject(err);
|
|
114
140
|
}
|
|
115
141
|
}
|
|
116
|
-
|
|
117
|
-
buffers.push(Buffer.from(pending, options.charset));
|
|
118
|
-
}
|
|
119
|
-
return Promise.resolve(Buffer.concat(buffers));
|
|
142
|
+
return Promise.resolve(encodeFragmented(msg, options.charset));
|
|
120
143
|
}
|
|
121
144
|
};
|
|
@@ -29,14 +29,15 @@ type ExportedTypes = typeof import("../../javascript/package-exported-types.js")
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Handle a DependencyTypes request: resolve one dependency, named by its npm coordinate, against
|
|
32
|
-
* the most recently parsed project
|
|
33
|
-
* back terminated by END_OF_OBJECT. Built once, returned in
|
|
34
|
-
* client's repeated pulls. A {@code null} version names a
|
|
35
|
-
* (e.g. {@code lib.dom}) instead of an installed package.
|
|
32
|
+
* the node_modules of {@code path} or else of the most recently parsed project, enumerate its
|
|
33
|
+
* public types, and stream them back terminated by END_OF_OBJECT. Built once, returned in
|
|
34
|
+
* {@code batchSize} slices across the client's repeated pulls. A {@code null} version names a
|
|
35
|
+
* TypeScript runtime declaration file (e.g. {@code lib.dom}) instead of an installed package.
|
|
36
36
|
*/
|
|
37
37
|
export class DependencyTypes {
|
|
38
38
|
constructor(private readonly name: string,
|
|
39
|
-
private readonly version?: string | null
|
|
39
|
+
private readonly version?: string | null,
|
|
40
|
+
private readonly path?: string | null) {
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
static handle(connection: rpc.MessageConnection, batchSize: number, metricsCsv?: string): void {
|
|
@@ -51,15 +52,17 @@ export class DependencyTypes {
|
|
|
51
52
|
metricsCsv,
|
|
52
53
|
(context) => async (request) => {
|
|
53
54
|
const version = request.version ?? null;
|
|
55
|
+
const projectDir = request.path ?? null;
|
|
54
56
|
context.target = version ? `${request.name}@${version}` : request.name;
|
|
55
57
|
|
|
56
|
-
const key = JSON.stringify([request.name, version]);
|
|
58
|
+
const key = JSON.stringify([request.name, version, projectDir]);
|
|
57
59
|
if (!pending.has(key)) {
|
|
58
60
|
// Dynamic import to break the circular dependency with the javascript module.
|
|
59
61
|
const {exportedTypes} = await import("../../javascript/package-exported-types.js");
|
|
60
|
-
const types = version
|
|
61
|
-
?
|
|
62
|
-
:
|
|
62
|
+
const types = version === null
|
|
63
|
+
? exportedTypes([runtimeLibFile(request.name)], [])
|
|
64
|
+
: packageTypes(request.name, exportedTypes,
|
|
65
|
+
projectDir === null ? projectNodeModules() : path.join(projectDir, "node_modules"));
|
|
63
66
|
|
|
64
67
|
const q = new RpcSendQueue(new ReferenceMap(), JAVA_TYPE_CLASS, false);
|
|
65
68
|
const sender = new JavaSender();
|
|
@@ -98,8 +101,7 @@ function projectNodeModules(): string {
|
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
/** The installed package's own types, falling back to its DefinitelyTyped package when it ships none. */
|
|
101
|
-
function packageTypes(name: string, exportedTypes: ExportedTypes): Type.FullyQualified[] {
|
|
102
|
-
const nodeModules = projectNodeModules();
|
|
104
|
+
function packageTypes(name: string, exportedTypes: ExportedTypes, nodeModules: string): Type.FullyQualified[] {
|
|
103
105
|
const pkgDir = path.join(nodeModules, ...name.split("/"));
|
|
104
106
|
if (!fs.existsSync(pkgDir)) {
|
|
105
107
|
throw new Error(`Package '${name}' is not installed under ${nodeModules}`);
|