@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.
Files changed (60) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +2 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/migrate/code-style.d.ts +14 -0
  5. package/dist/migrate/code-style.d.ts.map +1 -0
  6. package/dist/migrate/code-style.js +70 -0
  7. package/dist/migrate/code-style.js.map +1 -0
  8. package/dist/migrate/hpagent/find-hpagent-manual-migrations.d.ts.map +1 -1
  9. package/dist/migrate/hpagent/find-hpagent-manual-migrations.js +36 -133
  10. package/dist/migrate/hpagent/find-hpagent-manual-migrations.js.map +1 -1
  11. package/dist/migrate/hpagent/hpagent-support.d.ts +36 -15
  12. package/dist/migrate/hpagent/hpagent-support.d.ts.map +1 -1
  13. package/dist/migrate/hpagent/hpagent-support.js +294 -55
  14. package/dist/migrate/hpagent/hpagent-support.js.map +1 -1
  15. package/dist/migrate/hpagent/replace-hpagent.d.ts.map +1 -1
  16. package/dist/migrate/hpagent/replace-hpagent.js +510 -153
  17. package/dist/migrate/hpagent/replace-hpagent.js.map +1 -1
  18. package/dist/migrate/source-map-support/enable-source-maps-at-launch.d.ts.map +1 -1
  19. package/dist/migrate/source-map-support/enable-source-maps-at-launch.js +8 -16
  20. package/dist/migrate/source-map-support/enable-source-maps-at-launch.js.map +1 -1
  21. package/dist/migrate/source-map-support/find-source-map-support-manual-migrations.d.ts.map +1 -1
  22. package/dist/migrate/source-map-support/find-source-map-support-manual-migrations.js +26 -9
  23. package/dist/migrate/source-map-support/find-source-map-support-manual-migrations.js.map +1 -1
  24. package/dist/migrate/source-map-support/migrate-source-map-support-to-native.d.ts.map +1 -1
  25. package/dist/migrate/source-map-support/migrate-source-map-support-to-native.js +7 -4
  26. package/dist/migrate/source-map-support/migrate-source-map-support-to-native.js.map +1 -1
  27. package/dist/migrate/source-map-support/replace-source-map-support-api.d.ts +9 -0
  28. package/dist/migrate/source-map-support/replace-source-map-support-api.d.ts.map +1 -0
  29. package/dist/migrate/source-map-support/replace-source-map-support-api.js +275 -0
  30. package/dist/migrate/source-map-support/replace-source-map-support-api.js.map +1 -0
  31. package/dist/migrate/source-map-support/replace-source-map-support-install.d.ts.map +1 -1
  32. package/dist/migrate/source-map-support/replace-source-map-support-install.js +2 -1
  33. package/dist/migrate/source-map-support/replace-source-map-support-install.js.map +1 -1
  34. package/dist/migrate/source-map-support/source-map-support-api-source.d.ts +14 -0
  35. package/dist/migrate/source-map-support/source-map-support-api-source.d.ts.map +1 -0
  36. package/dist/migrate/source-map-support/source-map-support-api-source.js +426 -0
  37. package/dist/migrate/source-map-support/source-map-support-api-source.js.map +1 -0
  38. package/dist/migrate/source-map-support/source-map-support-rewrites.d.ts.map +1 -1
  39. package/dist/migrate/source-map-support/source-map-support-rewrites.js +2 -0
  40. package/dist/migrate/source-map-support/source-map-support-rewrites.js.map +1 -1
  41. package/dist/migrate/source-map-support/source-map-support-support.d.ts +11 -3
  42. package/dist/migrate/source-map-support/source-map-support-support.d.ts.map +1 -1
  43. package/dist/migrate/source-map-support/source-map-support-support.js +69 -10
  44. package/dist/migrate/source-map-support/source-map-support-support.js.map +1 -1
  45. package/dist/resources/advisories-npm.csv +342 -6
  46. package/package.json +2 -2
  47. package/src/index.ts +2 -0
  48. package/src/migrate/code-style.ts +92 -0
  49. package/src/migrate/fs-extra/replace-fs-extra-native-methods.ts +2 -2
  50. package/src/migrate/hpagent/find-hpagent-manual-migrations.ts +45 -142
  51. package/src/migrate/hpagent/hpagent-support.ts +358 -60
  52. package/src/migrate/hpagent/replace-hpagent.ts +580 -189
  53. package/src/migrate/source-map-support/enable-source-maps-at-launch.ts +17 -26
  54. package/src/migrate/source-map-support/find-source-map-support-manual-migrations.ts +25 -9
  55. package/src/migrate/source-map-support/migrate-source-map-support-to-native.ts +7 -4
  56. package/src/migrate/source-map-support/replace-source-map-support-api.ts +281 -0
  57. package/src/migrate/source-map-support/replace-source-map-support-install.ts +2 -1
  58. package/src/migrate/source-map-support/source-map-support-api-source.ts +470 -0
  59. package/src/migrate/source-map-support/source-map-support-rewrites.ts +3 -0
  60. package/src/migrate/source-map-support/source-map-support-support.ts +79 -10
@@ -4,16 +4,222 @@
4
4
  * Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
5
5
  */
6
6
 
7
- import {Column, DataTable, emptyMarkers, randomId} from "@openrewrite/rewrite";
7
+ import {Column, Cursor, DataTable, emptyMarkers, randomId} from "@openrewrite/rewrite";
8
8
  import {emptySpace, isIdentifier, J, rightPadded, space} from "@openrewrite/rewrite/java";
9
- import {JS, walk} from "@openrewrite/rewrite/javascript";
10
- import {stringLiteral} from "../module-bindings";
9
+ import {bindingNames, declarationsOf, JS, scopeOf, walk} from "@openrewrite/rewrite/javascript";
10
+ import {CodeStyle, DEFAULT_CODE_STYLE, indented} from "../code-style";
11
+ import {bindingsOf, importModule, ModuleBindings, requiredModule, stringLiteral} from "../module-bindings";
11
12
 
12
- export {bindingsOf, isModuleScoped, sourcePathOf, stringLiteral} from "../module-bindings";
13
+ export {sourcePathOf, stringLiteral} from "../module-bindings";
13
14
  export type {ModuleBindings} from "../module-bindings";
14
15
 
15
16
  export const HPAGENT_MODULES = new Set(['hpagent']);
16
17
 
18
+ /** `require('hpagent')`, also when cast as in `require('hpagent') as typeof import('hpagent')`. */
19
+ export function isHpagentRequire(e: J | undefined): boolean {
20
+ const call = e?.kind === JS.Kind.As ? (e as JS.As).left.element : e;
21
+ return HPAGENT_MODULES.has(requiredModule(call) ?? '');
22
+ }
23
+
24
+ /**
25
+ * What a file's hpagent imports and requires bind, and the id of the scope each binds in: the
26
+ * compilation unit for a top-level binding, or the block a `const {...} = require('hpagent')` sits in,
27
+ * as when a CLI requires the proxy agent only once it knows a proxy is configured.
28
+ */
29
+ export interface HpagentBindings {
30
+ bindings: ModuleBindings;
31
+ scopes: Map<string, string>;
32
+ /** The `require` calls these bindings come from, so a report can tell a shape it doesn't follow. */
33
+ requires: Set<string>;
34
+ /** Names a `require` binds, which the rewrite moves itself since `maybeRebind` moves only imports. */
35
+ requireBound: Set<string>;
36
+ /** Names a default import binds, which has to become a namespace import: `https-proxy-agent` has no default export. */
37
+ defaults: Set<string>;
38
+ /** Names bound by `({...} = require('hpagent'))` into a `let` declared without a value, which keep their names. */
39
+ assigned: Set<string>;
40
+ /** The identifiers such an assignment writes, which are no use of the class as a value. */
41
+ assignmentTargets: Set<string>;
42
+ /** Names a type-only import binds, which can only ever name a type. */
43
+ typeOnly: Set<string>;
44
+ }
45
+
46
+ /** The local and the export each member of `({X, Y: local} = ...)` binds, with the identifiers it writes. */
47
+ function assignmentPattern(pattern: J): {name: string; member: string; ids: string[]}[] | undefined {
48
+ if (!isObjectLiteral(pattern)) return undefined;
49
+ const bound = [];
50
+ for (const member of objectMembers(pattern)) {
51
+ if (member.kind !== JS.Kind.PropertyAssignment) return undefined;
52
+ const prop = member as JS.PropertyAssignment;
53
+ const property = propertyName(prop);
54
+ const local = prop.initializer ?? prop.name.element;
55
+ if (property === undefined || !isIdentifier(local)) return undefined;
56
+ bound.push({name: local.simpleName, member: property, ids: [prop.name.element.id, local.id]});
57
+ }
58
+ return bound;
59
+ }
60
+
61
+ export function emptyHpagentBindings(): HpagentBindings {
62
+ return {
63
+ bindings: new Map(), scopes: new Map(), requires: new Set(), requireBound: new Set(), defaults: new Set(),
64
+ assigned: new Set(), assignmentTargets: new Set(), typeOnly: new Set()
65
+ };
66
+ }
67
+
68
+ export function hpagentBindings(cu: JS.CompilationUnit): HpagentBindings {
69
+ const bindings = bindingsOf(cu, HPAGENT_MODULES);
70
+ const defaults = new Set<string>();
71
+ const typeOnly = new Set<string>();
72
+ for (const padded of cu.statements) {
73
+ const imp = padded.element as JS.Import;
74
+ if (imp?.kind !== JS.Kind.Import || !HPAGENT_MODULES.has(importModule(imp) ?? '') || !imp.importClause) continue;
75
+ const clause = imp.importClause;
76
+ const name = clause.name?.element;
77
+ if (name && !clause.typeOnly) defaults.add(name.simpleName);
78
+ // Type-only imports name nothing at runtime, but their names still have to move off hpagent.
79
+ if (clause.typeOnly && clause.namedBindings?.kind === JS.Kind.Alias) {
80
+ const alias = (clause.namedBindings as JS.Alias).alias;
81
+ if (isIdentifier(alias)) {
82
+ bindings.set(alias.simpleName, undefined);
83
+ typeOnly.add(alias.simpleName);
84
+ }
85
+ }
86
+ if (clause.namedBindings?.kind !== JS.Kind.NamedImports) continue;
87
+ for (const el of (clause.namedBindings as JS.NamedImports).elements.elements) {
88
+ const spec = el.element as JS.ImportSpecifier;
89
+ if (spec.kind !== JS.Kind.ImportSpecifier || !(clause.typeOnly || spec.importType?.element)) continue;
90
+ if (isIdentifier(spec.specifier)) {
91
+ bindings.set(spec.specifier.simpleName, spec.specifier.simpleName);
92
+ typeOnly.add(spec.specifier.simpleName);
93
+ } else if (spec.specifier.kind === JS.Kind.Alias && isIdentifier((spec.specifier as JS.Alias).alias)) {
94
+ const alias = spec.specifier as JS.Alias;
95
+ bindings.set((alias.alias as J.Identifier).simpleName, alias.propertyName.element.simpleName);
96
+ typeOnly.add((alias.alias as J.Identifier).simpleName);
97
+ }
98
+ }
99
+ }
100
+ const scopes = new Map([...bindings.keys()].map(local => [local, cu.id]));
101
+ const requires = new Set<string>();
102
+ const requireBound = new Set<string>();
103
+ const assigned = new Set<string>();
104
+ const assignmentTargets = new Set<string>();
105
+ // Each name a `let` or `var` declares without a value, with the scopes declaring it so.
106
+ const unassigned = new Map<string, string[]>();
107
+ const assignments: J.Assignment[] = [];
108
+ const declare = (statement: J, scope: J) => {
109
+ for (const decl of declarationsOf(statement)) {
110
+ for (const v of decl.variables) {
111
+ const initializer = v.element.initializer?.element;
112
+ const declared = v.element.name as J;
113
+ if (!initializer && isIdentifier(declared)) {
114
+ unassigned.set(declared.simpleName, [...(unassigned.get(declared.simpleName) ?? []), scope.id]);
115
+ }
116
+ if (!isHpagentRequire(initializer)) continue;
117
+ requires.add((initializer!.kind === JS.Kind.As ? (initializer as JS.As).left.element : initializer)!.id);
118
+ const pattern = v.element.name as J;
119
+ const names = isIdentifier(pattern) ? [{name: pattern.simpleName, member: undefined}] : bindingNames(pattern);
120
+ for (const {name, member} of names) {
121
+ // A name bound twice is left to the report rather than guessed between.
122
+ if (scope !== cu && scopes.has(name)) continue;
123
+ bindings.set(name, member);
124
+ scopes.set(name, scope.id);
125
+ requireBound.add(name);
126
+ }
127
+ }
128
+ }
129
+ };
130
+ for (const padded of cu.statements) declare(padded.element, cu);
131
+ walk(cu, (n: any) => {
132
+ if (n?.kind === J.Kind.Block) {
133
+ for (const padded of (n as J.Block).statements) declare(padded.element, n);
134
+ } else if (n?.kind === J.Kind.Assignment && isHpagentRequire((n as J.Assignment).assignment.element)) {
135
+ assignments.push(n);
136
+ }
137
+ return true;
138
+ });
139
+ // `let HttpsProxyAgent; ... ({HttpsProxyAgent} = require('hpagent'))`, as code does that requires hpagent
140
+ // only once a proxy is configured. The binding lives where the `let` is, if one declaration settles it.
141
+ for (const assignment of assignments) {
142
+ const pattern = assignmentPattern(assignment.variable);
143
+ if (!pattern || pattern.some(({name}) => unassigned.get(name)?.length !== 1 || bindings.has(name))) continue;
144
+ const call = assignment.assignment.element;
145
+ requires.add((call.kind === JS.Kind.As ? (call as JS.As).left.element : call).id);
146
+ for (const {name, member, ids} of pattern) {
147
+ bindings.set(name, member);
148
+ scopes.set(name, unassigned.get(name)![0]);
149
+ requireBound.add(name);
150
+ assigned.add(name);
151
+ ids.forEach(id => assignmentTargets.add(id));
152
+ }
153
+ }
154
+ return {bindings, scopes, requires, requireBound, defaults, assigned, assignmentTargets, typeOnly};
155
+ }
156
+
157
+ /** Whether `name` at `cursor` resolves to the hpagent binding rather than a nearer declaration. */
158
+ export function resolvesToBinding(cursor: Cursor, name: string, found: HpagentBindings): boolean {
159
+ const scope = found.scopes.get(name);
160
+ return scope !== undefined && scopeOf(cursor).declaringScope(name)?.id === scope;
161
+ }
162
+
163
+ /** Trees that hold an identifier as a declaration or import rather than a use of it. */
164
+ const DECLARING_KINDS = new Set<string>([
165
+ JS.Kind.Import, JS.Kind.ImportClause, JS.Kind.ImportSpecifier, JS.Kind.Alias, JS.Kind.NamedImports,
166
+ JS.Kind.ExportSpecifier, JS.Kind.BindingElement, JS.Kind.ObjectBindingPattern,
167
+ ]);
168
+
169
+ export function inTypePosition(cursor: Cursor): boolean {
170
+ // The type an `as` casts to, however deep, as `ConstructorParameters<typeof HttpProxyAgent>[0]` is.
171
+ for (let at: Cursor | undefined = cursor; at; at = at.parentTree()) {
172
+ const parent = at.parentTree()?.value as J | undefined;
173
+ if (parent?.kind === JS.Kind.As && (parent as JS.As).right === at.value) return true;
174
+ }
175
+ return !!cursor.firstEnclosing((t: any): t is J =>
176
+ t?.kind === JS.Kind.TypeInfo || t?.kind === JS.Kind.TypeQuery || t?.kind === JS.Kind.TypeDeclaration);
177
+ }
178
+
179
+ /**
180
+ * Whether the identifier or member access at `cursor` stands for the class it names as a value: not
181
+ * constructed, not tested with `instanceof`, not a type, not declared or imported, and not the
182
+ * module a member is read from.
183
+ */
184
+ export function isValueUse(cursor: Cursor): boolean {
185
+ const tree = cursor.value as J;
186
+ const parent = cursor.parentTree()?.value as J | undefined;
187
+ if (!parent || DECLARING_KINDS.has(parent.kind) || inTypePosition(cursor)) return false;
188
+ switch (parent.kind) {
189
+ case J.Kind.NewClass:
190
+ return (parent as J.NewClass).class !== tree;
191
+ case J.Kind.InstanceOf:
192
+ return (parent as J.InstanceOf).class !== tree;
193
+ case J.Kind.FieldAccess:
194
+ return (parent as J.FieldAccess).target !== tree;
195
+ case J.Kind.MethodInvocation: {
196
+ const m = parent as J.MethodInvocation;
197
+ return m.select?.element !== tree && m.name !== tree;
198
+ }
199
+ case J.Kind.NamedVariable:
200
+ return (parent as J.VariableDeclarations.NamedVariable).name !== tree;
201
+ default:
202
+ return true;
203
+ }
204
+ }
205
+
206
+ const MOCK_OWNERS = new Set(['vi', 'jest']);
207
+ const MOCK_METHODS = new Set(['mock', 'doMock', 'unmock', 'unstable_mockModule']);
208
+
209
+ /** `jest.mock(...)`, `vi.mock(...)` and their kin, which name the module they replace. */
210
+ export function isMockCall(m: J.MethodInvocation): boolean {
211
+ const select = m.select?.element;
212
+ return !!select && isIdentifier(select) && MOCK_OWNERS.has(select.simpleName) && MOCK_METHODS.has(m.name.simpleName);
213
+ }
214
+
215
+ /** `jest.spyOn(hpagent, 'HttpsProxyAgent')`, which reads the class off the module by name; the class it spies on. */
216
+ export function spiedClass(m: J.MethodInvocation): string | undefined {
217
+ const select = m.select?.element;
218
+ if (!select || !isIdentifier(select) || !MOCK_OWNERS.has(select.simpleName) || m.name.simpleName !== 'spyOn') return undefined;
219
+ const member = stringLiteral(m.arguments.elements[1]?.element);
220
+ return member !== undefined && HPAGENT_CLASSES.has(member) ? member : undefined;
221
+ }
222
+
17
223
  /** Both hpagent classes tunnel with `CONNECT`, which is what `https-proxy-agent` does for either target scheme. */
18
224
  export const HPAGENT_CLASSES = new Set(['HttpProxyAgent', 'HttpsProxyAgent']);
19
225
 
@@ -50,44 +256,28 @@ const DESTINATION_TLS_OPTIONS = new Set([
50
256
  /** The `proxyRequestOptions` members, which configure the `CONNECT` request and so the proxy hop. */
51
257
  const PROXY_REQUEST_OPTIONS = new Set(['ca', 'headers', 'rejectUnauthorized']);
52
258
 
259
+ /** hpagent's option interfaces, which the adapter's own options type stands in for. */
260
+ export const ADAPTED_TYPES = new Set(['HttpProxyAgentOptions', 'HttpsProxyAgentOptions']);
261
+
262
+ export const ADAPTER_FUNCTION = 'createProxyAgent';
263
+ export const ADAPTER_TYPE = 'ProxyAgentOptions';
264
+ /** What hpagent called `ProxyAgentRequestOptions`, declared beside the adapter's type where a file names it. */
265
+ export const REQUEST_TYPE = 'ProxyRequestOptions';
266
+
53
267
  export const REASONS = {
54
- noProxy: "hpagent's agent options have no `proxy`, so there is no proxy URL to pass as " +
55
- "`https-proxy-agent`'s first argument. Supply it at the call site.",
56
- spread: "The agent options are spread from another object. `proxy` is given here, so the call could become " +
57
- "`new HttpsProxyAgent(proxy, {...spread})`, but the spread may itself carry a `proxyRequestOptions` or a " +
58
- "destination TLS option, which would have to move rather than be passed through. Check what it holds and " +
59
- "either migrate by hand or re-run with `migrateSpreadOptions` enabled.",
60
- spreadNoProxy: "The agent options are spread from another object and no `proxy` is given here, so the proxy URL " +
61
- "can only come from the spread and can't be read out statically. Pass it as `https-proxy-agent`'s first " +
62
- "argument explicitly.",
63
- notLiteral: "The agent options aren't an object literal, so `proxy` can't be separated from the rest. " +
64
- "Call `new HttpsProxyAgent(options.proxy, options)` explicitly, folding any `proxyRequestOptions` " +
65
- "members into the second argument.",
66
- destinationTls: (option: string) => `\`${option}\` configured the TLS handshake with the destination, since ` +
67
- "hpagent's `HttpsProxyAgent` extends `https.Agent`. `https-proxy-agent` spends its constructor options on " +
68
- "the connection to the proxy and takes the destination's TLS options from each request, so pass this " +
69
- "option to `https.request`/`fetch` instead, or to the client library making the request.",
70
- unknownOption: (option: string) => `\`${option}\` isn't a recognized hpagent option, so it isn't known which ` +
71
- "hop it configures. Check it against `https-proxy-agent`'s options and move it by hand.",
72
- computedOption: "An agent option has a computed name, so it can't be classified. Name the option literally, " +
73
- "or build the two argument lists by hand.",
74
- nameTaken: "Both `HttpProxyAgent` and `HttpsProxyAgent` are bound here and both migrate to " +
75
- "`https-proxy-agent`'s `HttpsProxyAgent`, which would collide. Migrate the two bindings by hand; both " +
76
- "tunnel with `CONNECT`, so one agent serves http and https targets alike.",
77
- value: "hpagent's agent class is used as a value rather than instantiated, so its constructor call can't be " +
78
- "rewritten here. `https-proxy-agent` takes the proxy URL as its first argument, not as an option.",
79
- type: "hpagent's option types don't match `https-proxy-agent`'s, whose options are generic in the proxy URL's " +
80
- "scheme. Use `HttpsProxyAgentOptions<string>` from `https-proxy-agent` or declare a local type.",
268
+ arguments: "hpagent's constructor takes one options object, and this call doesn't pass exactly one, so it " +
269
+ "can't be handed to the adapter. Pass the options as a single object.",
270
+ value: "hpagent's agent class is used as a value rather than instantiated, as in `const Agent = HttpProxyAgent`, " +
271
+ "so whatever constructs it later still passes hpagent's options object. `https-proxy-agent` takes the " +
272
+ "proxy URL as its first argument; construct it directly, or through the adapter this migration writes.",
81
273
  reExport: "Re-exporting hpagent makes it part of this module's API. Re-export from `https-proxy-agent`, whose " +
82
274
  "constructor takes the proxy URL positionally.",
83
275
  dynamicImport: "Dynamic import of hpagent, which the automated migration doesn't follow. Import " +
84
276
  "`https-proxy-agent` instead and migrate its uses.",
85
- nestedRequire: "hpagent required outside module scope, which the automated migration doesn't follow. Move the " +
86
- "require to the top of the file or migrate its uses by hand.",
277
+ nestedRequire: "hpagent required in a shape the automated migration doesn't follow; it moves a block's " +
278
+ "`const {...} = require('hpagent')` but not this. Bind it that way or migrate its uses by hand.",
87
279
  mock: "Test mocks hpagent. Once the code under test constructs `https-proxy-agent`'s `HttpsProxyAgent`, mock " +
88
280
  "that module instead; its constructor takes the proxy URL as its first argument.",
89
- defaultImport: "hpagent has no default export, and `https-proxy-agent` none either. Import `HttpsProxyAgent` " +
90
- "as a named binding.",
91
281
  };
92
282
 
93
283
  /** What one member of an hpagent options object literal is, and so where it goes. */
@@ -96,7 +286,7 @@ export type OptionKind =
96
286
  | {kind: 'proxyRequestOptions'; value: J}
97
287
  | {kind: 'agent'; member: JS.PropertyAssignment}
98
288
  | {kind: 'spread'; value: J | undefined}
99
- | {kind: 'unmappable'; option: string; reason: string};
289
+ | {kind: 'unmappable'};
100
290
 
101
291
  export function propertyName(prop: JS.PropertyAssignment): string | undefined {
102
292
  const name = prop.name.element;
@@ -115,22 +305,120 @@ export function objectMembers(literal: J.NewClass): J[] {
115
305
  /** Classifies one member of an hpagent options object literal. */
116
306
  export function classifyOption(statement: J): OptionKind {
117
307
  if (statement.kind === JS.Kind.Spread) return {kind: 'spread', value: (statement as JS.Spread).expression};
118
- if (statement.kind !== JS.Kind.PropertyAssignment) {
119
- // A method or a getter: neither a name/value pair this can read.
120
- return {kind: 'unmappable', option: '(not a property)', reason: REASONS.notLiteral};
121
- }
308
+ // A method or a getter is no name/value pair this can read.
309
+ if (statement.kind !== JS.Kind.PropertyAssignment) return {kind: 'unmappable'};
122
310
  const prop = statement as JS.PropertyAssignment;
123
311
  const name = propertyName(prop);
124
- if (name === undefined) return {kind: 'unmappable', option: '(computed)', reason: REASONS.computedOption};
312
+ if (name === undefined) return {kind: 'unmappable'};
125
313
  // A shorthand `{proxy}` carries the value in its name, which the caller reads back from the property.
126
314
  const value = prop.initializer ?? prop.name.element;
127
315
  if (name === 'proxy') return {kind: 'proxy', value};
128
316
  if (name === 'proxyRequestOptions') return {kind: 'proxyRequestOptions', value};
129
- if (DESTINATION_TLS_OPTIONS.has(name)) {
130
- return {kind: 'unmappable', option: name, reason: REASONS.destinationTls(name)};
317
+ // A destination TLS option would land on the proxy hop in the two-argument form, so it takes the adapter.
318
+ if (DESTINATION_TLS_OPTIONS.has(name)) return {kind: 'unmappable'};
319
+ return AGENT_OPTIONS.has(name) ? {kind: 'agent', member: prop} : {kind: 'unmappable'};
320
+ }
321
+
322
+ /** The one options argument a construction passes, which is what the adapter takes, or undefined. */
323
+ export function constructionArgument(n: J.NewClass): J | undefined {
324
+ const args = n.arguments.elements.map(a => a.element).filter(a => a.kind !== J.Kind.Empty);
325
+ return args.length === 1 && args[0].kind !== JS.Kind.Spread ? args[0] : undefined;
326
+ }
327
+
328
+ /** Source for a declaration the migration writes, with the text of the doc comment written above it. */
329
+ export interface GeneratedDeclaration {
330
+ doc: string;
331
+ code: string;
332
+ }
333
+
334
+ /** The pool options hpagent gave `http.Agent`; it sent no other agent option to the proxy. */
335
+ const POOL_OPTIONS = ['keepAlive', 'keepAliveMsecs', 'maxSockets', 'maxTotalSockets', 'maxFreeSockets', 'scheduling', 'timeout'];
336
+
337
+ /** How a braced list separates its members: on one line, and on lines of their own. */
338
+ interface Separators {
339
+ inline: string;
340
+ each: string;
341
+ /** After the last member on a line of its own. */
342
+ last: string;
343
+ }
344
+
345
+ /**
346
+ * `head{members}tail` on one line at `level`, or with a member to a line where that runs past the file's
347
+ * print width. In a `pattern`, a rest element takes no trailing comma, which the language forbids there.
348
+ */
349
+ function braced(style: CodeStyle, level: number, head: string, members: string[], tail: string, pattern = false,
350
+ separators: Separators = {inline: ', ', each: ',', last: style.trailingCommas ? ',' : ''}): string {
351
+ const margin = ' '.repeat(level);
352
+ const inner = members.join(separators.inline);
353
+ const line = `${margin}${head}${style.braceSpacing ? `{ ${inner} }` : `{${inner}}`}${tail}`;
354
+ const indentWidth = style.indent === '\t' ? 4 : style.indent.length;
355
+ if (line.length - margin.length + indentWidth * level <= style.printWidth) return line;
356
+ const lines = members.map((member, i) => `${margin} ${member}${i < members.length - 1
357
+ ? separators.each
358
+ : pattern && member.startsWith('...') ? '' : separators.last}`);
359
+ return [`${margin}${head}{`, ...lines, `${margin}}${tail}`].join('\n');
360
+ }
361
+
362
+ /**
363
+ * The adapter a file gets when one of its constructions can't be rewritten to the two-argument form.
364
+ * `agentClass` is how the file now names `https-proxy-agent`'s class.
365
+ */
366
+ export function adapterSource(name: string, typeName: string, agentClass: string, typeScript: boolean,
367
+ style: CodeStyle = DEFAULT_CODE_STYLE): GeneratedDeclaration {
368
+ const end = style.semicolons ? ';' : '';
369
+ const head = `function ${name}${style.functionParenSpace ? ' ' : ''}(`;
370
+ const parameters = ['proxy', 'proxyRequestOptions', '...options'];
371
+ return {
372
+ doc: `*
373
+ * hpagent's options-object constructor on \`https-proxy-agent\`, whose constructor options configure the
374
+ * connection to the proxy. hpagent sent the proxy only \`proxyRequestOptions\`, and handed every other
375
+ * option to \`https.Agent\`, which merges an agent's options into each request and so into the
376
+ * destination's TLS handshake. \`agent.options\` is that same per-request object here.
377
+ `,
378
+ code: indented(style, [
379
+ braced(style, 0, head, parameters, typeScript ? `: ${typeName}): ${agentClass}<string> {` : ') {', true),
380
+ braced(style, 1, 'const ', POOL_OPTIONS, ` = options${end}`, true),
381
+ braced(style, 1, `const agent = new ${agentClass}(proxy, `, [...POOL_OPTIONS, '...proxyRequestOptions'], `)${end}`),
382
+ ` Object.assign(agent.options || {}, options)${end}`,
383
+ ` return agent${end}`,
384
+ '}',
385
+ ].join('\n'))
386
+ };
387
+ }
388
+
389
+ /**
390
+ * The options hpagent's agents took, which the adapter takes in their place, and hpagent's
391
+ * `ProxyAgentRequestOptions` as `requestTypeName` where the file names that. `specifier` spells a builtin the file's way.
392
+ */
393
+ export function adapterTypeSource(typeName: string | undefined, requestTypeName: string | undefined,
394
+ specifier: (module: string) => string,
395
+ style: CodeStyle = DEFAULT_CODE_STYLE): GeneratedDeclaration[] {
396
+ const module = (name: string) => `import(${style.quote}${specifier(name)}${style.quote})`;
397
+ const end = style.semicolons ? ';' : '';
398
+ const request = ['ca?: string[]', `headers?: ${module('http')}.OutgoingHttpHeaders`, 'rejectUnauthorized?: boolean'];
399
+ // A type literal written on one line keeps its delimiters either way; one written a member to a line needs them only with semicolons.
400
+ const separators = {inline: style.semicolons ? '; ' : ', ', each: end, last: end};
401
+ const declarations: GeneratedDeclaration[] = [];
402
+ if (typeName) {
403
+ declarations.push({
404
+ doc: "* The options hpagent's agents took: an `https.Agent`'s, plus the proxy to tunnel through. ",
405
+ code: indented(style, [
406
+ `type ${typeName} = ${module('https')}.AgentOptions & {`,
407
+ ` proxy: string | ${module('url')}.URL${end}`,
408
+ requestTypeName
409
+ ? ` proxyRequestOptions?: ${requestTypeName}${end}`
410
+ : braced(style, 1, 'proxyRequestOptions?: ', request, end, false, separators),
411
+ `}${end}`,
412
+ ].join('\n'))
413
+ });
414
+ }
415
+ if (requestTypeName) {
416
+ declarations.push({
417
+ doc: "* What hpagent sent the proxy with its `CONNECT` request, its `ProxyAgentRequestOptions`. ",
418
+ code: indented(style, braced(style, 0, `type ${requestTypeName} = `, request, end, false, separators))
419
+ });
131
420
  }
132
- if (AGENT_OPTIONS.has(name)) return {kind: 'agent', member: prop};
133
- return {kind: 'unmappable', option: name, reason: REASONS.unknownOption(name)};
421
+ return declarations;
134
422
  }
135
423
 
136
424
  /** Whether every member of a `proxyRequestOptions` literal is one hpagent forwards to the `CONNECT` request. */
@@ -187,24 +475,34 @@ export function spreadIsPlainAgentOptions(cu: JS.CompilationUnit, spread: J | un
187
475
  return literal !== undefined && objectMembers(literal).every(m => classifyOption(m).kind === 'agent');
188
476
  }
189
477
 
478
+ /** The builtin module whose agent an hpagent class extends. */
479
+ export function builtinAgentModule(member: string): 'http' | 'https' {
480
+ return member === 'HttpProxyAgent' ? 'http' : 'https';
481
+ }
482
+
190
483
  /**
191
- * `HttpsProxyAgent<string>` as a type tree. `https-proxy-agent`'s class is generic in the proxy
192
- * URL's scheme with no default, so the bare name is a type error where hpagent's was not.
484
+ * The builtin agent an hpagent class extends, which is its type exactly: hpagent declares nothing beyond
485
+ * it. `https-proxy-agent`'s class extends `http.Agent` too, and is assignable to `https.Agent`, so anything
486
+ * the type accepted it still does. Through `local` where the file binds the builtin, else `import('…')`.
193
487
  */
194
- export function targetTypeReference(local: string, prefix: J.Space): J.ParameterizedType {
195
- const name: J.Identifier = {
196
- kind: J.Kind.Identifier, id: randomId(), prefix: emptySpace, markers: emptyMarkers,
197
- annotations: [], simpleName: local
198
- } as unknown as J.Identifier;
199
- const stringType: J.Identifier = {...name, id: randomId(), simpleName: 'string'};
488
+ export function builtinAgentType(local: string | undefined, specifier: string, prefix: J.Space): J {
489
+ const identifier = (name: string, at: J.Space = emptySpace): J.Identifier => ({
490
+ kind: J.Kind.Identifier, id: randomId(), prefix: at, markers: emptyMarkers, annotations: [], simpleName: name
491
+ } as unknown as J.Identifier);
492
+ const agent = {kind: J.Kind.LeftPadded, before: emptySpace, element: identifier('Agent'), markers: emptyMarkers};
493
+ if (local) {
494
+ return {kind: J.Kind.FieldAccess, id: randomId(), prefix, markers: emptyMarkers, target: identifier(local), name: agent} as unknown as J;
495
+ }
496
+ const module = {
497
+ kind: JS.Kind.LiteralType, id: randomId(), prefix: emptySpace, markers: emptyMarkers,
498
+ literal: {kind: J.Kind.Literal, id: randomId(), prefix: emptySpace, markers: emptyMarkers, value: specifier, valueSource: `'${specifier}'`}
499
+ };
200
500
  return {
201
- kind: J.Kind.ParameterizedType, id: randomId(), prefix, markers: emptyMarkers,
202
- class: name,
203
- typeParameters: {
204
- kind: J.Kind.Container, id: randomId(), before: emptySpace, markers: emptyMarkers,
205
- elements: [rightPadded(stringType, space(''))]
206
- }
207
- } as unknown as J.ParameterizedType;
501
+ kind: JS.Kind.ImportType, id: randomId(), prefix, markers: emptyMarkers,
502
+ hasTypeof: rightPadded(false, emptySpace),
503
+ argumentAndAttributes: {kind: J.Kind.Container, before: emptySpace, elements: [rightPadded(module, emptySpace)], markers: emptyMarkers},
504
+ qualifier: agent
505
+ } as unknown as J;
208
506
  }
209
507
 
210
508
  export class HpagentManualMigrationStep {