openapi-typesafe-migrate 1.0.0-rc.7 → 1.0.0-rc.9

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/cli/main.mjs CHANGED
@@ -4,7 +4,7 @@ import { win32, posix, join, dirname, isAbsolute, relative, sep, basename, resol
4
4
  import { realpath, lstat, stat, access, readFile, writeFile, chmod, rename, rm, readdir } from 'node:fs/promises';
5
5
  import { spawn } from 'node:child_process';
6
6
  import ts from '@typescript/typescript6';
7
- import { Scalar, parseAllDocuments, isScalar as isScalar$1, isSeq, isMap as isMap$1, isNode, CST } from 'yaml';
7
+ import { Scalar, parseAllDocuments, isScalar as isScalar$1, isSeq, isMap as isMap$1, isAlias, isNode, CST } from 'yaml';
8
8
  import { parseArgs } from 'node:util';
9
9
 
10
10
  const WINDOWS = "win32";
@@ -609,15 +609,17 @@ function columnAt(text, offset) {
609
609
  return offset - text.lastIndexOf("\n", offset - 1);
610
610
  }
611
611
 
612
+ class MigrationBugError extends Error {
613
+ name = "MigrationBugError";
614
+ }
615
+
612
616
  function applyTextEdits(text, edits) {
613
617
  const sorted = [...edits].sort((left, right) => left.start - right.start);
614
618
  let edited = "";
615
619
  let copiedUpTo = 0;
616
620
  for (const edit of sorted) {
617
621
  if (edit.start < copiedUpTo) {
618
- throw new Error(
619
- `Edits overlap at offset ${edit.start}; the file was not changed (a migration bug).`
620
- );
622
+ throw new MigrationBugError(`edits overlap at offset ${edit.start}`);
621
623
  }
622
624
  edited += `${text.slice(copiedUpTo, edit.start)}${edit.text}`;
623
625
  copiedUpTo = edit.end;
@@ -625,6 +627,80 @@ function applyTextEdits(text, edits) {
625
627
  return edited + text.slice(copiedUpTo);
626
628
  }
627
629
 
630
+ const TYPE_TEXT = "string";
631
+ const TYPESCRIPT_MODULES$1 = /* @__PURE__ */ new Set(["typescript", "@typescript/typescript6"]);
632
+ function isTypescriptReference(node, bindings) {
633
+ const name = node.typeName;
634
+ if (ts.isIdentifier(name)) {
635
+ return bindings.bindingOf(name)?.kind === "typescript-import";
636
+ }
637
+ return ts.isIdentifier(name.left) && bindings.bindingOf(name.left)?.kind === "typescript-module";
638
+ }
639
+ function isTypescriptImportType(node) {
640
+ const { argument } = node;
641
+ return !node.isTypeOf && ts.isLiteralTypeNode(argument) && ts.isStringLiteral(argument.literal) && TYPESCRIPT_MODULES$1.has(argument.literal.text);
642
+ }
643
+ function joined$1(found) {
644
+ const edits = [];
645
+ for (const part of found) {
646
+ if (part === void 0) {
647
+ return void 0;
648
+ }
649
+ edits.push(...part);
650
+ }
651
+ return edits;
652
+ }
653
+ const memberEdits = (node, bindings) => joined$1(
654
+ node.members.map(
655
+ (member) => ts.isPropertySignature(member) ? typeEdits(member.type, bindings) : void 0
656
+ )
657
+ );
658
+ const replaced = (node) => [
659
+ { start: node.getStart(), end: node.getEnd(), text: TYPE_TEXT }
660
+ ];
661
+ const isKeyword = (node) => node.kind >= ts.SyntaxKind.FirstKeyword && node.kind <= ts.SyntaxKind.LastKeyword;
662
+ function typeEdits(node, bindings) {
663
+ if (node === void 0) {
664
+ return void 0;
665
+ }
666
+ if (ts.isLiteralTypeNode(node) || isKeyword(node)) {
667
+ return [];
668
+ }
669
+ if (ts.isUnionTypeNode(node) || ts.isIntersectionTypeNode(node)) {
670
+ return joined$1(node.types.map((part) => typeEdits(part, bindings)));
671
+ }
672
+ if (ts.isParenthesizedTypeNode(node)) {
673
+ return typeEdits(node.type, bindings);
674
+ }
675
+ if (ts.isTypeLiteralNode(node)) {
676
+ return memberEdits(node, bindings);
677
+ }
678
+ const isNodeType = ts.isTypeReferenceNode(node) ? isTypescriptReference(node, bindings) : ts.isImportTypeNode(node) && isTypescriptImportType(node);
679
+ return isNodeType ? replaced(node) : void 0;
680
+ }
681
+ function functionReturnType(node) {
682
+ return ts.isFunctionTypeNode(node) || ts.isJSDocFunctionType(node) ? node.type : void 0;
683
+ }
684
+ function declaredType(node) {
685
+ const annotation = ts.isVariableDeclaration(node) ? node.type : void 0;
686
+ const isJavaScript = (node.flags & ts.NodeFlags.JavaScriptFile) !== 0;
687
+ return annotation ?? (isJavaScript ? ts.getJSDocType(node) : void 0);
688
+ }
689
+ function annotationEdits(type, bindings) {
690
+ const returned = functionReturnType(type);
691
+ if (returned !== void 0) {
692
+ return typeEdits(returned, bindings);
693
+ }
694
+ return ts.isIndexedAccessTypeNode(type) || isKeyword(type) ? [] : void 0;
695
+ }
696
+ function callbackTypeEdits(annotations, callback, bindings) {
697
+ const returnsTag = ts.getJSDocReturnTag(callback)?.typeExpression?.type;
698
+ return joined$1([
699
+ ...annotations.map((type) => annotationEdits(type, bindings)),
700
+ returnsTag === void 0 ? [] : typeEdits(returnsTag, bindings)
701
+ ]);
702
+ }
703
+
628
704
  const hasExportModifier = (declaration) => (ts.getCombinedModifierFlags(declaration) & ts.ModifierFlags.Export) !== 0;
629
705
  const GENERATOR_MODULES = /* @__PURE__ */ new Set([
630
706
  "openapi-typesafe",
@@ -848,6 +924,22 @@ function generatorCalls(file, bindings) {
848
924
  return calls;
849
925
  }
850
926
 
927
+ const TRANSFORM = "transform";
928
+ const LEGACY_ONLY_CALLBACKS = ["postTransform", "transformProperty"];
929
+ const IR_CALLBACKS_OPTION = "irCallbacks";
930
+ function propertyNamed(options, name) {
931
+ return options.properties.find((property) => {
932
+ const key = property.name;
933
+ return key !== void 0 && (ts.isIdentifier(key) || ts.isStringLiteral(key)) && key.text === name;
934
+ });
935
+ }
936
+ function legacyCallbackSteps(options, text) {
937
+ return LEGACY_ONLY_CALLBACKS.flatMap((callback) => {
938
+ const property = propertyNamed(options, callback);
939
+ return property === void 0 ? [] : [{ kind: "legacy-callback", line: lineAt(text, property.getStart()), callback }];
940
+ });
941
+ }
942
+
851
943
  const TS_NODE_OPTIONS = "OpenAPITSOptions";
852
944
  const IR_OPTIONS = "OpenAPITSIrOptions";
853
945
  const TYPE_MODIFIER = "type ";
@@ -1080,6 +1172,41 @@ function scriptRegions(text, path) {
1080
1172
  return componentRegions(text, extension === ASTRO_EXTENSION);
1081
1173
  }
1082
1174
 
1175
+ function spreadObject(expression, bindings) {
1176
+ const { value } = unwrapped(expression);
1177
+ if (ts.isObjectLiteralExpression(value)) {
1178
+ return value;
1179
+ }
1180
+ const binding = ts.isIdentifier(value) ? bindings.bindingOf(value) : void 0;
1181
+ const initializer = binding?.kind === "constant" ? unwrapped(binding.initializer).value : void 0;
1182
+ return initializer !== void 0 && ts.isObjectLiteralExpression(initializer) ? initializer : void 0;
1183
+ }
1184
+ function spreadObjectSteps(source, text, bindings, seen) {
1185
+ if (seen.has(source) || propertyNamed(source, IR_CALLBACKS_OPTION) !== void 0) {
1186
+ return [];
1187
+ }
1188
+ seen.add(source);
1189
+ const transform = propertyNamed(source, TRANSFORM);
1190
+ const kept = transform === void 0 ? [] : [
1191
+ {
1192
+ kind: "transform-not-rewritten",
1193
+ line: lineAt(text, transform.getStart()),
1194
+ reason: "in-spread"
1195
+ }
1196
+ ];
1197
+ return [
1198
+ ...legacyCallbackSteps(source, text),
1199
+ ...kept,
1200
+ ...spreadSteps(source, text, bindings, seen)
1201
+ ];
1202
+ }
1203
+ function spreadSteps(options, text, bindings, seen = /* @__PURE__ */ new Set()) {
1204
+ return options.properties.filter(ts.isSpreadAssignment).flatMap((spread) => {
1205
+ const source = spreadObject(spread.expression, bindings);
1206
+ return source === void 0 ? [{ kind: "options-spread-not-inspected", line: lineAt(text, spread.getStart()) }] : spreadObjectSteps(source, text, bindings, seen);
1207
+ });
1208
+ }
1209
+
1083
1210
  function namedFunction(name, names) {
1084
1211
  const binding = names.bindings.bindingOf(name);
1085
1212
  if (binding?.kind === "function") {
@@ -1108,7 +1235,7 @@ function callbackFunction(property, names) {
1108
1235
  function callbackAnnotations(property, names) {
1109
1236
  const name = namedValue(property);
1110
1237
  const declaration = name === void 0 ? void 0 : names.bindings.declarationOf(name);
1111
- const type = declaration !== void 0 && ts.isVariableDeclaration(declaration) ? declaration.type : void 0;
1238
+ const type = declaredType(declaration ?? property);
1112
1239
  return type === void 0 ? [] : [type];
1113
1240
  }
1114
1241
  function isSharedFunction(property, names) {
@@ -1209,10 +1336,14 @@ function joined(elements, operator, names) {
1209
1336
  if (parts.some((part) => part === void 0)) {
1210
1337
  return void 0;
1211
1338
  }
1212
- const texts = parts.map(
1213
- (part) => operator === "&" && part.operator === "|" ? `(${part.text})` : part.text
1339
+ const members = parts.map(
1340
+ (part) => operator === "&" && part.operator === "|" ? plain$1(`(${part.text})`) : part
1214
1341
  );
1215
- return { text: texts.join(` ${operator} `), operator: texts.length > 1 ? operator : null };
1342
+ const [only] = members;
1343
+ if (members.length === 1 && only !== void 0) {
1344
+ return only;
1345
+ }
1346
+ return { text: members.map((member) => member.text).join(` ${operator} `), operator };
1216
1347
  }
1217
1348
  function factoryCallText(call, method, names) {
1218
1349
  const [first, second] = call.arguments;
@@ -1438,9 +1569,6 @@ function removalEdits(removed, text) {
1438
1569
  return [...statements].flatMap((statement) => statementEdits(statement, isRemoved, text));
1439
1570
  }
1440
1571
 
1441
- const TRANSFORM = "transform";
1442
- const LEGACY_ONLY_CALLBACKS = ["postTransform", "transformProperty"];
1443
- const IR_CALLBACKS_OPTION = "irCallbacks";
1444
1572
  const IR_CALLBACKS_TRUE = "true";
1445
1573
  const IR_CALLBACKS_TRUE_CONST = "true as const";
1446
1574
  const IR_CALLBACKS_TRUE_CONST_JS = "/** @type {const} */ (true)";
@@ -1450,12 +1578,6 @@ const noFindings = () => ({
1450
1578
  manualSteps: [],
1451
1579
  typeReferences: []
1452
1580
  });
1453
- function propertyNamed(options, name) {
1454
- return options.properties.find((property) => {
1455
- const key = property.name;
1456
- return key !== void 0 && (ts.isIdentifier(key) || ts.isStringLiteral(key)) && key.text === name;
1457
- });
1458
- }
1459
1581
  function returnTypeEdits(callback, text) {
1460
1582
  const type = callback.type;
1461
1583
  if (type === void 0) {
@@ -1472,7 +1594,15 @@ function transformEdits(property, names, text) {
1472
1594
  if (isSharedFunction(property, names)) {
1473
1595
  return "shared-function";
1474
1596
  }
1475
- const edits = returnTypeEdits(callback, text);
1597
+ const declared = callbackTypeEdits(
1598
+ callbackAnnotations(property, names),
1599
+ callback,
1600
+ names.bindings
1601
+ );
1602
+ if (declared === void 0) {
1603
+ return "declared-type";
1604
+ }
1605
+ const edits = [...returnTypeEdits(callback, text), ...declared];
1476
1606
  for (const returned of returnedExpressions(callback.body)) {
1477
1607
  const returnEditList = returnEdits(returned, names);
1478
1608
  if (returnEditList === void 0) {
@@ -1503,12 +1633,6 @@ function keepReason(options) {
1503
1633
  }
1504
1634
  return options.properties.some(ts.isSpreadAssignment) ? "spread-options" : void 0;
1505
1635
  }
1506
- function legacyCallbackSteps(options, text) {
1507
- return LEGACY_ONLY_CALLBACKS.flatMap((callback) => {
1508
- const property = propertyNamed(options, callback);
1509
- return property === void 0 ? [] : [{ kind: "legacy-callback", line: lineAt(text, property.getStart()), callback }];
1510
- });
1511
- }
1512
1636
  function transformRewrite(transform, options, names, text) {
1513
1637
  const edits = keepReason(options.options) ?? transformEdits(transform, names, text);
1514
1638
  if (typeof edits === "string") {
@@ -1524,6 +1648,7 @@ function migrateOptions(options, text, names) {
1524
1648
  }
1525
1649
  const findings = noFindings();
1526
1650
  findings.manualSteps.push(...legacyCallbackSteps(options.options, text));
1651
+ findings.manualSteps.push(...spreadSteps(options.options, text, names.bindings));
1527
1652
  const transform = propertyNamed(options.options, TRANSFORM);
1528
1653
  if (transform === void 0) {
1529
1654
  return findings;
@@ -1542,12 +1667,13 @@ function migrateOptions(options, text, names) {
1542
1667
  findings.changes.push({ kind: "transform-rewritten", line });
1543
1668
  return findings;
1544
1669
  }
1545
- function sharedOptionsFindings(options, text) {
1670
+ function sharedOptionsFindings(options, text, bindings) {
1546
1671
  const findings = noFindings();
1547
1672
  if (propertyNamed(options, IR_CALLBACKS_OPTION) !== void 0) {
1548
1673
  return findings;
1549
1674
  }
1550
1675
  findings.manualSteps.push(...legacyCallbackSteps(options, text));
1676
+ findings.manualSteps.push(...spreadSteps(options, text, bindings));
1551
1677
  const transform = propertyNamed(options, TRANSFORM);
1552
1678
  if (transform !== void 0) {
1553
1679
  const line = lineAt(text, transform.getStart());
@@ -1563,7 +1689,7 @@ function callFindings(found, text, names) {
1563
1689
  case "own":
1564
1690
  return migrateOptions(options, text, names);
1565
1691
  case "shared":
1566
- return sharedOptionsFindings(options.options, text);
1692
+ return sharedOptionsFindings(options.options, text, names.bindings);
1567
1693
  case "unknown": {
1568
1694
  const line = lineAt(text, options.expression.getStart());
1569
1695
  return { ...noFindings(), manualSteps: [{ kind: "options-not-inspected", line }] };
@@ -1832,6 +1958,7 @@ function convertScalar(node, isFlow) {
1832
1958
  const isReplaceable = quotings !== void 0 && node.anchor === void 0 && node.tag === void 0;
1833
1959
  return {
1834
1960
  kind: "scalar",
1961
+ ...anchorOf(node),
1835
1962
  value,
1836
1963
  content: contentRange(node.srcToken, span),
1837
1964
  isFolded: node.type !== Scalar.BLOCK_LITERAL,
@@ -1840,6 +1967,7 @@ function convertScalar(node, isFlow) {
1840
1967
  ...span
1841
1968
  };
1842
1969
  }
1970
+ const anchorOf = (node) => node.anchor === void 0 ? {} : { anchor: node.anchor };
1843
1971
  const flowOf = (conversion, node) => ({
1844
1972
  isFlow: conversion.isFlow || node.flow === true
1845
1973
  });
@@ -1849,7 +1977,8 @@ function convert(node, conversion) {
1849
1977
  }
1850
1978
  if (isSeq(node)) {
1851
1979
  const inner = flowOf(conversion, node);
1852
- return { kind: "list", items: node.items.map((item) => convert(item, inner)), ...spanOf(node) };
1980
+ const items = node.items.map((item) => convert(item, inner));
1981
+ return { kind: "list", items, ...anchorOf(node), ...spanOf(node) };
1853
1982
  }
1854
1983
  if (isMap$1(node)) {
1855
1984
  const inner = flowOf(conversion, node);
@@ -1857,7 +1986,10 @@ function convert(node, conversion) {
1857
1986
  key: convert(pair.key, inner),
1858
1987
  value: convert(pair.value, inner)
1859
1988
  }));
1860
- return { kind: "map", entries, ...spanOf(node) };
1989
+ return { kind: "map", entries, ...anchorOf(node), ...spanOf(node) };
1990
+ }
1991
+ if (isAlias(node)) {
1992
+ return { kind: "other", alias: node.source, ...spanOf(node) };
1861
1993
  }
1862
1994
  return { kind: "other", ...spanOf(isNode(node) ? node : {}) };
1863
1995
  }
@@ -1940,10 +2072,10 @@ const PNPM_SETTINGS_FIELD = "pnpm";
1940
2072
  const OLD_PEER_DEPENDENTS = /* @__PURE__ */ new Map([
1941
2073
  ["swr-openapi", { peer: "openapi-fetch", range: ">=0.15" }]
1942
2074
  ]);
1943
- const PATH_SEPARATOR$9 = "/";
2075
+ const PATH_SEPARATOR$a = "/";
1944
2076
  const QUERY_START = "?";
1945
2077
  function renamedSpecifier(value) {
1946
- const ends = [value.indexOf(PATH_SEPARATOR$9), value.indexOf(QUERY_START)].filter(
2078
+ const ends = [value.indexOf(PATH_SEPARATOR$a), value.indexOf(QUERY_START)].filter(
1947
2079
  (at) => at !== -1
1948
2080
  );
1949
2081
  const name = value.slice(0, ends.length === 0 ? value.length : Math.min(...ends));
@@ -1953,12 +2085,12 @@ function renamedSpecifier(value) {
1953
2085
  }
1954
2086
  const rest = value.slice(name.length);
1955
2087
  const subpath = rest.split(QUERY_START, 1)[0] ?? rest;
1956
- return { to: `${renamed}${rest}`, isDeep: subpath !== "" && subpath !== PATH_SEPARATOR$9 };
2088
+ return { to: `${renamed}${rest}`, isDeep: subpath !== "" && subpath !== PATH_SEPARATOR$a };
1957
2089
  }
1958
2090
  const NODE_MODULES_FOLDER = "node_modules/";
1959
2091
  function oldPackageInNodeModules(path) {
1960
2092
  return [...PACKAGE_RENAMES.keys()].find(
1961
- (name) => path.includes(`${NODE_MODULES_FOLDER}${name}${PATH_SEPARATOR$9}`)
2093
+ (name) => path.includes(`${NODE_MODULES_FOLDER}${name}${PATH_SEPARATOR$a}`) || path.endsWith(`${NODE_MODULES_FOLDER}${name}`)
1962
2094
  );
1963
2095
  }
1964
2096
 
@@ -1973,22 +2105,40 @@ function valuesAt(map, key) {
1973
2105
  function mapsAt(map, key) {
1974
2106
  return valuesAt(map, key).filter(isMap);
1975
2107
  }
1976
- function stringsBelow(node, path) {
2108
+ const NO_ALIASES = () => void 0;
2109
+ function stringsBelow(node, path, targetOf) {
1977
2110
  switch (node.kind) {
1978
2111
  case "scalar":
1979
2112
  return [{ ...node, path }];
1980
2113
  case "list":
1981
- return node.items.flatMap((item, index) => stringsBelow(item, [...path, index]));
2114
+ return node.items.flatMap((item, index) => stringsBelow(item, [...path, index], targetOf));
1982
2115
  case "map":
1983
2116
  return textEntries(node).flatMap(
1984
- ({ key, value }) => stringsBelow(value, [...path, key.value])
2117
+ ({ key, value }) => stringsBelow(value, [...path, key.value], targetOf)
1985
2118
  );
1986
- default:
1987
- return [];
2119
+ default: {
2120
+ const target = targetOf(node);
2121
+ return target === void 0 ? [] : stringsBelow(target, path, targetOf);
2122
+ }
1988
2123
  }
1989
2124
  }
1990
2125
  function documentStrings(documents) {
1991
- return documents.flatMap((document) => stringsBelow(document, []));
2126
+ return documents.flatMap((document) => stringsBelow(document, [], NO_ALIASES));
2127
+ }
2128
+ function nodesBelow(node) {
2129
+ const children = node.kind === "list" ? node.items : node.kind === "map" ? node.entries.flatMap(({ key, value }) => [key, value]) : [];
2130
+ return [node, ...children.flatMap(nodesBelow)];
2131
+ }
2132
+ function aliasTargets(document) {
2133
+ const anchored = nodesBelow(document).filter(
2134
+ (node) => node.kind !== "other" && node.anchor !== void 0
2135
+ );
2136
+ return (alias) => anchored.findLast(
2137
+ (node) => node.kind !== "other" && node.anchor === alias.alias && node.end <= alias.start
2138
+ );
2139
+ }
2140
+ function usedDocumentStrings(documents) {
2141
+ return documents.flatMap((document) => stringsBelow(document, [], aliasTargets(document)));
1992
2142
  }
1993
2143
  function ownerKey(path) {
1994
2144
  return path.findLast((part) => typeof part === "string");
@@ -2037,8 +2187,11 @@ const DESCRIPTION_KEY_LINE_RE = new RegExp(
2037
2187
  "m"
2038
2188
  );
2039
2189
  const mayBeApiDescription = (text) => DESCRIPTION_KEY_LINE_RE.test(text);
2190
+ const SPEC_VERSION_RE = /^\d+\.\d+(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?$/;
2040
2191
  const isApiDescription = (documents) => documents.some(
2041
- (root) => isMap(root) && textEntries(root).some(({ key }) => API_DESCRIPTION_KEY_SET.has(key.value))
2192
+ (root) => isMap(root) && textEntries(root).some(
2193
+ ({ key, value }) => API_DESCRIPTION_KEY_SET.has(key.value) && isScalar(value) && SPEC_VERSION_RE.test(value.value)
2194
+ )
2042
2195
  );
2043
2196
  const REF_KEY = "$ref";
2044
2197
  const FRAGMENT_START = "#";
@@ -2183,6 +2336,64 @@ const INSTALLERS = [
2183
2336
  ];
2184
2337
  const NPM_SPECIFIER_PREFIX = "npm:";
2185
2338
 
2339
+ const EXPRESSION_OPEN = "${{";
2340
+ const EXPRESSION_CLOSE = "}}";
2341
+ const LITERAL_QUOTE = "'";
2342
+ const SHELL_SINGLE_QUOTE = "'";
2343
+ const BACKSLASH$3 = "\\";
2344
+ const DOUBLE_QUOTE$1 = '"';
2345
+ function expressionEnd(text, start) {
2346
+ for (let index = start + EXPRESSION_OPEN.length; index < text.length; ) {
2347
+ if (text.startsWith(EXPRESSION_CLOSE, index)) {
2348
+ return index + EXPRESSION_CLOSE.length;
2349
+ }
2350
+ const close = text[index] === LITERAL_QUOTE ? text.indexOf(LITERAL_QUOTE, index + 1) : index;
2351
+ index = close === -1 ? text.length : close + 1;
2352
+ }
2353
+ return void 0;
2354
+ }
2355
+ function literalsIn(raw, start, end) {
2356
+ const found = [];
2357
+ let open = raw.indexOf(LITERAL_QUOTE, start);
2358
+ while (open !== -1 && open < end) {
2359
+ let close = raw.indexOf(LITERAL_QUOTE, open + 1);
2360
+ let isEscaped = false;
2361
+ while (close !== -1 && raw[close + 1] === LITERAL_QUOTE) {
2362
+ isEscaped = true;
2363
+ close = raw.indexOf(LITERAL_QUOTE, close + 2);
2364
+ }
2365
+ if (close === -1) {
2366
+ break;
2367
+ }
2368
+ found.push(...isEscaped ? [] : [{ text: raw.slice(open + 1, close), start: open + 1 }]);
2369
+ open = raw.indexOf(LITERAL_QUOTE, close + 1);
2370
+ }
2371
+ return found;
2372
+ }
2373
+ function expressionLiterals(raw) {
2374
+ const found = [];
2375
+ for (let index = 0; index < raw.length; ) {
2376
+ if (raw[index] === SHELL_SINGLE_QUOTE) {
2377
+ const close = raw.indexOf(SHELL_SINGLE_QUOTE, index + 1);
2378
+ index = close === -1 ? raw.length : close + 1;
2379
+ } else if (raw[index] === BACKSLASH$3) {
2380
+ index += 2;
2381
+ } else if (raw.startsWith(EXPRESSION_OPEN, index)) {
2382
+ const end = expressionEnd(raw, index) ?? raw.length;
2383
+ found.push(...literalsIn(raw, index + EXPRESSION_OPEN.length, end));
2384
+ index = end;
2385
+ } else {
2386
+ index += 1;
2387
+ }
2388
+ }
2389
+ return found;
2390
+ }
2391
+ function isWholeExpression(raw) {
2392
+ const isQuoted = raw.length > 1 && raw.startsWith(DOUBLE_QUOTE$1) && raw.endsWith(DOUBLE_QUOTE$1);
2393
+ const inner = isQuoted ? 1 : 0;
2394
+ return raw.startsWith(EXPRESSION_OPEN, inner) && expressionEnd(raw, inner) === raw.length - inner;
2395
+ }
2396
+
2186
2397
  const COMMAND_SEPARATORS = /* @__PURE__ */ new Set([
2187
2398
  ";",
2188
2399
  "&",
@@ -2231,7 +2442,9 @@ const PARAMETER_CLOSE = "}";
2231
2442
  function parameterExpansionEnd(text, start) {
2232
2443
  let depth = 1;
2233
2444
  for (let index = start + PARAMETER_OPEN.length; index < text.length; ) {
2234
- if (text.startsWith(PARAMETER_OPEN, index)) {
2445
+ if (text.startsWith(EXPRESSION_OPEN, index)) {
2446
+ index = expressionEnd(text, index) ?? text.length;
2447
+ } else if (text.startsWith(PARAMETER_OPEN, index)) {
2235
2448
  depth += 1;
2236
2449
  index += PARAMETER_OPEN.length;
2237
2450
  } else if (text[index] === PARAMETER_CLOSE) {
@@ -2363,9 +2576,13 @@ function skipAssignments(words, start) {
2363
2576
  }
2364
2577
  return index;
2365
2578
  }
2579
+ const WINDOWS_PROGRAM_SUFFIX_RE = /\.(?:cmd|exe|ps1)$/i;
2580
+ const windowsSuffix = (value) => WINDOWS_PROGRAM_SUFFIX_RE.exec(value)?.[0] ?? "";
2581
+ const withoutWindowsSuffix = (value) => value.slice(0, value.length - windowsSuffix(value).length);
2582
+ const entryWord = (word) => word === void 0 ? void 0 : withoutWindowsSuffix(word.value);
2366
2583
  function matchingPrefix(entries, words, index) {
2367
2584
  return entries.find(
2368
- (entry) => entry.words.every((name, offset) => words[index + offset]?.value === name)
2585
+ (entry) => entry.words.every((name, offset) => entryWord(words[index + offset]) === name)
2369
2586
  );
2370
2587
  }
2371
2588
  function inlineOptionPackage(word, launcher) {
@@ -2446,7 +2663,58 @@ function runsGenerator(command) {
2446
2663
  const words = withoutRecipePrefix(command);
2447
2664
  const { program } = commandRoles(words);
2448
2665
  const word = program === void 0 ? void 0 : words[program];
2449
- return word !== void 0 && packageReference(word.value).name === GENERATOR_COMMAND;
2666
+ return word !== void 0 && packageReference(withoutWindowsSuffix(word.value)).name === GENERATOR_COMMAND;
2667
+ }
2668
+
2669
+ const CODE_OPTIONS = /* @__PURE__ */ new Map([
2670
+ ["python", /* @__PURE__ */ new Set(["-c"])],
2671
+ ["node", /* @__PURE__ */ new Set(["-e", "--eval", "-p", "--print"])],
2672
+ ["bun", /* @__PURE__ */ new Set(["-e", "--eval", "-p", "--print"])],
2673
+ ["ruby", /* @__PURE__ */ new Set(["-e"])],
2674
+ ["perl", /* @__PURE__ */ new Set(["-e", "-E"])]
2675
+ ]);
2676
+ const PYTHON_RE = /^(?:python[0-9.]*|py)$/;
2677
+ const PYTHON = "python";
2678
+ const PATH_SEPARATOR_RE$1 = /[/\\]/;
2679
+ const WINDOWS_EXECUTABLE_RE$1 = /\.exe$/i;
2680
+ const OPTION_START$1 = "-";
2681
+ function interpreterOf(word) {
2682
+ const name = (word.value.split(PATH_SEPARATOR_RE$1).at(-1) ?? "").replace(
2683
+ WINDOWS_EXECUTABLE_RE$1,
2684
+ ""
2685
+ );
2686
+ return PYTHON_RE.test(name) ? PYTHON : name;
2687
+ }
2688
+ function codeAfter(command, program) {
2689
+ const options = CODE_OPTIONS.get(interpreterOf(command[program]));
2690
+ for (let index = program + 1; options !== void 0 && index < command.length; index += 1) {
2691
+ const word = command[index];
2692
+ if (!word.value.startsWith(OPTION_START$1)) {
2693
+ return void 0;
2694
+ }
2695
+ if (options.has(word.value)) {
2696
+ return command[index + 1];
2697
+ }
2698
+ }
2699
+ return void 0;
2700
+ }
2701
+ function inlineCode(command) {
2702
+ for (const [index, program] of command.entries()) {
2703
+ const code = codeAfter(command, index);
2704
+ if (code !== void 0) {
2705
+ return { code, program };
2706
+ }
2707
+ }
2708
+ return void 0;
2709
+ }
2710
+
2711
+ const PATH_SEPARATOR$9 = "/";
2712
+ const WINDOWS_SEPARATOR_RE = /\\/g;
2713
+ const QUOTE_RE = /["']/g;
2714
+ function programPath(word) {
2715
+ const suffix = windowsSuffix(word.value);
2716
+ const windowsPath = word.raw.replace(QUOTE_RE, "").replace(WINDOWS_SEPARATOR_RE, PATH_SEPARATOR$9);
2717
+ return { path: suffix === "" ? word.value : windowsPath, suffix };
2450
2718
  }
2451
2719
 
2452
2720
  const EXPANSION_HEAD_RE = /^\$\{([A-Za-z_][A-Za-z0-9_]*):?([-=+])/;
@@ -2504,7 +2772,7 @@ function wholeWordExpansion(raw) {
2504
2772
  const HEREDOC_OPERATOR = "<<";
2505
2773
  const HERE_STRING_OPERATOR = "<<<";
2506
2774
  const STRIP_TABS = "-";
2507
- const LINE_BREAK$4 = "\n";
2775
+ const LINE_BREAK$5 = "\n";
2508
2776
  const LEADING_TABS_RE = /^\t+/;
2509
2777
  const CARRIAGE_RETURN_RE = /\r$/;
2510
2778
  function heredocAt(text, index, where) {
@@ -2536,7 +2804,7 @@ function isDelimiterLine(text, lineStart, end, heredoc) {
2536
2804
  function readBody(text, start, heredoc) {
2537
2805
  let lineStart = start;
2538
2806
  while (lineStart < text.length) {
2539
- const lineEnd = text.indexOf(LINE_BREAK$4, lineStart);
2807
+ const lineEnd = text.indexOf(LINE_BREAK$5, lineStart);
2540
2808
  const end = lineEnd === -1 ? text.length : lineEnd;
2541
2809
  if (isDelimiterLine(text, lineStart, end, heredoc)) {
2542
2810
  const body = { command: heredoc.command, start, text: text.slice(start, lineStart) };
@@ -2558,7 +2826,7 @@ function readBodies(text, start, pending) {
2558
2826
  }
2559
2827
 
2560
2828
  const COMMENT = "#";
2561
- const LINE_BREAK$3 = "\n";
2829
+ const LINE_BREAK$4 = "\n";
2562
2830
  function skipTrivia(text, index) {
2563
2831
  const char = text[index];
2564
2832
  if (WORD_BREAKS.has(char)) {
@@ -2588,7 +2856,7 @@ function topLevelCommands(text, indent) {
2588
2856
  index = heredoc.end;
2589
2857
  } else if (afterTrivia !== index) {
2590
2858
  index = afterTrivia;
2591
- } else if (text[index] === LINE_BREAK$3 && pending.length > 0) {
2859
+ } else if (text[index] === LINE_BREAK$4 && pending.length > 0) {
2592
2860
  const read = readBodies(text, index + 1, pending);
2593
2861
  heredocs.push(...read.bodies);
2594
2862
  pending = [];
@@ -2676,13 +2944,20 @@ const shifted = (words, offset) => words.map((word) => ({ ...word, start: word.s
2676
2944
  function shellCommandsAt(text, offset, indent = 0) {
2677
2945
  return shellCommands(text, indent).map((words) => shifted(words, offset));
2678
2946
  }
2679
- const expansionCommands = (word) => wordExpansions(word.raw).flatMap(
2680
- (expansion) => shellCommandsAt(expansion.text, word.start + expansion.start)
2681
- );
2947
+ function expansionCommands(word) {
2948
+ const expansions = wordExpansions(word.raw);
2949
+ const literals = expressionLiterals(word.raw).filter(
2950
+ ({ start }) => expansions.every((expansion) => start < expansion.start || start >= expansion.expansionEnd)
2951
+ );
2952
+ return [...expansions, ...literals].flatMap(
2953
+ (part) => shellCommandsAt(part.text, word.start + part.start)
2954
+ );
2955
+ }
2682
2956
  function innerCommands(command) {
2683
2957
  const assignments = new Map(shellAssignments(command).map((found) => [found.word, found]));
2958
+ const code = inlineCode(command)?.code;
2684
2959
  return command.flatMap((word) => {
2685
- const inner = nestedCommandText(word);
2960
+ const inner = word === code ? void 0 : nestedCommandText(word);
2686
2961
  if (inner !== void 0) {
2687
2962
  return shellCommandsAt(inner, word.start + 1);
2688
2963
  }
@@ -2718,6 +2993,8 @@ function canRunGenerator(definition, generators) {
2718
2993
  return generators.has(definition.target);
2719
2994
  case "append":
2720
2995
  return false;
2996
+ case "run-time":
2997
+ return true;
2721
2998
  default:
2722
2999
  return assertNever(definition);
2723
3000
  }
@@ -2730,6 +3007,8 @@ function canRunOther(definition, others, defined) {
2730
3007
  return others.has(definition.target) || !defined.has(definition.target);
2731
3008
  case "append":
2732
3009
  return false;
3010
+ case "run-time":
3011
+ return true;
2733
3012
  default:
2734
3013
  return assertNever(definition);
2735
3014
  }
@@ -2758,15 +3037,24 @@ function variablePrograms(definitions) {
2758
3037
  return new Map([...defined].map((name) => [name, programOf(name)]));
2759
3038
  }
2760
3039
 
2761
- const SHELL_VARIABLE_RE = /^"?\$(?:([A-Za-z_][A-Za-z0-9_]*)|\{([A-Za-z_][A-Za-z0-9_]*)\})"?$/;
2762
- function expandedName(text) {
2763
- const match = SHELL_VARIABLE_RE.exec(text);
3040
+ const SHELL_VARIABLE_RE$1 = /^"?\$(?:([A-Za-z_][A-Za-z0-9_]*)|\{([A-Za-z_][A-Za-z0-9_]*)\})"?$/;
3041
+ function expandedName$1(text) {
3042
+ const match = SHELL_VARIABLE_RE$1.exec(text);
2764
3043
  return match === null ? void 0 : match[1] ?? match[2];
2765
3044
  }
2766
3045
  const textRunsGenerator = (text) => shellCommands(text).some(runsGenerator);
3046
+ function runsExpression(command) {
3047
+ const { program } = commandRoles(command);
3048
+ const word = program === void 0 ? void 0 : command[program];
3049
+ return word !== void 0 && isWholeExpression(word.raw);
3050
+ }
2767
3051
  function textDefinition(name, text) {
2768
- const target = expandedName(text.trim());
2769
- return target === void 0 ? { name, kind: "command", runsGenerator: textRunsGenerator(text) } : { name, kind: "reference", target };
3052
+ const target = expandedName$1(text.trim());
3053
+ if (target !== void 0) {
3054
+ return { name, kind: "reference", target };
3055
+ }
3056
+ const commands = shellCommands(text);
3057
+ return commands.some(runsExpression) ? { name, kind: "run-time" } : { name, kind: "command", runsGenerator: commands.some(runsGenerator) };
2770
3058
  }
2771
3059
  function expansionDefinitions(name, expansion) {
2772
3060
  const word = textDefinition(name, expansion.text);
@@ -2784,7 +3072,7 @@ function shellVariables(definitions) {
2784
3072
  values.set(name, [values.get(name), value].filter((part) => part !== void 0).join(" "));
2785
3073
  }
2786
3074
  const programs = variablePrograms(definitions.flatMap(programDefinitions$1));
2787
- const nameAt = (word) => expandedName(word.raw);
3075
+ const nameAt = (word) => expandedName$1(word.raw);
2788
3076
  return {
2789
3077
  programAt: (program) => {
2790
3078
  const name = nameAt(program);
@@ -2829,7 +3117,7 @@ function expansionUse(word, variables) {
2829
3117
  return void 0;
2830
3118
  }
2831
3119
  const programOf = (name) => (name === void 0 ? void 0 : variables.programAt({ ...word, raw: `$${name}` })?.program) ?? "other";
2832
- const target = expandedName(expansion.text.trim());
3120
+ const target = expandedName$1(expansion.text.trim());
2833
3121
  const textProgram = textRunsGenerator(expansion.text) ? "generator" : "other";
2834
3122
  const own = target === void 0 ? textProgram : programOf(target);
2835
3123
  const variable = programOf(expansion.use === "alternative" ? void 0 : expansion.name);
@@ -2847,7 +3135,7 @@ const VARIABLE_WORD_RE = new RegExp(
2847
3135
  );
2848
3136
  function withUnreadVariables(variables) {
2849
3137
  return {
2850
- programAt: (program) => variables.programAt(program) ?? (VARIABLE_WORD_RE.test(program.raw) ? { written: program.raw, program: "generator-or-other" } : void 0),
3138
+ programAt: (program) => variables.programAt(program) ?? (VARIABLE_WORD_RE.test(program.raw) || isWholeExpression(program.raw) ? { written: program.raw, program: "generator-or-other" } : void 0),
2851
3139
  referenceAt: (word) => variables.referenceAt(word)
2852
3140
  };
2853
3141
  }
@@ -2856,10 +3144,11 @@ const QUOTES$1 = /* @__PURE__ */ new Set(["'", '"']);
2856
3144
  const PATH_SEPARATOR$7 = "/";
2857
3145
  const BIN_FOLDER = "/.bin/";
2858
3146
  const OLD_PACKAGE_FOLDER = `node_modules/${GENERATOR_COMMAND}/`;
2859
- function renameAtEnd(word, name, to) {
3147
+ function renameAtEnd(word, name, to, suffix = "") {
2860
3148
  const at = word.raw.lastIndexOf(name);
2861
3149
  const tail = word.raw.slice(at + name.length);
2862
- if (at === -1 || tail !== "" && !QUOTES$1.has(tail)) {
3150
+ const quote = tail.slice(suffix.length);
3151
+ if (at === -1 || quote !== "" && !QUOTES$1.has(quote)) {
2863
3152
  return void 0;
2864
3153
  }
2865
3154
  return { start: word.start + at, end: word.start + at + name.length, text: to };
@@ -2879,27 +3168,30 @@ function packageFindings(found, locate) {
2879
3168
  const change = { kind: "package-name-renamed", location, from: name, to };
2880
3169
  return edit === void 0 ? [] : [{ offset, edit, change }];
2881
3170
  }
2882
- function programPathFindings(word, location) {
3171
+ function programPathFindings(word, { path, suffix }, location) {
2883
3172
  const offset = word.start;
2884
- if (word.value.includes(OLD_PACKAGE_FOLDER)) {
2885
- return [{ offset, manualStep: { kind: "old-package-file", location, path: word.value } }];
3173
+ if (path.includes(OLD_PACKAGE_FOLDER)) {
3174
+ return [{ offset, manualStep: { kind: "old-package-file", location, path } }];
2886
3175
  }
2887
- const isBin = word.value.endsWith(`${BIN_FOLDER}${GENERATOR_COMMAND}`);
2888
- const edit = isBin ? renameAtEnd(word, GENERATOR_COMMAND, MIGRATED_GENERATOR_COMMAND) : void 0;
3176
+ const isBin = path.endsWith(`${BIN_FOLDER}${GENERATOR_COMMAND}${suffix}`);
3177
+ const to = MIGRATED_GENERATOR_COMMAND;
3178
+ const edit = isBin ? renameAtEnd(word, GENERATOR_COMMAND, to, suffix) : void 0;
2889
3179
  return edit === void 0 ? [] : [{ offset, edit, change: { kind: "command-renamed", location } }];
2890
3180
  }
2891
3181
  function programFindings(word, location) {
2892
- if (word.value.includes(PATH_SEPARATOR$7)) {
2893
- return programPathFindings(word, location);
3182
+ const program = programPath(word);
3183
+ if (program.path.includes(PATH_SEPARATOR$7)) {
3184
+ return programPathFindings(word, program, location);
2894
3185
  }
2895
- const { name, isPinned } = packageReference(word.value);
3186
+ const { path, suffix } = program;
3187
+ const { name, isPinned } = packageReference(path.slice(0, path.length - suffix.length));
2896
3188
  if (name !== GENERATOR_COMMAND) {
2897
3189
  return [];
2898
3190
  }
2899
3191
  if (isPinned) {
2900
3192
  return [{ offset: word.start, manualStep: { kind: "versioned-command", location } }];
2901
3193
  }
2902
- const edit = renameAtEnd(word, GENERATOR_COMMAND, MIGRATED_GENERATOR_COMMAND);
3194
+ const edit = renameAtEnd(word, GENERATOR_COMMAND, MIGRATED_GENERATOR_COMMAND, suffix);
2903
3195
  const change = { kind: "command-renamed", location };
2904
3196
  return edit === void 0 ? [] : [{ offset: word.start, edit, change }];
2905
3197
  }
@@ -2961,6 +3253,14 @@ function commandFindings(command, locate, variables) {
2961
3253
  const handled = new Set(found.map((finding) => finding.offset));
2962
3254
  return [...found, ...unmigratedWordFindings(words, handled, locate)];
2963
3255
  }
3256
+ function inlineCodeFindings({ code, program }, locate) {
3257
+ if (!namesOriginalPackage(code.value)) {
3258
+ return [];
3259
+ }
3260
+ const location = locate(code.start);
3261
+ const manualStep = { kind: "script-not-shell", location, shell: program.value };
3262
+ return [{ offset: code.start, manualStep }];
3263
+ }
2964
3264
  function knownFindings(words, locate, variables) {
2965
3265
  const roles = commandRoles(words);
2966
3266
  const findings = roles.packages.flatMap((found2) => packageFindings(found2, locate));
@@ -2968,6 +3268,10 @@ function knownFindings(words, locate, variables) {
2968
3268
  if (program === void 0) {
2969
3269
  return findings;
2970
3270
  }
3271
+ const code = inlineCode(words);
3272
+ if (code !== void 0) {
3273
+ return [...findings, ...inlineCodeFindings(code, locate)];
3274
+ }
2971
3275
  const found = programFindings(program, locate(program.start));
2972
3276
  findings.push(...found);
2973
3277
  const isRenamed = found.some((finding) => finding.change?.kind === "command-renamed");
@@ -3289,36 +3593,52 @@ function assignedVariable(word) {
3289
3593
  const isQuoted = raw.length >= 2 && QUOTES.has(raw[0]) && raw.at(-1) === raw[0];
3290
3594
  const value = isQuoted ? raw.slice(1, -1) : raw;
3291
3595
  const start = word.start + at + 1 + (isQuoted ? 1 : 0);
3292
- return { name: word.raw.slice(0, at), commands: shellCommandsAt(value, start) };
3596
+ return { name: word.raw.slice(0, at), value, start };
3293
3597
  }
3294
- function dockerVariables(commands) {
3295
- return commands.flatMap(([instruction, first, ...rest]) => {
3598
+ function instructionVariables(text, words) {
3599
+ const [instruction, first, ...rest] = words;
3600
+ const last = rest.at(-1);
3601
+ if (instruction.value === LEGACY_ENV_INSTRUCTION && !first.raw.includes(ASSIGNMENT$1)) {
3602
+ const value = last === void 0 ? "" : text.slice(rest[0]?.start, last.end);
3603
+ return [{ name: first.value, value, start: rest[0]?.start ?? first.end }];
3604
+ }
3605
+ return [first, ...rest].flatMap((word) => assignedVariable(word) ?? []);
3606
+ }
3607
+ function variableInstructions(text, commands) {
3608
+ return commands.flatMap((words) => {
3609
+ const [instruction, first] = words;
3296
3610
  if (instruction === void 0 || first === void 0) {
3297
3611
  return [];
3298
3612
  }
3299
3613
  if (!VARIABLE_INSTRUCTIONS.has(instruction.value)) {
3300
3614
  return [];
3301
3615
  }
3302
- if (instruction.value === LEGACY_ENV_INSTRUCTION && !first.raw.includes(ASSIGNMENT$1)) {
3303
- return [{ name: first.value, commands: [rest] }];
3304
- }
3305
- return [first, ...rest].flatMap((word) => assignedVariable(word) ?? []);
3616
+ const end = words.at(-1).end;
3617
+ return [{ start: instruction.start, end, variables: instructionVariables(text, words) }];
3306
3618
  });
3307
3619
  }
3308
- const definitionOf = ({ name, commands }) => ({
3309
- name,
3310
- value: commands.flatMap((words) => words.map((word) => word.raw)).join(" ")
3311
- });
3620
+ function withoutVariableInstructions(commands, instructions) {
3621
+ const isInInstruction = (words) => {
3622
+ const start = words[0]?.start ?? -1;
3623
+ return instructions.some(
3624
+ (instruction) => start >= instruction.start && start < instruction.end
3625
+ );
3626
+ };
3627
+ return commands.filter((words) => !isInInstruction(words));
3628
+ }
3629
+ const valueCommands = ({ value, start }) => shellCommandsAt(value, start);
3312
3630
  function migrateDockerfile(text, file) {
3313
3631
  const lineOf = lineLocator(text);
3314
3632
  const locate = (offset) => `${file}:${lineOf(offset)}`;
3315
3633
  const forms = execForms(text);
3316
3634
  const exec = execCommands(forms);
3317
3635
  const shellForm = shellCommands(withoutExecForms(text, forms));
3318
- const definitions = dockerVariables(shellForm);
3319
- const values = definitions.flatMap((variable) => variable.commands);
3320
- const commands = [...shellForm, ...values, ...exec.commands];
3321
- const variables = shellVariables(definitions.map(definitionOf));
3636
+ const instructions = variableInstructions(text, shellForm);
3637
+ const definitions = instructions.flatMap((instruction) => instruction.variables);
3638
+ const values = definitions.flatMap(valueCommands);
3639
+ const run = withoutVariableInstructions(shellForm, instructions);
3640
+ const commands = [...run, ...values, ...exec.commands];
3641
+ const variables = shellVariables(definitions);
3322
3642
  const { edits, changes, manualSteps } = commandWordFindings(commands, locate, variables);
3323
3643
  const steps = [...manualSteps, ...escapedScriptSteps(exec.escaped, lineOf)];
3324
3644
  return { text: applyTextEdits(text, edits), changes, manualSteps: steps };
@@ -3670,8 +3990,8 @@ function distinctStrings(strings) {
3670
3990
  outer = enclosing.at(-1);
3671
3991
  }
3672
3992
  if (outer !== void 0 && found.end > outer.end) {
3673
- throw new Error(
3674
- `Source strings overlap at offsets ${outer.start}-${outer.end} and ${found.start}-${found.end}; the file was not changed.`
3993
+ throw new MigrationBugError(
3994
+ `source strings overlap at offsets ${outer.start}-${outer.end} and ${found.start}-${found.end}`
3675
3995
  );
3676
3996
  }
3677
3997
  enclosing.push(found);
@@ -3799,7 +4119,7 @@ const BACKTICK = "`";
3799
4119
  const ESCAPING_DELIMITER = '"';
3800
4120
  const ESCAPE = "\\";
3801
4121
  const COMMENT_START$1 = "#";
3802
- const LINE_BREAK$2 = "\n";
4122
+ const LINE_BREAK$3 = "\n";
3803
4123
  function closingOffset(text, delimiter, from) {
3804
4124
  if (delimiter !== ESCAPING_DELIMITER) {
3805
4125
  const close = text.indexOf(delimiter, from);
@@ -3809,7 +4129,7 @@ function closingOffset(text, delimiter, from) {
3809
4129
  const char = text[index];
3810
4130
  if (char === ESCAPE) {
3811
4131
  index += 1;
3812
- } else if (char === ESCAPING_DELIMITER || char === LINE_BREAK$2) {
4132
+ } else if (char === ESCAPING_DELIMITER || char === LINE_BREAK$3) {
3813
4133
  return char === ESCAPING_DELIMITER ? index : void 0;
3814
4134
  }
3815
4135
  }
@@ -3832,7 +4152,7 @@ function literalAt(text, offset) {
3832
4152
  function expressionAt(text, start) {
3833
4153
  const literals = [];
3834
4154
  let index = start;
3835
- while (index < text.length && text[index] !== LINE_BREAK$2 && text[index] !== COMMENT_START$1) {
4155
+ while (index < text.length && text[index] !== LINE_BREAK$3 && text[index] !== COMMENT_START$1) {
3836
4156
  const literal = literalAt(text, index);
3837
4157
  literals.push(...literal === void 0 ? [] : [literal]);
3838
4158
  index = literal?.end ?? index + 1;
@@ -3856,7 +4176,7 @@ function interpolationAt(text, start) {
3856
4176
  if (text.startsWith(INTERPOLATION_CLOSE, index)) {
3857
4177
  return { start, end: index + INTERPOLATION_CLOSE.length, literals };
3858
4178
  }
3859
- if (text[index] === LINE_BREAK$2) {
4179
+ if (text[index] === LINE_BREAK$3) {
3860
4180
  return void 0;
3861
4181
  }
3862
4182
  const literal = literalAt(text, index);
@@ -3877,18 +4197,19 @@ function interpolationsOf(text) {
3877
4197
  }
3878
4198
 
3879
4199
  const RECIPE_NAME_RE = /^@?[A-Za-z_][A-Za-z0-9_-]*(?=[ \t:])/gm;
3880
- const PARAMETER_RE = /^[+*$]*([A-Za-z_][A-Za-z0-9_-]*)(?:=([\s\S]*))?$/;
4200
+ const PARAMETER_RE = /^([+*$]*)([A-Za-z_][A-Za-z0-9_-]*)(?:=([\s\S]*))?$/;
4201
+ const EXPORT_PREFIX = "$";
3881
4202
  const NAME_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
3882
4203
  const COLON = ":";
3883
4204
  const EQUALS = "=";
3884
4205
  const OPEN_PAREN = "(";
3885
4206
  const CLOSE_PAREN = ")";
3886
4207
  const COMMENT_START = "#";
3887
- const LINE_BREAK$1 = "\n";
4208
+ const LINE_BREAK$2 = "\n";
3888
4209
  const BLANK_RE = /[ \t]/;
3889
4210
  const depthChange = (char) => char === OPEN_PAREN ? 1 : char === CLOSE_PAREN ? -1 : 0;
3890
4211
  function headerColon(text, from) {
3891
- for (let index = from; index < text.length && text[index] !== LINE_BREAK$1; ) {
4212
+ for (let index = from; index < text.length && text[index] !== LINE_BREAK$2; ) {
3892
4213
  const literal = literalAt(text, index);
3893
4214
  const char = text[index];
3894
4215
  if (literal !== void 0) {
@@ -3924,14 +4245,15 @@ function parameterWords(text, start, end) {
3924
4245
  function parameterDefault(text, word) {
3925
4246
  const written = text.slice(word.start, word.end);
3926
4247
  const match = PARAMETER_RE.exec(written);
3927
- const value = match?.[2];
4248
+ const value = match?.[3];
3928
4249
  if (match === null || value === void 0) {
3929
4250
  return [];
3930
4251
  }
3931
4252
  const defaultStart = word.end - value.length;
3932
4253
  const literals = literalsBetween(text, defaultStart, word.end);
3933
4254
  const target = NAME_RE.test(value) ? value : void 0;
3934
- return [{ name: match[1], literals, target }];
4255
+ const isExported = match[1].includes(EXPORT_PREFIX);
4256
+ return [{ name: match[2], isExported, literals, target }];
3935
4257
  }
3936
4258
  function recipeHeaders(text) {
3937
4259
  return [...text.matchAll(RECIPE_NAME_RE)].flatMap((match) => {
@@ -3946,7 +4268,136 @@ function recipeHeaders(text) {
3946
4268
  });
3947
4269
  }
3948
4270
 
3949
- const ASSIGNMENT_RE$1 = /^(?:export[ \t]+)?([A-Za-z_][A-Za-z0-9_-]*)[ \t]*:=[ \t]*/gm;
4271
+ const STEP_SHELL_KEY = "shell";
4272
+ const DEFAULT_SHELL_PATH = ["defaults", "run", STEP_SHELL_KEY];
4273
+ const COMMAND_LINE_SHELLS = /* @__PURE__ */ new Set([
4274
+ ...["sh", "bash", "dash", "ash", "zsh", "ksh"],
4275
+ ...["pwsh", "powershell", "cmd"],
4276
+ ...["msys2", "wsl-bash"]
4277
+ ]);
4278
+ const ENV_PROGRAM = "env";
4279
+ const ENV_OPTIONS_WITH_VALUE = /* @__PURE__ */ new Set(["-u", "--unset", "-C", "--chdir"]);
4280
+ const OPTION_START = "-";
4281
+ const ASSIGNMENT = "=";
4282
+ const PATH_SEPARATOR_RE = /[/\\]/;
4283
+ const WINDOWS_EXECUTABLE_RE = /\.exe$/i;
4284
+ const WHITESPACE_RE = /\s+/;
4285
+ const EXECUTORS_KEY = "executors";
4286
+ const EXECUTOR_SHELL_PATH_LENGTH = 3;
4287
+ const EXECUTOR_KEY = "executor";
4288
+ const EXECUTOR_NAME_KEY = "name";
4289
+ const programName = (word) => (word.split(PATH_SEPARATOR_RE).at(-1) ?? "").replace(WINDOWS_EXECUTABLE_RE, "");
4290
+ function envPrefixLength(words) {
4291
+ let index = 0;
4292
+ while (index < words.length) {
4293
+ const word = words[index] ?? "";
4294
+ if (ENV_OPTIONS_WITH_VALUE.has(word)) {
4295
+ index += 2;
4296
+ } else if (word.startsWith(OPTION_START) || word.includes(ASSIGNMENT)) {
4297
+ index += 1;
4298
+ } else {
4299
+ break;
4300
+ }
4301
+ }
4302
+ return index;
4303
+ }
4304
+ function shellProgram(shell) {
4305
+ const words = shell.trim().split(WHITESPACE_RE);
4306
+ const first = programName(words[0] ?? "");
4307
+ if (first !== ENV_PROGRAM) {
4308
+ return first;
4309
+ }
4310
+ const rest = words.slice(1);
4311
+ return programName(rest[envPrefixLength(rest)] ?? "");
4312
+ }
4313
+ const isCommandLineShell = (shell) => COMMAND_LINE_SHELLS.has(shellProgram(shell));
4314
+ const endsWith = (path, keys) => keys.every((key, index) => path[path.length - keys.length + index] === key);
4315
+ const isDefaultShell = (value) => endsWith(value.path, DEFAULT_SHELL_PATH);
4316
+ function scopeOf(value) {
4317
+ const keys = isDefaultShell(value) ? DEFAULT_SHELL_PATH.length : 1;
4318
+ return JSON.stringify(value.path.slice(0, -keys));
4319
+ }
4320
+ function executorShells(shells) {
4321
+ const isExecutorShell = ({ path }) => path.length === EXECUTOR_SHELL_PATH_LENGTH && path[0] === EXECUTORS_KEY;
4322
+ return new Map(
4323
+ shells.filter(isExecutorShell).map((shell) => [String(shell.path[1]), shell.value])
4324
+ );
4325
+ }
4326
+ function executorJob({ path }) {
4327
+ if (path.at(-1) === EXECUTOR_KEY) {
4328
+ return path.slice(0, -1);
4329
+ }
4330
+ const isNamed = path.at(-1) === EXECUTOR_NAME_KEY && path.at(-2) === EXECUTOR_KEY;
4331
+ return isNamed ? path.slice(0, -2) : void 0;
4332
+ }
4333
+ function inheritedShells(strings, shells) {
4334
+ const byExecutor = executorShells(shells);
4335
+ return strings.flatMap((value) => {
4336
+ const job = executorJob(value);
4337
+ const shell = byExecutor.get(value.value);
4338
+ return job === void 0 || shell === void 0 ? [] : [[JSON.stringify(job), shell]];
4339
+ });
4340
+ }
4341
+ function stepShells(strings) {
4342
+ const shells = strings.filter((value) => value.path.at(-1) === STEP_SHELL_KEY);
4343
+ const isOwnShell = (value) => !isDefaultShell(value);
4344
+ const ordered = [...shells.filter(isDefaultShell), ...shells.filter(isOwnShell)];
4345
+ const byScope = new Map([
4346
+ ...inheritedShells(strings, shells),
4347
+ ...ordered.map((value) => [scopeOf(value), value.value])
4348
+ ]);
4349
+ return (value) => {
4350
+ for (let depth = value.path.length - 1; depth >= 0; depth -= 1) {
4351
+ const shell = byScope.get(JSON.stringify(value.path.slice(0, depth)));
4352
+ if (shell !== void 0) {
4353
+ return isCommandLineShell(shell) ? void 0 : shell;
4354
+ }
4355
+ }
4356
+ return void 0;
4357
+ };
4358
+ }
4359
+ function otherShellFindings(values, shellOf, locate) {
4360
+ const manualSteps = values.flatMap((value) => {
4361
+ const shell = shellOf(value);
4362
+ return shell === void 0 || !namesOriginalPackage(value.value) ? [] : [{ kind: "script-not-shell", location: locate(value.start), shell }];
4363
+ });
4364
+ return { edits: [], changes: [], manualSteps };
4365
+ }
4366
+
4367
+ const LINE_BREAK$1 = "\n";
4368
+ const SHEBANG = "#!";
4369
+ const BODY_LINE_RE = /^(?:[ \t][^\n]*|[ \t]*\r?)$/;
4370
+ function nextLineStart(text, offset) {
4371
+ const lineBreak = text.indexOf(LINE_BREAK$1, offset);
4372
+ return lineBreak === -1 ? text.length : lineBreak + 1;
4373
+ }
4374
+ function bodyEnd(text, start) {
4375
+ let end = start;
4376
+ while (end < text.length) {
4377
+ const lineEnd = nextLineStart(text, end);
4378
+ if (!BODY_LINE_RE.test(text.slice(end, lineEnd).replace(LINE_BREAK$1, ""))) {
4379
+ break;
4380
+ }
4381
+ end = lineEnd;
4382
+ }
4383
+ return end;
4384
+ }
4385
+ function shebangRecipe(text, header) {
4386
+ const start = nextLineStart(text, header.end);
4387
+ const end = bodyEnd(text, start);
4388
+ const firstLine = text.slice(start, end).split(LINE_BREAK$1).map((line) => line.trim()).find((line) => line.length > 0);
4389
+ if (firstLine === void 0 || !firstLine.startsWith(SHEBANG)) {
4390
+ return [];
4391
+ }
4392
+ const shell = firstLine.slice(SHEBANG.length).trim();
4393
+ const shebang = text.indexOf(SHEBANG, start);
4394
+ return isCommandLineShell(shell) ? [] : [{ start, end, shebang, shell }];
4395
+ }
4396
+ const shebangRecipes = (text, headers) => headers.flatMap((header) => shebangRecipe(text, header));
4397
+
4398
+ const ASSIGNMENT_RE$1 = /^(export[ \t]+)?([A-Za-z_][A-Za-z0-9_-]*)[ \t]*:=[ \t]*/gm;
4399
+ const SET_EXPORT_RE = /^set[ \t]+export(?:[ \t]*:=[ \t]*true)?[ \t]*(?:#.*)?$/m;
4400
+ const SHELL_VARIABLE_RE = /^[@+-]*"?\$(?:([A-Za-z_][A-Za-z0-9_]*)|\{([A-Za-z_][A-Za-z0-9_]*)\})"?$/;
3950
4401
  const NAME_VALUE_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
3951
4402
  const VARIABLE_INTERPOLATION_RE = /^[@+-]*\{\{[ \t]*([A-Za-z_][A-Za-z0-9_-]*)[ \t]*\}\}$/;
3952
4403
  const MASK$1 = "_";
@@ -3956,7 +4407,8 @@ function assignmentsOf$1(text) {
3956
4407
  const valueStart = match.index + match[0].length;
3957
4408
  const { literals, end } = expressionAt(text, valueStart);
3958
4409
  const value = text.slice(valueStart, end).trim();
3959
- return { name: match[1], value, start: match.index, end, literals };
4410
+ const name = match[2];
4411
+ return { name, isExported: match[1] !== void 0, value, start: match.index, end, literals };
3960
4412
  });
3961
4413
  }
3962
4414
  function onlyString({ value, literals }) {
@@ -4001,11 +4453,15 @@ function shellText$1(text, spans) {
4001
4453
  return { text: masked, interpolations };
4002
4454
  }
4003
4455
  const literalCommands = (literals) => literals.flatMap((literal) => shellCommandsAt(literal.content, literal.contentStart));
4004
- function variableValues$1(assignments) {
4456
+ const defaultValue = ({ literals }) => literals.map((literal) => literal.content).join(BLANK);
4457
+ function variableValues$1(assignments, defaults) {
4005
4458
  const values = /* @__PURE__ */ new Map();
4006
- for (const assignment of assignments) {
4007
- const value = onlyString(assignment)?.content ?? assignment.value;
4008
- values.set(assignment.name, [values.get(assignment.name), value].join(" ").trim());
4459
+ const named = [
4460
+ ...assignments.map((found) => ({ ...found, value: onlyString(found)?.content ?? found.value })),
4461
+ ...defaults.map((found) => ({ name: found.name, value: defaultValue(found) }))
4462
+ ];
4463
+ for (const { name, value } of named) {
4464
+ values.set(name, [values.get(name), value].join(BLANK).trim());
4009
4465
  }
4010
4466
  return values;
4011
4467
  }
@@ -4013,9 +4469,20 @@ function computedProgram(written) {
4013
4469
  const literals = interpolationsOf(written).flatMap((interpolation) => interpolation.literals);
4014
4470
  return literals.some(stringRunsGenerator) ? "generator-or-other" : void 0;
4015
4471
  }
4016
- function justfileVariables(text, values, programs) {
4472
+ function exportedNames(text, assignments, defaults) {
4473
+ const isAll = SET_EXPORT_RE.test(text);
4474
+ const names = [...assignments, ...defaults].filter((found) => isAll || found.isExported);
4475
+ return new Set(names.map((found) => found.name));
4476
+ }
4477
+ function expandedName(written, exported) {
4478
+ const interpolated = VARIABLE_INTERPOLATION_RE.exec(written)?.[1];
4479
+ const match = SHELL_VARIABLE_RE.exec(written);
4480
+ const shellName = match === null ? void 0 : match[1] ?? match[2];
4481
+ return interpolated ?? (shellName !== void 0 && exported.has(shellName) ? shellName : void 0);
4482
+ }
4483
+ function justfileVariables(text, { values, programs, exported }) {
4017
4484
  const writtenAt = (word) => text.slice(word.start, word.end);
4018
- const nameAt = (word) => VARIABLE_INTERPOLATION_RE.exec(writtenAt(word))?.[1];
4485
+ const nameAt = (word) => expandedName(writtenAt(word), exported);
4019
4486
  return {
4020
4487
  programAt: (word) => {
4021
4488
  const name = nameAt(word);
@@ -4030,6 +4497,7 @@ function justfileVariables(text, values, programs) {
4030
4497
  }
4031
4498
  };
4032
4499
  }
4500
+ const shebangSteps = (text, recipes, locate) => recipes.filter((recipe) => namesOriginalPackage(text.slice(recipe.start, recipe.end))).map(({ shebang, shell }) => ({ kind: "script-not-shell", location: locate(shebang), shell }));
4033
4501
  function migrateJustfile(text, file) {
4034
4502
  const lineOf = lineLocator(text);
4035
4503
  const assignments = assignmentsOf$1(text);
@@ -4039,8 +4507,13 @@ function migrateJustfile(text, file) {
4039
4507
  ...assignments.flatMap(assignmentDefinitions),
4040
4508
  ...defaults.flatMap(parameterDefinitions)
4041
4509
  ]);
4042
- const variables = justfileVariables(text, variableValues$1(assignments), programs);
4043
- const shell = shellText$1(text, [...assignments, ...headers]);
4510
+ const variables = justfileVariables(text, {
4511
+ values: variableValues$1(assignments, defaults),
4512
+ programs,
4513
+ exported: exportedNames(text, assignments, defaults)
4514
+ });
4515
+ const scripts = shebangRecipes(text, headers);
4516
+ const shell = shellText$1(text, [...assignments, ...headers, ...scripts]);
4044
4517
  const commands = [
4045
4518
  ...shellCommands(shell.text),
4046
4519
  ...literalCommands(assignments.flatMap((assignment) => assignment.literals)),
@@ -4049,26 +4522,14 @@ function migrateJustfile(text, file) {
4049
4522
  ];
4050
4523
  const locate = (offset) => `${file}:${lineOf(offset)}`;
4051
4524
  const { edits, changes, manualSteps } = commandWordFindings(commands, locate, variables);
4052
- return { text: applyTextEdits(text, edits), changes, manualSteps };
4525
+ const steps = [...manualSteps, ...shebangSteps(text, scripts, locate)];
4526
+ return { text: applyTextEdits(text, edits), changes, manualSteps: steps };
4053
4527
  }
4054
4528
 
4055
- const ASSIGNMENT_RE = new RegExp(
4056
- [
4057
- "^(?:[^\\s:#=][^:#=\\n]*:[ \\t]*)?",
4058
- "(?:(?:export|override|private)[ \\t]+)*",
4059
- "([A-Za-z_][A-Za-z0-9_.-]*)",
4060
- "[ \\t]*(:::=|::=|:=|\\?=|\\+=|!=|=)"
4061
- ].join(""),
4062
- "gm"
4063
- );
4064
- const VARIABLE_REFERENCE_RE = /^\$(?:\(([A-Za-z_][A-Za-z0-9_.-]*)\)|\{([A-Za-z_][A-Za-z0-9_.-]*)\}|([A-Za-z_]))$/;
4065
- const RECIPE_PREFIX_RE = /^[@+-]+/;
4066
4529
  const DOLLAR = "$";
4067
4530
  const CLOSING = { "(": ")", "{": "}" };
4068
- const NOT_LINE_BREAK_RE = /[^\n]/g;
4069
4531
  const MASK = "_";
4070
- const LINE_BREAK = "\n";
4071
- const BACKSLASH = "\\";
4532
+ const ARGUMENT_SEPARATOR = ",";
4072
4533
  function expansionEnd(text, start) {
4073
4534
  const open = text[start + 1] ?? "";
4074
4535
  const close = CLOSING[open];
@@ -4085,9 +4546,8 @@ function expansionEnd(text, start) {
4085
4546
  }
4086
4547
  return void 0;
4087
4548
  }
4088
- function maskExpansions(text) {
4089
- const parts = [];
4090
- let copiedUpTo = 0;
4549
+ function expansionsOf(text) {
4550
+ const found = [];
4091
4551
  for (let index = text.indexOf(DOLLAR); index !== -1; index = text.indexOf(DOLLAR, index + 1)) {
4092
4552
  if (text[index + 1] === DOLLAR) {
4093
4553
  index++;
@@ -4095,15 +4555,75 @@ function maskExpansions(text) {
4095
4555
  }
4096
4556
  const end = expansionEnd(text, index);
4097
4557
  if (end !== void 0) {
4098
- parts.push(text.slice(copiedUpTo, index + 1), MASK.repeat(end - index - 1));
4099
- copiedUpTo = end;
4558
+ found.push({ start: index, end });
4100
4559
  index = end - 1;
4101
4560
  }
4102
4561
  }
4562
+ return found;
4563
+ }
4564
+ function maskExpansions(text) {
4565
+ const parts = [];
4566
+ let copiedUpTo = 0;
4567
+ for (const { start, end } of expansionsOf(text)) {
4568
+ parts.push(text.slice(copiedUpTo, start + 1), MASK.repeat(end - start - 1));
4569
+ copiedUpTo = end;
4570
+ }
4103
4571
  parts.push(text.slice(copiedUpTo));
4104
4572
  return parts.join("");
4105
4573
  }
4574
+ const VALUE_ARGUMENTS = /* @__PURE__ */ new Map([
4575
+ ["if", 1],
4576
+ ["or", 0],
4577
+ ["and", 0]
4578
+ ]);
4579
+ const FUNCTION_HEAD_RE = /^\$[({]([a-z]+)[ \t]+/;
4580
+ const OPENING = new Set(Object.keys(CLOSING));
4581
+ const CLOSINGS = new Set(Object.values(CLOSING));
4582
+ function callArguments(text, start, end) {
4583
+ const found = [];
4584
+ let argumentStart = start;
4585
+ let depth = 0;
4586
+ for (let index = start; index < end; index++) {
4587
+ const char = text[index];
4588
+ depth += OPENING.has(char) ? 1 : CLOSINGS.has(char) ? -1 : 0;
4589
+ if (char === ARGUMENT_SEPARATOR && depth === 0) {
4590
+ found.push({ text: text.slice(argumentStart, index), start: argumentStart });
4591
+ argumentStart = index + 1;
4592
+ }
4593
+ }
4594
+ return [...found, { text: text.slice(argumentStart, end), start: argumentStart }];
4595
+ }
4596
+ function valueArguments(text, { start, end }) {
4597
+ const head = FUNCTION_HEAD_RE.exec(text.slice(start, end));
4598
+ const first = head === null ? void 0 : VALUE_ARGUMENTS.get(head[1]);
4599
+ if (head === null || first === void 0) {
4600
+ return [];
4601
+ }
4602
+ return callArguments(text, start + head[0].length, end - 1).slice(first);
4603
+ }
4604
+ function functionCommands(text, offset = 0) {
4605
+ return expansionsOf(text).flatMap((expansion) => valueArguments(text, expansion)).flatMap((argument) => [
4606
+ ...shellCommandsAt(maskExpansions(argument.text), offset + argument.start),
4607
+ ...functionCommands(argument.text, offset + argument.start)
4608
+ ]);
4609
+ }
4610
+
4611
+ const ASSIGNMENT_RE = new RegExp(
4612
+ [
4613
+ "^(?:[^\\s:#=][^:#=\\n]*:[ \\t]*)?",
4614
+ "(?:(?:export|override|private)[ \\t]+)*",
4615
+ "([A-Za-z_][A-Za-z0-9_.-]*)",
4616
+ "[ \\t]*(:::=|::=|:=|\\?=|\\+=|!=|=)"
4617
+ ].join(""),
4618
+ "gm"
4619
+ );
4620
+ const VARIABLE_REFERENCE_RE = /^\$(?:\(([A-Za-z_][A-Za-z0-9_.-]*)\)|\{([A-Za-z_][A-Za-z0-9_.-]*)\}|([A-Za-z_]))$/;
4621
+ const RECIPE_PREFIX_RE = /^[@+-]+/;
4622
+ const NOT_LINE_BREAK_RE = /[^\n]/g;
4623
+ const LINE_BREAK = "\n";
4624
+ const BACKSLASH = "\\";
4106
4625
  const APPEND_OPERATOR = "+=";
4626
+ const CONDITIONAL_OPERATOR = "?=";
4107
4627
  function logicalLineEnd(text, index) {
4108
4628
  let end = text.indexOf(LINE_BREAK, index);
4109
4629
  while (end !== -1 && text[end - 1] === BACKSLASH) {
@@ -4115,8 +4635,8 @@ const assignmentsOf = (text) => [...text.matchAll(ASSIGNMENT_RE)].map((match) =>
4115
4635
  const valueStart = match.index + match[0].length;
4116
4636
  const valueEnd = logicalLineEnd(text, valueStart);
4117
4637
  const name = match[1];
4118
- const isAppend = match[2] === APPEND_OPERATOR;
4119
- return { name, isAppend, start: match.index, valueStart, valueEnd };
4638
+ const operator = match[2];
4639
+ return { name, operator, start: match.index, valueStart, valueEnd };
4120
4640
  });
4121
4641
  function withoutAssignmentHeads(text, assignments) {
4122
4642
  const parts = [];
@@ -4131,22 +4651,40 @@ function withoutAssignmentHeads(text, assignments) {
4131
4651
  parts.push(text.slice(copiedUpTo));
4132
4652
  return parts.join("");
4133
4653
  }
4654
+ const COMPUTED_RE = /^\$[({]/;
4655
+ const isComputed = (written) => COMPUTED_RE.test(written.trim().replace(RECIPE_PREFIX_RE, ""));
4134
4656
  function referencedName(written) {
4135
4657
  const match = VARIABLE_REFERENCE_RE.exec(written.trim().replace(RECIPE_PREFIX_RE, ""));
4136
4658
  return match === null ? void 0 : match[1] ?? match[2] ?? match[3];
4137
4659
  }
4660
+ function programWritten(text, command) {
4661
+ const { program } = commandRoles(command);
4662
+ const word = program === void 0 ? void 0 : command[program];
4663
+ return word === void 0 ? "" : text.slice(word.start, word.end);
4664
+ }
4665
+ function valueDefinition(text, { name, valueStart, valueEnd }, commands) {
4666
+ const inValue = commands.filter(
4667
+ ([first2]) => first2 !== void 0 && valueStart <= first2.start && first2.start < valueEnd
4668
+ );
4669
+ const [first] = inValue.sort((left, right) => (left[0]?.start ?? 0) - (right[0]?.start ?? 0));
4670
+ const program = first === void 0 ? "" : programWritten(text, first);
4671
+ const target = referencedName(program);
4672
+ if (target !== void 0) {
4673
+ return { name, kind: "reference", target };
4674
+ }
4675
+ if (isComputed(program)) {
4676
+ return { name, kind: "run-time" };
4677
+ }
4678
+ return { name, kind: "command", runsGenerator: inValue.some(runsGenerator) };
4679
+ }
4138
4680
  function programDefinitions(text, commands, assignments) {
4139
- const generatorStarts = commands.filter(runsGenerator).map((command) => command[0].start);
4140
- return assignments.map(({ name, isAppend, valueStart, valueEnd }) => {
4141
- if (isAppend) {
4142
- return { name, kind: "append" };
4143
- }
4144
- const target = referencedName(text.slice(valueStart, valueEnd));
4145
- if (target !== void 0) {
4146
- return { name, kind: "reference", target };
4681
+ return assignments.flatMap((assignment) => {
4682
+ const { name, operator } = assignment;
4683
+ if (operator === APPEND_OPERATOR) {
4684
+ return [{ name, kind: "append" }];
4147
4685
  }
4148
- const runs = generatorStarts.some((start) => valueStart <= start && start < valueEnd);
4149
- return { name, kind: "command", runsGenerator: runs };
4686
+ const value = valueDefinition(text, assignment, commands);
4687
+ return operator === CONDITIONAL_OPERATOR ? [value, { name, kind: "run-time" }] : [value];
4150
4688
  });
4151
4689
  }
4152
4690
  function variableValues(text, assignments) {
@@ -4162,10 +4700,12 @@ function makefileVariables(text, assignments, programs) {
4162
4700
  const nameAt = (word) => referencedName(text.slice(word.start, word.end));
4163
4701
  const variables = {
4164
4702
  programAt: (program) => {
4165
- const name = nameAt(program);
4166
- const found = name === void 0 ? void 0 : programs.get(name) ?? "generator-or-other";
4167
4703
  const written = text.slice(program.start, program.end);
4168
- return found === void 0 ? void 0 : { written, program: found };
4704
+ const name = nameAt(program);
4705
+ if (name === void 0) {
4706
+ return isComputed(written) ? { written, program: "generator-or-other" } : void 0;
4707
+ }
4708
+ return { written, program: programs.get(name) ?? "generator-or-other" };
4169
4709
  },
4170
4710
  referenceAt: (word) => {
4171
4711
  const name = nameAt(word);
@@ -4179,7 +4719,10 @@ function makefileVariables(text, assignments, programs) {
4179
4719
  function migrateMakefile(text, file) {
4180
4720
  const lineOf = lineLocator(text);
4181
4721
  const assignments = assignmentsOf(text);
4182
- const commands = shellCommands(withoutAssignmentHeads(maskExpansions(text), assignments));
4722
+ const commands = [
4723
+ ...shellCommands(withoutAssignmentHeads(maskExpansions(text), assignments)),
4724
+ ...functionCommands(text)
4725
+ ];
4183
4726
  const programs = variablePrograms(programDefinitions(text, commands, assignments));
4184
4727
  const locate = (offset) => `${file}:${lineOf(offset)}`;
4185
4728
  const variables = makefileVariables(text, assignments, programs);
@@ -4527,72 +5070,30 @@ function oldPackagePatchSteps(fileName) {
4527
5070
  return name === void 0 || to === void 0 ? [] : [{ kind: "old-package-patch", name, to }];
4528
5071
  }
4529
5072
 
4530
- const STEP_SHELL_KEY = "shell";
4531
- const DEFAULT_SHELL_PATH = ["defaults", "run", STEP_SHELL_KEY];
4532
- const COMMAND_LINE_SHELLS = /* @__PURE__ */ new Set([
4533
- ...["sh", "bash", "dash", "ash", "zsh", "ksh"],
4534
- ...["pwsh", "powershell", "cmd"],
4535
- ...["msys2", "wsl-bash"]
4536
- ]);
4537
- const ENV_PROGRAM = "env";
4538
- const ENV_OPTIONS_WITH_VALUE = /* @__PURE__ */ new Set(["-u", "--unset", "-C", "--chdir"]);
4539
- const OPTION_START = "-";
4540
- const ASSIGNMENT = "=";
4541
- const PATH_SEPARATOR_RE = /[/\\]/;
4542
- const WINDOWS_EXECUTABLE_RE = /\.exe$/i;
4543
- const WHITESPACE_RE = /\s+/;
4544
- const programName = (word) => (word.split(PATH_SEPARATOR_RE).at(-1) ?? "").replace(WINDOWS_EXECUTABLE_RE, "");
4545
- function envPrefixLength(words) {
4546
- let index = 0;
4547
- while (index < words.length) {
4548
- const word = words[index] ?? "";
4549
- if (ENV_OPTIONS_WITH_VALUE.has(word)) {
4550
- index += 2;
4551
- } else if (word.startsWith(OPTION_START) || word.includes(ASSIGNMENT)) {
4552
- index += 1;
4553
- } else {
4554
- break;
4555
- }
4556
- }
4557
- return index;
4558
- }
4559
- function shellProgram(shell) {
4560
- const words = shell.trim().split(WHITESPACE_RE);
4561
- const first = programName(words[0] ?? "");
4562
- if (first !== ENV_PROGRAM) {
4563
- return first;
5073
+ const INPUTS_KEY = "inputs";
5074
+ const DEFAULT_KEY = "default";
5075
+ const INPUT_DEFAULT_DEPTH = 3;
5076
+ const INPUT_NAME_INDEX = -2;
5077
+ const isInputDefault = (value) => value.path.length >= INPUT_DEFAULT_DEPTH && value.path.at(-1) === DEFAULT_KEY && value.path.at(-INPUT_DEFAULT_DEPTH) === INPUTS_KEY;
5078
+ const INPUT_EXPRESSION_RE = /^"?\$\{\{[ \t]*inputs\.([A-Za-z_][A-Za-z0-9_-]*)[ \t]*\}\}"?$/;
5079
+ function withInputDefaults(variables, strings) {
5080
+ const values = /* @__PURE__ */ new Map();
5081
+ for (const value of strings.filter(isInputDefault)) {
5082
+ const name = String(value.path.at(INPUT_NAME_INDEX));
5083
+ values.set(
5084
+ name,
5085
+ [values.get(name), value.value].filter((part) => part !== void 0).join(" ")
5086
+ );
4564
5087
  }
4565
- const rest = words.slice(1);
4566
- return programName(rest[envPrefixLength(rest)] ?? "");
4567
- }
4568
- const endsWith = (path, keys) => keys.every((key, index) => path[path.length - keys.length + index] === key);
4569
- const isDefaultShell = (value) => endsWith(value.path, DEFAULT_SHELL_PATH);
4570
- function scopeOf(value) {
4571
- const keys = isDefaultShell(value) ? DEFAULT_SHELL_PATH.length : 1;
4572
- return JSON.stringify(value.path.slice(0, -keys));
4573
- }
4574
- function stepShells(strings) {
4575
- const shells = strings.filter((value) => value.path.at(-1) === STEP_SHELL_KEY);
4576
- const isOwnShell = (value) => !isDefaultShell(value);
4577
- const ordered = [...shells.filter(isDefaultShell), ...shells.filter(isOwnShell)];
4578
- const byScope = new Map(ordered.map((value) => [scopeOf(value), value.value]));
4579
- return (value) => {
4580
- for (let depth = value.path.length - 1; depth >= 0; depth -= 1) {
4581
- const shell = byScope.get(JSON.stringify(value.path.slice(0, depth)));
4582
- if (shell !== void 0) {
4583
- return COMMAND_LINE_SHELLS.has(shellProgram(shell)) ? void 0 : shell;
4584
- }
5088
+ return {
5089
+ programAt: (program) => variables.programAt(program),
5090
+ referenceAt: (word) => {
5091
+ const name = INPUT_EXPRESSION_RE.exec(word.raw)?.[1];
5092
+ const value = name === void 0 ? void 0 : values.get(name);
5093
+ return variables.referenceAt(word) ?? (value === void 0 ? void 0 : { written: word.raw, value });
4585
5094
  }
4586
- return void 0;
4587
5095
  };
4588
5096
  }
4589
- function otherShellFindings(values, shellOf, locate) {
4590
- const manualSteps = values.flatMap((value) => {
4591
- const shell = shellOf(value);
4592
- return shell === void 0 || !namesOriginalPackage(value.value) ? [] : [{ kind: "script-not-shell", location: locate(value.start), shell }];
4593
- });
4594
- return { edits: [], changes: [], manualSteps };
4595
- }
4596
5097
 
4597
5098
  const YAML_COMMAND_KEYS = /* @__PURE__ */ new Set([
4598
5099
  "run",
@@ -4647,7 +5148,15 @@ function blockIndent(shell) {
4647
5148
  const indents = lines.map((line) => LEADING_SPACES_RE.exec(line)?.[0].length ?? 0);
4648
5149
  return indents.length === 0 ? 0 : Math.min(...indents);
4649
5150
  }
4650
- const valuesOf = (strings, keys) => strings.filter((string) => keys.has(ownerKey(string.path) ?? ""));
5151
+ function oncePerPlace(strings) {
5152
+ const starts = /* @__PURE__ */ new Set();
5153
+ return strings.filter((string) => {
5154
+ const isFirst = !starts.has(string.start);
5155
+ starts.add(string.start);
5156
+ return isFirst;
5157
+ });
5158
+ }
5159
+ const valuesOf = (strings, keys) => oncePerPlace(strings.filter((string) => keys.has(ownerKey(string.path) ?? "")));
4651
5160
  const EXEC_FORM_KEYS = /* @__PURE__ */ new Set(["command", "entrypoint"]);
4652
5161
  const isExecFormItem = (value) => typeof value.path.at(-1) === "number" && EXEC_FORM_KEYS.has(String(value.path.at(-2)));
4653
5162
  const listOf = (item) => item.path.slice(0, -1).join(".");
@@ -4702,10 +5211,12 @@ function migrateYamlCommands(text, file, strings, keys = YAML_COMMAND_KEYS) {
4702
5211
  const shellOf = stepShells(strings);
4703
5212
  const values = valuesOf(strings, keys).filter((value) => shellOf(value) === void 0);
4704
5213
  const commandValues = new Set(values.filter((value) => !isExecFormItem(value)));
4705
- const shellValues = strings.filter(
4706
- (value) => commandValues.has(value) || isEnvironmentValue(value)
5214
+ const shellValues = oncePerPlace(
5215
+ strings.filter(
5216
+ (value) => commandValues.has(value) || isEnvironmentValue(value) || isInputDefault(value)
5217
+ )
4707
5218
  );
4708
- const variables = environmentVariables(text, strings);
5219
+ const variables = withInputDefaults(environmentVariables(text, strings), strings);
4709
5220
  return appliedFindings(text, [
4710
5221
  ...shellValues.map((value) => valueFindings(text, value, locate, variables)),
4711
5222
  commandWordFindings(execFormCommands(text, values.filter(isExecFormItem)), locate),
@@ -4879,6 +5390,7 @@ function migrateConfig(text, read, steps) {
4879
5390
  );
4880
5391
  }
4881
5392
  const stringsStep = (migrate) => (text, read) => read.kind === "unreadable" ? unreadableConfig(text, read.offset) : migrate(text, documentStrings(read.documents));
5393
+ const usedStringsStep = (migrate) => (text, read) => read.kind === "unreadable" ? unreadableConfig(text, read.offset) : migrate(text, usedDocumentStrings(read.documents));
4882
5394
  const jsonConfig = (steps) => (text, path, context) => migrateConfig(text, (current) => context.readers.json.read(current), steps(context, path));
4883
5395
  const yamlConfig = (steps) => (text, path, context) => migrateConfig(text, (current) => context.readers.yaml.read(current), steps(context, path));
4884
5396
  const whenNamed = (migrate) => (text, path, context) => namesOriginalPackage(text) ? migrate(text, path, context) : unchanged(text);
@@ -4927,6 +5439,28 @@ const JSON5_MANIFEST_FILE = "package.json5";
4927
5439
  const MANIFEST_FILE = "package.json";
4928
5440
  const PROJECT_CONFIG_NAME = /^[jt]sconfig(\.[^/]+)?\.json$/;
4929
5441
  const LOCK_FILES = /* @__PURE__ */ new Set(["pnpm-lock.yaml", "yarn.lock", "bun.lock"]);
5442
+ const CI_CONFIG_FILES = /* @__PURE__ */ new Set([
5443
+ ".gitlab-ci.yml",
5444
+ ".gitlab-ci.yaml",
5445
+ "bitbucket-pipelines.yml",
5446
+ "azure-pipelines.yml",
5447
+ ".drone.yml",
5448
+ ".woodpecker.yml",
5449
+ ".travis.yml"
5450
+ ]);
5451
+ const CI_CONFIG_FOLDERS = [
5452
+ ".github/workflows/",
5453
+ ".gitea/workflows/",
5454
+ ".forgejo/workflows/",
5455
+ ".circleci/",
5456
+ ".gitlab/",
5457
+ ".woodpecker/"
5458
+ ];
5459
+ const FOLDER_SEPARATOR = "/";
5460
+ function isCiConfigFile(path) {
5461
+ const inFolder = (folder) => path.startsWith(folder) || path.includes(`${FOLDER_SEPARATOR}${folder}`);
5462
+ return CI_CONFIG_FILES.has(baseName(path)) || CI_CONFIG_FOLDERS.some(inFolder);
5463
+ }
4930
5464
  function baseName(path) {
4931
5465
  return path.slice(path.lastIndexOf("/") + 1);
4932
5466
  }
@@ -5012,7 +5546,7 @@ const COMMAND_CONFIG_KINDS = [
5012
5546
  matches: (path) => TASKFILE_NAMES.has(baseName(path)),
5013
5547
  migrate: whenNamed(
5014
5548
  yamlConfig((_context, path) => [
5015
- stringsStep(
5549
+ usedStringsStep(
5016
5550
  (text, strings) => migrateYamlCommands(text, path, strings, TASKFILE_COMMAND_KEYS)
5017
5551
  )
5018
5552
  ])
@@ -5027,7 +5561,10 @@ const COMMAND_CONFIG_KINDS = [
5027
5561
  )
5028
5562
  }
5029
5563
  ];
5030
- const yamlCommandsStep = (path, isPart) => (text, read) => isPart || read.kind === "read" && isApiDescription(read.documents) ? unchanged(text) : stringsStep((current, strings) => migrateYamlCommands(current, path, strings))(text, read);
5564
+ const yamlCommandsStep = (path, isPart) => (text, read) => isPart || !isCiConfigFile(path) && read.kind === "read" && isApiDescription(read.documents) ? unchanged(text) : usedStringsStep((current, strings) => migrateYamlCommands(current, path, strings))(
5565
+ text,
5566
+ read
5567
+ );
5031
5568
  const manifestTarget = ({ version, catalogs, aliases }, path) => ({
5032
5569
  version,
5033
5570
  catalogs,
@@ -5139,7 +5676,7 @@ function fileMigrationFor(path, namedImportMaps = NO_NAMED_IMPORT_MAPS) {
5139
5676
  const NO_API_DESCRIPTION_PARTS = /* @__PURE__ */ new Set();
5140
5677
  const JSON_EXTENSION = ".json";
5141
5678
  const isYaml = (path) => YAML_EXTENSIONS.has(extensionOf(path));
5142
- const isDescriptionFormat = (path) => isYaml(path) || extensionOf(path) === JSON_EXTENSION;
5679
+ const isDescriptionFormat = (path) => (isYaml(path) || extensionOf(path) === JSON_EXTENSION) && !isCiConfigFile(path);
5143
5680
  async function textOf(path, files) {
5144
5681
  const read = await files.read(path);
5145
5682
  return read.kind === "read" ? read.text : void 0;
@@ -5275,6 +5812,16 @@ async function planProjectTarget(paths, dependencies, version) {
5275
5812
  };
5276
5813
  }
5277
5814
 
5815
+ function attemptMigration(migrate) {
5816
+ try {
5817
+ return { kind: "migrated", migration: migrate() };
5818
+ } catch (error) {
5819
+ if (error instanceof MigrationBugError) {
5820
+ return { kind: "skipped", reason: { kind: "migration-bug", message: error.message } };
5821
+ }
5822
+ throw error;
5823
+ }
5824
+ }
5278
5825
  async function planFile(path, dependencies, target) {
5279
5826
  const migrate = fileMigrationFor(path, target.importMaps);
5280
5827
  if (migrate === void 0) {
@@ -5290,7 +5837,11 @@ async function planFile(path, dependencies, target) {
5290
5837
  ...{ version, catalogs, manifests, aliases, apiDescriptionParts },
5291
5838
  ...{ rewriter, scanner, readers }
5292
5839
  };
5293
- const migration = migrate(read.text, path, context);
5840
+ const attempt = attemptMigration(() => migrate(read.text, path, context));
5841
+ if (attempt.kind === "skipped") {
5842
+ return attempt;
5843
+ }
5844
+ const { migration } = attempt;
5294
5845
  const isChanged = migration.text !== read.text;
5295
5846
  const { changes, manualSteps } = migration;
5296
5847
  if (isChanged || manualSteps.length > 0) {
@@ -5418,7 +5969,7 @@ function commandStepMessage(step) {
5418
5969
  case "old-name-in-command":
5419
5970
  return `${step.location}: "${step.word}" ${OLD_NAME_IN_COMMAND_NOTE}`;
5420
5971
  case "script-not-shell":
5421
- return `${step.location}: the step runs with shell: ${step.shell}, ${SCRIPT_NOT_SHELL_NOTE}`;
5972
+ return `${step.location}: the script runs with ${step.shell}, ${SCRIPT_NOT_SHELL_NOTE}`;
5422
5973
  case "command-string":
5423
5974
  return `line ${step.line}: ${COMMAND_STRING_NOTE} "${step.to}"`;
5424
5975
  default:
@@ -5434,9 +5985,12 @@ const KEEP_REASONS = {
5434
5985
  "shared-options": "its options object is used elsewhere too",
5435
5986
  "unknown-return": "a return value is not a plain ts.factory type",
5436
5987
  "typed-options": "its options are declared with a type built from OpenAPITSOptions, which the tool cannot change to OpenAPITSIrOptions",
5437
- "spread-options": "its options take keys from another object (`...base`), whose callbacks and type the tool cannot see"
5988
+ "spread-options": "its options take keys from another object (`...base`), whose callbacks and type the tool cannot see",
5989
+ "declared-type": "its declared type (a `const` type, JSDoc `@type` or `@returns`) names a type the tool cannot change to type text",
5990
+ "in-spread": "it is in an object that the options take keys from (`...base`), and the tool rewrites only a transform written in the options"
5438
5991
  };
5439
5992
  const OPTIONS_NOTE = `the options of openapiTS() are not an object written in this file: check them for transform, postTransform and transformProperty. See ${IR_CALLBACKS_GUIDE}`;
5993
+ const SPREAD_NOTE = `the options of openapiTS() take keys from an object the tool cannot read (\`...base\`): check it for transform, postTransform and transformProperty. See ${IR_CALLBACKS_GUIDE}`;
5440
5994
  const DEEP_IMPORT_NOTE = "imports a file inside the package; check that it exists";
5441
5995
  const OLD_FILE_NOTE = "imports a file from node_modules of";
5442
5996
  const OLD_FILE_FIX_NOTE = "which is no longer installed: import from";
@@ -5452,6 +6006,7 @@ function packageNotDeclaredMessage(step) {
5452
6006
  }
5453
6007
  const SOURCE_STEP_KINDS = /* @__PURE__ */ new Set([
5454
6008
  "options-not-inspected",
6009
+ "options-spread-not-inspected",
5455
6010
  "deep-import",
5456
6011
  "old-package-import",
5457
6012
  "dynamic-specifier",
@@ -5465,6 +6020,8 @@ function sourceStepMessage(step) {
5465
6020
  switch (step.kind) {
5466
6021
  case "options-not-inspected":
5467
6022
  return `line ${step.line}: ${OPTIONS_NOTE}`;
6023
+ case "options-spread-not-inspected":
6024
+ return `line ${step.line}: ${SPREAD_NOTE}`;
5468
6025
  case "deep-import":
5469
6026
  return `line ${step.line}: "${step.specifier}" ${DEEP_IMPORT_NOTE}`;
5470
6027
  case "old-package-import":
@@ -5619,7 +6176,7 @@ function changeMessage(change) {
5619
6176
  return assertNever(change);
5620
6177
  }
5621
6178
  }
5622
- const SKIPPED_HEADING = "Skipped (not read, not written):";
6179
+ const SKIPPED_HEADING = "Skipped (not migrated, not written):";
5623
6180
  function skippedMessage(reason) {
5624
6181
  switch (reason.kind) {
5625
6182
  case "outside-root":
@@ -5628,6 +6185,8 @@ function skippedMessage(reason) {
5628
6185
  return "it is not UTF-8 text, so it was not changed; check it by hand";
5629
6186
  case "io-error":
5630
6187
  return `it could not be read (${reason.code})`;
6188
+ case "migration-bug":
6189
+ return `the tool could not migrate it (${reason.message}), a bug of the tool: it was not changed, so migrate it by hand, and please report the bug`;
5631
6190
  default:
5632
6191
  return assertNever(reason);
5633
6192
  }
@@ -5656,6 +6215,7 @@ const NEXT_STEPS = [
5656
6215
  const DRY_RUN_NOTE = "Dry run: nothing was written. Run again with --write to apply.";
5657
6216
  const WRITTEN_NOTE = "Files were written.";
5658
6217
  const NOTHING_FOUND = "Nothing to migrate: no openapi-typescript, openapi-fetch, openapi-react-query or openapi-typescript-helpers found.";
6218
+ const NOTHING_ELSE_FOUND = "Nothing else to migrate: migrate the skipped files above by hand.";
5659
6219
 
5660
6220
  function changesSection(reports) {
5661
6221
  const changed = reports.filter((report) => report.changes.length > 0);
@@ -5711,7 +6271,8 @@ function renderReport(migration) {
5711
6271
  const reports = migration.files;
5712
6272
  const skipped = skippedSection(migration.skipped);
5713
6273
  if (reports.length === 0) {
5714
- return printed([...skipped, NOTHING_FOUND]);
6274
+ const hasBug = migration.skipped.some((file) => file.reason.kind === "migration-bug");
6275
+ return printed([...skipped, hasBug ? NOTHING_ELSE_FOUND : NOTHING_FOUND]);
5715
6276
  }
5716
6277
  const lines = [
5717
6278
  ...changesSection(reports),
@@ -5814,7 +6375,8 @@ async function migrateRoot(request, root, logger, dependencies) {
5814
6375
  const input = { version: dependencies.version, isWrite: request.isWrite };
5815
6376
  const migration = await migrateProject({ files, rewriter, scanner, readers, logger }, input);
5816
6377
  dependencies.writeStdout(renderReport(migration));
5817
- return migration.write.kind === "failed" ? EXIT_CODES.failure : EXIT_CODES.success;
6378
+ const hasBug = migration.skipped.some((file) => file.reason.kind === "migration-bug");
6379
+ return migration.write.kind === "failed" || hasBug ? EXIT_CODES.failure : EXIT_CODES.success;
5818
6380
  }
5819
6381
  async function runMigration(request, dependencies) {
5820
6382
  const log = openLog(request, dependencies);