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

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
@@ -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) {
@@ -1438,9 +1565,6 @@ function removalEdits(removed, text) {
1438
1565
  return [...statements].flatMap((statement) => statementEdits(statement, isRemoved, text));
1439
1566
  }
1440
1567
 
1441
- const TRANSFORM = "transform";
1442
- const LEGACY_ONLY_CALLBACKS = ["postTransform", "transformProperty"];
1443
- const IR_CALLBACKS_OPTION = "irCallbacks";
1444
1568
  const IR_CALLBACKS_TRUE = "true";
1445
1569
  const IR_CALLBACKS_TRUE_CONST = "true as const";
1446
1570
  const IR_CALLBACKS_TRUE_CONST_JS = "/** @type {const} */ (true)";
@@ -1450,12 +1574,6 @@ const noFindings = () => ({
1450
1574
  manualSteps: [],
1451
1575
  typeReferences: []
1452
1576
  });
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
1577
  function returnTypeEdits(callback, text) {
1460
1578
  const type = callback.type;
1461
1579
  if (type === void 0) {
@@ -1472,7 +1590,15 @@ function transformEdits(property, names, text) {
1472
1590
  if (isSharedFunction(property, names)) {
1473
1591
  return "shared-function";
1474
1592
  }
1475
- const edits = returnTypeEdits(callback, text);
1593
+ const declared = callbackTypeEdits(
1594
+ callbackAnnotations(property, names),
1595
+ callback,
1596
+ names.bindings
1597
+ );
1598
+ if (declared === void 0) {
1599
+ return "declared-type";
1600
+ }
1601
+ const edits = [...returnTypeEdits(callback, text), ...declared];
1476
1602
  for (const returned of returnedExpressions(callback.body)) {
1477
1603
  const returnEditList = returnEdits(returned, names);
1478
1604
  if (returnEditList === void 0) {
@@ -1503,12 +1629,6 @@ function keepReason(options) {
1503
1629
  }
1504
1630
  return options.properties.some(ts.isSpreadAssignment) ? "spread-options" : void 0;
1505
1631
  }
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
1632
  function transformRewrite(transform, options, names, text) {
1513
1633
  const edits = keepReason(options.options) ?? transformEdits(transform, names, text);
1514
1634
  if (typeof edits === "string") {
@@ -1524,6 +1644,7 @@ function migrateOptions(options, text, names) {
1524
1644
  }
1525
1645
  const findings = noFindings();
1526
1646
  findings.manualSteps.push(...legacyCallbackSteps(options.options, text));
1647
+ findings.manualSteps.push(...spreadSteps(options.options, text, names.bindings));
1527
1648
  const transform = propertyNamed(options.options, TRANSFORM);
1528
1649
  if (transform === void 0) {
1529
1650
  return findings;
@@ -1542,12 +1663,13 @@ function migrateOptions(options, text, names) {
1542
1663
  findings.changes.push({ kind: "transform-rewritten", line });
1543
1664
  return findings;
1544
1665
  }
1545
- function sharedOptionsFindings(options, text) {
1666
+ function sharedOptionsFindings(options, text, bindings) {
1546
1667
  const findings = noFindings();
1547
1668
  if (propertyNamed(options, IR_CALLBACKS_OPTION) !== void 0) {
1548
1669
  return findings;
1549
1670
  }
1550
1671
  findings.manualSteps.push(...legacyCallbackSteps(options, text));
1672
+ findings.manualSteps.push(...spreadSteps(options, text, bindings));
1551
1673
  const transform = propertyNamed(options, TRANSFORM);
1552
1674
  if (transform !== void 0) {
1553
1675
  const line = lineAt(text, transform.getStart());
@@ -1563,7 +1685,7 @@ function callFindings(found, text, names) {
1563
1685
  case "own":
1564
1686
  return migrateOptions(options, text, names);
1565
1687
  case "shared":
1566
- return sharedOptionsFindings(options.options, text);
1688
+ return sharedOptionsFindings(options.options, text, names.bindings);
1567
1689
  case "unknown": {
1568
1690
  const line = lineAt(text, options.expression.getStart());
1569
1691
  return { ...noFindings(), manualSteps: [{ kind: "options-not-inspected", line }] };
@@ -1958,7 +2080,7 @@ function renamedSpecifier(value) {
1958
2080
  const NODE_MODULES_FOLDER = "node_modules/";
1959
2081
  function oldPackageInNodeModules(path) {
1960
2082
  return [...PACKAGE_RENAMES.keys()].find(
1961
- (name) => path.includes(`${NODE_MODULES_FOLDER}${name}${PATH_SEPARATOR$9}`)
2083
+ (name) => path.includes(`${NODE_MODULES_FOLDER}${name}${PATH_SEPARATOR$9}`) || path.endsWith(`${NODE_MODULES_FOLDER}${name}`)
1962
2084
  );
1963
2085
  }
1964
2086
 
@@ -2037,8 +2159,11 @@ const DESCRIPTION_KEY_LINE_RE = new RegExp(
2037
2159
  "m"
2038
2160
  );
2039
2161
  const mayBeApiDescription = (text) => DESCRIPTION_KEY_LINE_RE.test(text);
2162
+ const SPEC_VERSION_RE = /^\d+\.\d+(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?$/;
2040
2163
  const isApiDescription = (documents) => documents.some(
2041
- (root) => isMap(root) && textEntries(root).some(({ key }) => API_DESCRIPTION_KEY_SET.has(key.value))
2164
+ (root) => isMap(root) && textEntries(root).some(
2165
+ ({ key, value }) => API_DESCRIPTION_KEY_SET.has(key.value) && isScalar(value) && SPEC_VERSION_RE.test(value.value)
2166
+ )
2042
2167
  );
2043
2168
  const REF_KEY = "$ref";
2044
2169
  const FRAGMENT_START = "#";
@@ -2193,8 +2318,8 @@ const COMMAND_SEPARATORS = /* @__PURE__ */ new Set([
2193
2318
  "\n"
2194
2319
  ]);
2195
2320
  const WORD_BREAKS = /* @__PURE__ */ new Set([" ", " ", "\r", "<", ">"]);
2196
- const BACKSLASH$2 = "\\";
2197
- const DOUBLE_QUOTE_ESCAPES = /* @__PURE__ */ new Set(["$", "`", '"', BACKSLASH$2, "\n"]);
2321
+ const BACKSLASH$3 = "\\";
2322
+ const DOUBLE_QUOTE_ESCAPES = /* @__PURE__ */ new Set(["$", "`", '"', BACKSLASH$3, "\n"]);
2198
2323
  function singleQuoted(text, start) {
2199
2324
  const close = text.indexOf("'", start + 1);
2200
2325
  const end = close === -1 ? text.length : close + 1;
@@ -2205,13 +2330,13 @@ function doubleQuoted(text, start) {
2205
2330
  let index = start + 1;
2206
2331
  while (index < text.length && text[index] !== '"') {
2207
2332
  const next = text[index + 1] ?? "";
2208
- const isEscape = text[index] === BACKSLASH$2 && DOUBLE_QUOTE_ESCAPES.has(next);
2333
+ const isEscape = text[index] === BACKSLASH$3 && DOUBLE_QUOTE_ESCAPES.has(next);
2209
2334
  value += isEscape ? next === "\n" ? "" : next : text[index];
2210
2335
  index += isEscape ? 2 : 1;
2211
2336
  }
2212
2337
  return { end: Math.min(index + 1, text.length), value };
2213
2338
  }
2214
- const isLineContinuation = (text, index) => text[index] === BACKSLASH$2 && (text[index + 1] === "\n" || text.startsWith("\r\n", index + 1));
2339
+ const isLineContinuation = (text, index) => text[index] === BACKSLASH$3 && (text[index + 1] === "\n" || text.startsWith("\r\n", index + 1));
2215
2340
  const ARITHMETIC_OPEN = "$((";
2216
2341
  const NESTING_OPEN = "(";
2217
2342
  const NESTING_CLOSE = ")";
@@ -2254,7 +2379,7 @@ function quotedOrEscapedEnd(text, index) {
2254
2379
  if (char === '"') {
2255
2380
  return doubleQuoted(text, index).end;
2256
2381
  }
2257
- return char === BACKSLASH$2 ? index + 2 : void 0;
2382
+ return char === BACKSLASH$3 ? index + 2 : void 0;
2258
2383
  }
2259
2384
  function expansionPart(text, index) {
2260
2385
  if (text.startsWith(ARITHMETIC_OPEN, index)) {
@@ -2275,7 +2400,7 @@ function wordPart(text, index) {
2275
2400
  if (char === '"') {
2276
2401
  return doubleQuoted(text, index);
2277
2402
  }
2278
- if (char === BACKSLASH$2 && index + 1 < text.length) {
2403
+ if (char === BACKSLASH$3 && index + 1 < text.length) {
2279
2404
  return { end: index + 2, value: text[index + 1] };
2280
2405
  }
2281
2406
  return { end: index + 1, value: char };
@@ -2363,9 +2488,11 @@ function skipAssignments(words, start) {
2363
2488
  }
2364
2489
  return index;
2365
2490
  }
2491
+ const WINDOWS_PROGRAM_SUFFIX_RE = /\.(?:cmd|exe|ps1)$/i;
2492
+ const entryWord = (word) => word?.value.replace(WINDOWS_PROGRAM_SUFFIX_RE, "");
2366
2493
  function matchingPrefix(entries, words, index) {
2367
2494
  return entries.find(
2368
- (entry) => entry.words.every((name, offset) => words[index + offset]?.value === name)
2495
+ (entry) => entry.words.every((name, offset) => entryWord(words[index + offset]) === name)
2369
2496
  );
2370
2497
  }
2371
2498
  function inlineOptionPackage(word, launcher) {
@@ -2449,6 +2576,48 @@ function runsGenerator(command) {
2449
2576
  return word !== void 0 && packageReference(word.value).name === GENERATOR_COMMAND;
2450
2577
  }
2451
2578
 
2579
+ const CODE_OPTIONS = /* @__PURE__ */ new Map([
2580
+ ["python", /* @__PURE__ */ new Set(["-c"])],
2581
+ ["node", /* @__PURE__ */ new Set(["-e", "--eval", "-p", "--print"])],
2582
+ ["bun", /* @__PURE__ */ new Set(["-e", "--eval", "-p", "--print"])],
2583
+ ["ruby", /* @__PURE__ */ new Set(["-e"])],
2584
+ ["perl", /* @__PURE__ */ new Set(["-e", "-E"])]
2585
+ ]);
2586
+ const PYTHON_RE = /^(?:python[0-9.]*|py)$/;
2587
+ const PYTHON = "python";
2588
+ const PATH_SEPARATOR_RE$1 = /[/\\]/;
2589
+ const WINDOWS_EXECUTABLE_RE$1 = /\.exe$/i;
2590
+ const OPTION_START$1 = "-";
2591
+ function interpreterOf(word) {
2592
+ const name = (word.value.split(PATH_SEPARATOR_RE$1).at(-1) ?? "").replace(
2593
+ WINDOWS_EXECUTABLE_RE$1,
2594
+ ""
2595
+ );
2596
+ return PYTHON_RE.test(name) ? PYTHON : name;
2597
+ }
2598
+ function codeAfter(command, program) {
2599
+ const options = CODE_OPTIONS.get(interpreterOf(command[program]));
2600
+ for (let index = program + 1; options !== void 0 && index < command.length; index += 1) {
2601
+ const word = command[index];
2602
+ if (!word.value.startsWith(OPTION_START$1)) {
2603
+ return void 0;
2604
+ }
2605
+ if (options.has(word.value)) {
2606
+ return command[index + 1];
2607
+ }
2608
+ }
2609
+ return void 0;
2610
+ }
2611
+ function inlineCode(command) {
2612
+ for (const [index, program] of command.entries()) {
2613
+ const code = codeAfter(command, index);
2614
+ if (code !== void 0) {
2615
+ return { code, program };
2616
+ }
2617
+ }
2618
+ return void 0;
2619
+ }
2620
+
2452
2621
  const EXPANSION_HEAD_RE = /^\$\{([A-Za-z_][A-Za-z0-9_]*):?([-=+])/;
2453
2622
  const USES = {
2454
2623
  "-": "default",
@@ -2456,7 +2625,7 @@ const USES = {
2456
2625
  "+": "alternative"
2457
2626
  };
2458
2627
  const SINGLE_QUOTE = "'";
2459
- const BACKSLASH$1 = "\\";
2628
+ const BACKSLASH$2 = "\\";
2460
2629
  function expansionAt(text, start) {
2461
2630
  const head = EXPANSION_HEAD_RE.exec(text.slice(start));
2462
2631
  const end = head === null ? void 0 : parameterExpansionEnd(text, start);
@@ -2480,7 +2649,7 @@ function wordExpansions(raw) {
2480
2649
  if (char === SINGLE_QUOTE) {
2481
2650
  const close = raw.indexOf(SINGLE_QUOTE, index + 1);
2482
2651
  index = close === -1 ? raw.length : close + 1;
2483
- } else if (char === BACKSLASH$1) {
2652
+ } else if (char === BACKSLASH$2) {
2484
2653
  index += 2;
2485
2654
  } else if (raw.startsWith(PARAMETER_OPEN, index)) {
2486
2655
  const expansion = expansionAt(raw, index);
@@ -2492,9 +2661,9 @@ function wordExpansions(raw) {
2492
2661
  }
2493
2662
  return found;
2494
2663
  }
2495
- const DOUBLE_QUOTE = '"';
2664
+ const DOUBLE_QUOTE$1 = '"';
2496
2665
  function wholeWordExpansion(raw) {
2497
- const isQuoted = raw.length > 1 && raw.startsWith(DOUBLE_QUOTE) && raw.endsWith(DOUBLE_QUOTE);
2666
+ const isQuoted = raw.length > 1 && raw.startsWith(DOUBLE_QUOTE$1) && raw.endsWith(DOUBLE_QUOTE$1);
2498
2667
  const inner = isQuoted ? 1 : 0;
2499
2668
  const [expansion] = wordExpansions(raw);
2500
2669
  const isWhole = expansion?.expansionStart === inner && expansion.expansionEnd === raw.length - inner;
@@ -2504,7 +2673,7 @@ function wholeWordExpansion(raw) {
2504
2673
  const HEREDOC_OPERATOR = "<<";
2505
2674
  const HERE_STRING_OPERATOR = "<<<";
2506
2675
  const STRIP_TABS = "-";
2507
- const LINE_BREAK$4 = "\n";
2676
+ const LINE_BREAK$5 = "\n";
2508
2677
  const LEADING_TABS_RE = /^\t+/;
2509
2678
  const CARRIAGE_RETURN_RE = /\r$/;
2510
2679
  function heredocAt(text, index, where) {
@@ -2536,7 +2705,7 @@ function isDelimiterLine(text, lineStart, end, heredoc) {
2536
2705
  function readBody(text, start, heredoc) {
2537
2706
  let lineStart = start;
2538
2707
  while (lineStart < text.length) {
2539
- const lineEnd = text.indexOf(LINE_BREAK$4, lineStart);
2708
+ const lineEnd = text.indexOf(LINE_BREAK$5, lineStart);
2540
2709
  const end = lineEnd === -1 ? text.length : lineEnd;
2541
2710
  if (isDelimiterLine(text, lineStart, end, heredoc)) {
2542
2711
  const body = { command: heredoc.command, start, text: text.slice(start, lineStart) };
@@ -2557,8 +2726,66 @@ function readBodies(text, start, pending) {
2557
2726
  return { bodies, end };
2558
2727
  }
2559
2728
 
2729
+ const EXPRESSION_OPEN = "${{";
2730
+ const EXPRESSION_CLOSE = "}}";
2731
+ const LITERAL_QUOTE = "'";
2732
+ const SHELL_SINGLE_QUOTE = "'";
2733
+ const BACKSLASH$1 = "\\";
2734
+ const DOUBLE_QUOTE = '"';
2735
+ function expressionEnd(text, start) {
2736
+ for (let index = start + EXPRESSION_OPEN.length; index < text.length; ) {
2737
+ if (text.startsWith(EXPRESSION_CLOSE, index)) {
2738
+ return index + EXPRESSION_CLOSE.length;
2739
+ }
2740
+ const close = text[index] === LITERAL_QUOTE ? text.indexOf(LITERAL_QUOTE, index + 1) : index;
2741
+ index = close === -1 ? text.length : close + 1;
2742
+ }
2743
+ return void 0;
2744
+ }
2745
+ function literalsIn(raw, start, end) {
2746
+ const found = [];
2747
+ let open = raw.indexOf(LITERAL_QUOTE, start);
2748
+ while (open !== -1 && open < end) {
2749
+ let close = raw.indexOf(LITERAL_QUOTE, open + 1);
2750
+ let isEscaped = false;
2751
+ while (close !== -1 && raw[close + 1] === LITERAL_QUOTE) {
2752
+ isEscaped = true;
2753
+ close = raw.indexOf(LITERAL_QUOTE, close + 2);
2754
+ }
2755
+ if (close === -1) {
2756
+ break;
2757
+ }
2758
+ found.push(...isEscaped ? [] : [{ text: raw.slice(open + 1, close), start: open + 1 }]);
2759
+ open = raw.indexOf(LITERAL_QUOTE, close + 1);
2760
+ }
2761
+ return found;
2762
+ }
2763
+ function expressionLiterals(raw) {
2764
+ const found = [];
2765
+ for (let index = 0; index < raw.length; ) {
2766
+ if (raw[index] === SHELL_SINGLE_QUOTE) {
2767
+ const close = raw.indexOf(SHELL_SINGLE_QUOTE, index + 1);
2768
+ index = close === -1 ? raw.length : close + 1;
2769
+ } else if (raw[index] === BACKSLASH$1) {
2770
+ index += 2;
2771
+ } else if (raw.startsWith(EXPRESSION_OPEN, index)) {
2772
+ const end = expressionEnd(raw, index) ?? raw.length;
2773
+ found.push(...literalsIn(raw, index + EXPRESSION_OPEN.length, end));
2774
+ index = end;
2775
+ } else {
2776
+ index += 1;
2777
+ }
2778
+ }
2779
+ return found;
2780
+ }
2781
+ function isWholeExpression(raw) {
2782
+ const isQuoted = raw.length > 1 && raw.startsWith(DOUBLE_QUOTE) && raw.endsWith(DOUBLE_QUOTE);
2783
+ const inner = isQuoted ? 1 : 0;
2784
+ return raw.startsWith(EXPRESSION_OPEN, inner) && expressionEnd(raw, inner) === raw.length - inner;
2785
+ }
2786
+
2560
2787
  const COMMENT = "#";
2561
- const LINE_BREAK$3 = "\n";
2788
+ const LINE_BREAK$4 = "\n";
2562
2789
  function skipTrivia(text, index) {
2563
2790
  const char = text[index];
2564
2791
  if (WORD_BREAKS.has(char)) {
@@ -2588,7 +2815,7 @@ function topLevelCommands(text, indent) {
2588
2815
  index = heredoc.end;
2589
2816
  } else if (afterTrivia !== index) {
2590
2817
  index = afterTrivia;
2591
- } else if (text[index] === LINE_BREAK$3 && pending.length > 0) {
2818
+ } else if (text[index] === LINE_BREAK$4 && pending.length > 0) {
2592
2819
  const read = readBodies(text, index + 1, pending);
2593
2820
  heredocs.push(...read.bodies);
2594
2821
  pending = [];
@@ -2676,13 +2903,14 @@ const shifted = (words, offset) => words.map((word) => ({ ...word, start: word.s
2676
2903
  function shellCommandsAt(text, offset, indent = 0) {
2677
2904
  return shellCommands(text, indent).map((words) => shifted(words, offset));
2678
2905
  }
2679
- const expansionCommands = (word) => wordExpansions(word.raw).flatMap(
2680
- (expansion) => shellCommandsAt(expansion.text, word.start + expansion.start)
2906
+ const expansionCommands = (word) => [...wordExpansions(word.raw), ...expressionLiterals(word.raw)].flatMap(
2907
+ (part) => shellCommandsAt(part.text, word.start + part.start)
2681
2908
  );
2682
2909
  function innerCommands(command) {
2683
2910
  const assignments = new Map(shellAssignments(command).map((found) => [found.word, found]));
2911
+ const code = inlineCode(command)?.code;
2684
2912
  return command.flatMap((word) => {
2685
- const inner = nestedCommandText(word);
2913
+ const inner = word === code ? void 0 : nestedCommandText(word);
2686
2914
  if (inner !== void 0) {
2687
2915
  return shellCommandsAt(inner, word.start + 1);
2688
2916
  }
@@ -2847,7 +3075,7 @@ const VARIABLE_WORD_RE = new RegExp(
2847
3075
  );
2848
3076
  function withUnreadVariables(variables) {
2849
3077
  return {
2850
- programAt: (program) => variables.programAt(program) ?? (VARIABLE_WORD_RE.test(program.raw) ? { written: program.raw, program: "generator-or-other" } : void 0),
3078
+ programAt: (program) => variables.programAt(program) ?? (VARIABLE_WORD_RE.test(program.raw) || isWholeExpression(program.raw) ? { written: program.raw, program: "generator-or-other" } : void 0),
2851
3079
  referenceAt: (word) => variables.referenceAt(word)
2852
3080
  };
2853
3081
  }
@@ -2961,6 +3189,14 @@ function commandFindings(command, locate, variables) {
2961
3189
  const handled = new Set(found.map((finding) => finding.offset));
2962
3190
  return [...found, ...unmigratedWordFindings(words, handled, locate)];
2963
3191
  }
3192
+ function inlineCodeFindings({ code, program }, locate) {
3193
+ if (!namesOriginalPackage(code.value)) {
3194
+ return [];
3195
+ }
3196
+ const location = locate(code.start);
3197
+ const manualStep = { kind: "script-not-shell", location, shell: program.value };
3198
+ return [{ offset: code.start, manualStep }];
3199
+ }
2964
3200
  function knownFindings(words, locate, variables) {
2965
3201
  const roles = commandRoles(words);
2966
3202
  const findings = roles.packages.flatMap((found2) => packageFindings(found2, locate));
@@ -2968,6 +3204,10 @@ function knownFindings(words, locate, variables) {
2968
3204
  if (program === void 0) {
2969
3205
  return findings;
2970
3206
  }
3207
+ const code = inlineCode(words);
3208
+ if (code !== void 0) {
3209
+ return [...findings, ...inlineCodeFindings(code, locate)];
3210
+ }
2971
3211
  const found = programFindings(program, locate(program.start));
2972
3212
  findings.push(...found);
2973
3213
  const isRenamed = found.some((finding) => finding.change?.kind === "command-renamed");
@@ -3289,36 +3529,52 @@ function assignedVariable(word) {
3289
3529
  const isQuoted = raw.length >= 2 && QUOTES.has(raw[0]) && raw.at(-1) === raw[0];
3290
3530
  const value = isQuoted ? raw.slice(1, -1) : raw;
3291
3531
  const start = word.start + at + 1 + (isQuoted ? 1 : 0);
3292
- return { name: word.raw.slice(0, at), commands: shellCommandsAt(value, start) };
3532
+ return { name: word.raw.slice(0, at), value, start };
3293
3533
  }
3294
- function dockerVariables(commands) {
3295
- return commands.flatMap(([instruction, first, ...rest]) => {
3534
+ function instructionVariables(text, words) {
3535
+ const [instruction, first, ...rest] = words;
3536
+ const last = rest.at(-1);
3537
+ if (instruction.value === LEGACY_ENV_INSTRUCTION && !first.raw.includes(ASSIGNMENT$1)) {
3538
+ const value = last === void 0 ? "" : text.slice(rest[0]?.start, last.end);
3539
+ return [{ name: first.value, value, start: rest[0]?.start ?? first.end }];
3540
+ }
3541
+ return [first, ...rest].flatMap((word) => assignedVariable(word) ?? []);
3542
+ }
3543
+ function variableInstructions(text, commands) {
3544
+ return commands.flatMap((words) => {
3545
+ const [instruction, first] = words;
3296
3546
  if (instruction === void 0 || first === void 0) {
3297
3547
  return [];
3298
3548
  }
3299
3549
  if (!VARIABLE_INSTRUCTIONS.has(instruction.value)) {
3300
3550
  return [];
3301
3551
  }
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) ?? []);
3552
+ const end = words.at(-1).end;
3553
+ return [{ start: instruction.start, end, variables: instructionVariables(text, words) }];
3306
3554
  });
3307
3555
  }
3308
- const definitionOf = ({ name, commands }) => ({
3309
- name,
3310
- value: commands.flatMap((words) => words.map((word) => word.raw)).join(" ")
3311
- });
3556
+ function withoutVariableInstructions(commands, instructions) {
3557
+ const isInInstruction = (words) => {
3558
+ const start = words[0]?.start ?? -1;
3559
+ return instructions.some(
3560
+ (instruction) => start >= instruction.start && start < instruction.end
3561
+ );
3562
+ };
3563
+ return commands.filter((words) => !isInInstruction(words));
3564
+ }
3565
+ const valueCommands = ({ value, start }) => shellCommandsAt(value, start);
3312
3566
  function migrateDockerfile(text, file) {
3313
3567
  const lineOf = lineLocator(text);
3314
3568
  const locate = (offset) => `${file}:${lineOf(offset)}`;
3315
3569
  const forms = execForms(text);
3316
3570
  const exec = execCommands(forms);
3317
3571
  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));
3572
+ const instructions = variableInstructions(text, shellForm);
3573
+ const definitions = instructions.flatMap((instruction) => instruction.variables);
3574
+ const values = definitions.flatMap(valueCommands);
3575
+ const run = withoutVariableInstructions(shellForm, instructions);
3576
+ const commands = [...run, ...values, ...exec.commands];
3577
+ const variables = shellVariables(definitions);
3322
3578
  const { edits, changes, manualSteps } = commandWordFindings(commands, locate, variables);
3323
3579
  const steps = [...manualSteps, ...escapedScriptSteps(exec.escaped, lineOf)];
3324
3580
  return { text: applyTextEdits(text, edits), changes, manualSteps: steps };
@@ -3670,8 +3926,8 @@ function distinctStrings(strings) {
3670
3926
  outer = enclosing.at(-1);
3671
3927
  }
3672
3928
  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.`
3929
+ throw new MigrationBugError(
3930
+ `source strings overlap at offsets ${outer.start}-${outer.end} and ${found.start}-${found.end}`
3675
3931
  );
3676
3932
  }
3677
3933
  enclosing.push(found);
@@ -3799,7 +4055,7 @@ const BACKTICK = "`";
3799
4055
  const ESCAPING_DELIMITER = '"';
3800
4056
  const ESCAPE = "\\";
3801
4057
  const COMMENT_START$1 = "#";
3802
- const LINE_BREAK$2 = "\n";
4058
+ const LINE_BREAK$3 = "\n";
3803
4059
  function closingOffset(text, delimiter, from) {
3804
4060
  if (delimiter !== ESCAPING_DELIMITER) {
3805
4061
  const close = text.indexOf(delimiter, from);
@@ -3809,7 +4065,7 @@ function closingOffset(text, delimiter, from) {
3809
4065
  const char = text[index];
3810
4066
  if (char === ESCAPE) {
3811
4067
  index += 1;
3812
- } else if (char === ESCAPING_DELIMITER || char === LINE_BREAK$2) {
4068
+ } else if (char === ESCAPING_DELIMITER || char === LINE_BREAK$3) {
3813
4069
  return char === ESCAPING_DELIMITER ? index : void 0;
3814
4070
  }
3815
4071
  }
@@ -3832,7 +4088,7 @@ function literalAt(text, offset) {
3832
4088
  function expressionAt(text, start) {
3833
4089
  const literals = [];
3834
4090
  let index = start;
3835
- while (index < text.length && text[index] !== LINE_BREAK$2 && text[index] !== COMMENT_START$1) {
4091
+ while (index < text.length && text[index] !== LINE_BREAK$3 && text[index] !== COMMENT_START$1) {
3836
4092
  const literal = literalAt(text, index);
3837
4093
  literals.push(...literal === void 0 ? [] : [literal]);
3838
4094
  index = literal?.end ?? index + 1;
@@ -3856,7 +4112,7 @@ function interpolationAt(text, start) {
3856
4112
  if (text.startsWith(INTERPOLATION_CLOSE, index)) {
3857
4113
  return { start, end: index + INTERPOLATION_CLOSE.length, literals };
3858
4114
  }
3859
- if (text[index] === LINE_BREAK$2) {
4115
+ if (text[index] === LINE_BREAK$3) {
3860
4116
  return void 0;
3861
4117
  }
3862
4118
  const literal = literalAt(text, index);
@@ -3884,11 +4140,11 @@ const EQUALS = "=";
3884
4140
  const OPEN_PAREN = "(";
3885
4141
  const CLOSE_PAREN = ")";
3886
4142
  const COMMENT_START = "#";
3887
- const LINE_BREAK$1 = "\n";
4143
+ const LINE_BREAK$2 = "\n";
3888
4144
  const BLANK_RE = /[ \t]/;
3889
4145
  const depthChange = (char) => char === OPEN_PAREN ? 1 : char === CLOSE_PAREN ? -1 : 0;
3890
4146
  function headerColon(text, from) {
3891
- for (let index = from; index < text.length && text[index] !== LINE_BREAK$1; ) {
4147
+ for (let index = from; index < text.length && text[index] !== LINE_BREAK$2; ) {
3892
4148
  const literal = literalAt(text, index);
3893
4149
  const char = text[index];
3894
4150
  if (literal !== void 0) {
@@ -3946,6 +4202,133 @@ function recipeHeaders(text) {
3946
4202
  });
3947
4203
  }
3948
4204
 
4205
+ const STEP_SHELL_KEY = "shell";
4206
+ const DEFAULT_SHELL_PATH = ["defaults", "run", STEP_SHELL_KEY];
4207
+ const COMMAND_LINE_SHELLS = /* @__PURE__ */ new Set([
4208
+ ...["sh", "bash", "dash", "ash", "zsh", "ksh"],
4209
+ ...["pwsh", "powershell", "cmd"],
4210
+ ...["msys2", "wsl-bash"]
4211
+ ]);
4212
+ const ENV_PROGRAM = "env";
4213
+ const ENV_OPTIONS_WITH_VALUE = /* @__PURE__ */ new Set(["-u", "--unset", "-C", "--chdir"]);
4214
+ const OPTION_START = "-";
4215
+ const ASSIGNMENT = "=";
4216
+ const PATH_SEPARATOR_RE = /[/\\]/;
4217
+ const WINDOWS_EXECUTABLE_RE = /\.exe$/i;
4218
+ const WHITESPACE_RE = /\s+/;
4219
+ const EXECUTORS_KEY = "executors";
4220
+ const EXECUTOR_SHELL_PATH_LENGTH = 3;
4221
+ const EXECUTOR_KEY = "executor";
4222
+ const EXECUTOR_NAME_KEY = "name";
4223
+ const programName = (word) => (word.split(PATH_SEPARATOR_RE).at(-1) ?? "").replace(WINDOWS_EXECUTABLE_RE, "");
4224
+ function envPrefixLength(words) {
4225
+ let index = 0;
4226
+ while (index < words.length) {
4227
+ const word = words[index] ?? "";
4228
+ if (ENV_OPTIONS_WITH_VALUE.has(word)) {
4229
+ index += 2;
4230
+ } else if (word.startsWith(OPTION_START) || word.includes(ASSIGNMENT)) {
4231
+ index += 1;
4232
+ } else {
4233
+ break;
4234
+ }
4235
+ }
4236
+ return index;
4237
+ }
4238
+ function shellProgram(shell) {
4239
+ const words = shell.trim().split(WHITESPACE_RE);
4240
+ const first = programName(words[0] ?? "");
4241
+ if (first !== ENV_PROGRAM) {
4242
+ return first;
4243
+ }
4244
+ const rest = words.slice(1);
4245
+ return programName(rest[envPrefixLength(rest)] ?? "");
4246
+ }
4247
+ const isCommandLineShell = (shell) => COMMAND_LINE_SHELLS.has(shellProgram(shell));
4248
+ const endsWith = (path, keys) => keys.every((key, index) => path[path.length - keys.length + index] === key);
4249
+ const isDefaultShell = (value) => endsWith(value.path, DEFAULT_SHELL_PATH);
4250
+ function scopeOf(value) {
4251
+ const keys = isDefaultShell(value) ? DEFAULT_SHELL_PATH.length : 1;
4252
+ return JSON.stringify(value.path.slice(0, -keys));
4253
+ }
4254
+ function executorShells(shells) {
4255
+ const isExecutorShell = ({ path }) => path.length === EXECUTOR_SHELL_PATH_LENGTH && path[0] === EXECUTORS_KEY;
4256
+ return new Map(
4257
+ shells.filter(isExecutorShell).map((shell) => [String(shell.path[1]), shell.value])
4258
+ );
4259
+ }
4260
+ function executorJob({ path }) {
4261
+ if (path.at(-1) === EXECUTOR_KEY) {
4262
+ return path.slice(0, -1);
4263
+ }
4264
+ const isNamed = path.at(-1) === EXECUTOR_NAME_KEY && path.at(-2) === EXECUTOR_KEY;
4265
+ return isNamed ? path.slice(0, -2) : void 0;
4266
+ }
4267
+ function inheritedShells(strings, shells) {
4268
+ const byExecutor = executorShells(shells);
4269
+ return strings.flatMap((value) => {
4270
+ const job = executorJob(value);
4271
+ const shell = byExecutor.get(value.value);
4272
+ return job === void 0 || shell === void 0 ? [] : [[JSON.stringify(job), shell]];
4273
+ });
4274
+ }
4275
+ function stepShells(strings) {
4276
+ const shells = strings.filter((value) => value.path.at(-1) === STEP_SHELL_KEY);
4277
+ const isOwnShell = (value) => !isDefaultShell(value);
4278
+ const ordered = [...shells.filter(isDefaultShell), ...shells.filter(isOwnShell)];
4279
+ const byScope = new Map([
4280
+ ...inheritedShells(strings, shells),
4281
+ ...ordered.map((value) => [scopeOf(value), value.value])
4282
+ ]);
4283
+ return (value) => {
4284
+ for (let depth = value.path.length - 1; depth >= 0; depth -= 1) {
4285
+ const shell = byScope.get(JSON.stringify(value.path.slice(0, depth)));
4286
+ if (shell !== void 0) {
4287
+ return isCommandLineShell(shell) ? void 0 : shell;
4288
+ }
4289
+ }
4290
+ return void 0;
4291
+ };
4292
+ }
4293
+ function otherShellFindings(values, shellOf, locate) {
4294
+ const manualSteps = values.flatMap((value) => {
4295
+ const shell = shellOf(value);
4296
+ return shell === void 0 || !namesOriginalPackage(value.value) ? [] : [{ kind: "script-not-shell", location: locate(value.start), shell }];
4297
+ });
4298
+ return { edits: [], changes: [], manualSteps };
4299
+ }
4300
+
4301
+ const LINE_BREAK$1 = "\n";
4302
+ const SHEBANG = "#!";
4303
+ const BODY_LINE_RE = /^(?:[ \t][^\n]*|[ \t]*\r?)$/;
4304
+ function nextLineStart(text, offset) {
4305
+ const lineBreak = text.indexOf(LINE_BREAK$1, offset);
4306
+ return lineBreak === -1 ? text.length : lineBreak + 1;
4307
+ }
4308
+ function bodyEnd(text, start) {
4309
+ let end = start;
4310
+ while (end < text.length) {
4311
+ const lineEnd = nextLineStart(text, end);
4312
+ if (!BODY_LINE_RE.test(text.slice(end, lineEnd).replace(LINE_BREAK$1, ""))) {
4313
+ break;
4314
+ }
4315
+ end = lineEnd;
4316
+ }
4317
+ return end;
4318
+ }
4319
+ function shebangRecipe(text, header) {
4320
+ const start = nextLineStart(text, header.end);
4321
+ const end = bodyEnd(text, start);
4322
+ const firstLine = text.slice(start, end).split(LINE_BREAK$1).map((line) => line.trim()).find((line) => line.length > 0);
4323
+ if (firstLine === void 0 || !firstLine.startsWith(SHEBANG)) {
4324
+ return [];
4325
+ }
4326
+ const shell = firstLine.slice(SHEBANG.length).trim();
4327
+ const shebang = text.indexOf(SHEBANG, start);
4328
+ return isCommandLineShell(shell) ? [] : [{ start, end, shebang, shell }];
4329
+ }
4330
+ const shebangRecipes = (text, headers) => headers.flatMap((header) => shebangRecipe(text, header));
4331
+
3949
4332
  const ASSIGNMENT_RE$1 = /^(?:export[ \t]+)?([A-Za-z_][A-Za-z0-9_-]*)[ \t]*:=[ \t]*/gm;
3950
4333
  const NAME_VALUE_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
3951
4334
  const VARIABLE_INTERPOLATION_RE = /^[@+-]*\{\{[ \t]*([A-Za-z_][A-Za-z0-9_-]*)[ \t]*\}\}$/;
@@ -4001,11 +4384,15 @@ function shellText$1(text, spans) {
4001
4384
  return { text: masked, interpolations };
4002
4385
  }
4003
4386
  const literalCommands = (literals) => literals.flatMap((literal) => shellCommandsAt(literal.content, literal.contentStart));
4004
- function variableValues$1(assignments) {
4387
+ const defaultValue = ({ literals }) => literals.map((literal) => literal.content).join(BLANK);
4388
+ function variableValues$1(assignments, defaults) {
4005
4389
  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());
4390
+ const named = [
4391
+ ...assignments.map((found) => ({ ...found, value: onlyString(found)?.content ?? found.value })),
4392
+ ...defaults.map((found) => ({ name: found.name, value: defaultValue(found) }))
4393
+ ];
4394
+ for (const { name, value } of named) {
4395
+ values.set(name, [values.get(name), value].join(BLANK).trim());
4009
4396
  }
4010
4397
  return values;
4011
4398
  }
@@ -4030,6 +4417,7 @@ function justfileVariables(text, values, programs) {
4030
4417
  }
4031
4418
  };
4032
4419
  }
4420
+ 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
4421
  function migrateJustfile(text, file) {
4034
4422
  const lineOf = lineLocator(text);
4035
4423
  const assignments = assignmentsOf$1(text);
@@ -4039,8 +4427,9 @@ function migrateJustfile(text, file) {
4039
4427
  ...assignments.flatMap(assignmentDefinitions),
4040
4428
  ...defaults.flatMap(parameterDefinitions)
4041
4429
  ]);
4042
- const variables = justfileVariables(text, variableValues$1(assignments), programs);
4043
- const shell = shellText$1(text, [...assignments, ...headers]);
4430
+ const variables = justfileVariables(text, variableValues$1(assignments, defaults), programs);
4431
+ const scripts = shebangRecipes(text, headers);
4432
+ const shell = shellText$1(text, [...assignments, ...headers, ...scripts]);
4044
4433
  const commands = [
4045
4434
  ...shellCommands(shell.text),
4046
4435
  ...literalCommands(assignments.flatMap((assignment) => assignment.literals)),
@@ -4049,7 +4438,8 @@ function migrateJustfile(text, file) {
4049
4438
  ];
4050
4439
  const locate = (offset) => `${file}:${lineOf(offset)}`;
4051
4440
  const { edits, changes, manualSteps } = commandWordFindings(commands, locate, variables);
4052
- return { text: applyTextEdits(text, edits), changes, manualSteps };
4441
+ const steps = [...manualSteps, ...shebangSteps(text, scripts, locate)];
4442
+ return { text: applyTextEdits(text, edits), changes, manualSteps: steps };
4053
4443
  }
4054
4444
 
4055
4445
  const ASSIGNMENT_RE = new RegExp(
@@ -4131,6 +4521,8 @@ function withoutAssignmentHeads(text, assignments) {
4131
4521
  parts.push(text.slice(copiedUpTo));
4132
4522
  return parts.join("");
4133
4523
  }
4524
+ const COMPUTED_RE = /^\$[({]/;
4525
+ const isComputed = (written) => COMPUTED_RE.test(written.trim().replace(RECIPE_PREFIX_RE, ""));
4134
4526
  function referencedName(written) {
4135
4527
  const match = VARIABLE_REFERENCE_RE.exec(written.trim().replace(RECIPE_PREFIX_RE, ""));
4136
4528
  return match === null ? void 0 : match[1] ?? match[2] ?? match[3];
@@ -4162,10 +4554,12 @@ function makefileVariables(text, assignments, programs) {
4162
4554
  const nameAt = (word) => referencedName(text.slice(word.start, word.end));
4163
4555
  const variables = {
4164
4556
  programAt: (program) => {
4165
- const name = nameAt(program);
4166
- const found = name === void 0 ? void 0 : programs.get(name) ?? "generator-or-other";
4167
4557
  const written = text.slice(program.start, program.end);
4168
- return found === void 0 ? void 0 : { written, program: found };
4558
+ const name = nameAt(program);
4559
+ if (name === void 0) {
4560
+ return isComputed(written) ? { written, program: "generator-or-other" } : void 0;
4561
+ }
4562
+ return { written, program: programs.get(name) ?? "generator-or-other" };
4169
4563
  },
4170
4564
  referenceAt: (word) => {
4171
4565
  const name = nameAt(word);
@@ -4527,73 +4921,6 @@ function oldPackagePatchSteps(fileName) {
4527
4921
  return name === void 0 || to === void 0 ? [] : [{ kind: "old-package-patch", name, to }];
4528
4922
  }
4529
4923
 
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;
4564
- }
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
- }
4585
- }
4586
- return void 0;
4587
- };
4588
- }
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
-
4597
4924
  const YAML_COMMAND_KEYS = /* @__PURE__ */ new Set([
4598
4925
  "run",
4599
4926
  "script",
@@ -4620,6 +4947,10 @@ const TASKFILE_COMMAND_KEYS = /* @__PURE__ */ new Set([
4620
4947
 
4621
4948
  const ENVIRONMENT_KEYS = /* @__PURE__ */ new Set(["env"]);
4622
4949
  const isEnvironmentValue = (value) => value.path.length >= 2 && ENVIRONMENT_KEYS.has(String(value.path.at(-2)));
4950
+ const INPUTS_KEY = "inputs";
4951
+ const DEFAULT_KEY = "default";
4952
+ const INPUT_DEFAULT_DEPTH = 3;
4953
+ const isInputDefault = (value) => value.path.length >= INPUT_DEFAULT_DEPTH && value.path.at(-1) === DEFAULT_KEY && value.path.at(-INPUT_DEFAULT_DEPTH) === INPUTS_KEY;
4623
4954
  function environmentVariables(text, strings) {
4624
4955
  return shellVariables(
4625
4956
  strings.filter(isEnvironmentValue).map((value) => ({
@@ -4703,7 +5034,7 @@ function migrateYamlCommands(text, file, strings, keys = YAML_COMMAND_KEYS) {
4703
5034
  const values = valuesOf(strings, keys).filter((value) => shellOf(value) === void 0);
4704
5035
  const commandValues = new Set(values.filter((value) => !isExecFormItem(value)));
4705
5036
  const shellValues = strings.filter(
4706
- (value) => commandValues.has(value) || isEnvironmentValue(value)
5037
+ (value) => commandValues.has(value) || isEnvironmentValue(value) || isInputDefault(value)
4707
5038
  );
4708
5039
  const variables = environmentVariables(text, strings);
4709
5040
  return appliedFindings(text, [
@@ -4927,6 +5258,28 @@ const JSON5_MANIFEST_FILE = "package.json5";
4927
5258
  const MANIFEST_FILE = "package.json";
4928
5259
  const PROJECT_CONFIG_NAME = /^[jt]sconfig(\.[^/]+)?\.json$/;
4929
5260
  const LOCK_FILES = /* @__PURE__ */ new Set(["pnpm-lock.yaml", "yarn.lock", "bun.lock"]);
5261
+ const CI_CONFIG_FILES = /* @__PURE__ */ new Set([
5262
+ ".gitlab-ci.yml",
5263
+ ".gitlab-ci.yaml",
5264
+ "bitbucket-pipelines.yml",
5265
+ "azure-pipelines.yml",
5266
+ ".drone.yml",
5267
+ ".woodpecker.yml",
5268
+ ".travis.yml"
5269
+ ]);
5270
+ const CI_CONFIG_FOLDERS = [
5271
+ ".github/workflows/",
5272
+ ".gitea/workflows/",
5273
+ ".forgejo/workflows/",
5274
+ ".circleci/",
5275
+ ".gitlab/",
5276
+ ".woodpecker/"
5277
+ ];
5278
+ const FOLDER_SEPARATOR = "/";
5279
+ function isCiConfigFile(path) {
5280
+ const inFolder = (folder) => path.startsWith(folder) || path.includes(`${FOLDER_SEPARATOR}${folder}`);
5281
+ return CI_CONFIG_FILES.has(baseName(path)) || CI_CONFIG_FOLDERS.some(inFolder);
5282
+ }
4930
5283
  function baseName(path) {
4931
5284
  return path.slice(path.lastIndexOf("/") + 1);
4932
5285
  }
@@ -5027,7 +5380,7 @@ const COMMAND_CONFIG_KINDS = [
5027
5380
  )
5028
5381
  }
5029
5382
  ];
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);
5383
+ const yamlCommandsStep = (path, isPart) => (text, read) => isPart || !isCiConfigFile(path) && read.kind === "read" && isApiDescription(read.documents) ? unchanged(text) : stringsStep((current, strings) => migrateYamlCommands(current, path, strings))(text, read);
5031
5384
  const manifestTarget = ({ version, catalogs, aliases }, path) => ({
5032
5385
  version,
5033
5386
  catalogs,
@@ -5139,7 +5492,7 @@ function fileMigrationFor(path, namedImportMaps = NO_NAMED_IMPORT_MAPS) {
5139
5492
  const NO_API_DESCRIPTION_PARTS = /* @__PURE__ */ new Set();
5140
5493
  const JSON_EXTENSION = ".json";
5141
5494
  const isYaml = (path) => YAML_EXTENSIONS.has(extensionOf(path));
5142
- const isDescriptionFormat = (path) => isYaml(path) || extensionOf(path) === JSON_EXTENSION;
5495
+ const isDescriptionFormat = (path) => (isYaml(path) || extensionOf(path) === JSON_EXTENSION) && !isCiConfigFile(path);
5143
5496
  async function textOf(path, files) {
5144
5497
  const read = await files.read(path);
5145
5498
  return read.kind === "read" ? read.text : void 0;
@@ -5275,6 +5628,16 @@ async function planProjectTarget(paths, dependencies, version) {
5275
5628
  };
5276
5629
  }
5277
5630
 
5631
+ function attemptMigration(migrate) {
5632
+ try {
5633
+ return { kind: "migrated", migration: migrate() };
5634
+ } catch (error) {
5635
+ if (error instanceof MigrationBugError) {
5636
+ return { kind: "skipped", reason: { kind: "migration-bug", message: error.message } };
5637
+ }
5638
+ throw error;
5639
+ }
5640
+ }
5278
5641
  async function planFile(path, dependencies, target) {
5279
5642
  const migrate = fileMigrationFor(path, target.importMaps);
5280
5643
  if (migrate === void 0) {
@@ -5290,7 +5653,11 @@ async function planFile(path, dependencies, target) {
5290
5653
  ...{ version, catalogs, manifests, aliases, apiDescriptionParts },
5291
5654
  ...{ rewriter, scanner, readers }
5292
5655
  };
5293
- const migration = migrate(read.text, path, context);
5656
+ const attempt = attemptMigration(() => migrate(read.text, path, context));
5657
+ if (attempt.kind === "skipped") {
5658
+ return attempt;
5659
+ }
5660
+ const { migration } = attempt;
5294
5661
  const isChanged = migration.text !== read.text;
5295
5662
  const { changes, manualSteps } = migration;
5296
5663
  if (isChanged || manualSteps.length > 0) {
@@ -5418,7 +5785,7 @@ function commandStepMessage(step) {
5418
5785
  case "old-name-in-command":
5419
5786
  return `${step.location}: "${step.word}" ${OLD_NAME_IN_COMMAND_NOTE}`;
5420
5787
  case "script-not-shell":
5421
- return `${step.location}: the step runs with shell: ${step.shell}, ${SCRIPT_NOT_SHELL_NOTE}`;
5788
+ return `${step.location}: the script runs with ${step.shell}, ${SCRIPT_NOT_SHELL_NOTE}`;
5422
5789
  case "command-string":
5423
5790
  return `line ${step.line}: ${COMMAND_STRING_NOTE} "${step.to}"`;
5424
5791
  default:
@@ -5434,9 +5801,12 @@ const KEEP_REASONS = {
5434
5801
  "shared-options": "its options object is used elsewhere too",
5435
5802
  "unknown-return": "a return value is not a plain ts.factory type",
5436
5803
  "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"
5804
+ "spread-options": "its options take keys from another object (`...base`), whose callbacks and type the tool cannot see",
5805
+ "declared-type": "its declared type (a `const` type, JSDoc `@type` or `@returns`) names a type the tool cannot change to type text",
5806
+ "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
5807
  };
5439
5808
  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}`;
5809
+ 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
5810
  const DEEP_IMPORT_NOTE = "imports a file inside the package; check that it exists";
5441
5811
  const OLD_FILE_NOTE = "imports a file from node_modules of";
5442
5812
  const OLD_FILE_FIX_NOTE = "which is no longer installed: import from";
@@ -5452,6 +5822,7 @@ function packageNotDeclaredMessage(step) {
5452
5822
  }
5453
5823
  const SOURCE_STEP_KINDS = /* @__PURE__ */ new Set([
5454
5824
  "options-not-inspected",
5825
+ "options-spread-not-inspected",
5455
5826
  "deep-import",
5456
5827
  "old-package-import",
5457
5828
  "dynamic-specifier",
@@ -5465,6 +5836,8 @@ function sourceStepMessage(step) {
5465
5836
  switch (step.kind) {
5466
5837
  case "options-not-inspected":
5467
5838
  return `line ${step.line}: ${OPTIONS_NOTE}`;
5839
+ case "options-spread-not-inspected":
5840
+ return `line ${step.line}: ${SPREAD_NOTE}`;
5468
5841
  case "deep-import":
5469
5842
  return `line ${step.line}: "${step.specifier}" ${DEEP_IMPORT_NOTE}`;
5470
5843
  case "old-package-import":
@@ -5619,7 +5992,7 @@ function changeMessage(change) {
5619
5992
  return assertNever(change);
5620
5993
  }
5621
5994
  }
5622
- const SKIPPED_HEADING = "Skipped (not read, not written):";
5995
+ const SKIPPED_HEADING = "Skipped (not migrated, not written):";
5623
5996
  function skippedMessage(reason) {
5624
5997
  switch (reason.kind) {
5625
5998
  case "outside-root":
@@ -5628,6 +6001,8 @@ function skippedMessage(reason) {
5628
6001
  return "it is not UTF-8 text, so it was not changed; check it by hand";
5629
6002
  case "io-error":
5630
6003
  return `it could not be read (${reason.code})`;
6004
+ case "migration-bug":
6005
+ 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
6006
  default:
5632
6007
  return assertNever(reason);
5633
6008
  }
@@ -5814,7 +6189,8 @@ async function migrateRoot(request, root, logger, dependencies) {
5814
6189
  const input = { version: dependencies.version, isWrite: request.isWrite };
5815
6190
  const migration = await migrateProject({ files, rewriter, scanner, readers, logger }, input);
5816
6191
  dependencies.writeStdout(renderReport(migration));
5817
- return migration.write.kind === "failed" ? EXIT_CODES.failure : EXIT_CODES.success;
6192
+ const hasBug = migration.skipped.some((file) => file.reason.kind === "migration-bug");
6193
+ return migration.write.kind === "failed" || hasBug ? EXIT_CODES.failure : EXIT_CODES.success;
5818
6194
  }
5819
6195
  async function runMigration(request, dependencies) {
5820
6196
  const log = openLog(request, dependencies);