@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
|
@@ -4,25 +4,57 @@
|
|
|
4
4
|
* Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {emptyMarkers, ExecutionContext, Option, randomId, Recipe, TreeVisitor} from "@openrewrite/rewrite";
|
|
8
|
-
import {isIdentifier, J, rightPadded, space} from "@openrewrite/rewrite/java";
|
|
9
|
-
import {compilationUnitOf, declarationsOf, JavaScriptVisitor, JS, maybeRebind, maybeUnbind, namesDeclaredIn} from "@openrewrite/rewrite/javascript";
|
|
10
|
-
import {requiredModule} from "../module-bindings";
|
|
7
|
+
import {Cursor, emptyMarkers, ExecutionContext, findMarker, Option, randomId, Recipe, TreePrinters, TreeVisitor} from "@openrewrite/rewrite";
|
|
8
|
+
import {emptySpace, isIdentifier, J, rightPadded, space} from "@openrewrite/rewrite/java";
|
|
11
9
|
import {
|
|
12
|
-
|
|
10
|
+
autoFormat,
|
|
11
|
+
bindingNames,
|
|
12
|
+
compilationUnitOf,
|
|
13
|
+
declarationsOf,
|
|
14
|
+
deconflict,
|
|
15
|
+
isReference,
|
|
16
|
+
JavaScriptParser,
|
|
17
|
+
JavaScriptVisitor,
|
|
18
|
+
JS,
|
|
19
|
+
maybeRebind,
|
|
20
|
+
maybeUnbind,
|
|
21
|
+
namesUsedIn,
|
|
22
|
+
StyleKind
|
|
23
|
+
} from "@openrewrite/rewrite/javascript";
|
|
24
|
+
import {detectCodeStyle} from "../code-style";
|
|
25
|
+
import {bindingsOf, importModule, requiredModule} from "../module-bindings";
|
|
26
|
+
import {
|
|
27
|
+
ADAPTED_TYPES,
|
|
28
|
+
ADAPTER_FUNCTION,
|
|
29
|
+
ADAPTER_TYPE,
|
|
30
|
+
adapterSource,
|
|
31
|
+
adapterTypeSource,
|
|
32
|
+
builtinAgentModule,
|
|
33
|
+
builtinAgentType,
|
|
13
34
|
classifyOption,
|
|
35
|
+
constructionArgument,
|
|
36
|
+
emptyHpagentBindings,
|
|
37
|
+
GeneratedDeclaration,
|
|
14
38
|
HPAGENT_CLASSES,
|
|
15
39
|
HPAGENT_MODULES,
|
|
40
|
+
hpagentBindings,
|
|
41
|
+
HpagentBindings,
|
|
42
|
+
inTypePosition,
|
|
43
|
+
isHpagentRequire,
|
|
16
44
|
isKnownProxyRequestOptions,
|
|
45
|
+
isMockCall,
|
|
17
46
|
isObjectLiteral,
|
|
18
|
-
|
|
19
|
-
spreadIsPlainAgentOptions,
|
|
47
|
+
isValueUse,
|
|
20
48
|
ModuleBindings,
|
|
21
49
|
objectMembers,
|
|
22
50
|
propertyName,
|
|
51
|
+
REQUEST_TYPE,
|
|
52
|
+
resolvesToBinding,
|
|
53
|
+
spiedClass,
|
|
54
|
+
spreadIsPlainAgentOptions,
|
|
55
|
+
stringLiteral,
|
|
23
56
|
TARGET_CLASS,
|
|
24
57
|
TARGET_MODULE,
|
|
25
|
-
targetTypeReference,
|
|
26
58
|
} from "./hpagent-support";
|
|
27
59
|
|
|
28
60
|
/**
|
|
@@ -36,43 +68,25 @@ interface Rewrite {
|
|
|
36
68
|
literal: J.NewClass;
|
|
37
69
|
}
|
|
38
70
|
|
|
39
|
-
/** The
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const variable = v.element;
|
|
46
|
-
if (!HPAGENT_MODULES.has(requiredModule(variable.initializer?.element) ?? '')) continue;
|
|
47
|
-
for (const name of boundNames(variable.name as J)) names.add(name);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return names;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** The local names a `require`'s binding target introduces. */
|
|
55
|
-
function boundNames(target: J): string[] {
|
|
56
|
-
if (isIdentifier(target)) return [target.simpleName];
|
|
57
|
-
if (target.kind !== JS.Kind.ObjectBindingPattern) return [];
|
|
58
|
-
return (target as JS.ObjectBindingPattern).bindings.elements.flatMap(padded => {
|
|
59
|
-
const binding = padded.element as J;
|
|
60
|
-
if (isIdentifier(binding)) return [binding.simpleName];
|
|
61
|
-
if (binding.kind !== JS.Kind.BindingElement) return [];
|
|
62
|
-
const name = (binding as JS.BindingElement).name as J;
|
|
63
|
-
return isIdentifier(name) ? [name.simpleName] : [];
|
|
64
|
-
});
|
|
71
|
+
/** The local name a binding pattern element introduces. */
|
|
72
|
+
function boundLocal(binding: J): string | undefined {
|
|
73
|
+
if (isIdentifier(binding)) return binding.simpleName;
|
|
74
|
+
if (binding.kind !== JS.Kind.BindingElement) return undefined;
|
|
75
|
+
const name = (binding as JS.BindingElement).name as J;
|
|
76
|
+
return isIdentifier(name) ? name.simpleName : undefined;
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
/**
|
|
68
80
|
* Renames a `require`'s destructured bindings to the names the migration gives them. A shorthand
|
|
69
81
|
* `{HttpProxyAgent}` becomes `{HttpsProxyAgent}`; one that already names a property — `{X: local}` —
|
|
70
|
-
* keeps its local name and takes the new property.
|
|
82
|
+
* keeps its local name and takes the new property. A binding folded into another is dropped.
|
|
71
83
|
*/
|
|
72
|
-
function renameBindings(target: J, targetLocals: ReadonlyMap<string, string>): J {
|
|
84
|
+
function renameBindings(target: J, targetLocals: ReadonlyMap<string, string>, merged: ReadonlySet<string>): J {
|
|
73
85
|
if (target.kind !== JS.Kind.ObjectBindingPattern) return target;
|
|
74
86
|
const pattern = target as JS.ObjectBindingPattern;
|
|
75
|
-
const
|
|
87
|
+
const original = pattern.bindings.elements;
|
|
88
|
+
const kept = original.filter(padded => !merged.has(boundLocal(padded.element as J) ?? ''));
|
|
89
|
+
const elements = kept.map(padded => {
|
|
76
90
|
const binding = padded.element as J;
|
|
77
91
|
if (isIdentifier(binding)) {
|
|
78
92
|
const renamed = targetLocals.get(binding.simpleName);
|
|
@@ -92,19 +106,76 @@ function renameBindings(target: J, targetLocals: ReadonlyMap<string, string>): J
|
|
|
92
106
|
const name = element.name as J;
|
|
93
107
|
if (!isIdentifier(name)) return padded;
|
|
94
108
|
const renamed = targetLocals.get(name.simpleName);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
if (!renamed || renamed === name.simpleName) return padded;
|
|
110
|
+
if (renamed === TARGET_CLASS) return {...padded, element: {...element, name: {...name, simpleName: renamed}} as J};
|
|
111
|
+
// A deconflicted name can't be shorthand, so it reads the class off as `{HttpsProxyAgent: renamed}`.
|
|
112
|
+
return {
|
|
113
|
+
...padded,
|
|
114
|
+
element: {
|
|
115
|
+
...element,
|
|
116
|
+
propertyName: rightPadded({...name, id: randomId(), simpleName: TARGET_CLASS} as J.Identifier, emptySpace),
|
|
117
|
+
name: {...name, prefix: space(' '), simpleName: renamed}
|
|
118
|
+
} as J
|
|
119
|
+
};
|
|
98
120
|
});
|
|
121
|
+
// The padding after the last element holds the space before `}`, so the new last element takes it.
|
|
122
|
+
if (elements.length > 0 && kept[kept.length - 1] !== original[original.length - 1]) {
|
|
123
|
+
elements[elements.length - 1] = {...elements[elements.length - 1], after: original[original.length - 1].after};
|
|
124
|
+
}
|
|
99
125
|
return {...pattern, bindings: {...pattern.bindings, elements}} as J;
|
|
100
126
|
}
|
|
101
127
|
|
|
128
|
+
function retargeted(literal: J.Literal): J.Literal {
|
|
129
|
+
const quote = (literal.valueSource ?? "'").charAt(0);
|
|
130
|
+
return {...literal, value: TARGET_MODULE, valueSource: `${quote}${TARGET_MODULE}${quote}`};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** `require('hpagent')` naming the new module, together with a `typeof import('hpagent')` it is cast to. */
|
|
134
|
+
function movedRequireCall(e: J): J {
|
|
135
|
+
if (e.kind === JS.Kind.As) {
|
|
136
|
+
const as = e as JS.As;
|
|
137
|
+
return {...as, left: {...as.left, element: movedRequireCall(as.left.element)}, right: movedImportType(as.right)} as J;
|
|
138
|
+
}
|
|
139
|
+
const call = e as J.MethodInvocation;
|
|
140
|
+
const specifier = call.arguments.elements[0];
|
|
141
|
+
return {...call, arguments: {...call.arguments, elements: [{...specifier, element: retargeted(specifier.element as J.Literal)}]}} as J.MethodInvocation;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function movedImportType(type: J): J {
|
|
145
|
+
if (type.kind !== JS.Kind.ImportType) return type;
|
|
146
|
+
const importType = type as JS.ImportType;
|
|
147
|
+
const elements = importType.argumentAndAttributes.elements.map(padded => {
|
|
148
|
+
const argument = padded.element;
|
|
149
|
+
if (argument.kind !== JS.Kind.LiteralType) return padded;
|
|
150
|
+
const literal = (argument as JS.LiteralType).literal as J.Literal;
|
|
151
|
+
return HPAGENT_MODULES.has(stringLiteral(literal) ?? '')
|
|
152
|
+
? {...padded, element: {...argument, literal: retargeted(literal)} as J}
|
|
153
|
+
: padded;
|
|
154
|
+
});
|
|
155
|
+
return {...importType, argumentAndAttributes: {...importType.argumentAndAttributes, elements}} as J;
|
|
156
|
+
}
|
|
157
|
+
|
|
102
158
|
/** What a type arm denotes, for spotting two arms that say the same thing. */
|
|
103
159
|
function typeKey(type: J): string | undefined {
|
|
104
160
|
if (isIdentifier(type)) return type.simpleName;
|
|
161
|
+
if (type.kind === J.Kind.FieldAccess) {
|
|
162
|
+
const access = type as J.FieldAccess;
|
|
163
|
+
return isIdentifier(access.target) ? `${access.target.simpleName}.${access.name.element.simpleName}` : undefined;
|
|
164
|
+
}
|
|
165
|
+
if (type.kind === JS.Kind.ImportType) {
|
|
166
|
+
const importType = type as JS.ImportType;
|
|
167
|
+
const module = importType.argumentAndAttributes.elements[0]?.element as JS.LiteralType | undefined;
|
|
168
|
+
const name = importType.qualifier?.element;
|
|
169
|
+
return module?.kind === JS.Kind.LiteralType && name && isIdentifier(name)
|
|
170
|
+
? `import(${stringLiteral(module.literal)}).${name.simpleName}`
|
|
171
|
+
: undefined;
|
|
172
|
+
}
|
|
105
173
|
if (type.kind !== J.Kind.ParameterizedType) return undefined;
|
|
106
174
|
const clazz = (type as J.ParameterizedType).class as J;
|
|
107
|
-
|
|
175
|
+
if (isIdentifier(clazz)) return `${clazz.simpleName}<>`;
|
|
176
|
+
if (clazz.kind !== J.Kind.FieldAccess) return undefined;
|
|
177
|
+
const access = clazz as J.FieldAccess;
|
|
178
|
+
return isIdentifier(access.target) ? `${access.target.simpleName}.${access.name.element.simpleName}<>` : undefined;
|
|
108
179
|
}
|
|
109
180
|
|
|
110
181
|
/**
|
|
@@ -160,7 +231,7 @@ function isJavaScript(sourcePath: string): boolean {
|
|
|
160
231
|
}
|
|
161
232
|
|
|
162
233
|
/** `proxy ? construction : undefined`, built directly so the construction keeps its layout. */
|
|
163
|
-
function whenDefined(proxy: J, construction: J
|
|
234
|
+
function whenDefined(proxy: J, construction: J): J.Ternary {
|
|
164
235
|
const padded = <T extends J>(element: T): J.LeftPadded<T> =>
|
|
165
236
|
({kind: J.Kind.LeftPadded, before: space(' '), element, markers: emptyMarkers});
|
|
166
237
|
const undefinedValue: J.Identifier = {
|
|
@@ -172,7 +243,7 @@ function whenDefined(proxy: J, construction: J.NewClass): J.Ternary {
|
|
|
172
243
|
prefix: construction.prefix,
|
|
173
244
|
markers: emptyMarkers,
|
|
174
245
|
condition: {...proxy, id: randomId(), prefix: space('')} as J,
|
|
175
|
-
truePart: padded({...construction, prefix: space(' ')} as J
|
|
246
|
+
truePart: padded({...construction, prefix: space(' ')} as J),
|
|
176
247
|
falsePart: padded(undefinedValue),
|
|
177
248
|
} as J.Ternary;
|
|
178
249
|
}
|
|
@@ -182,6 +253,61 @@ function paddedMembers(literal: J.NewClass): J.RightPadded<J>[] {
|
|
|
182
253
|
return (literal.body?.statements ?? []).filter(padded => padded.element.kind !== J.Kind.Empty) as J.RightPadded<J>[];
|
|
183
254
|
}
|
|
184
255
|
|
|
256
|
+
/**
|
|
257
|
+
* A builtin's specifier for a type query. The bare name unless the file already writes `node:`, since
|
|
258
|
+
* `@types/node` declared the prefixed names only from 16, after hpagent's Node 14 floor.
|
|
259
|
+
*/
|
|
260
|
+
function builtinSpecifier(cu: JS.CompilationUnit, module: string): string {
|
|
261
|
+
const prefixed = cu.statements.some(padded => {
|
|
262
|
+
const statement = padded.element;
|
|
263
|
+
const specifier = statement?.kind === JS.Kind.Import
|
|
264
|
+
? importModule(statement as JS.Import)
|
|
265
|
+
: declarationsOf(statement).map(d => d.variables.map(v => requiredModule(v.element.initializer?.element)).find(Boolean)).find(Boolean);
|
|
266
|
+
return specifier?.startsWith('node:');
|
|
267
|
+
});
|
|
268
|
+
return prefixed ? `node:${module}` : module;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** One of hpagent's option types, which the adapter's own types stand in for. */
|
|
272
|
+
function isOptionType(member: string): boolean {
|
|
273
|
+
return ADAPTED_TYPES.has(member) || member === 'ProxyAgentRequestOptions';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** `declare module 'hpagent' {...}`, which declared types for the module the migration drops. */
|
|
277
|
+
function isAmbientHpagent(statement: J | undefined): boolean {
|
|
278
|
+
if (statement?.kind !== JS.Kind.NamespaceDeclaration) return false;
|
|
279
|
+
const declaration = statement as JS.NamespaceDeclaration;
|
|
280
|
+
return declaration.keywordType.element === JS.NamespaceDeclaration.KeywordType.Module &&
|
|
281
|
+
HPAGENT_MODULES.has(stringLiteral(declaration.name.element) ?? '');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** `ConstructorParameters<typeof X>[0]`, the options a constructor takes; the `typeof` operand, if it is that. */
|
|
285
|
+
function constructorOptionsOf(type: JS.IndexedAccessType): J | undefined {
|
|
286
|
+
const index = (type.indexType as JS.IndexedAccessType.IndexType).element.element;
|
|
287
|
+
if (index.kind !== JS.Kind.LiteralType || (index as JS.LiteralType).literal.kind !== J.Kind.Literal ||
|
|
288
|
+
((index as JS.LiteralType).literal as J.Literal).value !== 0) {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
if (type.objectType.kind !== J.Kind.ParameterizedType) return undefined;
|
|
292
|
+
const parameterized = type.objectType as J.ParameterizedType;
|
|
293
|
+
const query = parameterized.typeParameters?.elements;
|
|
294
|
+
if (!isIdentifier(parameterized.class) || parameterized.class.simpleName !== 'ConstructorParameters' ||
|
|
295
|
+
query?.length !== 1 || query[0].element.kind !== JS.Kind.TypeQuery) {
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
return (query[0].element as JS.TypeQuery).typeExpression;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** The `proxy` an options literal names, if it is one. */
|
|
302
|
+
function literalProxy(options: J): J | undefined {
|
|
303
|
+
if (!isObjectLiteral(options)) return undefined;
|
|
304
|
+
for (const padded of paddedMembers(options)) {
|
|
305
|
+
const option = classifyOption(padded.element);
|
|
306
|
+
if (option.kind === 'proxy') return option.value;
|
|
307
|
+
}
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
|
|
185
311
|
export class ReplaceHpagent extends Recipe {
|
|
186
312
|
readonly name = "org.openrewrite.node.migrate.hpagent.replace-hpagent";
|
|
187
313
|
readonly displayName = "Replace `hpagent` with `https-proxy-agent`";
|
|
@@ -192,21 +318,29 @@ export class ReplaceHpagent extends Recipe {
|
|
|
192
318
|
"`https:` target, so one class serves both. `proxyRequestOptions` members move up into the options object, " +
|
|
193
319
|
"where `https-proxy-agent` applies them to the connection to the proxy. hpagent accepted a missing proxy until " +
|
|
194
320
|
"a request needed it, but `https-proxy-agent` reads it in the constructor, so in JavaScript a proxy that may be " +
|
|
195
|
-
"missing, such as `process.env.HTTP_PROXY`, guards the construction: `proxy ? new HttpsProxyAgent(proxy) : undefined
|
|
196
|
-
"
|
|
197
|
-
"
|
|
198
|
-
"
|
|
199
|
-
"
|
|
321
|
+
"missing, such as `process.env.HTTP_PROXY`, guards the construction: `proxy ? new HttpsProxyAgent(proxy) : undefined`.\n\n" +
|
|
322
|
+
"A construction that can't take the two-argument form — options that aren't an object literal, a spread " +
|
|
323
|
+
"that can't be checked, or a TLS option for the destination handshake (`ca`, `rejectUnauthorized`, `cert`, " +
|
|
324
|
+
"...), which `https-proxy-agent` would apply to the proxy hop — calls a `createProxyAgent` adapter written " +
|
|
325
|
+
"into the same file instead. It takes hpagent's options object as it was, gives the proxy hop what hpagent " +
|
|
326
|
+
"gave it, and puts every other option on the agent's per-request options, which is where hpagent's " +
|
|
327
|
+
"`https.Agent` kept them for the destination. hpagent's option types become the adapter's own " +
|
|
328
|
+
"`ProxyAgentOptions`, `ConstructorParameters<typeof HttpProxyAgent>[0]` included, and `ProxyAgentRequestOptions` " +
|
|
329
|
+
"a `ProxyRequestOptions` of its own; `instanceof` checks and type references move to the new class. A default " +
|
|
330
|
+
"import becomes a namespace import, since `https-proxy-agent` has no default export; a block's " +
|
|
331
|
+
"`const {...} = require('hpagent')`, and `({...} = require('hpagent'))` into a `let`, move like a top-level one; " +
|
|
332
|
+
"`jest.mock`/`vi.mock` and `spyOn` follow the module in a file whose hpagent bindings all moved; and a " +
|
|
333
|
+
"`declare module 'hpagent'` is deleted. `FindHpagentManualMigrations` reports every usage left behind.";
|
|
200
334
|
readonly tags = ["hpagent", "https-proxy-agent"];
|
|
201
335
|
|
|
202
336
|
@Option({
|
|
203
337
|
displayName: "Migrate spread options",
|
|
204
|
-
description: "
|
|
205
|
-
"spread through to the new options object
|
|
206
|
-
"
|
|
207
|
-
"`https-proxy-agent` would apply to the
|
|
208
|
-
"
|
|
209
|
-
"
|
|
338
|
+
description: "Rewrite options that spread another object, as in `{...shared, proxy: url}`, to the " +
|
|
339
|
+
"two-argument form, passing the spread through to the new options object, instead of calling the " +
|
|
340
|
+
"adapter. Left off by default: the spread may itself carry a `proxyRequestOptions`, which has to be " +
|
|
341
|
+
"flattened, or a TLS option for the destination handshake, which `https-proxy-agent` would apply to the " +
|
|
342
|
+
"proxy connection instead. A spread that resolves to a literal of plain agent options in the same file " +
|
|
343
|
+
"takes the two-argument form either way, since there it can be checked rather than assumed.",
|
|
210
344
|
required: false
|
|
211
345
|
})
|
|
212
346
|
migrateSpreadOptions?: boolean;
|
|
@@ -218,82 +352,114 @@ export class ReplaceHpagent extends Recipe {
|
|
|
218
352
|
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
219
353
|
const migrateSpreadOptions = this.migrateSpreadOptions ?? false;
|
|
220
354
|
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
355
|
+
private found: HpagentBindings = emptyHpagentBindings();
|
|
221
356
|
private bindings: ModuleBindings = new Map();
|
|
222
|
-
/** Bindings
|
|
357
|
+
/** Bindings with no site left behind, so the import can move with them. */
|
|
223
358
|
private migrated = new Set<string>();
|
|
224
359
|
/** Bindings with a site left behind, which keeps their hpagent import in place. */
|
|
225
360
|
private stranded = new Set<string>();
|
|
226
361
|
/** What each binding is called once migrated, which the rewritten call sites have to use. */
|
|
227
362
|
private targetLocals = new Map<string, string>();
|
|
228
|
-
/** Bindings a `require` introduces, which `maybeRebind` leaves alone; this visitor moves them itself. */
|
|
229
|
-
private requireBound = new Set<string>();
|
|
230
363
|
/** Set for the survey pass, which only records what can move and changes nothing. */
|
|
231
364
|
private surveying = false;
|
|
232
365
|
/** Class bindings folded into another one, whose own binding is dropped once its sites point at that. */
|
|
233
366
|
private merged = new Set<string>();
|
|
234
367
|
/** Bindings that share one name after the move, and so move together or not at all. */
|
|
235
368
|
private folds: string[][] = [];
|
|
236
|
-
/** Bindings
|
|
237
|
-
private
|
|
369
|
+
/** Bindings with a construction the adapter takes, any of which can name the class for it. */
|
|
370
|
+
private adapterLocals = new Set<string>();
|
|
371
|
+
/** Bindings something still names as a value; one named only in types is unbound rather than moved. */
|
|
372
|
+
private referenced = new Set<string>();
|
|
373
|
+
/** The file's own bindings of the whole `http` and `https` modules, to spell a type through. */
|
|
374
|
+
private builtinLocals = new Map<string, string>();
|
|
375
|
+
/** Type arms this rewrote, which a union may drop as duplicates of another arm. */
|
|
376
|
+
private producedTypes = new Set<string>();
|
|
377
|
+
private adapterName = ADAPTER_FUNCTION;
|
|
378
|
+
private typeName = ADAPTER_TYPE;
|
|
379
|
+
private requestTypeName = REQUEST_TYPE;
|
|
380
|
+
private adapterNeeded = false;
|
|
381
|
+
private typeNeeded = false;
|
|
382
|
+
private requestTypeNeeded = false;
|
|
383
|
+
/** Whole-module imports rewritten where they stand, which the rebinding after the visit leaves alone. */
|
|
384
|
+
private movedInPlace = new Set<string>();
|
|
238
385
|
|
|
239
386
|
protected override async visitJsCompilationUnit(cu: JS.CompilationUnit, p: ExecutionContext): Promise<J | undefined> {
|
|
240
|
-
|
|
387
|
+
// Types declared for hpagent go with it; without the module there is nothing for them to describe.
|
|
388
|
+
if (!this.surveying && cu.statements.some(padded => isAmbientHpagent(padded.element))) {
|
|
389
|
+
cu = {...cu, statements: cu.statements.filter(padded => !isAmbientHpagent(padded.element))};
|
|
390
|
+
}
|
|
391
|
+
this.found = hpagentBindings(cu);
|
|
392
|
+
this.bindings = this.found.bindings;
|
|
241
393
|
this.migrated = new Set();
|
|
242
394
|
this.stranded = new Set();
|
|
243
395
|
this.targetLocals = new Map();
|
|
244
396
|
this.merged = new Set();
|
|
245
397
|
this.folds = [];
|
|
246
|
-
this.
|
|
247
|
-
this.
|
|
398
|
+
this.adapterLocals = new Set();
|
|
399
|
+
this.referenced = new Set();
|
|
400
|
+
this.producedTypes = new Set();
|
|
401
|
+
this.builtinLocals = new Map();
|
|
402
|
+
this.adapterNeeded = false;
|
|
403
|
+
this.typeNeeded = false;
|
|
404
|
+
this.requestTypeNeeded = false;
|
|
405
|
+
this.movedInPlace = new Set();
|
|
248
406
|
if (this.bindings.size === 0) return cu;
|
|
249
407
|
this.planNames(cu);
|
|
408
|
+
for (const module of ['http', 'https']) {
|
|
409
|
+
const whole = [...bindingsOf(cu, new Set([module, `node:${module}`]))].find(([, member]) => member === undefined);
|
|
410
|
+
if (whole) this.builtinLocals.set(module, whole[0]);
|
|
411
|
+
}
|
|
412
|
+
const used = namesUsedIn(cu);
|
|
413
|
+
this.adapterName = deconflict(ADAPTER_FUNCTION, name => used.has(name));
|
|
414
|
+
this.typeName = deconflict(ADAPTER_TYPE, name => used.has(name));
|
|
415
|
+
this.requestTypeName = deconflict(REQUEST_TYPE, name => used.has(name));
|
|
250
416
|
|
|
251
|
-
// A binding moves only if every one of its
|
|
417
|
+
// A binding moves only if every one of its sites can: one left behind keeps hpagent's
|
|
252
418
|
// options-object constructor, so rewriting any site of theirs would break it. Hence a survey
|
|
253
419
|
// pass first, whose result is discarded, and only then the pass that edits.
|
|
254
420
|
this.surveying = true;
|
|
255
421
|
await super.visitJsCompilationUnit(cu, p);
|
|
256
422
|
this.surveying = false;
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
for (const local of this.
|
|
423
|
+
// Every site of a binding not stranded can move, including one with no construction at all,
|
|
424
|
+
// named only as a type or by `instanceof`, or not used anywhere.
|
|
425
|
+
for (const local of this.targetLocals.keys()) this.migrated.add(local);
|
|
260
426
|
// Folded bindings end up under one name, so every one of them has to move. One left
|
|
261
|
-
// behind
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
// fold leaves each binding as it was.
|
|
427
|
+
// behind keeps its specifier on hpagent, and the shared name would then denote hpagent's
|
|
428
|
+
// class while the rewritten calls pass the proxy positionally. Refusing the whole fold
|
|
429
|
+
// leaves each binding as it was.
|
|
265
430
|
for (const fold of this.folds) {
|
|
266
|
-
|
|
431
|
+
// A use of any member is a use of the binding they all end up as.
|
|
432
|
+
if (fold.some(local => this.referenced.has(local))) for (const local of fold) this.referenced.add(local);
|
|
433
|
+
if (fold.some(local => this.stranded.has(local))) {
|
|
267
434
|
for (const local of fold) this.stranded.add(local);
|
|
268
435
|
}
|
|
269
436
|
}
|
|
270
|
-
|
|
437
|
+
const optionTypes = [...this.bindings.values()].some(member => member !== undefined && isOptionType(member));
|
|
438
|
+
if ([...this.migrated].every(local => this.stranded.has(local)) && !optionTypes) return cu;
|
|
271
439
|
|
|
272
|
-
|
|
440
|
+
let result = await super.visitJsCompilationUnit(cu, p) as JS.CompilationUnit;
|
|
441
|
+
if (this.adapterNeeded || this.typeNeeded || this.requestTypeNeeded) result = await this.withAdapter(result, p);
|
|
273
442
|
this.rebindMigrated();
|
|
274
|
-
return
|
|
443
|
+
return result;
|
|
275
444
|
}
|
|
276
445
|
|
|
277
446
|
/**
|
|
278
|
-
* What each hpagent binding will be called after the move. An unaliased class binding takes
|
|
279
|
-
* new module's own name for it; an alias, and a binding
|
|
280
|
-
* the
|
|
447
|
+
* What each hpagent class binding will be called after the move. An unaliased class binding takes
|
|
448
|
+
* the new module's own name for it, deconflicted against the file's own; an alias, and a binding
|
|
449
|
+
* of the whole module, keep theirs.
|
|
281
450
|
*/
|
|
282
451
|
private planNames(cu: JS.CompilationUnit): void {
|
|
283
|
-
const
|
|
452
|
+
const used = namesUsedIn(cu);
|
|
284
453
|
// Both agent classes become the one class, so a file binding both folds them together
|
|
285
454
|
// rather than colliding: `https-proxy-agent` tunnels either way and picks TLS per request.
|
|
286
|
-
const
|
|
287
|
-
const foldable = new Set(
|
|
455
|
+
const movable = [...this.bindings].filter(([, member]) => member === undefined || HPAGENT_CLASSES.has(member));
|
|
456
|
+
const foldable = new Set(movable.filter(([local, member]) => local === member).map(([local]) => local));
|
|
288
457
|
|
|
458
|
+
const targetClass = deconflict(TARGET_CLASS, name => used.has(name) && !foldable.has(name));
|
|
289
459
|
const targets = new Map<string, string[]>();
|
|
290
|
-
for (const [local, member] of
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (target !== local && declared.has(target) && !foldable.has(target)) {
|
|
294
|
-
this.stranded.add(local);
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
460
|
+
for (const [local, member] of movable) {
|
|
461
|
+
// A name assigned into a `let` keeps it, since the declaration and every use would otherwise have to follow.
|
|
462
|
+
const target = member !== undefined && local === member && !this.found.assigned.has(local) ? targetClass : local;
|
|
297
463
|
this.targetLocals.set(local, target);
|
|
298
464
|
targets.set(target, [...(targets.get(target) ?? []), local]);
|
|
299
465
|
}
|
|
@@ -314,27 +480,86 @@ export class ReplaceHpagent extends Recipe {
|
|
|
314
480
|
/**
|
|
315
481
|
* Moves the imports of bindings whose every site was rewritten. A named binding of either hpagent
|
|
316
482
|
* class becomes `https-proxy-agent`'s `HttpsProxyAgent`; a whole-module binding moves module and
|
|
317
|
-
* keeps its name, since the new module exports that same class name.
|
|
483
|
+
* keeps its name, since the new module exports that same class name. hpagent's option types are
|
|
484
|
+
* dropped once the adapter's type has taken every reference to them.
|
|
318
485
|
*/
|
|
319
486
|
private rebindMigrated(): void {
|
|
320
487
|
for (const local of this.migrated) {
|
|
321
|
-
if (this.stranded.has(local) || this.requireBound.has(local)) continue;
|
|
488
|
+
if (this.stranded.has(local) || this.found.requireBound.has(local) || this.movedInPlace.has(local)) continue;
|
|
322
489
|
const member = this.bindings.get(local);
|
|
323
490
|
if (this.merged.has(local)) {
|
|
324
491
|
// Its sites now name the surviving binding, so this specifier is unused.
|
|
325
492
|
maybeUnbind(this, {module: 'hpagent', member});
|
|
326
493
|
continue;
|
|
327
494
|
}
|
|
495
|
+
if (!this.referenced.has(local)) {
|
|
496
|
+
// Named only in types, which are builtin agents now.
|
|
497
|
+
maybeUnbind(this, member === undefined ? {module: 'hpagent'} : {module: 'hpagent', member});
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
328
500
|
if (member === undefined) {
|
|
329
501
|
maybeRebind(this, {from: {module: 'hpagent'}, to: {module: TARGET_MODULE}});
|
|
330
502
|
} else {
|
|
503
|
+
const target = this.targetLocals.get(local)!;
|
|
331
504
|
maybeRebind(this, {
|
|
332
505
|
from: {module: 'hpagent', member},
|
|
333
506
|
// An unaliased `HttpProxyAgent` takes the new member's name; an alias the file chose survives.
|
|
334
|
-
to: {module: TARGET_MODULE, member: TARGET_CLASS, ...(
|
|
507
|
+
to: {module: TARGET_MODULE, member: TARGET_CLASS, ...(target === TARGET_CLASS ? {} : {alias: target})}
|
|
335
508
|
});
|
|
336
509
|
}
|
|
337
510
|
}
|
|
511
|
+
for (const [local, member] of this.bindings) {
|
|
512
|
+
if (member !== undefined && isOptionType(member) && !this.found.requireBound.has(local)) {
|
|
513
|
+
maybeUnbind(this, {module: 'hpagent', member});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* `import hpagent from 'hpagent'` or `import * as hpagent from 'hpagent'`, rewritten where it stands to
|
|
520
|
+
* `import * as hpagent from 'https-proxy-agent'`: the new module has no default export, so the whole
|
|
521
|
+
* module is its namespace, and rewriting in place keeps the import where and how the file wrote it.
|
|
522
|
+
*/
|
|
523
|
+
protected override async visitImportDeclaration(jsImport: JS.Import, p: ExecutionContext): Promise<J | undefined> {
|
|
524
|
+
const imp = await super.visitImportDeclaration(jsImport, p) as JS.Import;
|
|
525
|
+
const clause = imp?.importClause;
|
|
526
|
+
if (this.surveying || imp?.kind !== JS.Kind.Import || !clause || clause.typeOnly ||
|
|
527
|
+
!HPAGENT_MODULES.has(importModule(imp) ?? '')) {
|
|
528
|
+
return imp;
|
|
529
|
+
}
|
|
530
|
+
const named = clause.namedBindings;
|
|
531
|
+
const namespace = named?.kind === JS.Kind.Alias ? named as JS.Alias : undefined;
|
|
532
|
+
const defaultName = !named ? clause.name?.element : undefined;
|
|
533
|
+
const local = defaultName ?? (!clause.name && namespace && isIdentifier(namespace.alias) ? namespace.alias : undefined);
|
|
534
|
+
if (!local || !this.migrated.has(local.simpleName) || this.stranded.has(local.simpleName) ||
|
|
535
|
+
!this.referenced.has(local.simpleName)) {
|
|
536
|
+
return imp;
|
|
537
|
+
}
|
|
538
|
+
this.movedInPlace.add(local.simpleName);
|
|
539
|
+
const moved = {...imp, moduleSpecifier: {...imp.moduleSpecifier!, element: retargeted(imp.moduleSpecifier!.element as J.Literal)}};
|
|
540
|
+
if (!defaultName) return moved;
|
|
541
|
+
const star = {kind: J.Kind.Identifier, id: randomId(), prefix: emptySpace, markers: emptyMarkers, annotations: [], simpleName: '*'};
|
|
542
|
+
const alias = {
|
|
543
|
+
kind: JS.Kind.Alias, id: randomId(), prefix: defaultName.prefix, markers: emptyMarkers,
|
|
544
|
+
propertyName: rightPadded(star, space(' ')), alias: {...defaultName, prefix: space(' ')}
|
|
545
|
+
};
|
|
546
|
+
return {
|
|
547
|
+
...moved,
|
|
548
|
+
importClause: {...clause, name: undefined, namedBindings: alias},
|
|
549
|
+
// The space before `from` sat after the default name.
|
|
550
|
+
moduleSpecifier: {...moved.moduleSpecifier, before: clause.name!.after}
|
|
551
|
+
} as unknown as JS.Import;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Whether `name` here is a binding of a whole hpagent module rather than something nearer. */
|
|
555
|
+
private isWholeModule(name: string): boolean {
|
|
556
|
+
return this.bindings.has(name) && this.bindings.get(name) === undefined && resolvesToBinding(this.cursor, name, this.found);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** The hpagent class binding `name` is here, or undefined. */
|
|
560
|
+
private isClass(name: string): boolean {
|
|
561
|
+
const member = this.bindings.get(name);
|
|
562
|
+
return member !== undefined && HPAGENT_CLASSES.has(member) && resolvesToBinding(this.cursor, name, this.found);
|
|
338
563
|
}
|
|
339
564
|
|
|
340
565
|
override async visitNewClass(newClass: J.NewClass, p: ExecutionContext): Promise<J | undefined> {
|
|
@@ -344,90 +569,255 @@ export class ReplaceHpagent extends Recipe {
|
|
|
344
569
|
if (!site) return n;
|
|
345
570
|
|
|
346
571
|
const rewrite = this.rewriteOf(n);
|
|
572
|
+
const adaptable = !rewrite && constructionArgument(n) !== undefined;
|
|
347
573
|
if (this.surveying) {
|
|
348
|
-
(rewrite ? this.migrated : this.stranded).add(site.local);
|
|
574
|
+
(rewrite || adaptable ? this.migrated : this.stranded).add(site.local);
|
|
575
|
+
if (rewrite || adaptable) this.referenced.add(site.local);
|
|
576
|
+
if (adaptable) this.adapterLocals.add(site.local);
|
|
349
577
|
return n;
|
|
350
578
|
}
|
|
351
|
-
if (
|
|
352
|
-
const migrated = this.applyRewrite(n, site, rewrite);
|
|
579
|
+
if (this.stranded.has(site.local) || !(rewrite || adaptable)) return n;
|
|
580
|
+
const migrated = rewrite ? this.applyRewrite(n, site, rewrite) : this.adapterCall(n);
|
|
581
|
+
const proxy = rewrite ? rewrite.proxy : literalProxy(constructionArgument(n)!);
|
|
353
582
|
// Constructing with a missing proxy used to succeed and now throws, as when a module builds its
|
|
354
583
|
// agent from an environment variable a test run doesn't set, so the construction is skipped then.
|
|
355
|
-
return isJavaScript(compilationUnitOf(this)!.sourcePath) && !alwaysDefined(
|
|
356
|
-
? whenDefined(
|
|
584
|
+
return proxy && isJavaScript(compilationUnitOf(this)!.sourcePath) && !alwaysDefined(proxy) && readsOnly(proxy)
|
|
585
|
+
? whenDefined(proxy, migrated)
|
|
357
586
|
: migrated;
|
|
358
587
|
}
|
|
359
588
|
|
|
589
|
+
/** `createProxyAgent(options)` in place of `new HttpsProxyAgent(options)`, with the arguments as written. */
|
|
590
|
+
private adapterCall(n: J.NewClass): J.MethodInvocation {
|
|
591
|
+
this.adapterNeeded = true;
|
|
592
|
+
this.typeNeeded = true;
|
|
593
|
+
return {
|
|
594
|
+
kind: J.Kind.MethodInvocation,
|
|
595
|
+
id: randomId(),
|
|
596
|
+
prefix: n.prefix,
|
|
597
|
+
markers: emptyMarkers,
|
|
598
|
+
name: {
|
|
599
|
+
kind: J.Kind.Identifier, id: randomId(), prefix: emptySpace, markers: emptyMarkers,
|
|
600
|
+
annotations: [], simpleName: this.adapterName
|
|
601
|
+
} as J.Identifier,
|
|
602
|
+
arguments: n.arguments,
|
|
603
|
+
} as unknown as J.MethodInvocation;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** `x instanceof HttpProxyAgent`, which tests for the one class both hpagent classes became. */
|
|
607
|
+
override async visitInstanceOf(instanceOf: J.InstanceOf, p: ExecutionContext): Promise<J | undefined> {
|
|
608
|
+
const i = await super.visitInstanceOf(instanceOf, p) as J.InstanceOf;
|
|
609
|
+
if (i?.kind !== J.Kind.InstanceOf) return i;
|
|
610
|
+
const site = this.hpagentSite(i.class);
|
|
611
|
+
if (!site) return i;
|
|
612
|
+
if (this.surveying) {
|
|
613
|
+
this.referenced.add(site.local);
|
|
614
|
+
return i;
|
|
615
|
+
}
|
|
616
|
+
return this.stranded.has(site.local) ? i : {...i, class: this.migratedCallee(i.class, site)} as J.InstanceOf;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
override async visitIdentifier(identifier: J.Identifier, p: ExecutionContext): Promise<J | undefined> {
|
|
620
|
+
const id = await super.visitIdentifier(identifier, p) as J.Identifier;
|
|
621
|
+
if (id?.kind !== J.Kind.Identifier || !this.bindings.has(id.simpleName)) return id;
|
|
622
|
+
if (this.found.assignmentTargets.has(identifier.id)) return id;
|
|
623
|
+
if (!isReference(this.cursor, identifier) || !resolvesToBinding(this.cursor, id.simpleName, this.found)) return id;
|
|
624
|
+
const member = this.bindings.get(id.simpleName);
|
|
625
|
+
if (member !== undefined && isOptionType(member)) return this.optionType(id);
|
|
626
|
+
if (member !== undefined && !HPAGENT_CLASSES.has(member)) return id;
|
|
627
|
+
if (member !== undefined && (inTypePosition(this.cursor) || this.found.typeOnly.has(id.simpleName))) {
|
|
628
|
+
return this.typeReference(id, id.simpleName);
|
|
629
|
+
}
|
|
630
|
+
if (member === undefined && this.isSpiedModule(id)) {
|
|
631
|
+
if (this.surveying) this.referenced.add(id.simpleName);
|
|
632
|
+
return id;
|
|
633
|
+
}
|
|
634
|
+
// A class or module passed around as a value is constructed somewhere this can't see.
|
|
635
|
+
if (this.surveying && isValueUse(this.cursor)) this.stranded.add(id.simpleName);
|
|
636
|
+
return id;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** The adapter's type for one of hpagent's option types, at a site that names it. */
|
|
640
|
+
private optionType(type: J): J {
|
|
641
|
+
if (this.surveying) return type;
|
|
642
|
+
const request = type.kind === J.Kind.FieldAccess
|
|
643
|
+
? (type as J.FieldAccess).name.element.simpleName === 'ProxyAgentRequestOptions'
|
|
644
|
+
: this.bindings.get((type as J.Identifier).simpleName) === 'ProxyAgentRequestOptions';
|
|
645
|
+
if (request) this.requestTypeNeeded = true;
|
|
646
|
+
else this.typeNeeded = true;
|
|
647
|
+
return {
|
|
648
|
+
kind: J.Kind.Identifier, id: randomId(), prefix: type.prefix, markers: emptyMarkers,
|
|
649
|
+
annotations: [], simpleName: request ? this.requestTypeName : this.typeName
|
|
650
|
+
} as J.Identifier;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** Whether `id` is the module `jest.spyOn(module, 'HttpsProxyAgent')` reads a class off. */
|
|
654
|
+
private isSpiedModule(id: J.Identifier): boolean {
|
|
655
|
+
const parent = this.cursor.parentTree()?.value as J | undefined;
|
|
656
|
+
return parent?.kind === J.Kind.MethodInvocation && spiedClass(parent as J.MethodInvocation) !== undefined &&
|
|
657
|
+
(parent as J.MethodInvocation).arguments.elements[0]?.element === this.cursor.value;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/** `hpagent.HttpProxyAgent` or `hpagent.HttpsProxyAgentOptions` through a binding of the whole module. */
|
|
661
|
+
override async visitFieldAccess(fieldAccess: J.FieldAccess, p: ExecutionContext): Promise<J | undefined> {
|
|
662
|
+
const fa = await super.visitFieldAccess(fieldAccess, p) as J.FieldAccess;
|
|
663
|
+
if (fa?.kind !== J.Kind.FieldAccess || !isIdentifier(fa.target) || !this.isWholeModule(fa.target.simpleName)) return fa;
|
|
664
|
+
const local = fa.target.simpleName;
|
|
665
|
+
const name = fa.name.element.simpleName;
|
|
666
|
+
if (isOptionType(name) && inTypePosition(this.cursor)) return this.optionType(fa);
|
|
667
|
+
if (!HPAGENT_CLASSES.has(name)) return fa;
|
|
668
|
+
if (inTypePosition(this.cursor) || this.found.typeOnly.has(local)) return this.typeReference(fa, local);
|
|
669
|
+
if (this.surveying && isValueUse(this.cursor)) this.stranded.add(local);
|
|
670
|
+
return fa;
|
|
671
|
+
}
|
|
672
|
+
|
|
360
673
|
/**
|
|
361
|
-
*
|
|
362
|
-
*
|
|
674
|
+
* The builtin agent for a type naming an hpagent class, which is all hpagent declared it as. `typeof`
|
|
675
|
+
* names the constructor instead, and so becomes `https-proxy-agent`'s class.
|
|
676
|
+
*/
|
|
677
|
+
private typeReference(type: J.Identifier | J.FieldAccess, local: string): J {
|
|
678
|
+
const typeQuery = this.cursor.parentTree()?.value?.kind === JS.Kind.TypeQuery;
|
|
679
|
+
if (this.surveying) {
|
|
680
|
+
if (typeQuery) this.referenced.add(local);
|
|
681
|
+
return type;
|
|
682
|
+
}
|
|
683
|
+
if (this.stranded.has(local)) return type;
|
|
684
|
+
const memberAccess = type.kind === J.Kind.FieldAccess ? type : undefined;
|
|
685
|
+
if (typeQuery) return this.migratedCallee(type, {local, memberAccess});
|
|
686
|
+
const member = memberAccess ? memberAccess.name.element.simpleName : this.bindings.get(local)!;
|
|
687
|
+
const module = builtinAgentModule(member);
|
|
688
|
+
const agent = builtinAgentType(this.builtinLocals.get(module), builtinSpecifier(compilationUnitOf(this)!, module), type.prefix);
|
|
689
|
+
this.producedTypes.add(agent.id);
|
|
690
|
+
return agent;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* A union of types, whose hpagent arms have already become builtin agents, which may now repeat an arm
|
|
695
|
+
* the union already had, as `http.Agent | HttpProxyAgent` does. A repeat this produced is dropped.
|
|
363
696
|
*/
|
|
364
697
|
protected override async visitUnion(union: JS.Union, p: ExecutionContext): Promise<J | undefined> {
|
|
365
698
|
const u = await super.visitUnion(union, p) as JS.Union;
|
|
366
|
-
if (u?.kind !== JS.Kind.Union) return u;
|
|
367
|
-
let changed = false;
|
|
699
|
+
if (u?.kind !== JS.Kind.Union || this.surveying) return u;
|
|
368
700
|
const seen = new Set<string>();
|
|
369
701
|
const types: J.RightPadded<any>[] = [];
|
|
370
702
|
for (const padded of u.types) {
|
|
371
|
-
const
|
|
372
|
-
if (replacement) changed = true;
|
|
373
|
-
const element = replacement ?? padded.element;
|
|
374
|
-
const key = typeKey(element);
|
|
703
|
+
const key = typeKey(padded.element);
|
|
375
704
|
// A duplicate arm says nothing the first one didn't.
|
|
376
|
-
if (key !== undefined && seen.has(key))
|
|
377
|
-
changed = true;
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
705
|
+
if (key !== undefined && this.producedTypes.has(padded.element.id) && seen.has(key)) continue;
|
|
380
706
|
if (key !== undefined) seen.add(key);
|
|
381
|
-
types.push(
|
|
707
|
+
types.push(padded);
|
|
382
708
|
}
|
|
383
|
-
if (
|
|
709
|
+
if (types.length === u.types.length) return u;
|
|
384
710
|
// The space before `|` lives on each arm's right padding, so dropping the last arm
|
|
385
711
|
// has to hand its padding to whichever arm is last now.
|
|
386
712
|
types[types.length - 1] = {...types[types.length - 1], after: u.types[u.types.length - 1].after};
|
|
387
713
|
return {...u, types} as JS.Union;
|
|
388
714
|
}
|
|
389
715
|
|
|
390
|
-
/**
|
|
391
|
-
protected override async
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
716
|
+
/** `ConstructorParameters<typeof HttpProxyAgent>[0]`, hpagent's options object, which the adapter now takes. */
|
|
717
|
+
protected override async visitIndexedAccessType(indexedAccessType: JS.IndexedAccessType, p: ExecutionContext): Promise<J | undefined> {
|
|
718
|
+
const operand = constructorOptionsOf(indexedAccessType);
|
|
719
|
+
const site = operand && this.hpagentSite(operand);
|
|
720
|
+
if (!site || this.surveying || this.stranded.has(site.local)) return super.visitIndexedAccessType(indexedAccessType, p);
|
|
721
|
+
this.typeNeeded = true;
|
|
722
|
+
return {
|
|
723
|
+
kind: J.Kind.Identifier, id: randomId(), prefix: indexedAccessType.prefix, markers: emptyMarkers,
|
|
724
|
+
annotations: [], simpleName: this.typeName
|
|
725
|
+
} as J.Identifier;
|
|
396
726
|
}
|
|
397
727
|
|
|
398
|
-
/** `HttpsProxyAgent
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
return
|
|
728
|
+
/** `({HttpsProxyAgent} = require('hpagent'))` into a `let`, once every name it assigns has moved. */
|
|
729
|
+
override async visitAssignment(assignment: J.Assignment, p: ExecutionContext): Promise<J | undefined> {
|
|
730
|
+
const a = await super.visitAssignment(assignment, p) as J.Assignment;
|
|
731
|
+
if (this.surveying || a?.kind !== J.Kind.Assignment || !isHpagentRequire(a.assignment.element) ||
|
|
732
|
+
!isObjectLiteral(a.variable)) {
|
|
733
|
+
return a;
|
|
404
734
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
735
|
+
const pattern = a.variable;
|
|
736
|
+
const members = objectMembers(pattern) as JS.PropertyAssignment[];
|
|
737
|
+
const locals = members.map(m => (m.initializer ?? m.name.element) as J.Identifier);
|
|
738
|
+
if (!locals.every(l => this.found.assigned.has(l.simpleName) && !this.stranded.has(l.simpleName))) return a;
|
|
739
|
+
const statements = pattern.body!.statements.map(padded => {
|
|
740
|
+
const prop = padded.element as JS.PropertyAssignment;
|
|
741
|
+
if (prop.kind !== JS.Kind.PropertyAssignment || propertyName(prop) === TARGET_CLASS) return padded;
|
|
742
|
+
const name = prop.name.element as J.Identifier;
|
|
743
|
+
// `{HttpProxyAgent}` keeps its local, so it reads the new class off as `{HttpsProxyAgent: HttpProxyAgent}`.
|
|
744
|
+
const element = prop.initializer
|
|
745
|
+
? {...prop, name: {...prop.name, element: {...name, simpleName: TARGET_CLASS}}}
|
|
746
|
+
: {
|
|
747
|
+
...prop,
|
|
748
|
+
name: {...prop.name, element: {...name, id: randomId(), simpleName: TARGET_CLASS}},
|
|
749
|
+
assigmentToken: JS.PropertyAssignment.Token.Colon,
|
|
750
|
+
initializer: {...name, prefix: space(' ')}
|
|
751
|
+
};
|
|
752
|
+
return {...padded, element};
|
|
753
|
+
});
|
|
754
|
+
return {
|
|
755
|
+
...a,
|
|
756
|
+
variable: {...pattern, body: {...pattern.body!, statements}},
|
|
757
|
+
assignment: {...a.assignment, element: movedRequireCall(a.assignment.element)}
|
|
758
|
+
} as J.Assignment;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* `jest.mock('hpagent')` and `vi.mock('hpagent', ...)` replace the module the code under test now imports
|
|
763
|
+
* instead, once this file's own hpagent bindings have all moved; `jest.spyOn(hpagent, 'HttpProxyAgent')`
|
|
764
|
+
* spies on the one class both became.
|
|
765
|
+
*/
|
|
766
|
+
override async visitMethodInvocation(method: J.MethodInvocation, p: ExecutionContext): Promise<J | undefined> {
|
|
767
|
+
const m = await super.visitMethodInvocation(method, p) as J.MethodInvocation;
|
|
768
|
+
if (this.surveying || m?.kind !== J.Kind.MethodInvocation) return m;
|
|
769
|
+
const args = m.arguments.elements;
|
|
770
|
+
const first = args[0]?.element;
|
|
771
|
+
if (isMockCall(m) && first?.kind === J.Kind.Literal && HPAGENT_MODULES.has(stringLiteral(first) ?? '') &&
|
|
772
|
+
[...this.bindings.keys()].every(local => !this.stranded.has(local))) {
|
|
773
|
+
return {...m, arguments: {...m.arguments, elements: [{...args[0], element: retargeted(first as J.Literal)}, ...args.slice(1)]}} as J.MethodInvocation;
|
|
774
|
+
}
|
|
775
|
+
const spied = spiedClass(m);
|
|
776
|
+
if (spied && spied !== TARGET_CLASS && first && isIdentifier(first) && this.bindings.has(first.simpleName) &&
|
|
777
|
+
this.bindings.get(first.simpleName) === undefined && !this.stranded.has(first.simpleName)) {
|
|
778
|
+
const literal = args[1].element as J.Literal;
|
|
779
|
+
const quote = (literal.valueSource ?? "'").charAt(0);
|
|
780
|
+
const renamed = {...literal, value: TARGET_CLASS, valueSource: `${quote}${TARGET_CLASS}${quote}`};
|
|
781
|
+
return {...m, arguments: {...m.arguments, elements: [args[0], {...args[1], element: renamed}, ...args.slice(2)]}} as J.MethodInvocation;
|
|
408
782
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
783
|
+
return m;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** A `require` of hpagent, at the top level or in a block, once every name it binds has moved. */
|
|
787
|
+
override async visitVariableDeclarations(varDecls: J.VariableDeclarations, p: ExecutionContext): Promise<J | undefined> {
|
|
788
|
+
const v = await super.visitVariableDeclarations(varDecls, p) as J.VariableDeclarations;
|
|
789
|
+
if (this.surveying || v?.kind !== J.Kind.VariableDeclarations) return v;
|
|
790
|
+
let changed = false;
|
|
791
|
+
const variables = v.variables.map(padded => {
|
|
792
|
+
const variable = padded.element;
|
|
793
|
+
const initializer = variable.initializer?.element;
|
|
794
|
+
if (!isHpagentRequire(initializer)) return padded;
|
|
795
|
+
const pattern = variable.name as J;
|
|
796
|
+
const names = isIdentifier(pattern) ? [pattern.simpleName] : bindingNames(pattern).map(b => b.name);
|
|
797
|
+
// One specifier serves every name the declaration binds, so it moves once they all have.
|
|
798
|
+
if (names.length === 0 || names.some(n => !this.migrated.has(n) || this.stranded.has(n))) return padded;
|
|
799
|
+
changed = true;
|
|
800
|
+
return {
|
|
801
|
+
...padded,
|
|
802
|
+
element: {
|
|
803
|
+
...variable,
|
|
804
|
+
name: renameBindings(pattern, this.targetLocals, this.merged),
|
|
805
|
+
initializer: {...variable.initializer!, element: movedRequireCall(initializer!)}
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
});
|
|
809
|
+
return changed ? {...v, variables} as J.VariableDeclarations : v;
|
|
412
810
|
}
|
|
413
811
|
|
|
414
|
-
/** The local binding a `new` site
|
|
812
|
+
/** The local binding a `new` or `instanceof` site names the class through, and the member it reads. */
|
|
415
813
|
private hpagentSite(clazz: J): {local: string; memberAccess?: J.FieldAccess} | undefined {
|
|
416
|
-
if (isIdentifier(clazz)) {
|
|
417
|
-
const member = this.bindings.get(clazz.simpleName);
|
|
418
|
-
return member !== undefined && HPAGENT_CLASSES.has(member) && isModuleScoped(this.cursor, clazz.simpleName)
|
|
419
|
-
? {local: clazz.simpleName}
|
|
420
|
-
: undefined;
|
|
421
|
-
}
|
|
814
|
+
if (isIdentifier(clazz)) return this.isClass(clazz.simpleName) ? {local: clazz.simpleName} : undefined;
|
|
422
815
|
if (clazz.kind !== J.Kind.FieldAccess) return undefined;
|
|
423
816
|
const access = clazz as J.FieldAccess;
|
|
424
817
|
const target = access.target;
|
|
425
818
|
if (!isIdentifier(target) || !HPAGENT_CLASSES.has(access.name.element.simpleName)) return undefined;
|
|
426
819
|
// Only a binding of the whole module has the classes as its members.
|
|
427
|
-
return this.
|
|
428
|
-
isModuleScoped(this.cursor, target.simpleName)
|
|
429
|
-
? {local: target.simpleName, memberAccess: access}
|
|
430
|
-
: undefined;
|
|
820
|
+
return this.isWholeModule(target.simpleName) ? {local: target.simpleName, memberAccess: access} : undefined;
|
|
431
821
|
}
|
|
432
822
|
|
|
433
823
|
/** The migrated argument list, or undefined where the options can't be split into one. */
|
|
@@ -471,7 +861,7 @@ export class ReplaceHpagent extends Recipe {
|
|
|
471
861
|
}
|
|
472
862
|
}
|
|
473
863
|
if (!proxy) return undefined;
|
|
474
|
-
// A name given twice would change which value wins; leave that to
|
|
864
|
+
// A name given twice would change which value wins; leave that to the adapter.
|
|
475
865
|
const names = members
|
|
476
866
|
.filter(m => m.element.kind === JS.Kind.PropertyAssignment)
|
|
477
867
|
.map(m => propertyName(m.element as JS.PropertyAssignment));
|
|
@@ -479,53 +869,6 @@ export class ReplaceHpagent extends Recipe {
|
|
|
479
869
|
return {proxy, members, literal};
|
|
480
870
|
}
|
|
481
871
|
|
|
482
|
-
/**
|
|
483
|
-
* Moves the `require`s of bindings whose every site was rewritten: the specifier becomes the new
|
|
484
|
-
* module, and an unaliased `HttpProxyAgent` the new module's name for the class. A declaration is
|
|
485
|
-
* only touched once every name it binds has migrated, since one specifier serves them all.
|
|
486
|
-
*/
|
|
487
|
-
private rewriteRequires(cu: JS.CompilationUnit): JS.CompilationUnit {
|
|
488
|
-
if (this.requireBound.size === 0) return cu;
|
|
489
|
-
let changed = false;
|
|
490
|
-
const statements = cu.statements.map(padded => {
|
|
491
|
-
const names = requireBoundNames({...cu, statements: [padded]} as JS.CompilationUnit);
|
|
492
|
-
if (names.size === 0 || [...names].some(n => !this.migrated.has(n) || this.stranded.has(n))) return padded;
|
|
493
|
-
const moved = this.movedRequire(padded.element);
|
|
494
|
-
if (moved === padded.element) return padded;
|
|
495
|
-
changed = true;
|
|
496
|
-
return {...padded, element: moved};
|
|
497
|
-
});
|
|
498
|
-
return changed ? {...cu, statements} : cu;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/** `const {HttpProxyAgent} = require('hpagent')` with both the specifier and the member moved. */
|
|
502
|
-
private movedRequire(statement: J): J {
|
|
503
|
-
const decls = statement as J.VariableDeclarations;
|
|
504
|
-
if (decls?.kind !== J.Kind.VariableDeclarations) return statement;
|
|
505
|
-
const variables = decls.variables.map(padded => {
|
|
506
|
-
const variable = padded.element;
|
|
507
|
-
const initializer = variable.initializer?.element;
|
|
508
|
-
if (requiredModule(initializer) === undefined) return padded;
|
|
509
|
-
const call = initializer as J.MethodInvocation;
|
|
510
|
-
const specifier = call.arguments.elements[0];
|
|
511
|
-
const literal = specifier.element as J.Literal;
|
|
512
|
-
const quote = (literal.valueSource ?? "'").charAt(0);
|
|
513
|
-
const moved: J.Literal = {...literal, value: TARGET_MODULE, valueSource: `${quote}${TARGET_MODULE}${quote}`};
|
|
514
|
-
return {
|
|
515
|
-
...padded,
|
|
516
|
-
element: {
|
|
517
|
-
...variable,
|
|
518
|
-
name: renameBindings(variable.name as J, this.targetLocals),
|
|
519
|
-
initializer: {
|
|
520
|
-
...variable.initializer!,
|
|
521
|
-
element: {...call, arguments: {...call.arguments, elements: [{...specifier, element: moved}]}}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
};
|
|
525
|
-
});
|
|
526
|
-
return {...decls, variables} as J;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
872
|
/**
|
|
530
873
|
* `new X({proxy: p, ...rest})` as `new X(p, {...rest})`: the proxy expression takes the space the
|
|
531
874
|
* literal opened with, and the literal itself is reused for the second argument so its members keep
|
|
@@ -543,17 +886,28 @@ export class ReplaceHpagent extends Recipe {
|
|
|
543
886
|
return {...n, class: this.migratedCallee(n.class!, site), arguments: {...n.arguments, elements}} as J.NewClass;
|
|
544
887
|
}
|
|
545
888
|
|
|
546
|
-
/**
|
|
889
|
+
/**
|
|
890
|
+
* The options literal with `proxy` gone, its first surviving member opening the object as the old one
|
|
891
|
+
* did and its last closing it as the old one did, trailing comma included.
|
|
892
|
+
*/
|
|
547
893
|
private withMembers(rewrite: Rewrite): J.NewClass {
|
|
548
894
|
const original = paddedMembers(rewrite.literal);
|
|
549
|
-
const
|
|
895
|
+
const trailing = (padded: J.RightPadded<J>) => padded.markers.markers.some(m => m.kind === J.Markers.TrailingComma);
|
|
896
|
+
// A member flattened out of `proxyRequestOptions` may close that literal with a comma of its own.
|
|
897
|
+
const members = rewrite.members.map((m, i) => i < rewrite.members.length - 1 && trailing(m)
|
|
898
|
+
? {...m, markers: {...m.markers, markers: m.markers.markers.filter(k => k.kind !== J.Markers.TrailingComma)}}
|
|
899
|
+
: m);
|
|
550
900
|
if (members[0] !== original[0]) {
|
|
551
901
|
members[0] = {...members[0], element: {...members[0].element, prefix: original[0].element.prefix}};
|
|
552
902
|
}
|
|
903
|
+
const last = original[original.length - 1];
|
|
904
|
+
if (rewrite.members[rewrite.members.length - 1] !== last) {
|
|
905
|
+
members[members.length - 1] = {...members[members.length - 1], after: last.after, markers: last.markers};
|
|
906
|
+
}
|
|
553
907
|
return {...rewrite.literal, prefix: space(' '), body: {...rewrite.literal.body!, statements: members}} as J.NewClass;
|
|
554
908
|
}
|
|
555
909
|
|
|
556
|
-
/** The
|
|
910
|
+
/** The class's name after the move: the binding's new local name, or the new class on a namespace. */
|
|
557
911
|
private migratedCallee(clazz: J, site: {local: string; memberAccess?: J.FieldAccess}): any {
|
|
558
912
|
if (site.memberAccess) {
|
|
559
913
|
const access = site.memberAccess;
|
|
@@ -562,6 +916,43 @@ export class ReplaceHpagent extends Recipe {
|
|
|
562
916
|
const local = this.targetLocals.get(site.local) ?? site.local;
|
|
563
917
|
return {...(clazz as J.Identifier), simpleName: local};
|
|
564
918
|
}
|
|
919
|
+
|
|
920
|
+
/** How the adapter names `https-proxy-agent`'s class: through a binding one of its call sites moved. */
|
|
921
|
+
private agentClass(): string {
|
|
922
|
+
const local = [...this.adapterLocals].find(l => !this.stranded.has(l))!;
|
|
923
|
+
return this.bindings.get(local) === undefined
|
|
924
|
+
? `${local}.${TARGET_CLASS}`
|
|
925
|
+
: this.targetLocals.get(local) ?? local;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/** Appends the adapter and its options type, after the statements that use them. */
|
|
929
|
+
private async withAdapter(cu: JS.CompilationUnit, p: ExecutionContext): Promise<JS.CompilationUnit> {
|
|
930
|
+
const typeScript = !isJavaScript(cu.sourcePath);
|
|
931
|
+
const style = detectCodeStyle(await TreePrinters.print(cu));
|
|
932
|
+
const sources: GeneratedDeclaration[] = [];
|
|
933
|
+
if (this.adapterNeeded) sources.push(adapterSource(this.adapterName, this.typeName, this.agentClass(), typeScript, style));
|
|
934
|
+
if (typeScript) {
|
|
935
|
+
sources.push(...adapterTypeSource(this.typeNeeded ? this.typeName : undefined,
|
|
936
|
+
this.requestTypeNeeded ? this.requestTypeName : undefined, module => builtinSpecifier(cu, module), style));
|
|
937
|
+
}
|
|
938
|
+
// Where the project formats with Prettier, its own configuration lays the declarations out.
|
|
939
|
+
const prettier = findMarker(cu, StyleKind.PrettierStyle) !== undefined;
|
|
940
|
+
let result = cu;
|
|
941
|
+
for (const {doc, code} of sources) {
|
|
942
|
+
// Parsed rather than templated, so the code keeps the layout written for this file's style.
|
|
943
|
+
const parsed = await new JavaScriptParser().parseOnly({
|
|
944
|
+
text: `/*${doc}*/\n${code}\n`, sourcePath: typeScript ? 'adapter.ts' : 'adapter.js'
|
|
945
|
+
}) as JS.CompilationUnit;
|
|
946
|
+
if (parsed.kind !== JS.Kind.CompilationUnit || parsed.statements.length !== 1) continue;
|
|
947
|
+
const statement = parsed.statements[0];
|
|
948
|
+
const parent = new Cursor(result, this.cursor);
|
|
949
|
+
// The parser files a leading comment under the compilation unit rather than the statement.
|
|
950
|
+
let declaration = {...statement.element, prefix: {...parsed.prefix, whitespace: '\n\n'}} as J;
|
|
951
|
+
if (prettier) declaration = await autoFormat(declaration, p, undefined, parent);
|
|
952
|
+
result = {...result, statements: [...result.statements, {...statement, element: declaration}]} as JS.CompilationUnit;
|
|
953
|
+
}
|
|
954
|
+
return result;
|
|
955
|
+
}
|
|
565
956
|
};
|
|
566
957
|
}
|
|
567
958
|
}
|