@openrewrite/recipes-nodejs 0.52.0-20261009-102010 → 0.52.1
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/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/migrate/code-style.d.ts +14 -0
- package/dist/migrate/code-style.d.ts.map +1 -0
- package/dist/migrate/code-style.js +70 -0
- package/dist/migrate/code-style.js.map +1 -0
- package/dist/migrate/hpagent/find-hpagent-manual-migrations.d.ts.map +1 -1
- package/dist/migrate/hpagent/find-hpagent-manual-migrations.js +36 -133
- package/dist/migrate/hpagent/find-hpagent-manual-migrations.js.map +1 -1
- package/dist/migrate/hpagent/hpagent-support.d.ts +36 -15
- package/dist/migrate/hpagent/hpagent-support.d.ts.map +1 -1
- package/dist/migrate/hpagent/hpagent-support.js +294 -55
- package/dist/migrate/hpagent/hpagent-support.js.map +1 -1
- package/dist/migrate/hpagent/replace-hpagent.d.ts.map +1 -1
- package/dist/migrate/hpagent/replace-hpagent.js +510 -153
- package/dist/migrate/hpagent/replace-hpagent.js.map +1 -1
- package/dist/migrate/source-map-support/enable-source-maps-at-launch.d.ts.map +1 -1
- package/dist/migrate/source-map-support/enable-source-maps-at-launch.js +8 -16
- package/dist/migrate/source-map-support/enable-source-maps-at-launch.js.map +1 -1
- package/dist/migrate/source-map-support/find-source-map-support-manual-migrations.d.ts.map +1 -1
- package/dist/migrate/source-map-support/find-source-map-support-manual-migrations.js +26 -9
- package/dist/migrate/source-map-support/find-source-map-support-manual-migrations.js.map +1 -1
- package/dist/migrate/source-map-support/migrate-source-map-support-to-native.d.ts.map +1 -1
- package/dist/migrate/source-map-support/migrate-source-map-support-to-native.js +7 -4
- package/dist/migrate/source-map-support/migrate-source-map-support-to-native.js.map +1 -1
- package/dist/migrate/source-map-support/replace-source-map-support-api.d.ts +9 -0
- package/dist/migrate/source-map-support/replace-source-map-support-api.d.ts.map +1 -0
- package/dist/migrate/source-map-support/replace-source-map-support-api.js +275 -0
- package/dist/migrate/source-map-support/replace-source-map-support-api.js.map +1 -0
- package/dist/migrate/source-map-support/replace-source-map-support-install.d.ts.map +1 -1
- package/dist/migrate/source-map-support/replace-source-map-support-install.js +2 -1
- package/dist/migrate/source-map-support/replace-source-map-support-install.js.map +1 -1
- package/dist/migrate/source-map-support/source-map-support-api-source.d.ts +14 -0
- package/dist/migrate/source-map-support/source-map-support-api-source.d.ts.map +1 -0
- package/dist/migrate/source-map-support/source-map-support-api-source.js +426 -0
- package/dist/migrate/source-map-support/source-map-support-api-source.js.map +1 -0
- package/dist/migrate/source-map-support/source-map-support-rewrites.d.ts.map +1 -1
- package/dist/migrate/source-map-support/source-map-support-rewrites.js +2 -0
- package/dist/migrate/source-map-support/source-map-support-rewrites.js.map +1 -1
- package/dist/migrate/source-map-support/source-map-support-support.d.ts +11 -3
- package/dist/migrate/source-map-support/source-map-support-support.d.ts.map +1 -1
- package/dist/migrate/source-map-support/source-map-support-support.js +69 -10
- package/dist/migrate/source-map-support/source-map-support-support.js.map +1 -1
- package/dist/resources/advisories-npm.csv +342 -6
- package/package.json +2 -2
- package/src/index.ts +2 -0
- package/src/migrate/code-style.ts +92 -0
- package/src/migrate/fs-extra/replace-fs-extra-native-methods.ts +2 -2
- package/src/migrate/hpagent/find-hpagent-manual-migrations.ts +45 -142
- package/src/migrate/hpagent/hpagent-support.ts +358 -60
- package/src/migrate/hpagent/replace-hpagent.ts +580 -189
- package/src/migrate/source-map-support/enable-source-maps-at-launch.ts +17 -26
- package/src/migrate/source-map-support/find-source-map-support-manual-migrations.ts +25 -9
- package/src/migrate/source-map-support/migrate-source-map-support-to-native.ts +7 -4
- package/src/migrate/source-map-support/replace-source-map-support-api.ts +281 -0
- package/src/migrate/source-map-support/replace-source-map-support-install.ts +2 -1
- package/src/migrate/source-map-support/source-map-support-api-source.ts +470 -0
- package/src/migrate/source-map-support/source-map-support-rewrites.ts +3 -0
- package/src/migrate/source-map-support/source-map-support-support.ts +79 -10
|
@@ -11,7 +11,14 @@ import {getMemberKeyName, isLiteral, isMember, isObject, Json, JsonVisitor} from
|
|
|
11
11
|
import {PlainText} from "@openrewrite/rewrite/text";
|
|
12
12
|
import {dirOf, isPackageJson} from "../package-json-support";
|
|
13
13
|
import {addSourceMapsFlag, launchesNode, REGISTER_MODULES, shebangWithSourceMapsFlag} from "./register-flag";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
hasOnlyNativeOptions,
|
|
16
|
+
isSourceMapSupport,
|
|
17
|
+
SourceMapSupportVisitor,
|
|
18
|
+
sourcePathOf,
|
|
19
|
+
stringLiteral,
|
|
20
|
+
withoutStatements
|
|
21
|
+
} from "./source-map-support-support";
|
|
15
22
|
|
|
16
23
|
interface Accumulator {
|
|
17
24
|
/** Files holding an `install()` or `register` entry point the in-place rewrite declines. */
|
|
@@ -69,10 +76,16 @@ export class EnableSourceMapsAtLaunch extends ScanningRecipe<Accumulator> {
|
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
protected override async visitMethodInvocation(method: J.MethodInvocation, p: ExecutionContext): Promise<J | undefined> {
|
|
72
|
-
if (this.
|
|
79
|
+
if (this.isNativeInstall(method) || isRegisterRequire(method)) this.recordIfDeclined();
|
|
73
80
|
return super.visitMethodInvocation(method, p);
|
|
74
81
|
}
|
|
75
82
|
|
|
83
|
+
/** An `install()` the launch flag can stand in for; the API rewrite takes one with other options. */
|
|
84
|
+
private isNativeInstall(m: J.MethodInvocation): boolean {
|
|
85
|
+
const args = m.arguments.elements.map(a => a.element).filter(a => a.kind !== J.Kind.Empty);
|
|
86
|
+
return this.isInstall(m) && args.length <= 1 && hasOnlyNativeOptions(args[0]);
|
|
87
|
+
}
|
|
88
|
+
|
|
76
89
|
/** Only a site the in-place rewrite turns down is this recipe's business. */
|
|
77
90
|
private recordIfDeclined(): void {
|
|
78
91
|
if (this.mapsEverythingLoaded()) return;
|
|
@@ -218,29 +231,6 @@ export class EnableSourceMapsAtLaunch extends ScanningRecipe<Accumulator> {
|
|
|
218
231
|
}
|
|
219
232
|
}
|
|
220
233
|
|
|
221
|
-
/**
|
|
222
|
-
* Drops the statements `remove` selects, handing the first of a removed run its successor's place
|
|
223
|
-
* in the layout, so the blank line that separated it from what came before isn't left behind. Only
|
|
224
|
-
* the whitespace moves: a comment sits in the prefix of the statement it documents, so taking the
|
|
225
|
-
* successor's prefix wholesale would delete the comment that belongs to it.
|
|
226
|
-
*/
|
|
227
|
-
function withoutStatements(statements: J.RightPadded<any>[], remove: (s: J) => boolean): J.RightPadded<any>[] {
|
|
228
|
-
const kept: J.RightPadded<any>[] = [];
|
|
229
|
-
let pendingWhitespace: string | undefined;
|
|
230
|
-
for (const padded of statements) {
|
|
231
|
-
if (remove(padded.element)) {
|
|
232
|
-
pendingWhitespace ??= (padded.element.prefix as J.Space).whitespace;
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
kept.push(pendingWhitespace === undefined ? padded : {
|
|
236
|
-
...padded,
|
|
237
|
-
element: {...padded.element, prefix: {...padded.element.prefix, whitespace: pendingWhitespace}}
|
|
238
|
-
});
|
|
239
|
-
pendingWhitespace = undefined;
|
|
240
|
-
}
|
|
241
|
-
return kept;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
234
|
/** The innermost package a repo-relative path belongs to, of those the scan saw. */
|
|
245
235
|
function enclosingPackage(path: string, packageDirs: string[]): string {
|
|
246
236
|
return packageOf(path, packageDirs) ?? '';
|
|
@@ -265,7 +255,8 @@ function isRedundantEntryPoint(statement: J): boolean {
|
|
|
265
255
|
if (call?.kind !== J.Kind.MethodInvocation) return false;
|
|
266
256
|
const m = call as J.MethodInvocation;
|
|
267
257
|
if (isRegisterRequire(m)) return true;
|
|
268
|
-
|
|
258
|
+
const args = m.arguments.elements.map(a => a.element).filter(a => a.kind !== J.Kind.Empty);
|
|
259
|
+
if (m.name.simpleName !== 'install' || args.length > 1 || !hasOnlyNativeOptions(args[0])) return false;
|
|
269
260
|
const select = m.select?.element;
|
|
270
261
|
return select?.kind === J.Kind.MethodInvocation
|
|
271
262
|
? isSourceMapSupport(requiredModuleOf(select as J.MethodInvocation))
|
|
@@ -10,8 +10,10 @@ import {JS} from "@openrewrite/rewrite/javascript";
|
|
|
10
10
|
import {getMemberKeyName, isLiteral, isMember, isObject, Json, JsonVisitor} from "@openrewrite/rewrite/json";
|
|
11
11
|
import {REGISTER_MODULES, replaceRegisterFlag} from "./register-flag";
|
|
12
12
|
import {
|
|
13
|
+
DECLARING_KINDS,
|
|
13
14
|
isModuleScoped,
|
|
14
15
|
isSourceMapSupport,
|
|
16
|
+
isWriteTarget,
|
|
15
17
|
MODULES,
|
|
16
18
|
NON_INSTALL_MEMBERS,
|
|
17
19
|
nonNativeOption,
|
|
@@ -25,12 +27,6 @@ import {
|
|
|
25
27
|
const MOCK_OWNERS = new Set(['vi', 'jest']);
|
|
26
28
|
const MOCK_METHODS = new Set(['mock', 'doMock', 'unmock', 'unstable_mockModule']);
|
|
27
29
|
|
|
28
|
-
/** Trees that hold an identifier as a declaration or import rather than a use of it. */
|
|
29
|
-
const DECLARING_KINDS = new Set<string>([
|
|
30
|
-
JS.Kind.Import, JS.Kind.ImportClause, JS.Kind.ImportSpecifier, JS.Kind.Alias, JS.Kind.NamedImports,
|
|
31
|
-
JS.Kind.ExportSpecifier, JS.Kind.BindingElement, JS.Kind.ObjectBindingPattern,
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
30
|
function isSourceMapSupportSpecifier(e: J | undefined): boolean {
|
|
35
31
|
return isSourceMapSupport(stringLiteral(e));
|
|
36
32
|
}
|
|
@@ -48,6 +44,9 @@ function isMockCall(m: J.MethodInvocation): boolean {
|
|
|
48
44
|
return !!select && isIdentifier(select) && MOCK_OWNERS.has(select.simpleName) && MOCK_METHODS.has(m.name.simpleName);
|
|
49
45
|
}
|
|
50
46
|
|
|
47
|
+
/** Where a specifier string is a value of its own, not the argument of a `require`, `import` or mock. */
|
|
48
|
+
const HELD_AS_DATA = new Set<string>([J.Kind.NewArray, JS.Kind.PropertyAssignment, J.Kind.NamedVariable, J.Kind.Assignment]);
|
|
49
|
+
|
|
51
50
|
/** Sections of a `package.json` whose string values are shell commands. */
|
|
52
51
|
const SCRIPT_SECTIONS = new Set(['scripts']);
|
|
53
52
|
|
|
@@ -56,8 +55,9 @@ export class FindSourceMapSupportManualMigrations extends Recipe {
|
|
|
56
55
|
readonly displayName = "Find source-map-support usages that need manual migration";
|
|
57
56
|
readonly description = "Marks every source-map-support usage the automated migration leaves in place — an " +
|
|
58
57
|
"`install()` or `register` entry point that would only map part of the process, options Node has no " +
|
|
59
|
-
"equivalent for (`retrieveSourceMap`, `retrieveFile`, `environment: 'browser'`)
|
|
60
|
-
"(`wrapCallSite`, `mapSourcePosition`, ...), bundler banner strings,
|
|
58
|
+
"equivalent for (`retrieveSourceMap`, `retrieveFile`, `environment: 'browser'`) and the API beyond `install` " +
|
|
59
|
+
"(`wrapCallSite`, `mapSourcePosition`, ...) where no replacement could be written, bundler banner strings, " +
|
|
60
|
+
"`package.json` scripts whose command " +
|
|
61
61
|
"doesn't take Node's flags, type imports, re-exports, dynamic imports and test mocks — and records each with " +
|
|
62
62
|
"a suggested replacement in the `SourceMapSupportManualMigrationSteps` data table.";
|
|
63
63
|
readonly tags = ["source-map-support"];
|
|
@@ -199,12 +199,28 @@ export class FindSourceMapSupportManualMigrations extends Recipe {
|
|
|
199
199
|
return this.canCallProcess() ? REASONS.partialMapping : REASONS.localProcess;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
/** A member the rewrite left in place: assigned to, or read through a binding it couldn't replace. */
|
|
203
|
+
override async visitFieldAccess(fieldAccess: J.FieldAccess, p: ExecutionContext): Promise<J | undefined> {
|
|
204
|
+
const f = await super.visitFieldAccess(fieldAccess, p) as J.FieldAccess;
|
|
205
|
+
const target = f.target;
|
|
206
|
+
if (!isIdentifier(target) || !this.isWholeModule(target.simpleName)) return f;
|
|
207
|
+
const member = f.name.element.simpleName;
|
|
208
|
+
const reason = isWriteTarget(this.cursor) ? REASONS.assigned(member)
|
|
209
|
+
: this.inTypePosition() ? REASONS.type
|
|
210
|
+
: NON_INSTALL_MEMBERS.has(member) ? REASONS.member(member) : REASONS.value;
|
|
211
|
+
return this.report(f, `${target.simpleName}.${member}`, reason, p);
|
|
212
|
+
}
|
|
213
|
+
|
|
202
214
|
/** A string naming the module that isn't a specifier: code destined for a bundle, as a webpack banner is. */
|
|
203
215
|
override async visitLiteral(literal: J.Literal, p: ExecutionContext): Promise<J | undefined> {
|
|
204
216
|
const l = await super.visitLiteral(literal, p) as J.Literal;
|
|
205
217
|
if (typeof l.value !== 'string' || !l.value.includes('source-map-support')) return l;
|
|
206
218
|
// A specifier stands alone; a banner carries surrounding code.
|
|
207
|
-
if (isSourceMapSupport(l.value))
|
|
219
|
+
if (isSourceMapSupport(l.value)) {
|
|
220
|
+
// Held as data rather than loaded, as a `--require` argument for a child process is.
|
|
221
|
+
const parent = this.cursor.parentTree()?.value as J | undefined;
|
|
222
|
+
return parent && HELD_AS_DATA.has(parent.kind) ? this.report(l, l.value, REASONS.specifierString, p) : l;
|
|
223
|
+
}
|
|
208
224
|
return this.report(l, 'banner', REASONS.bannerString, p);
|
|
209
225
|
}
|
|
210
226
|
|
|
@@ -19,10 +19,13 @@ export class MigrateSourceMapSupportToNative extends Recipe {
|
|
|
19
19
|
"— in an ES module every `import` is evaluated before any statement runs — the flag goes on the " +
|
|
20
20
|
"launcher instead, in the package's `bin` shebang or a `node` script, and the entry point is removed " +
|
|
21
21
|
"as redundant. " +
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
22
|
+
"What Node has no equivalent for — `install()` with a custom `retrieveSourceMap` or `retrieveFile`, and " +
|
|
23
|
+
"`wrapCallSite`, `getErrorSource`, `mapSourcePosition`, `retrieveSourceMap` and `resetRetrieveHandlers` — is " +
|
|
24
|
+
"written into the file that uses it, on Node's own `SourceMap`, and an `install()` configured for a browser is " +
|
|
25
|
+
"removed. The dependency is removed from each `package.json` that declares it, with the lock file updated to " +
|
|
26
|
+
"match. Anything left in place, such as a webpack banner, is recorded in the " +
|
|
27
|
+
"`SourceMapSupportManualMigrationSteps` data table with a suggested replacement, and has to be migrated " +
|
|
28
|
+
"before building, since the dependency is removed regardless.";
|
|
26
29
|
readonly tags = ["source-map-support"];
|
|
27
30
|
|
|
28
31
|
async recipeList(): Promise<Recipe[]> {
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 the original author or authors.
|
|
3
|
+
*
|
|
4
|
+
* Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {Cursor, emptyMarkers, ExecutionContext, findMarker, randomId, Recipe, TreePrinters, TreeVisitor} from "@openrewrite/rewrite";
|
|
8
|
+
import {emptySpace, isIdentifier, J} from "@openrewrite/rewrite/java";
|
|
9
|
+
import {
|
|
10
|
+
autoFormat,
|
|
11
|
+
deconflict,
|
|
12
|
+
isReference,
|
|
13
|
+
JavaScriptParser,
|
|
14
|
+
JavaScriptVisitor,
|
|
15
|
+
JS,
|
|
16
|
+
maybeUnbind,
|
|
17
|
+
namesDeclaredIn,
|
|
18
|
+
namesReferencedWithin,
|
|
19
|
+
referencesOf,
|
|
20
|
+
StyleKind
|
|
21
|
+
} from "@openrewrite/rewrite/javascript";
|
|
22
|
+
import {detectCodeStyle} from "../code-style";
|
|
23
|
+
import {bindingsOf, requiredModule, stringLiteral} from "../module-bindings";
|
|
24
|
+
import {
|
|
25
|
+
ApiBuiltin,
|
|
26
|
+
ApiDeclaration,
|
|
27
|
+
apiSource,
|
|
28
|
+
builtinsFor,
|
|
29
|
+
declarationsFor,
|
|
30
|
+
TYPE_MEMBERS,
|
|
31
|
+
VALUE_MEMBERS
|
|
32
|
+
} from "./source-map-support-api-source";
|
|
33
|
+
import {
|
|
34
|
+
hasOnlyNativeOptions,
|
|
35
|
+
isBrowserInstall,
|
|
36
|
+
isModuleScoped,
|
|
37
|
+
isWriteTarget,
|
|
38
|
+
MODULES,
|
|
39
|
+
SourceMapSupportVisitor,
|
|
40
|
+
withoutStatements
|
|
41
|
+
} from "./source-map-support-support";
|
|
42
|
+
|
|
43
|
+
/** A name no file uses, which a declaration carries until the imports it replaces are gone. */
|
|
44
|
+
function placeholder(declaration: ApiDeclaration): string {
|
|
45
|
+
return `__sourceMapSupport$${declaration}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** What `import type {...}` and `import {type ...}` of source-map-support bind, by local name. */
|
|
49
|
+
function typeBindingsOf(cu: JS.CompilationUnit): Map<string, string> {
|
|
50
|
+
const bindings = new Map<string, string>();
|
|
51
|
+
for (const padded of cu.statements) {
|
|
52
|
+
if (padded.element?.kind !== JS.Kind.Import) continue;
|
|
53
|
+
const imp = padded.element as JS.Import;
|
|
54
|
+
const clause = imp.importClause;
|
|
55
|
+
const named = clause?.namedBindings;
|
|
56
|
+
if (!MODULES.has(stringLiteral(imp.moduleSpecifier?.element) ?? '') || named?.kind !== JS.Kind.NamedImports) continue;
|
|
57
|
+
for (const el of (named as JS.NamedImports).elements.elements) {
|
|
58
|
+
const spec = el.element as JS.ImportSpecifier;
|
|
59
|
+
if (spec.kind !== JS.Kind.ImportSpecifier || !(clause!.typeOnly || spec.importType?.element)) continue;
|
|
60
|
+
if (isIdentifier(spec.specifier)) {
|
|
61
|
+
bindings.set(spec.specifier.simpleName, spec.specifier.simpleName);
|
|
62
|
+
} else if (spec.specifier.kind === JS.Kind.Alias && isIdentifier((spec.specifier as JS.Alias).alias)) {
|
|
63
|
+
const alias = spec.specifier as JS.Alias;
|
|
64
|
+
bindings.set((alias.alias as J.Identifier).simpleName, alias.propertyName.element.simpleName);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return bindings;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function identifier(prefix: J.Space, simpleName: string): J.Identifier {
|
|
72
|
+
return {kind: J.Kind.Identifier, id: randomId(), prefix, markers: emptyMarkers, annotations: [], simpleName} as unknown as J.Identifier;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Gives each declaration its final name, once the imports that held those names have been removed. */
|
|
76
|
+
class NameDeclarations extends JavaScriptVisitor<ExecutionContext> {
|
|
77
|
+
constructor(private readonly names: ReadonlyMap<string, string>) {
|
|
78
|
+
super();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
override async visitIdentifier(identifier: J.Identifier, p: ExecutionContext): Promise<J | undefined> {
|
|
82
|
+
const id = await super.visitIdentifier(identifier, p) as J.Identifier;
|
|
83
|
+
const name = this.names.get(id.simpleName);
|
|
84
|
+
return name ? {...id, simpleName: name} as J.Identifier : id;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class ReplaceSourceMapSupportApi extends Recipe {
|
|
89
|
+
readonly name = "org.openrewrite.node.migrate.source-map-support.replace-source-map-support-api";
|
|
90
|
+
readonly displayName = "Replace source-map-support's API with functions written into the file";
|
|
91
|
+
readonly description = "Writes the part of source-map-support that Node has no equivalent for into each file " +
|
|
92
|
+
"that uses it, built on Node's own source map decoder (`node:module`'s `SourceMap`): `install()` with a " +
|
|
93
|
+
"custom `retrieveSourceMap` or `retrieveFile`, or with options that can't be read statically, becomes " +
|
|
94
|
+
"`installSourceMapSupport()`, and `wrapCallSite`, `getErrorSource`, `mapSourcePosition`, `retrieveSourceMap` " +
|
|
95
|
+
"and `resetRetrieveHandlers` become functions of the same name, along with the types they take from " +
|
|
96
|
+
"`@types/source-map-support`. Only the functions a file calls, and what they depend on, are written, " +
|
|
97
|
+
"after its other statements. An `install()` configured for a browser is removed: browsers map stack " +
|
|
98
|
+
"traces in their developer tools, and Node's source maps don't run there.";
|
|
99
|
+
readonly tags = ["source-map-support"];
|
|
100
|
+
|
|
101
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
102
|
+
return new class extends SourceMapSupportVisitor<ExecutionContext> {
|
|
103
|
+
/** Declarations the rewritten sites name. */
|
|
104
|
+
private needed = new Set<ApiDeclaration>();
|
|
105
|
+
/** The declaration each reference to a named binding of the module is renamed to, by identifier id. */
|
|
106
|
+
private renames = new Map<string, ApiDeclaration>();
|
|
107
|
+
/** Named bindings whose every reference is renamed, so their names come free once they are unbound. */
|
|
108
|
+
private released = new Set<string>();
|
|
109
|
+
/** Members whose named bindings are renamed, which are unbound once unused. */
|
|
110
|
+
private members = new Set<string>();
|
|
111
|
+
/** Browser `install()` statements, which are removed. */
|
|
112
|
+
private browserInstalls = new Set<string>();
|
|
113
|
+
|
|
114
|
+
protected override async visitJsCompilationUnit(cu: JS.CompilationUnit, p: ExecutionContext): Promise<J | undefined> {
|
|
115
|
+
if (/\.d\.[cm]?ts$/.test(cu.sourcePath)) return cu;
|
|
116
|
+
this.needed = new Set();
|
|
117
|
+
this.released = new Set();
|
|
118
|
+
this.members = new Set();
|
|
119
|
+
this.browserInstalls = new Set();
|
|
120
|
+
this.renames = this.planRenames(cu);
|
|
121
|
+
this.resetBindings();
|
|
122
|
+
|
|
123
|
+
const visited = await super.visitJsCompilationUnit(cu, p) as JS.CompilationUnit;
|
|
124
|
+
if (this.needed.size === 0 && this.browserInstalls.size === 0) return cu;
|
|
125
|
+
|
|
126
|
+
const typeScript = /\.[cm]?tsx?$/.test(cu.sourcePath);
|
|
127
|
+
const declarations = declarationsFor(this.needed, typeScript);
|
|
128
|
+
const builtins = new Map<ApiBuiltin, string>();
|
|
129
|
+
for (const builtin of builtinsFor(declarations)) {
|
|
130
|
+
const [module, member] = builtin.split('#') as [string, string];
|
|
131
|
+
const local = this.bindNode(module, member);
|
|
132
|
+
// A file that can't import Node's builtins keeps source-map-support, for the finder to report.
|
|
133
|
+
if (!local) return cu;
|
|
134
|
+
builtins.set(builtin, local);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let result = await this.insertPendingRequires(cu, visited);
|
|
138
|
+
if (this.browserInstalls.size > 0) {
|
|
139
|
+
result = {...result, statements: withoutStatements(result.statements, s => this.browserInstalls.has(statementCall(s)?.id ?? ''))};
|
|
140
|
+
}
|
|
141
|
+
const names = new Map<string, string>();
|
|
142
|
+
if (declarations.length > 0) {
|
|
143
|
+
result = await this.withDeclarations(cu, result, declarations, builtins, names, typeScript, p);
|
|
144
|
+
}
|
|
145
|
+
maybeUnbind(this, {module: 'source-map-support'});
|
|
146
|
+
for (const member of this.members) maybeUnbind(this, {module: 'source-map-support', member});
|
|
147
|
+
// Last, so the imports that held these names are already gone.
|
|
148
|
+
if (names.size > 0) this.afterVisit.push(new NameDeclarations(names));
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Where each named binding of a member the migration writes is referenced. A binding with a
|
|
154
|
+
* reference that can't be renamed, as a shorthand property spells its key with it, is left alone.
|
|
155
|
+
*/
|
|
156
|
+
private planRenames(cu: JS.CompilationUnit): Map<string, ApiDeclaration> {
|
|
157
|
+
const renames = new Map<string, ApiDeclaration>();
|
|
158
|
+
for (const [local, member] of [...bindingsOf(cu, MODULES), ...typeBindingsOf(cu)]) {
|
|
159
|
+
// `install` is rewritten only at the calls whose options need it.
|
|
160
|
+
if (member === undefined || member === 'install') continue;
|
|
161
|
+
const declaration = VALUE_MEMBERS.get(member) ?? TYPE_MEMBERS.get(member);
|
|
162
|
+
if (!declaration) continue;
|
|
163
|
+
const references = referencesOf(this.cursor, local).filter(r => !r.declares);
|
|
164
|
+
if (references.some(r => !isReference(r.cursor, r.identifier))) continue;
|
|
165
|
+
for (const r of references) renames.set(r.identifier.id, declaration);
|
|
166
|
+
this.released.add(local);
|
|
167
|
+
this.members.add(member);
|
|
168
|
+
}
|
|
169
|
+
return renames;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Appends `declarations` under their placeholders, recording in `names` what each is finally called. */
|
|
173
|
+
private async withDeclarations(original: JS.CompilationUnit, cu: JS.CompilationUnit, declarations: ApiDeclaration[],
|
|
174
|
+
builtins: Map<ApiBuiltin, string>, names: Map<string, string>, typeScript: boolean,
|
|
175
|
+
p: ExecutionContext): Promise<JS.CompilationUnit> {
|
|
176
|
+
// A top-level declaration clashes with any name the file declares, and captures a global it reads.
|
|
177
|
+
const declared = namesDeclaredIn(original);
|
|
178
|
+
const read = namesReferencedWithin(original);
|
|
179
|
+
const builtinLocals = new Set(builtins.values());
|
|
180
|
+
for (const declaration of declarations) {
|
|
181
|
+
const taken = new Set(names.values());
|
|
182
|
+
names.set(placeholder(declaration), deconflict(declaration, name =>
|
|
183
|
+
((declared.has(name) || read.has(name)) && !this.released.has(name)) || builtinLocals.has(name) || taken.has(name)));
|
|
184
|
+
}
|
|
185
|
+
const source = apiSource(declarations, {
|
|
186
|
+
names: placeholder,
|
|
187
|
+
builtins: builtin => builtins.get(builtin)!,
|
|
188
|
+
typeScript,
|
|
189
|
+
style: detectCodeStyle(await TreePrinters.print(original)),
|
|
190
|
+
});
|
|
191
|
+
// Parsed rather than templated, so the code keeps the layout written for this file's style.
|
|
192
|
+
const parsed = await new JavaScriptParser().parseOnly({
|
|
193
|
+
text: source, sourcePath: typeScript ? 'source-map-support.ts' : 'source-map-support.js'
|
|
194
|
+
}) as JS.CompilationUnit;
|
|
195
|
+
// The call sites already name the declarations, so a source that doesn't parse can't be skipped.
|
|
196
|
+
if (parsed.kind !== JS.Kind.CompilationUnit) throw new Error(`The source-map-support API written for ${original.sourcePath} does not parse`);
|
|
197
|
+
// Where the project formats with Prettier, its own configuration lays the declarations out.
|
|
198
|
+
const prettier = findMarker(original, StyleKind.PrettierStyle) !== undefined;
|
|
199
|
+
const parent = new Cursor(cu, this.cursor);
|
|
200
|
+
const appended: J.RightPadded<any>[] = [];
|
|
201
|
+
for (const [i, padded] of parsed.statements.entries()) {
|
|
202
|
+
// The parser files the first statement's comment under the compilation unit.
|
|
203
|
+
let declaration = i === 0
|
|
204
|
+
? {...padded.element, prefix: {...parsed.prefix, whitespace: '\n\n'}} as J
|
|
205
|
+
: padded.element as J;
|
|
206
|
+
if (prettier) declaration = await autoFormat(declaration, p, undefined, parent);
|
|
207
|
+
appended.push({...padded, element: declaration});
|
|
208
|
+
}
|
|
209
|
+
return {...cu, statements: [...cu.statements, ...appended]} as JS.CompilationUnit;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
override async visitMethodInvocation(method: J.MethodInvocation, p: ExecutionContext): Promise<J | undefined> {
|
|
213
|
+
const m = await super.visitMethodInvocation(method, p) as J.MethodInvocation;
|
|
214
|
+
if (m?.kind !== J.Kind.MethodInvocation) return m;
|
|
215
|
+
if (this.isInstall(m)) {
|
|
216
|
+
const args = m.arguments.elements.map(a => a.element).filter(a => a.kind !== J.Kind.Empty);
|
|
217
|
+
// More than one argument is no `install` call this can read, and native options are the native recipes'.
|
|
218
|
+
if (args.length > 1 || hasOnlyNativeOptions(args[0])) return m;
|
|
219
|
+
if (isBrowserInstall(args[0])) {
|
|
220
|
+
if (this.isTopLevelStatement()) this.browserInstalls.add(m.id);
|
|
221
|
+
return m;
|
|
222
|
+
}
|
|
223
|
+
if (!m.select) this.members.add('install');
|
|
224
|
+
return this.call(m, 'installSourceMapSupport');
|
|
225
|
+
}
|
|
226
|
+
const select = m.select?.element;
|
|
227
|
+
const declaration = VALUE_MEMBERS.get(m.name.simpleName);
|
|
228
|
+
if (!select || !declaration || declaration === 'installSourceMapSupport' || !this.isModuleObject(select)) return m;
|
|
229
|
+
return this.call(m, declaration);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
override async visitFieldAccess(fieldAccess: J.FieldAccess, p: ExecutionContext): Promise<J | undefined> {
|
|
233
|
+
const f = await super.visitFieldAccess(fieldAccess, p) as J.FieldAccess;
|
|
234
|
+
// Assigning a member changes the library for other modules, which a local function can't.
|
|
235
|
+
if (f?.kind !== J.Kind.FieldAccess || !this.isModuleObject(f.target) || isWriteTarget(this.cursor)) return f;
|
|
236
|
+
const member = f.name.element.simpleName;
|
|
237
|
+
const declaration = this.inTypePosition() ? TYPE_MEMBERS.get(member) : VALUE_MEMBERS.get(member);
|
|
238
|
+
if (!declaration) return f;
|
|
239
|
+
this.needed.add(declaration);
|
|
240
|
+
return identifier(f.prefix, placeholder(declaration));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
override async visitIdentifier(ident: J.Identifier, p: ExecutionContext): Promise<J | undefined> {
|
|
244
|
+
const id = await super.visitIdentifier(ident, p) as J.Identifier;
|
|
245
|
+
const declaration = this.renames.get(ident.id);
|
|
246
|
+
if (id?.kind !== J.Kind.Identifier || !declaration) return id;
|
|
247
|
+
this.needed.add(declaration);
|
|
248
|
+
return {...id, simpleName: placeholder(declaration)} as J.Identifier;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** `m` calling `declaration` directly, its arguments as written. */
|
|
252
|
+
private call(m: J.MethodInvocation, declaration: ApiDeclaration): J.MethodInvocation {
|
|
253
|
+
this.needed.add(declaration);
|
|
254
|
+
return {...m, select: undefined, name: {...m.name, prefix: emptySpace, simpleName: placeholder(declaration)}};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** The module itself, as a whole-module binding or a `require` of it. */
|
|
258
|
+
private isModuleObject(e: J | undefined): boolean {
|
|
259
|
+
if (e?.kind === J.Kind.MethodInvocation) return MODULES.has(requiredModule(e) ?? '');
|
|
260
|
+
return !!e && isIdentifier(e) && this.bindings.has(e.simpleName) &&
|
|
261
|
+
[undefined, '*'].includes(this.bindings.get(e.simpleName)) && isModuleScoped(this.cursor, e.simpleName);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private inTypePosition(): boolean {
|
|
265
|
+
return !!this.cursor.firstEnclosing((t: any): t is J => t?.kind === JS.Kind.TypeInfo);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Whether the cursor's call is a statement of the compilation unit, which can be removed whole. */
|
|
269
|
+
private isTopLevelStatement(): boolean {
|
|
270
|
+
const parent = this.cursor.parentTree();
|
|
271
|
+
const statement = parent?.value?.kind === JS.Kind.ExpressionStatement ? parent.parentTree() : parent;
|
|
272
|
+
return statement?.value?.kind === JS.Kind.CompilationUnit;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The call a statement consists of. */
|
|
279
|
+
function statementCall(statement: J): J | undefined {
|
|
280
|
+
return statement.kind === JS.Kind.ExpressionStatement ? (statement as JS.ExpressionStatement).expression : statement;
|
|
281
|
+
}
|
|
@@ -16,7 +16,8 @@ export class ReplaceSourceMapSupportInstall extends Recipe {
|
|
|
16
16
|
readonly displayName = "Replace `source-map-support` with `process.setSourceMapsEnabled()`";
|
|
17
17
|
readonly description = "Replaces `require('source-map-support').install()`, `import 'source-map-support/register'` and " +
|
|
18
18
|
"their variants with Node's native `process.setSourceMapsEnabled(true)` (Node.js 16.6+). Calls passing options " +
|
|
19
|
-
"Node has no equivalent for (`retrieveSourceMap`, `retrieveFile`, `environment: 'browser'`, ...) are left
|
|
19
|
+
"Node has no equivalent for (`retrieveSourceMap`, `retrieveFile`, `environment: 'browser'`, ...) are left to " +
|
|
20
|
+
"`ReplaceSourceMapSupportApi`. " +
|
|
20
21
|
"Native source maps only apply to modules loaded after they are enabled, where `source-map-support` maps every " +
|
|
21
22
|
"stack frame, so by default a call is only replaced where no other module is loaded before it: no other runtime " +
|
|
22
23
|
"import in the file, and no earlier `require`. Elsewhere, launch Node with `--enable-source-maps`, which " +
|