@autter/runtime-cli 1.1.0 → 1.2.0

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.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { spawnSync } from "child_process";
5
- import { existsSync as existsSync3, readFileSync } from "fs";
4
+ import { spawnSync as spawnSync2 } from "child_process";
5
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
6
6
  import { createRequire } from "module";
7
- import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
7
+ import { dirname as dirname4, join as join5, relative as relative5, resolve as resolve4, sep as sep5 } from "path";
8
8
  import { parseArgs } from "util";
9
9
 
10
10
  // src/build.ts
@@ -63,6 +63,48 @@ async function walk(dir, out, pattern = BUNDLE_RE) {
63
63
  else if (entry.isFile() && pattern.test(entry.name)) out.push(full);
64
64
  }
65
65
  }
66
+ async function findMapFiles(dir) {
67
+ const out = [];
68
+ await walk(resolve(dir), out, /\.map$/);
69
+ return out.sort();
70
+ }
71
+ var CSS_SOURCE_MAPPING_URL_RE = /\/\*[#@] sourceMappingURL=([^\s*]+)\s*\*\/\s*$/;
72
+ function pointsAt(file, url, map) {
73
+ if (!url || /^[a-z][\w+.-]*:/i.test(url) || url.startsWith("/")) return false;
74
+ let path = url.split(/[?#]/)[0];
75
+ try {
76
+ path = decodeURIComponent(path);
77
+ } catch {
78
+ }
79
+ return resolve(dirname(file), path) === map;
80
+ }
81
+ async function stripReference(file, map) {
82
+ let code;
83
+ try {
84
+ code = await readFile(file, "utf8");
85
+ } catch {
86
+ return;
87
+ }
88
+ let stripped = code;
89
+ if (BUNDLE_RE.test(file)) {
90
+ if (pointsAt(file, readSourceMappingUrl(code), map)) stripped = stripSourceMappingUrl(code);
91
+ } else if (file.endsWith(".css")) {
92
+ const match = CSS_SOURCE_MAPPING_URL_RE.exec(code);
93
+ if (match && pointsAt(file, match[1], map)) stripped = code.slice(0, match.index).replace(/\n?$/, "\n");
94
+ }
95
+ if (stripped !== code) await writeFile(file, stripped);
96
+ }
97
+ async function removeSourceMaps(maps, owners = /* @__PURE__ */ new Map()) {
98
+ const removed = [];
99
+ for (const map of [...new Set(maps)].sort()) {
100
+ if (!existsSync(map)) continue;
101
+ await rm(map, { force: true });
102
+ removed.push(map);
103
+ const owner = owners.get(map) ?? map.replace(/\.map$/, "");
104
+ await stripReference(owner, map);
105
+ }
106
+ return removed;
107
+ }
66
108
  async function findBundles(dir, files) {
67
109
  const root = resolve(dir);
68
110
  const candidates = [];
@@ -642,6 +684,9 @@ var DEFAULT_ENDPOINT = "https://otlp.autter.dev";
642
684
  var MAX_MAP_CHARS = 32 * 1024 * 1024;
643
685
  var MAX_UPLOAD_BYTES = MAX_MAP_CHARS + 1024 * 1024;
644
686
  var LEGACY_MAX_BYTES = 5 * 1024 * 1024;
687
+ var UPLOAD_KEY_PREFIX = "autter_rtu_";
688
+ var CLIENT_KEY_PREFIX = "autter_rtc_";
689
+ var KEY_ADVICE = "set AUTTER_RUNTIME_KEY to an upload-only key (autter_rtu_\u2026, recommended for CI: it can only upload source maps) or the full server key (autter_rt_\u2026, also works)";
645
690
  var gzip = promisify(gzipCallback);
646
691
  var CHECK_BATCH = 500;
647
692
  var MAX_ATTEMPTS = 3;
@@ -710,7 +755,7 @@ async function prepare(bundle, urlPrefix, release, log, compress) {
710
755
  var FatalUploadError = class extends Error {
711
756
  };
712
757
  function sleep(ms) {
713
- return new Promise((resolve3) => setTimeout(resolve3, ms));
758
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
714
759
  }
715
760
  async function uploadDir(dir, options = {}) {
716
761
  const env = options.env ?? process.env;
@@ -750,10 +795,13 @@ async function uploadDir(dir, options = {}) {
750
795
  return summary;
751
796
  }
752
797
  const key = (options.key ?? env.AUTTER_RUNTIME_KEY ?? "").trim();
753
- if (!key) throw new FatalUploadError("no ingest key: set AUTTER_RUNTIME_KEY to a server key (autter_rt_\u2026)");
754
- if (key.startsWith("autter_rtc_")) {
755
- throw new FatalUploadError("AUTTER_RUNTIME_KEY is a publishable client key (autter_rtc_\u2026); uploads need a server key (autter_rt_\u2026)");
798
+ if (!key) throw new FatalUploadError(`no ingest key: ${KEY_ADVICE}`);
799
+ if (key.startsWith(CLIENT_KEY_PREFIX)) {
800
+ throw new FatalUploadError(`AUTTER_RUNTIME_KEY is a publishable client key (autter_rtc_\u2026), which cannot upload source maps: ${KEY_ADVICE}`);
756
801
  }
802
+ const rejected = (status, detail) => new FatalUploadError(
803
+ `the ingester rejected the key (${status}): ${detail}` + (key.startsWith(UPLOAD_KEY_PREFIX) ? " (upload keys, autter_rtu_\u2026, need ingester 1.7.0 or later; the server key works with any)" : "")
804
+ );
757
805
  const headers = { "content-type": "application/json", authorization: `Bearer ${key}` };
758
806
  const uploadHeaders = { ...headers, "content-encoding": "gzip" };
759
807
  const skipped = /* @__PURE__ */ new Set();
@@ -775,9 +823,7 @@ async function uploadDir(dir, options = {}) {
775
823
  );
776
824
  break;
777
825
  }
778
- if (res.status === 401 || res.status === 403) {
779
- throw new FatalUploadError(`the ingester rejected the key (${res.status}): ${(await res.text()).slice(0, 300)}`);
780
- }
826
+ if (res.status === 401 || res.status === 403) throw rejected(res.status, (await res.text()).slice(0, 300));
781
827
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
782
828
  const body = await res.json();
783
829
  const present = new Set(Array.isArray(body.present) ? body.present : []);
@@ -808,7 +854,7 @@ async function uploadDir(dir, options = {}) {
808
854
  detail = err.message;
809
855
  }
810
856
  if (status === 401 || status === 403) {
811
- fatal = new FatalUploadError(`the ingester rejected the key (${status}): ${detail}`);
857
+ fatal = rejected(status, detail);
812
858
  return;
813
859
  }
814
860
  const retryable = status === 0 || status === 429 || status >= 500;
@@ -864,15 +910,1720 @@ function formatUploadSummary(summary) {
864
910
  return lines;
865
911
  }
866
912
 
913
+ // src/setup.ts
914
+ import { spawnSync } from "child_process";
915
+ import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync } from "fs";
916
+ import { join as join3 } from "path";
917
+
918
+ // src/setup-detect.ts
919
+ import { execFileSync as execFileSync2 } from "child_process";
920
+ import { existsSync as existsSync3, readdirSync, readFileSync, statSync } from "fs";
921
+ import { dirname as dirname2, join as join2, relative as relative3, resolve as resolve2, sep as sep3 } from "path";
922
+
923
+ // src/setup-edit.ts
924
+ import { Parser } from "acorn";
925
+ import { tsPlugin } from "@sveltejs/acorn-typescript";
926
+ var TsParser = Parser.extend(tsPlugin());
927
+ var isTypeScriptFile = (file) => /\.[cm]?tsx?$/.test(file);
928
+ function parseFile(code, file) {
929
+ const parser = isTypeScriptFile(file) ? TsParser : Parser;
930
+ for (const sourceType of ["module", "script"]) {
931
+ const comments = [];
932
+ try {
933
+ const ast = parser.parse(code, {
934
+ ecmaVersion: "latest",
935
+ sourceType,
936
+ locations: true,
937
+ allowHashBang: true,
938
+ onComment: (_block, _text, start, end) => comments.push({ start, end })
939
+ });
940
+ ast.comments = comments;
941
+ return ast;
942
+ } catch {
943
+ }
944
+ }
945
+ return null;
946
+ }
947
+ function walk2(node, visit) {
948
+ if (!node || typeof node !== "object") return;
949
+ if (Array.isArray(node)) {
950
+ for (const child of node) walk2(child, visit);
951
+ return;
952
+ }
953
+ const n = node;
954
+ if (typeof n.type !== "string") return;
955
+ visit(n);
956
+ for (const key of Object.keys(n)) {
957
+ if (key === "loc" || key === "range" || key === "start" || key === "end") continue;
958
+ const value = n[key];
959
+ if (value && typeof value === "object") walk2(value, visit);
960
+ }
961
+ }
962
+ var TS_WRAPPERS = /* @__PURE__ */ new Set(["TSAsExpression", "TSSatisfiesExpression", "TSNonNullExpression", "TSTypeAssertion", "ParenthesizedExpression"]);
963
+ function unwrap(node) {
964
+ let n = node ?? null;
965
+ while (n && TS_WRAPPERS.has(n.type)) n = n.expression;
966
+ return n;
967
+ }
968
+ var isModuleExports = (node) => !!node && node.type === "MemberExpression" && !node.computed && node.object?.type === "Identifier" && node.object.name === "module" && node.property?.type === "Identifier" && node.property.name === "exports";
969
+ function findExport(ast) {
970
+ const sites = [];
971
+ for (const statement of ast.body) {
972
+ if (statement.type === "ExportDefaultDeclaration") {
973
+ const declaration = statement.declaration;
974
+ if (declaration.type === "FunctionDeclaration" || declaration.type === "ClassDeclaration") {
975
+ return { error: "the default export is a function or class declaration" };
976
+ }
977
+ sites.push({ kind: "esm", expr: declaration });
978
+ } else if (statement.type === "ExpressionStatement" && statement.expression.type === "AssignmentExpression" && statement.expression.operator === "=" && isModuleExports(statement.expression.left)) {
979
+ sites.push({ kind: "cjs", expr: statement.expression.right });
980
+ }
981
+ }
982
+ if (sites.length === 0) return { error: "no `export default` or `module.exports =` found" };
983
+ if (sites.length > 1) return { error: "the config is exported more than once" };
984
+ return sites[0];
985
+ }
986
+ function resolveBinding(ast, name) {
987
+ let init = null;
988
+ for (const statement of ast.body) {
989
+ const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
990
+ if (declaration?.type !== "VariableDeclaration") continue;
991
+ for (const declarator of declaration.declarations) {
992
+ if (declarator.id?.type === "Identifier" && declarator.id.name === name) init = declarator.init ?? null;
993
+ }
994
+ }
995
+ if (!init) return null;
996
+ let reassigned = false;
997
+ walk2(ast, (node) => {
998
+ if (node.type === "AssignmentExpression" && node.left?.type === "Identifier" && node.left.name === name) reassigned = true;
999
+ });
1000
+ return reassigned ? null : init;
1001
+ }
1002
+ function returnedObject(fn) {
1003
+ const body = fn.body;
1004
+ if (body.type !== "BlockStatement") {
1005
+ const value2 = unwrap(body);
1006
+ return value2?.type === "ObjectExpression" ? value2 : null;
1007
+ }
1008
+ const returns = [];
1009
+ walk2(body, (node) => {
1010
+ if (node.type === "ReturnStatement") returns.push(node);
1011
+ });
1012
+ if (returns.length !== 1) return null;
1013
+ const last = body.body.at(-1);
1014
+ if (last !== returns[0]) return null;
1015
+ const value = unwrap(returns[0].argument);
1016
+ return value?.type === "ObjectExpression" ? value : null;
1017
+ }
1018
+ function configObject(ast, expr, depth = 0) {
1019
+ const node = unwrap(expr);
1020
+ if (!node || depth > 4) return null;
1021
+ switch (node.type) {
1022
+ case "ObjectExpression":
1023
+ return node;
1024
+ case "Identifier": {
1025
+ const init = resolveBinding(ast, node.name);
1026
+ return init ? configObject(ast, init, depth + 1) : null;
1027
+ }
1028
+ case "CallExpression":
1029
+ if (node.callee?.type === "Identifier" && /^define\w*Config$/.test(node.callee.name) && node.arguments.length === 1) {
1030
+ return configObject(ast, node.arguments[0], depth + 1);
1031
+ }
1032
+ return null;
1033
+ case "ArrowFunctionExpression":
1034
+ case "FunctionExpression":
1035
+ return returnedObject(node);
1036
+ default:
1037
+ return null;
1038
+ }
1039
+ }
1040
+ function findProperty(obj, name) {
1041
+ const matches = obj.properties.filter(
1042
+ (p) => p.type === "Property" && !p.computed && (p.key.type === "Identifier" && p.key.name === name || p.key.type === "Literal" && p.key.value === name)
1043
+ );
1044
+ if (matches.length > 1) return "ambiguous";
1045
+ const [match] = matches;
1046
+ if (!match) return null;
1047
+ if (match.kind !== "init" || match.method) return "ambiguous";
1048
+ return match;
1049
+ }
1050
+ function literalProperty(obj, path) {
1051
+ let node = obj;
1052
+ for (const [i, name] of path.entries()) {
1053
+ if (!node || node.type !== "ObjectExpression") return void 0;
1054
+ const property = findProperty(node, name);
1055
+ if (!property || property === "ambiguous") return void 0;
1056
+ node = unwrap(property.value);
1057
+ if (i === path.length - 1) {
1058
+ if (node?.type === "Literal" && (typeof node.value !== "object" || node.value === null)) return node.value;
1059
+ if (node?.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0].value.cooked;
1060
+ return void 0;
1061
+ }
1062
+ }
1063
+ return void 0;
1064
+ }
1065
+ function applyEdits(code, edits) {
1066
+ let out = code;
1067
+ for (const edit of [...edits].sort((a, b) => b.start - a.start)) out = `${out.slice(0, edit.start)}${edit.text}${out.slice(edit.end)}`;
1068
+ return out;
1069
+ }
1070
+ function lineIndent(code, pos) {
1071
+ const lineStart = code.lastIndexOf("\n", pos - 1) + 1;
1072
+ return /^[ \t]*/.exec(code.slice(lineStart))[0];
1073
+ }
1074
+ var spansLines = (code, node) => code.slice(node.start, node.end).includes("\n");
1075
+ function appendAfter(code, container, last, text) {
1076
+ if (!spansLines(code, container)) return { start: last.end, end: last.end, text: `, ${text}` };
1077
+ const indent = lineIndent(code, last.start);
1078
+ const trailing = /^[^\S\n]*,[^\S\n]*(?:\/\/[^\n]*|\/\*[^\n]*?\*\/)?[^\S\n]*(?=\n)/.exec(code.slice(last.end, container.end));
1079
+ if (trailing) {
1080
+ const at = last.end + trailing[0].length;
1081
+ return { start: at, end: at, text: `
1082
+ ${indent}${text},` };
1083
+ }
1084
+ return { start: last.end, end: last.end, text: `,
1085
+ ${indent}${text}` };
1086
+ }
1087
+ function appendElement(code, array, text) {
1088
+ const elements = array.elements.filter((e) => !!e);
1089
+ const last = elements.at(-1);
1090
+ if (!last) {
1091
+ const inner = code.slice(array.start + 1, array.end - 1);
1092
+ return inner.trim() === "" ? { start: array.start + 1, end: array.end - 1, text } : { start: array.end - 1, end: array.end - 1, text };
1093
+ }
1094
+ return appendAfter(code, array, last, text);
1095
+ }
1096
+ function appendProperty(code, obj, text) {
1097
+ const last = obj.properties.at(-1);
1098
+ if (!last) {
1099
+ if (code.slice(obj.start + 1, obj.end - 1).trim() !== "") return null;
1100
+ return { start: obj.start, end: obj.end, text: `{ ${text} }` };
1101
+ }
1102
+ return appendAfter(code, obj, last, text);
1103
+ }
1104
+ function addPluginEdit(code, obj, element, container) {
1105
+ let target = obj;
1106
+ if (container) {
1107
+ const property = findProperty(obj, container);
1108
+ if (property === "ambiguous") return { error: `\`${container}\` is defined more than once or as a method` };
1109
+ if (!property) {
1110
+ if (obj.properties.some((p) => p.type === "SpreadElement")) {
1111
+ return { error: `the config spreads another object, which may already set \`${container}\`` };
1112
+ }
1113
+ return appendProperty(code, obj, `${container}: { plugins: [${element}] }`) ?? { error: "the config object holds only comments" };
1114
+ }
1115
+ const value2 = unwrap(property.value);
1116
+ if (value2?.type !== "ObjectExpression") return { error: `\`${container}\` is not an object literal` };
1117
+ target = value2;
1118
+ }
1119
+ const plugins = findProperty(target, "plugins");
1120
+ if (plugins === "ambiguous") return { error: "`plugins` is defined more than once or as a method" };
1121
+ if (!plugins) {
1122
+ if (target.properties.some((p) => p.type === "SpreadElement")) {
1123
+ return { error: "the config spreads another object, which may already set `plugins`" };
1124
+ }
1125
+ return appendProperty(code, target, `plugins: [${element}]`) ?? { error: "the config object holds only comments" };
1126
+ }
1127
+ const value = unwrap(plugins.value);
1128
+ if (value?.type !== "ArrayExpression") return { error: "`plugins` is not an array literal" };
1129
+ return appendElement(code, value, element);
1130
+ }
1131
+ function requireCall(init) {
1132
+ let node = unwrap(init);
1133
+ while (node) {
1134
+ if (node.type === "CallExpression") {
1135
+ if (node.callee?.type === "Identifier" && node.callee.name === "require") return node;
1136
+ node = unwrap(node.callee);
1137
+ } else if (node.type === "MemberExpression") node = unwrap(node.object);
1138
+ else return null;
1139
+ }
1140
+ return null;
1141
+ }
1142
+ function importEdit(code, ast, name, spec, kind) {
1143
+ const body = ast.body;
1144
+ let last;
1145
+ let source = null;
1146
+ if (kind === "esm") {
1147
+ last = body.filter((s) => s.type === "ImportDeclaration").at(-1);
1148
+ source = last?.source ?? null;
1149
+ } else {
1150
+ last = body.filter((s) => s.type === "VariableDeclaration" && s.declarations.every((d) => requireCall(d.init))).at(-1);
1151
+ source = last ? requireCall(last.declarations[0].init)?.arguments[0] ?? null : null;
1152
+ }
1153
+ const quote = source && code[source.start] === "'" ? "'" : '"';
1154
+ const semi = last ? code[last.end - 1] === ";" ? ";" : "" : ";";
1155
+ const line = kind === "esm" ? `import { ${name} } from ${quote}${spec}${quote}${semi}` : `const { ${name} } = require(${quote}${spec}${quote})${semi}`;
1156
+ if (last) return { start: last.end, end: last.end, text: `
1157
+ ${line}` };
1158
+ const first = body.find((s) => !(s.type === "ExpressionStatement" && typeof s.directive === "string"));
1159
+ if (!first) return { start: code.length, end: code.length, text: `${code.endsWith("\n") || !code ? "" : "\n"}${line}
1160
+ ` };
1161
+ let at = first.start;
1162
+ const comments = (ast.comments ?? []).filter((c) => c.end <= first.start);
1163
+ for (let i = comments.length - 1; i >= 0; i--) {
1164
+ const gap = code.slice(comments[i].end, at);
1165
+ if (gap.trim() !== "" || (gap.match(/\n/g) ?? []).length > 1) break;
1166
+ at = comments[i].start;
1167
+ }
1168
+ at = code.lastIndexOf("\n", at - 1) + 1;
1169
+ return { start: at, end: at, text: `${line}
1170
+
1171
+ ` };
1172
+ }
1173
+ function calls(ast, name) {
1174
+ const found = [];
1175
+ walk2(ast, (node) => {
1176
+ if ((node.type === "CallExpression" || node.type === "NewExpression") && node.callee?.type === "Identifier" && node.callee.name === name) {
1177
+ found.push(node);
1178
+ }
1179
+ });
1180
+ return found;
1181
+ }
1182
+ function finish(original, file, edits, name) {
1183
+ const code = applyEdits(original, edits);
1184
+ const ast = parseFile(code, file);
1185
+ if (!ast || calls(ast, name).length === 0) return { status: "manual", reason: "the edited file did not parse back as expected" };
1186
+ return { status: "edited", code };
1187
+ }
1188
+ function addBundlerPlugin(code, file, plugin) {
1189
+ const ast = parseFile(code, file);
1190
+ if (!ast) return { status: "manual", reason: `${file} does not parse as ${isTypeScriptFile(file) ? "TypeScript" : "JavaScript"}` };
1191
+ if (calls(ast, plugin.name).length) return { status: "already" };
1192
+ if (code.includes(plugin.spec)) return { status: "manual", reason: `${file} imports ${plugin.spec} but never uses ${plugin.name}` };
1193
+ const site = findExport(ast);
1194
+ if ("error" in site) return { status: "manual", reason: site.error };
1195
+ const obj = configObject(ast, site.expr);
1196
+ if (!obj) return { status: "manual", reason: "the exported config is not an object literal, a define\u2026Config({...}) call, a variable holding one, or a function returning one" };
1197
+ const edit = addPluginEdit(code, obj, plugin.element, plugin.container);
1198
+ if ("error" in edit) return { status: "manual", reason: edit.error };
1199
+ return finish(code, file, [edit, importEdit(code, ast, plugin.name, plugin.spec, site.kind)], plugin.name);
1200
+ }
1201
+ var NEXT_WRAPPABLE = /* @__PURE__ */ new Set(["ObjectExpression", "Identifier", "CallExpression", "ArrowFunctionExpression", "FunctionExpression", "MemberExpression"]);
1202
+ function wrapNextConfig(code, file) {
1203
+ const name = "withAutterSourceMaps";
1204
+ const spec = "@autter/runtime-cli/next";
1205
+ const ast = parseFile(code, file);
1206
+ if (!ast) return { status: "manual", reason: `${file} does not parse as ${isTypeScriptFile(file) ? "TypeScript" : "JavaScript"}` };
1207
+ const site = findExport(ast);
1208
+ const existing = calls(ast, name);
1209
+ if (existing.length) {
1210
+ if (!("error" in site) && unwrap(site.expr) === existing[0]) return { status: "already" };
1211
+ return {
1212
+ status: "manual",
1213
+ reason: `${name} is not the outermost wrapper of the exported config, so other wrappers' webpack and runAfterProductionCompile hooks may not be chained`
1214
+ };
1215
+ }
1216
+ if (code.includes(spec)) return { status: "manual", reason: `${file} imports ${spec} but never calls ${name}` };
1217
+ if ("error" in site) return { status: "manual", reason: site.error };
1218
+ const target = unwrap(site.expr);
1219
+ if (!target || !NEXT_WRAPPABLE.has(target.type)) {
1220
+ return { status: "manual", reason: "the exported config is not an object, a variable, a wrapper call or a function" };
1221
+ }
1222
+ const edits = [
1223
+ { start: site.expr.start, end: site.expr.start, text: `${name}(` },
1224
+ { start: site.expr.end, end: site.expr.end, text: ")" },
1225
+ importEdit(code, ast, name, spec, site.kind)
1226
+ ];
1227
+ return finish(code, file, edits, name);
1228
+ }
1229
+ function nextConfigObject(ast) {
1230
+ const site = findExport(ast);
1231
+ if ("error" in site) return null;
1232
+ let node = unwrap(site.expr);
1233
+ for (let depth = 0; node && depth < 6; depth++) {
1234
+ const obj = configObject(ast, node);
1235
+ if (obj) return obj;
1236
+ if (node.type === "CallExpression" && node.arguments.length) node = unwrap(node.arguments[0]);
1237
+ else return null;
1238
+ }
1239
+ return null;
1240
+ }
1241
+ function exportedConfigObject(ast) {
1242
+ const site = findExport(ast);
1243
+ return "error" in site ? null : configObject(ast, site.expr);
1244
+ }
1245
+
1246
+ // src/setup-detect.ts
1247
+ var posix = (path) => path.split(sep3).join("/");
1248
+ var rel = (from, to) => posix(relative3(from, to)) || ".";
1249
+ function readText(path) {
1250
+ try {
1251
+ return readFileSync(path, "utf8");
1252
+ } catch {
1253
+ return null;
1254
+ }
1255
+ }
1256
+ function readJson(path) {
1257
+ const text = readText(path);
1258
+ if (text === null) return null;
1259
+ try {
1260
+ return JSON.parse(text);
1261
+ } catch {
1262
+ return null;
1263
+ }
1264
+ }
1265
+ var firstExisting = (dir, names) => names.find((name) => existsSync3(join2(dir, name))) ?? null;
1266
+ function ancestors(dir, stop) {
1267
+ const out = [];
1268
+ for (let current = dir; ; current = dirname2(current)) {
1269
+ out.push(current);
1270
+ if (current === stop || dirname2(current) === current) break;
1271
+ }
1272
+ return out;
1273
+ }
1274
+ function gitRoot(dir) {
1275
+ for (const current of ancestors(dir, null)) if (existsSync3(join2(current, ".git"))) return current;
1276
+ return null;
1277
+ }
1278
+ function githubRepo(root) {
1279
+ if (!root) return null;
1280
+ const git = (args) => {
1281
+ try {
1282
+ return execFileSync2("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 1e4 }).trim();
1283
+ } catch {
1284
+ return "";
1285
+ }
1286
+ };
1287
+ const remotes = git(["remote"]).split("\n").filter(Boolean);
1288
+ for (const remote of remotes.includes("origin") ? ["origin", ...remotes.filter((r) => r !== "origin")] : remotes) {
1289
+ const url = git(["remote", "get-url", remote]);
1290
+ const match = /github\.com[:/]([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/.exec(url);
1291
+ if (match) return `${match[1]}/${match[2]}`;
1292
+ }
1293
+ return null;
1294
+ }
1295
+ function semver(version) {
1296
+ const match = version ? /(\d+)\.(\d+)\.(\d+)/.exec(version) : null;
1297
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
1298
+ }
1299
+ function atLeast(version, minimum) {
1300
+ const a = semver(version);
1301
+ const b = semver(minimum);
1302
+ if (!a) return null;
1303
+ for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
1304
+ return true;
1305
+ }
1306
+ function installedVersion(dir, name) {
1307
+ for (const current of ancestors(dir, null)) {
1308
+ const manifest = readJson(join2(current, "node_modules", ...name.split("/"), "package.json"));
1309
+ if (manifest?.version) return manifest.version;
1310
+ }
1311
+ return null;
1312
+ }
1313
+ function packageManager(dir, pkg, stop) {
1314
+ for (const current of ancestors(dir, stop)) {
1315
+ const manifest = current === dir ? pkg : readJson(join2(current, "package.json"));
1316
+ const declared = /^(npm|pnpm|yarn|bun)@(\S+)/.exec(manifest?.packageManager ?? "");
1317
+ if (declared) {
1318
+ const name = declared[1];
1319
+ const lockfile2 = lockfileIn(current);
1320
+ return { name, lockfile: lockfile2 ? rel(dir, join2(current, lockfile2)) : null, ...name === "yarn" ? { yarnBerry: !declared[2].startsWith("1.") } : {} };
1321
+ }
1322
+ const lockfile = lockfileIn(current);
1323
+ if (lockfile) {
1324
+ const name = lockfile.startsWith("pnpm") ? "pnpm" : lockfile === "yarn.lock" ? "yarn" : lockfile.startsWith("bun") ? "bun" : "npm";
1325
+ const berry = name === "yarn" && (existsSync3(join2(current, ".yarnrc.yml")) || (readText(join2(current, lockfile)) ?? "").includes("__metadata:"));
1326
+ return { name, lockfile: rel(dir, join2(current, lockfile)), ...name === "yarn" ? { yarnBerry: berry } : {} };
1327
+ }
1328
+ }
1329
+ return { name: "npm", lockfile: null };
1330
+ }
1331
+ function lockfileIn(dir) {
1332
+ return firstExisting(dir, ["pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb", "package-lock.json", "npm-shrinkwrap.json"]);
1333
+ }
1334
+ function scriptFlag(script, ...flags) {
1335
+ if (!script) return null;
1336
+ for (const flag of flags) {
1337
+ const match = new RegExp(`(?:^|\\s)${flag.replace(/[-]/g, "\\-")}(?:=|\\s+)(?:"([^"]+)"|'([^']+)'|([^\\s&|;]+))`).exec(script);
1338
+ if (match) return match[1] ?? match[2] ?? match[3] ?? null;
1339
+ }
1340
+ return null;
1341
+ }
1342
+ var CONFIG_NAMES = {
1343
+ vite: ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs", "vite.config.cts", "vite.config.cjs"],
1344
+ next: ["next.config.ts", "next.config.mts", "next.config.mjs", "next.config.js", "next.config.cjs"],
1345
+ webpack: ["webpack.config.js", "webpack.config.cjs", "webpack.config.mjs", "webpack.config.ts"],
1346
+ rollup: ["rollup.config.js", "rollup.config.mjs", "rollup.config.cjs", "rollup.config.ts"],
1347
+ nuxt: ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"],
1348
+ astro: ["astro.config.mjs", "astro.config.ts", "astro.config.js", "astro.config.mts", "astro.config.cjs"],
1349
+ "vue-cli": ["vue.config.js", "vue.config.cjs", "vue.config.mjs"],
1350
+ "solid-start": ["app.config.ts", "app.config.js"]
1351
+ };
1352
+ function buildTool(dir, pkg) {
1353
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1354
+ const has = (name) => typeof deps[name] === "string";
1355
+ const buildScript = pkg.scripts?.build ?? null;
1356
+ const scriptConfig = (tool2) => {
1357
+ if (!buildScript || !new RegExp(`\\b${tool2}\\b`).test(buildScript)) return null;
1358
+ const file = scriptFlag(buildScript, "--config", "-c");
1359
+ return file && existsSync3(join2(dir, file)) ? file : null;
1360
+ };
1361
+ const tool = (id, framework, config, versionOf) => ({
1362
+ id,
1363
+ framework,
1364
+ config,
1365
+ version: deps[versionOf] ?? null,
1366
+ buildScript
1367
+ });
1368
+ if (has("next")) return tool("next", null, firstExisting(dir, [...CONFIG_NAMES.next]), "next");
1369
+ if (has("react-scripts")) return tool("cra", has("@craco/craco") ? "craco" : null, null, "react-scripts");
1370
+ if (existsSync3(join2(dir, "angular.json")) && (has("@angular/core") || has("@angular/cli"))) {
1371
+ return tool("angular", null, "angular.json", has("@angular/cli") ? "@angular/cli" : "@angular/core");
1372
+ }
1373
+ const viteConfig = scriptConfig("vite") ?? firstExisting(dir, [...CONFIG_NAMES.vite]);
1374
+ if (has("nuxt")) return tool("vite", "nuxt", firstExisting(dir, [...CONFIG_NAMES.nuxt]), "nuxt");
1375
+ if (has("astro")) return tool("vite", "astro", firstExisting(dir, [...CONFIG_NAMES.astro]), "astro");
1376
+ if (has("@solidjs/start")) return tool("vite", "solid-start", firstExisting(dir, [...CONFIG_NAMES["solid-start"]]), "@solidjs/start");
1377
+ const viteFrameworks = [
1378
+ ["@sveltejs/kit", "sveltekit"],
1379
+ ["@remix-run/dev", "remix"],
1380
+ ["@react-router/dev", "react-router"],
1381
+ ["@builder.io/qwik", "qwik"],
1382
+ ["vite-plugin-solid", "solid"]
1383
+ ];
1384
+ for (const [dep, framework] of viteFrameworks) {
1385
+ if (has(dep) && (viteConfig || has("vite"))) return tool("vite", framework, viteConfig, has("vite") ? "vite" : dep);
1386
+ }
1387
+ const builds = (name) => has(name) && new RegExp(`\\b${name}\\b`).test(buildScript ?? "");
1388
+ if (viteConfig || builds("vite")) return tool("vite", null, viteConfig, "vite");
1389
+ if (has("@vue/cli-service")) return tool("webpack", "vue-cli", firstExisting(dir, [...CONFIG_NAMES["vue-cli"]]), "@vue/cli-service");
1390
+ const webpackConfig = scriptConfig("webpack") ?? firstExisting(dir, [...CONFIG_NAMES.webpack]);
1391
+ if (webpackConfig || builds("webpack")) return tool("webpack", null, webpackConfig, "webpack");
1392
+ const rollupConfig = scriptConfig("rollup") ?? firstExisting(dir, [...CONFIG_NAMES.rollup]);
1393
+ if (rollupConfig || builds("rollup")) return tool("rollup", null, rollupConfig, "rollup");
1394
+ if (has("parcel") || /\bparcel\b/.test(buildScript ?? "")) return tool("parcel", null, null, "parcel");
1395
+ if (/\besbuild\b/.test(buildScript ?? "")) return tool("esbuild", null, null, "esbuild");
1396
+ return tool("unknown", null, null, "");
1397
+ }
1398
+ function ciProviders(dir, root, env) {
1399
+ const found = [];
1400
+ const top = root ?? dir;
1401
+ const near = (names) => {
1402
+ for (const current of ancestors(dir, top)) {
1403
+ const name = firstExisting(current, names);
1404
+ if (name) return rel(dir, join2(current, name));
1405
+ }
1406
+ return null;
1407
+ };
1408
+ const workflows = join2(top, ".github", "workflows");
1409
+ let workflowFile = null;
1410
+ try {
1411
+ workflowFile = readdirSync(workflows).filter((f) => /\.ya?ml$/.test(f)).sort()[0] ?? null;
1412
+ } catch {
1413
+ }
1414
+ if (workflowFile) {
1415
+ found.push({ id: "github", name: "GitHub Actions", evidence: rel(dir, join2(workflows, workflowFile)), repo: githubRepo(root) });
1416
+ } else if (env.GITHUB_ACTIONS === "true") {
1417
+ found.push({ id: "github", name: "GitHub Actions", evidence: "GITHUB_ACTIONS", repo: env.GITHUB_REPOSITORY ?? githubRepo(root) });
1418
+ }
1419
+ const vercel = near(["vercel.json", ".vercel"]);
1420
+ if (vercel || env.VERCEL) found.push({ id: "vercel", name: "Vercel", evidence: vercel ?? "VERCEL" });
1421
+ const netlify = near(["netlify.toml"]);
1422
+ if (netlify || env.NETLIFY) found.push({ id: "netlify", name: "Netlify", evidence: netlify ?? "NETLIFY" });
1423
+ const wrangler = near(["wrangler.toml", "wrangler.json", "wrangler.jsonc"]);
1424
+ const pagesConfig = wrangler && /pages_build_output_dir/.test(readText(join2(dir, wrangler)) ?? "") ? wrangler : null;
1425
+ const pkg = readJson(join2(dir, "package.json"));
1426
+ const pagesScript = Object.values(pkg?.scripts ?? {}).some((s) => /wrangler\s+pages\b/.test(s)) ? "package.json (wrangler pages)" : null;
1427
+ if (pagesConfig || pagesScript || env.CF_PAGES) {
1428
+ found.push({ id: "cloudflare-pages", name: "Cloudflare Pages", evidence: pagesConfig ?? pagesScript ?? "CF_PAGES" });
1429
+ }
1430
+ const gitlab = near([".gitlab-ci.yml"]);
1431
+ if (gitlab || env.GITLAB_CI) found.push({ id: "gitlab", name: "GitLab CI", evidence: gitlab ?? "GITLAB_CI" });
1432
+ return found;
1433
+ }
1434
+ function workflowsPassKey(dir, root) {
1435
+ const workflows = join2(root ?? dir, ".github", "workflows");
1436
+ try {
1437
+ return readdirSync(workflows).some((f) => /\.ya?ml$/.test(f) && /secrets\.AUTTER_RUNTIME_KEY\b/.test(readText(join2(workflows, f)) ?? ""));
1438
+ } catch {
1439
+ return false;
1440
+ }
1441
+ }
1442
+ function angularInfo(json) {
1443
+ const projects = json.projects ?? {};
1444
+ const names = Object.keys(projects);
1445
+ const name = (typeof json.defaultProject === "string" && projects[json.defaultProject] ? json.defaultProject : null) ?? names.find((n) => projects[n].projectType === "application") ?? names[0];
1446
+ if (!name) return null;
1447
+ const project = projects[name];
1448
+ const build = (project.architect ?? project.targets)?.build;
1449
+ if (!build) return null;
1450
+ const options = build.options ?? {};
1451
+ const production = build.configurations?.production ?? {};
1452
+ const application = typeof build.builder === "string" && /:application$/.test(build.builder);
1453
+ const outputPath = production.outputPath ?? options.outputPath ?? `dist/${name}`;
1454
+ let outputDir;
1455
+ if (outputPath && typeof outputPath === "object") {
1456
+ const { base, browser } = outputPath;
1457
+ outputDir = browser === "" ? String(base) : `${base}/${browser ?? "browser"}`;
1458
+ } else {
1459
+ outputDir = application ? `${outputPath}/browser` : String(outputPath);
1460
+ }
1461
+ const prefix = production.deployUrl ?? options.deployUrl ?? production.baseHref ?? options.baseHref;
1462
+ return {
1463
+ project: name,
1464
+ outputDir,
1465
+ sourceMap: "sourceMap" in production ? production.sourceMap : options.sourceMap,
1466
+ urlPrefix: typeof prefix === "string" && prefix.startsWith("/") ? prefix : "/"
1467
+ };
1468
+ }
1469
+ function mapStateFromLiteral(value, onTrue = "public") {
1470
+ if (value === true) return onTrue;
1471
+ if (value === false) return "off";
1472
+ if (value === "hidden") return "hidden";
1473
+ if (value === "inline") return "inline";
1474
+ return "unknown";
1475
+ }
1476
+ var CRA_ENV_FILES = [".env.production.local", ".env.local", ".env.production", ".env"];
1477
+ function craEnv(dir, name) {
1478
+ for (const file of CRA_ENV_FILES) {
1479
+ const match = new RegExp(`^\\s*${name}\\s*=\\s*["']?([^"'\\s#]*)`, "m").exec(readText(join2(dir, file)) ?? "");
1480
+ if (match) return { value: match[1], file };
1481
+ }
1482
+ return null;
1483
+ }
1484
+ function parseConfig(dir, config) {
1485
+ if (!config) return null;
1486
+ const code = readText(join2(dir, config));
1487
+ return code === null ? null : parseFile(code, config);
1488
+ }
1489
+ function outputFacts(dir, pkg, tool) {
1490
+ const script = tool.buildScript;
1491
+ const none = (state, setting = null, file = null) => ({ state, setting, file });
1492
+ switch (tool.id) {
1493
+ case "vite": {
1494
+ const ast = parseConfig(dir, tool.config);
1495
+ const obj = ast ? exportedConfigObject(ast) : null;
1496
+ const base = tool.framework === "nuxt" || tool.framework === "astro" ? ["vite"] : [];
1497
+ const sourcemap = literalProperty(obj, [...base, "build", "sourcemap"]);
1498
+ const viteBase = literalProperty(obj, [...base, "base"]);
1499
+ const outDir = (tool.framework === "astro" ? literalProperty(obj, ["outDir"]) : literalProperty(obj, [...base, "build", "outDir"])) ?? scriptFlag(script, "--outDir");
1500
+ const defaults = {
1501
+ nuxt: ".output/public",
1502
+ "solid-start": ".output/public",
1503
+ sveltekit: "build",
1504
+ remix: "build/client",
1505
+ "react-router": "build/client"
1506
+ };
1507
+ return {
1508
+ outputDir: typeof outDir === "string" ? outDir : defaults[tool.framework ?? ""] ?? "dist",
1509
+ urlPrefix: typeof viteBase === "string" && viteBase.startsWith("/") ? viteBase : "/",
1510
+ sourceMaps: sourcemap === void 0 ? none(obj || !tool.config ? "default-off" : "unknown") : none(mapStateFromLiteral(sourcemap), `build.sourcemap: ${JSON.stringify(sourcemap)}`, tool.config)
1511
+ };
1512
+ }
1513
+ case "next": {
1514
+ const ast = parseConfig(dir, tool.config);
1515
+ const obj = ast ? nextConfigObject(ast) : null;
1516
+ const distDir = literalProperty(obj, ["distDir"]);
1517
+ const exported = literalProperty(obj, ["output"]) === "export";
1518
+ const publicMaps = literalProperty(obj, ["productionBrowserSourceMaps"]);
1519
+ const basePath = literalProperty(obj, ["assetPrefix"]) ?? literalProperty(obj, ["basePath"]);
1520
+ return {
1521
+ outputDir: exported ? "out" : `${typeof distDir === "string" ? distDir : ".next"}/static`,
1522
+ urlPrefix: exported ? `${typeof basePath === "string" ? basePath.replace(/\/+$/, "") : ""}/` : `${typeof basePath === "string" ? basePath.replace(/\/+$/, "") : ""}/_next/static/`,
1523
+ sourceMaps: publicMaps === true ? none("public", "productionBrowserSourceMaps: true", tool.config) : none(obj || !tool.config ? "default-off" : "unknown")
1524
+ };
1525
+ }
1526
+ case "webpack":
1527
+ case "rollup": {
1528
+ const ast = parseConfig(dir, tool.config);
1529
+ const obj = ast ? exportedConfigObject(ast) : null;
1530
+ if (tool.framework === "vue-cli") {
1531
+ const productionSourceMap = literalProperty(obj, ["productionSourceMap"]);
1532
+ const outputDir = literalProperty(obj, ["outputDir"]);
1533
+ const publicPath = literalProperty(obj, ["publicPath"]);
1534
+ return {
1535
+ outputDir: typeof outputDir === "string" ? outputDir : "dist",
1536
+ urlPrefix: typeof publicPath === "string" && publicPath.startsWith("/") ? publicPath : "/",
1537
+ sourceMaps: productionSourceMap === false ? none("off", "productionSourceMap: false", tool.config) : none("default-public", productionSourceMap === true ? "productionSourceMap: true" : null, tool.config)
1538
+ };
1539
+ }
1540
+ if (tool.id === "webpack") {
1541
+ const devtool = literalProperty(obj, ["devtool"]);
1542
+ const path = obj ? outputPathOf(obj) : null;
1543
+ const publicPath = literalProperty(obj, ["output", "publicPath"]);
1544
+ return {
1545
+ outputDir: path ?? "dist",
1546
+ urlPrefix: typeof publicPath === "string" && publicPath.startsWith("/") ? publicPath : "/",
1547
+ sourceMaps: devtool === void 0 ? none(obj || !tool.config ? "default-off" : "unknown") : none(
1548
+ devtool === false ? "off" : typeof devtool === "string" ? devtool.includes("hidden") ? "hidden" : devtool.includes("inline") || devtool.startsWith("eval") ? "inline" : "public" : "unknown",
1549
+ `devtool: ${JSON.stringify(devtool)}`,
1550
+ tool.config
1551
+ )
1552
+ };
1553
+ }
1554
+ const outDir = literalProperty(obj, ["output", "dir"]);
1555
+ const outFile = literalProperty(obj, ["output", "file"]);
1556
+ const sourcemap = literalProperty(obj, ["output", "sourcemap"]);
1557
+ return {
1558
+ outputDir: typeof outDir === "string" ? outDir : typeof outFile === "string" ? posix(dirname2(outFile)) : "dist",
1559
+ urlPrefix: "/",
1560
+ sourceMaps: sourcemap === void 0 ? none(obj || !tool.config ? "default-off" : "unknown") : none(mapStateFromLiteral(sourcemap), `output.sourcemap: ${JSON.stringify(sourcemap)}`, tool.config)
1561
+ };
1562
+ }
1563
+ case "cra": {
1564
+ const buildPath = (script && /BUILD_PATH=(\S+)/.exec(script)?.[1]) ?? craEnv(dir, "BUILD_PATH")?.value;
1565
+ const inScript = script ? /GENERATE_SOURCEMAP=(\w+)/.exec(script) : null;
1566
+ const fromEnv = craEnv(dir, "GENERATE_SOURCEMAP");
1567
+ const homepage = pkg.homepage ? homepagePath(pkg.homepage) : "/";
1568
+ const setting = inScript ? { value: inScript[1], file: "package.json" } : fromEnv;
1569
+ return {
1570
+ outputDir: buildPath || "build",
1571
+ urlPrefix: homepage,
1572
+ sourceMaps: setting ? none(setting.value === "false" ? "off" : "default-public", `GENERATE_SOURCEMAP=${setting.value}`, setting.file) : none("default-public")
1573
+ };
1574
+ }
1575
+ case "angular": {
1576
+ const json = readJson(join2(dir, "angular.json"));
1577
+ const info = json ? angularInfo(json) : null;
1578
+ if (!info) return { outputDir: null, urlPrefix: "/", sourceMaps: none("unknown", null, "angular.json") };
1579
+ const sm = info.sourceMap;
1580
+ const state = sm === void 0 ? "default-off" : sm === false ? "off" : sm === true ? "public" : sm.scripts === false ? "off" : sm.hidden ? "hidden" : "public";
1581
+ return {
1582
+ outputDir: info.outputDir,
1583
+ urlPrefix: info.urlPrefix,
1584
+ sourceMaps: none(state, sm === void 0 ? null : `sourceMap: ${JSON.stringify(sm)}`, "angular.json")
1585
+ };
1586
+ }
1587
+ case "parcel": {
1588
+ const off = /--no-source-maps\b/.test(script ?? "");
1589
+ const publicUrl = scriptFlag(script, "--public-url");
1590
+ return {
1591
+ outputDir: scriptFlag(script, "--dist-dir") ?? "dist",
1592
+ urlPrefix: publicUrl && publicUrl.startsWith("/") ? publicUrl : "/",
1593
+ sourceMaps: off ? none("off", "--no-source-maps", "package.json") : none("default-public")
1594
+ };
1595
+ }
1596
+ case "esbuild": {
1597
+ const flag = script ? /--sourcemap(?:=(\w+))?/.exec(script) : null;
1598
+ const outfile = scriptFlag(script, "--outfile");
1599
+ return {
1600
+ outputDir: scriptFlag(script, "--outdir") ?? (outfile ? posix(dirname2(outfile)) : "dist"),
1601
+ urlPrefix: "/",
1602
+ sourceMaps: flag ? none(flag[1] === "external" ? "hidden" : flag[1] === "inline" ? "inline" : "public", flag[0], "package.json") : none(script && /\besbuild\b/.test(script) ? "off" : "unknown")
1603
+ };
1604
+ }
1605
+ default: {
1606
+ const existing = ["dist", "build", "out"].find((d) => existsSync3(join2(dir, d)) && statSync(join2(dir, d)).isDirectory());
1607
+ return { outputDir: existing ?? null, urlPrefix: "/", sourceMaps: none("unknown") };
1608
+ }
1609
+ }
1610
+ }
1611
+ function outputPathOf(obj) {
1612
+ const output = findProperty(obj, "output");
1613
+ if (!output || output === "ambiguous") return null;
1614
+ const value = unwrap(output.value);
1615
+ if (value?.type !== "ObjectExpression") return null;
1616
+ const path = findProperty(value, "path");
1617
+ if (!path || path === "ambiguous") return null;
1618
+ const node = unwrap(path.value);
1619
+ if (node?.type === "Literal" && typeof node.value === "string") return node.value;
1620
+ if (node?.type === "CallExpression" && node.callee?.type === "MemberExpression" && ["resolve", "join"].includes(node.callee.property?.name)) {
1621
+ const parts = node.arguments.filter((a) => !(a.type === "Identifier" && a.name === "__dirname"));
1622
+ if (parts.every((a) => a.type === "Literal" && typeof a.value === "string")) return parts.map((a) => a.value).join("/") || null;
1623
+ }
1624
+ return null;
1625
+ }
1626
+ function homepagePath(homepage) {
1627
+ if (homepage === "." || homepage === "./") return "/";
1628
+ try {
1629
+ const path = new URL(homepage, "https://autter.invalid").pathname;
1630
+ return path.endsWith("/") ? path : `${path}/`;
1631
+ } catch {
1632
+ return "/";
1633
+ }
1634
+ }
1635
+ var SOURCE_EXTENSIONS = /\.(?:[cm]?[jt]sx?|vue|svelte|astro)$/;
1636
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage", "public", "static", "storybook-static"]);
1637
+ function releaseFacts(dir, skip) {
1638
+ const found = [];
1639
+ const visit = (current) => {
1640
+ let entries;
1641
+ try {
1642
+ entries = readdirSync(current, { withFileTypes: true });
1643
+ } catch {
1644
+ return;
1645
+ }
1646
+ for (const entry of entries) {
1647
+ if (entry.name.startsWith(".")) continue;
1648
+ const full = join2(current, entry.name);
1649
+ if (entry.isDirectory()) {
1650
+ if (!SKIP_DIRS.has(entry.name) && !skip.has(full)) visit(full);
1651
+ } else if (entry.isFile() && SOURCE_EXTENSIONS.test(entry.name)) {
1652
+ const code = readText(full);
1653
+ if (!code?.includes("initAutterBrowser")) continue;
1654
+ for (const passed of initCalls(code)) found.push({ file: rel(dir, full), passed });
1655
+ }
1656
+ }
1657
+ };
1658
+ visit(dir);
1659
+ if (!found.length) return { file: null, passed: null };
1660
+ const missing = found.find((f) => f.passed === false);
1661
+ const unknown = found.find((f) => f.passed === null);
1662
+ const pick = missing ?? unknown ?? found[0];
1663
+ return { file: pick.file, passed: missing ? false : unknown ? null : true };
1664
+ }
1665
+ function initCalls(code) {
1666
+ const out = [];
1667
+ const re = /\binitAutterBrowser\s*\(/g;
1668
+ for (let match = re.exec(code); match; match = re.exec(code)) {
1669
+ let i = match.index + match[0].length;
1670
+ while (/\s/.test(code[i] ?? "")) i++;
1671
+ if (code[i] !== "{") {
1672
+ out.push(code[i] === ")" ? false : null);
1673
+ continue;
1674
+ }
1675
+ let depth = 0;
1676
+ let quote = null;
1677
+ let sawRelease = false;
1678
+ let sawSpread = false;
1679
+ for (; i < code.length; i++) {
1680
+ const ch = code[i];
1681
+ if (quote) {
1682
+ if (ch === "\\") i++;
1683
+ else if (ch === quote) quote = null;
1684
+ continue;
1685
+ }
1686
+ if (ch === '"' || ch === "'" || ch === "`") quote = ch;
1687
+ else if (ch === "{" || ch === "(" || ch === "[") depth++;
1688
+ else if (ch === "}" || ch === ")" || ch === "]") {
1689
+ depth--;
1690
+ if (depth === 0) break;
1691
+ } else if (depth === 1) {
1692
+ if (code.startsWith("...", i)) sawSpread = true;
1693
+ if (/[\s{,]/.test(code[i - 1] ?? "") && /^release\s*[:,}(]/.test(code.slice(i, i + 20))) sawRelease = true;
1694
+ }
1695
+ }
1696
+ out.push(sawRelease ? true : sawSpread ? null : false);
1697
+ }
1698
+ return out;
1699
+ }
1700
+ function detect(dir, env = process.env) {
1701
+ const root = resolve2(dir);
1702
+ const pkg = readJson(join2(root, "package.json"));
1703
+ if (!pkg) throw new Error(`no readable package.json in ${root}`);
1704
+ const git = gitRoot(root);
1705
+ const tool = buildTool(root, pkg);
1706
+ const facts = outputFacts(root, pkg, tool);
1707
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1708
+ const sdkPackage = deps["@autter/runtime-next"] ? "@autter/runtime-next" : deps["@autter/runtime-browser"] ? "@autter/runtime-browser" : null;
1709
+ const sdkInstalled = sdkPackage ? installedVersion(root, sdkPackage) : null;
1710
+ const skip = new Set(facts.outputDir ? [resolve2(root, facts.outputDir.split("/")[0])] : []);
1711
+ return {
1712
+ dir: root,
1713
+ buildTool: tool,
1714
+ packageManager: packageManager(root, pkg, git),
1715
+ ci: ciProviders(root, git, env),
1716
+ outputDir: facts.outputDir,
1717
+ urlPrefix: facts.urlPrefix,
1718
+ sourceMaps: facts.sourceMaps,
1719
+ sdk: {
1720
+ package: sdkPackage,
1721
+ range: sdkPackage ? deps[sdkPackage] : null,
1722
+ installed: sdkInstalled,
1723
+ debugIds: sdkPackage ? atLeast(sdkInstalled ?? deps[sdkPackage], "1.6.0") : null
1724
+ },
1725
+ release: releaseFacts(root, skip),
1726
+ cli: { range: deps["@autter/runtime-cli"] ?? null, installed: installedVersion(root, "@autter/runtime-cli") },
1727
+ gitRoot: git
1728
+ };
1729
+ }
1730
+
1731
+ // src/setup.ts
1732
+ var PKG = "@autter/runtime-cli";
1733
+ var KEY_TEXT = "An upload-only key (autter_rtu_\u2026) is recommended: it can only upload source maps. The full server key (autter_rt_\u2026) also works.";
1734
+ var MIN_CLI = "1.2.0";
1735
+ function ownVersion() {
1736
+ try {
1737
+ return JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8")).version;
1738
+ } catch {
1739
+ return MIN_CLI;
1740
+ }
1741
+ }
1742
+ var Changes = class {
1743
+ constructor(dir) {
1744
+ this.dir = dir;
1745
+ }
1746
+ dir;
1747
+ files = /* @__PURE__ */ new Map();
1748
+ read(file) {
1749
+ const path = join3(this.dir, file);
1750
+ return this.files.get(path) ?? readText(path);
1751
+ }
1752
+ write(file, content) {
1753
+ this.files.set(join3(this.dir, file), content);
1754
+ }
1755
+ };
1756
+ function rewriteJson(text, value) {
1757
+ const indent = /^\{\r?\n([ \t]+)"/.exec(text)?.[1] ?? " ";
1758
+ return `${JSON.stringify(value, null, indent)}${text.endsWith("\n") ? "\n" : ""}`;
1759
+ }
1760
+ function insertAfter(obj, after, key, value) {
1761
+ const out = {};
1762
+ let placed = false;
1763
+ for (const [k, v] of Object.entries(obj)) {
1764
+ out[k] = v;
1765
+ if (k === after) {
1766
+ out[key] = value;
1767
+ placed = true;
1768
+ }
1769
+ }
1770
+ if (!placed) out[key] = value;
1771
+ return out;
1772
+ }
1773
+ function addCommand(detected, spec, dev = true) {
1774
+ const pm = detected.packageManager.name;
1775
+ const verb = pm === "npm" ? "install" : "add";
1776
+ const flag = pm === "npm" || pm === "pnpm" ? "--save-dev" : "--dev";
1777
+ const argv = [pm, verb, ...dev ? [flag] : [], spec];
1778
+ return { command: argv.join(" "), argv };
1779
+ }
1780
+ var shellArg = (value) => /^[\w./@:~-]+$/.test(value) ? value : JSON.stringify(value);
1781
+ function sourcemapsScript(outputDir, urlPrefix) {
1782
+ const out = shellArg(outputDir);
1783
+ return `autter-runtime sourcemaps inject ${out} && autter-runtime sourcemaps upload ${out}${urlPrefix && urlPrefix !== "/" ? ` --url-prefix ${shellArg(urlPrefix)}` : ""} --delete-after-upload --best-effort`;
1784
+ }
1785
+ function commitVars(ci) {
1786
+ const byCi = {
1787
+ vercel: "VERCEL_GIT_COMMIT_SHA",
1788
+ netlify: "COMMIT_REF",
1789
+ "cloudflare-pages": "CF_PAGES_COMMIT_SHA",
1790
+ gitlab: "CI_COMMIT_SHA",
1791
+ github: "GITHUB_SHA"
1792
+ };
1793
+ const order = ["vercel", "netlify", "cloudflare-pages", "gitlab", "github"];
1794
+ const vars = order.filter((id) => ci.some((c) => c.id === id)).map((id) => byCi[id]);
1795
+ return [...vars, "GIT_SHA"];
1796
+ }
1797
+ function releaseSnippet(detected) {
1798
+ const vars = commitVars(detected.ci);
1799
+ const expr = `${vars.map((v) => `process.env.${v}`).join(" ?? ")} ?? ""`;
1800
+ const shellVar = `$${vars[0]}`;
1801
+ const init = detected.release.file ?? "the file that calls initAutterBrowser";
1802
+ const tool = detected.buildTool;
1803
+ const config = tool.config ?? void 0;
1804
+ const call = (value) => `// ${init}
1805
+ initAutterBrowser({ /* existing options */ release: ${value} });`;
1806
+ switch (tool.id) {
1807
+ case "vite": {
1808
+ const define = `define: {
1809
+ "import.meta.env.VITE_AUTTER_RELEASE": JSON.stringify(${expr}),
1810
+ },`;
1811
+ const wrapped = tool.framework === "nuxt" || tool.framework === "astro" ? `vite: {
1812
+ ${define.replace(/\n/g, "\n ")}
1813
+ },` : define;
1814
+ return {
1815
+ file: config,
1816
+ snippet: `// ${config ?? "vite.config.ts"}: the commit SHA is not a secret
1817
+ ${wrapped}
1818
+
1819
+ ${call("import.meta.env.VITE_AUTTER_RELEASE")}`
1820
+ };
1821
+ }
1822
+ case "next":
1823
+ return {
1824
+ file: config,
1825
+ snippet: `// ${config ?? "next.config.mjs"}: the commit SHA is not a secret
1826
+ env: { NEXT_PUBLIC_AUTTER_RELEASE: ${expr} },
1827
+
1828
+ ${call("process.env.NEXT_PUBLIC_AUTTER_RELEASE")}`
1829
+ };
1830
+ case "webpack":
1831
+ return {
1832
+ file: config,
1833
+ snippet: `// ${config ?? "webpack.config.js"}
1834
+ new webpack.DefinePlugin({ "process.env.AUTTER_RELEASE": JSON.stringify(${expr}) }),
1835
+
1836
+ ${call("process.env.AUTTER_RELEASE")}`
1837
+ };
1838
+ case "rollup":
1839
+ return {
1840
+ file: config,
1841
+ snippet: `// ${config ?? "rollup.config.js"} (@rollup/plugin-replace)
1842
+ replace({ preventAssignment: true, "process.env.AUTTER_RELEASE": JSON.stringify(${expr}) }),
1843
+
1844
+ ${call("process.env.AUTTER_RELEASE")}`
1845
+ };
1846
+ case "cra":
1847
+ return {
1848
+ file: "package.json",
1849
+ snippet: `// package.json: CRA inlines REACT_APP_* variables
1850
+ "build": "REACT_APP_AUTTER_RELEASE=${shellVar} react-scripts build",
1851
+
1852
+ ${call("process.env.REACT_APP_AUTTER_RELEASE")}`
1853
+ };
1854
+ case "angular":
1855
+ return {
1856
+ file: "package.json",
1857
+ snippet: `// package.json (Angular 17.2+ application builder)
1858
+ "build": "ng build --define \\"AUTTER_RELEASE='${shellVar}'\\"",
1859
+
1860
+ // ${init}
1861
+ declare const AUTTER_RELEASE: string | undefined;
1862
+ initAutterBrowser({ /* existing options */ release: typeof AUTTER_RELEASE === "string" ? AUTTER_RELEASE : undefined });`
1863
+ };
1864
+ case "parcel":
1865
+ return {
1866
+ file: "package.json",
1867
+ snippet: `// package.json: Parcel inlines process.env values at build time
1868
+ "build": "AUTTER_RELEASE=${shellVar} ${tool.buildScript ?? "parcel build"}",
1869
+
1870
+ ${call("process.env.AUTTER_RELEASE")}`
1871
+ };
1872
+ case "esbuild":
1873
+ return {
1874
+ file: "package.json",
1875
+ snippet: `// the esbuild command
1876
+ --define:process.env.AUTTER_RELEASE=\\"${shellVar}\\"
1877
+
1878
+ ${call("process.env.AUTTER_RELEASE")}`
1879
+ };
1880
+ default:
1881
+ return {
1882
+ file: void 0,
1883
+ snippet: `// Inline the commit SHA (${vars.join(", ")}) into the browser bundle with your build tool's define/env mechanism, then:
1884
+ ${call("<the inlined commit SHA>")}`
1885
+ };
1886
+ }
1887
+ }
1888
+ function secretTargets(detected) {
1889
+ const where = [];
1890
+ for (const ci of detected.ci) {
1891
+ switch (ci.id) {
1892
+ case "github":
1893
+ where.push({
1894
+ ci: ci.id,
1895
+ name: ci.name,
1896
+ command: `gh secret set AUTTER_RUNTIME_KEY${ci.repo ? ` --repo ${ci.repo}` : ""}`,
1897
+ note: "gh prompts for the value (or reads it from stdin), so the key never reaches your shell history. Then pass it to the step that runs the production build: env: AUTTER_RUNTIME_KEY: ${{ secrets.AUTTER_RUNTIME_KEY }}"
1898
+ });
1899
+ break;
1900
+ case "vercel":
1901
+ where.push({
1902
+ ci: ci.id,
1903
+ name: ci.name,
1904
+ command: "vercel env add AUTTER_RUNTIME_KEY production",
1905
+ note: "vercel prompts for the value (or reads it from stdin). Add it for preview too (vercel env add AUTTER_RUNTIME_KEY preview) if preview builds should upload."
1906
+ });
1907
+ break;
1908
+ case "netlify":
1909
+ where.push({
1910
+ ci: ci.id,
1911
+ name: ci.name,
1912
+ command: 'netlify env:set AUTTER_RUNTIME_KEY "$(cat)" --context production --scope builds --secret',
1913
+ note: "Paste the key, press Enter, then Ctrl-D: $(cat) reads it from stdin, so it stays out of your shell history. netlify env:set has no prompt; without a value it stores an empty one."
1914
+ });
1915
+ break;
1916
+ case "cloudflare-pages":
1917
+ where.push({
1918
+ ci: ci.id,
1919
+ name: ci.name,
1920
+ note: "Cloudflare dashboard \u2192 Workers & Pages \u2192 your project \u2192 Settings \u2192 Variables and Secrets \u2192 Add: type Secret, name AUTTER_RUNTIME_KEY, for Production (and Preview if preview builds should upload). Builds read it from the environment."
1921
+ });
1922
+ break;
1923
+ case "gitlab":
1924
+ where.push({
1925
+ ci: ci.id,
1926
+ name: ci.name,
1927
+ note: "GitLab \u2192 Settings \u2192 CI/CD \u2192 Variables \u2192 Add variable: key AUTTER_RUNTIME_KEY, visibility Masked (and Protected when only protected branches deploy). Jobs get it as an environment variable."
1928
+ });
1929
+ break;
1930
+ }
1931
+ }
1932
+ if (!where.length) {
1933
+ where.push({
1934
+ ci: "other",
1935
+ name: "your build environment",
1936
+ note: "Add AUTTER_RUNTIME_KEY as a secret environment variable wherever the production build runs. Never in a committed .env file, and never under a public prefix (VITE_, NEXT_PUBLIC_, REACT_APP_, PUBLIC_)."
1937
+ });
1938
+ }
1939
+ return where;
1940
+ }
1941
+ var PLUGINS = {
1942
+ vite: { name: "autterSourceMaps", spec: "@autter/runtime-cli/vite", element: "autterSourceMaps()" },
1943
+ rollup: { name: "autterSourceMaps", spec: "@autter/runtime-cli/rollup", element: "autterSourceMaps()" },
1944
+ webpack: { name: "AutterSourceMapsWebpackPlugin", spec: "@autter/runtime-cli/webpack", element: "new AutterSourceMapsWebpackPlugin()" }
1945
+ };
1946
+ function pluginSnippet(kind, container, cjs) {
1947
+ const plugin = PLUGINS[kind];
1948
+ const importLine = cjs ? `const { ${plugin.name} } = require("${plugin.spec}");` : `import { ${plugin.name} } from "${plugin.spec}";`;
1949
+ const plugins = `plugins: [/* existing plugins */ ${plugin.element}],`;
1950
+ const body = container ? `${container}: {
1951
+ ${plugins}
1952
+ },` : plugins;
1953
+ return `${importLine}
1954
+
1955
+ // in the exported config object:
1956
+ ${body}`;
1957
+ }
1958
+ function runSetup(dir, options = {}) {
1959
+ const env = options.env ?? process.env;
1960
+ const detected = detect(dir, env);
1961
+ const changes = new Changes(detected.dir);
1962
+ const actions = [];
1963
+ const next = [];
1964
+ const tool = detected.buildTool;
1965
+ const pkgText = changes.read("package.json");
1966
+ let pkg = JSON.parse(pkgText);
1967
+ const version = ownVersion();
1968
+ const range = `^${atLeast(version, MIN_CLI) ? version : MIN_CLI}`;
1969
+ const install = addCommand(detected, `${PKG}@${range}`);
1970
+ const manual = (description, file, snippet, command) => actions.push({ type: "manual", ...file ? { file } : {}, description, ...snippet ? { snippet } : {}, ...command ? { command } : {} });
1971
+ const savePkg = () => changes.write("package.json", rewriteJson(changes.read("package.json"), pkg));
1972
+ if (tool.id === "unknown" && !pkg.scripts?.build) {
1973
+ manual(
1974
+ "No build tool and no build script: if this app ships hand-written, unminified scripts there is nothing to upload. If another step builds its JavaScript, run this after it, on the output directory",
1975
+ "package.json",
1976
+ sourcemapsScript("<output dir>", "/")
1977
+ );
1978
+ return {
1979
+ dir: detected.dir,
1980
+ dryRun: Boolean(options.dryRun),
1981
+ detected,
1982
+ actions,
1983
+ secret: { name: "AUTTER_RUNTIME_KEY", key: KEY_TEXT, where: [] },
1984
+ next: ["Nothing was changed."],
1985
+ exitCode: 2
1986
+ };
1987
+ }
1988
+ const current = pkg.devDependencies?.[PKG] ?? pkg.dependencies?.[PKG];
1989
+ let dependencyChanged = false;
1990
+ if (!current) {
1991
+ const devDependencies = { ...pkg.devDependencies };
1992
+ const keys = Object.keys(devDependencies);
1993
+ const sorted = keys.every((k, i) => i === 0 || keys[i - 1].localeCompare(k) <= 0);
1994
+ devDependencies[PKG] = range;
1995
+ pkg = {
1996
+ ...pkg,
1997
+ devDependencies: sorted ? Object.fromEntries(Object.entries(devDependencies).sort(([a], [b]) => a.localeCompare(b))) : devDependencies
1998
+ };
1999
+ if (!("devDependencies" in JSON.parse(pkgText))) pkg = insertAfter(pkg, "dependencies", "devDependencies", pkg.devDependencies);
2000
+ dependencyChanged = true;
2001
+ actions.push({ type: "add-dependency", file: "package.json", description: `Add ${PKG}@${range} to devDependencies`, command: install.command });
2002
+ } else if (atLeast(current, MIN_CLI) === false) {
2003
+ const section = pkg.devDependencies?.[PKG] ? "devDependencies" : "dependencies";
2004
+ pkg = { ...pkg, [section]: { ...pkg[section], [PKG]: range } };
2005
+ dependencyChanged = true;
2006
+ actions.push({
2007
+ type: "add-dependency",
2008
+ file: "package.json",
2009
+ description: `Raise ${PKG} from ${current} to ${range} (setup, verify and upload --best-effort need ${MIN_CLI})`,
2010
+ command: install.command
2011
+ });
2012
+ }
2013
+ if (dependencyChanged) savePkg();
2014
+ const applyEdit = (result, file, description, fallback) => {
2015
+ if (result.status === "edited") {
2016
+ changes.write(file, result.code);
2017
+ actions.push({ type: "edit", file, description });
2018
+ } else if (result.status === "manual") {
2019
+ manual(`${result.reason}: ${description[0].toLowerCase()}${description.slice(1)} by hand`, file, fallback);
2020
+ } else if (result.note) {
2021
+ next.push(result.note);
2022
+ }
2023
+ };
2024
+ const addScript = () => {
2025
+ const out = detected.outputDir;
2026
+ const scripts = { ...pkg.scripts };
2027
+ if (Object.values(scripts).some((s) => /autter-runtime\s+sourcemaps\s+(inject|upload)/.test(s))) return;
2028
+ if (!out) {
2029
+ manual("Could not tell where the build writes its output: after the production build, inject and upload from that directory", void 0, sourcemapsScript("<output dir>", "/"));
2030
+ return;
2031
+ }
2032
+ const script = sourcemapsScript(out, detected.urlPrefix);
2033
+ if (!scripts.build) {
2034
+ manual(`No build script in package.json: run this after the production build (it writes ${out})`, "package.json", script);
2035
+ return;
2036
+ }
2037
+ const { name: pm, yarnBerry } = detected.packageManager;
2038
+ const post = pm === "npm" || pm === "yarn" && !yarnBerry;
2039
+ if (post) {
2040
+ pkg = { ...pkg, scripts: scripts.postbuild ? { ...scripts, postbuild: `${scripts.postbuild} && ${script}` } : insertAfter(scripts, "build", "postbuild", script) };
2041
+ actions.push({
2042
+ type: "add-script",
2043
+ file: "package.json",
2044
+ description: `${scripts.postbuild ? "Extend" : "Add"} the postbuild script: inject debug ids into ${out}, upload its maps, then delete them (with or without a key, so maps are never deployed; never fails the build)`,
2045
+ snippet: `"postbuild": ${JSON.stringify(pkg.scripts.postbuild)}`
2046
+ });
2047
+ } else {
2048
+ pkg = { ...pkg, scripts: { ...scripts, build: `${scripts.build} && ${script}` } };
2049
+ actions.push({
2050
+ type: "add-script",
2051
+ file: "package.json",
2052
+ description: `Append to the build script (${pm}${pm === "yarn" ? " 2+" : ""} does not run postbuild): inject debug ids into ${out}, upload its maps, then delete them (never fails the build)`,
2053
+ snippet: `"build": ${JSON.stringify(pkg.scripts.build)}`
2054
+ });
2055
+ }
2056
+ savePkg();
2057
+ };
2058
+ switch (tool.id) {
2059
+ case "vite":
2060
+ case "rollup": {
2061
+ const kind = tool.id;
2062
+ const container = tool.framework === "nuxt" || tool.framework === "astro" ? "vite" : void 0;
2063
+ if (tool.framework === "solid-start") {
2064
+ manual(
2065
+ "SolidStart's app.config wraps Vite through vinxi: add the plugin to the client router's Vite config by hand",
2066
+ tool.config ?? "app.config.ts",
2067
+ pluginSnippet("vite", "vite", false)
2068
+ );
2069
+ break;
2070
+ }
2071
+ if (!tool.config) {
2072
+ if (kind === "vite" && !tool.framework) {
2073
+ const file = existsSync4(join3(detected.dir, "tsconfig.json")) ? "vite.config.ts" : "vite.config.js";
2074
+ changes.write(
2075
+ file,
2076
+ `import { defineConfig } from "vite";
2077
+ import { autterSourceMaps } from "@autter/runtime-cli/vite";
2078
+
2079
+ export default defineConfig({
2080
+ plugins: [autterSourceMaps()],
2081
+ });
2082
+ `
2083
+ );
2084
+ actions.push({ type: "edit", file, description: `Create ${file} with the autterSourceMaps() plugin (the project had no Vite config)` });
2085
+ } else {
2086
+ manual(`No ${tool.framework ?? kind} config file found: create one with the plugin`, void 0, pluginSnippet(kind, container, false));
2087
+ }
2088
+ break;
2089
+ }
2090
+ const code = changes.read(tool.config);
2091
+ const cjs = /\bmodule\.exports\b/.test(code);
2092
+ applyEdit(
2093
+ addBundlerPlugin(code, tool.config, { ...PLUGINS[kind], container }),
2094
+ tool.config,
2095
+ `Add ${PLUGINS[kind].element} to the ${container ? `${container}.` : ""}plugins in ${tool.config}`,
2096
+ pluginSnippet(kind, container, cjs)
2097
+ );
2098
+ if (detected.sourceMaps.state === "inline") {
2099
+ manual(
2100
+ `${tool.config} writes inline source maps (${detected.sourceMaps.setting}): there are no map files to upload. Use "hidden"`,
2101
+ tool.config,
2102
+ kind === "vite" ? `build: { sourcemap: "hidden" },` : `output: { sourcemap: "hidden" },`
2103
+ );
2104
+ }
2105
+ break;
2106
+ }
2107
+ case "webpack": {
2108
+ const container = tool.framework === "vue-cli" ? "configureWebpack" : void 0;
2109
+ if (!tool.config) {
2110
+ manual(
2111
+ `No ${tool.framework === "vue-cli" ? "vue.config.js" : "webpack config"} found: add the plugin to the config your build uses`,
2112
+ void 0,
2113
+ pluginSnippet("webpack", container, true)
2114
+ );
2115
+ break;
2116
+ }
2117
+ const code = changes.read(tool.config);
2118
+ const cjs = /\bmodule\.exports\b/.test(code) || !/\b(import|export)\b/.test(code);
2119
+ applyEdit(
2120
+ addBundlerPlugin(code, tool.config, { ...PLUGINS.webpack, container }),
2121
+ tool.config,
2122
+ `Add new AutterSourceMapsWebpackPlugin() to the ${container ? `${container}.` : ""}plugins in ${tool.config}`,
2123
+ pluginSnippet("webpack", container, cjs)
2124
+ );
2125
+ if (detected.sourceMaps.state === "inline") {
2126
+ manual(`${tool.config} uses an inline/eval devtool: there are no map files to upload. Use "hidden-source-map" for production`, tool.config, `devtool: "hidden-source-map",`);
2127
+ }
2128
+ break;
2129
+ }
2130
+ case "next": {
2131
+ const snippet = `import { withAutterSourceMaps } from "@autter/runtime-cli/next";
2132
+
2133
+ // outermost, around any other wrappers:
2134
+ export default withAutterSourceMaps(withOtherPlugins(nextConfig));`;
2135
+ if (!tool.config) {
2136
+ changes.write("next.config.mjs", `import { withAutterSourceMaps } from "@autter/runtime-cli/next";
2137
+
2138
+ /** @type {import("next").NextConfig} */
2139
+ const nextConfig = {};
2140
+
2141
+ export default withAutterSourceMaps(nextConfig);
2142
+ `);
2143
+ actions.push({ type: "edit", file: "next.config.mjs", description: "Create next.config.mjs exporting withAutterSourceMaps(nextConfig) (the project had none)" });
2144
+ } else {
2145
+ applyEdit(wrapNextConfig(changes.read(tool.config), tool.config), tool.config, `Wrap the exported config in withAutterSourceMaps(...), outermost, in ${tool.config}`, snippet);
2146
+ }
2147
+ if (/--turbo(pack)?\b/.test(tool.buildScript ?? "") && atLeast(tool.version, "15.4.1") === false) {
2148
+ next.push(`Next.js ${tool.version} builds with Turbopack: withAutterSourceMaps covers Turbopack production builds from Next.js 15.4.1 (runAfterProductionCompile). Upgrade Next.js, or build with webpack.`);
2149
+ }
2150
+ break;
2151
+ }
2152
+ case "cra": {
2153
+ addScript();
2154
+ const sm = detected.sourceMaps;
2155
+ if (sm.state === "off" && sm.file && CRA_ENV_FILES.includes(sm.file)) {
2156
+ const text = changes.read(sm.file);
2157
+ changes.write(sm.file, text.replace(/^(\s*GENERATE_SOURCEMAP\s*=\s*)["']?false["']?/m, "$1true"));
2158
+ actions.push({
2159
+ type: "edit",
2160
+ file: sm.file,
2161
+ description: `Set GENERATE_SOURCEMAP=true in ${sm.file}: the postbuild step uploads the maps and deletes them, so they are never deployed`
2162
+ });
2163
+ } else if (sm.state === "off") {
2164
+ manual("The build script sets GENERATE_SOURCEMAP=false: remove it (or set true) so there are maps to upload; the postbuild step deletes them", "package.json", `"build": "react-scripts build"`);
2165
+ }
2166
+ break;
2167
+ }
2168
+ case "angular": {
2169
+ addScript();
2170
+ const text = changes.read("angular.json");
2171
+ let json = null;
2172
+ try {
2173
+ json = text ? JSON.parse(text) : null;
2174
+ } catch {
2175
+ json = null;
2176
+ }
2177
+ const info = json ? angularInfo(json) : null;
2178
+ const hidden = { scripts: true, styles: false, hidden: true, vendor: false };
2179
+ const sm = detected.sourceMaps.state;
2180
+ if (!json || !info) {
2181
+ manual("angular.json could not be read: turn on hidden script source maps for the production build", "angular.json", `"sourceMap": ${JSON.stringify(hidden)}`);
2182
+ } else if (sm === "default-off" || sm === "off") {
2183
+ const architect = json.projects[info.project].architect ?? json.projects[info.project].targets;
2184
+ const build = architect.build;
2185
+ const configurations = build.configurations ?? {};
2186
+ if (configurations.production) configurations.production.sourceMap = hidden;
2187
+ else build.options = { ...build.options, sourceMap: hidden };
2188
+ changes.write("angular.json", rewriteJson(text, json));
2189
+ actions.push({
2190
+ type: "edit",
2191
+ file: "angular.json",
2192
+ description: `Turn on hidden script source maps for ${info.project}'s ${configurations.production ? "production " : ""}build (written next to the bundles, not referenced by them; the postbuild step uploads and deletes them)`,
2193
+ snippet: `"sourceMap": ${JSON.stringify(hidden)}`
2194
+ });
2195
+ } else if (sm === "public") {
2196
+ next.push('angular.json emits public source maps (referenced by the bundles); the postbuild step deletes them after upload. Use "sourceMap": { "scripts": true, "hidden": true } to stop referencing them.');
2197
+ }
2198
+ break;
2199
+ }
2200
+ case "parcel":
2201
+ addScript();
2202
+ if (detected.sourceMaps.state === "off") {
2203
+ manual("The build script passes --no-source-maps: remove it so there are maps to upload (the postbuild step deletes them)", "package.json", (tool.buildScript ?? "").replace(/\s*--no-source-maps\b/, ""));
2204
+ }
2205
+ break;
2206
+ case "esbuild":
2207
+ addScript();
2208
+ if (detected.sourceMaps.state === "off" || detected.sourceMaps.state === "unknown") {
2209
+ manual(
2210
+ 'Make esbuild write external source maps (written next to the bundles, not referenced by them): add --sourcemap=external to the esbuild command, or sourcemap: "external" to its build() options',
2211
+ "package.json",
2212
+ "--sourcemap=external"
2213
+ );
2214
+ } else if (detected.sourceMaps.state === "inline") {
2215
+ manual("esbuild writes inline source maps: there are no map files to upload. Use --sourcemap=external", "package.json", "--sourcemap=external");
2216
+ }
2217
+ break;
2218
+ default:
2219
+ if (detected.outputDir && pkg.scripts?.build) {
2220
+ addScript();
2221
+ manual(
2222
+ `Build tool not recognised: make sure the production build writes source maps (hidden if it can) into ${detected.outputDir}, and that ${detected.outputDir} is the deployed output`,
2223
+ void 0
2224
+ );
2225
+ } else {
2226
+ manual(
2227
+ "Build tool not recognised: after the production build, inject debug ids and upload the maps from the output directory",
2228
+ void 0,
2229
+ sourcemapsScript(detected.outputDir ?? "<output dir>", "/")
2230
+ );
2231
+ }
2232
+ }
2233
+ if (detected.sdk.package && detected.release.file && detected.release.passed === false) {
2234
+ const { file, snippet } = releaseSnippet(detected);
2235
+ manual(
2236
+ `initAutterBrowser in ${detected.release.file} gets no release: pass the commit SHA through ${tool.id === "unknown" ? "your build tool's" : `${tool.id}'s`} public env or define mechanism, so release-based features match the upload (debug ids match maps either way)`,
2237
+ file ?? detected.release.file,
2238
+ snippet
2239
+ );
2240
+ } else if (detected.sdk.package && detected.release.file && detected.release.passed === null) {
2241
+ next.push(`Check that initAutterBrowser in ${detected.release.file} passes release: the commit SHA (setup could not read its options).`);
2242
+ }
2243
+ if (detected.sdk.package && detected.sdk.debugIds === false) {
2244
+ manual(
2245
+ `${detected.sdk.package} ${detected.sdk.installed ?? detected.sdk.range} predates 1.6.0, so exceptions carry no debug ids: upgrade it`,
2246
+ "package.json",
2247
+ void 0,
2248
+ addCommand(detected, `${detected.sdk.package}@latest`, false).command
2249
+ );
2250
+ }
2251
+ if (!detected.sdk.package) {
2252
+ next.push("No Autter browser SDK in package.json: source maps resolve browser errors that @autter/runtime-browser (or @autter/runtime-next) 1.6.0+ reports. Set it up first, or alongside.");
2253
+ }
2254
+ if (detected.sourceMaps.state === "public" && tool.id !== "angular") {
2255
+ next.push(
2256
+ `${detected.sourceMaps.file ?? "The build"} emits public source maps (${detected.sourceMaps.setting}): your source is downloadable from production. Switch to hidden maps, or set deleteAfterUpload: true on the plugin, if you don't publish them on purpose.`
2257
+ );
2258
+ }
2259
+ const where = secretTargets(detected);
2260
+ for (const target of where) {
2261
+ actions.push({
2262
+ type: "set-secret",
2263
+ description: `Store AUTTER_RUNTIME_KEY in ${target.name}${target.command ? "" : `: ${target.note}`}`,
2264
+ ...target.command ? { command: target.command } : {}
2265
+ });
2266
+ }
2267
+ const hosting = detected.ci.some((c) => c.id === "vercel" || c.id === "netlify" || c.id === "cloudflare-pages");
2268
+ if (detected.ci.some((c) => c.id === "github") && !workflowsPassKey(detected.dir, detected.gitRoot)) {
2269
+ const snippet = `- run: ${detected.packageManager.name === "npm" ? "npm run" : detected.packageManager.name} build
2270
+ env:
2271
+ AUTTER_RUNTIME_KEY: \${{ secrets.AUTTER_RUNTIME_KEY }}`;
2272
+ if (hosting) {
2273
+ next.push(`If a GitHub workflow builds the deployed output, give that step the key:
2274
+ ${snippet}`);
2275
+ } else {
2276
+ manual(
2277
+ "Pass the secret to the GitHub Actions step that runs the production build (only that job; never to jobs that run untrusted pull request code)",
2278
+ detected.ci.find((c) => c.id === "github").evidence,
2279
+ snippet
2280
+ );
2281
+ }
2282
+ }
2283
+ if (!options.dryRun) {
2284
+ for (const [path, content] of changes.files) writeFileSync(path, content);
2285
+ }
2286
+ const needsInstall = dependencyChanged || !detected.cli.installed;
2287
+ if (needsInstall && options.install && !options.dryRun) {
2288
+ const [command, ...args] = install.argv;
2289
+ const result = spawnSync(command, args, {
2290
+ cwd: detected.dir,
2291
+ stdio: options.installOutput === "stderr" ? ["ignore", 2, 2] : "inherit",
2292
+ shell: process.platform === "win32"
2293
+ });
2294
+ if (result.status !== 0) throw new Error(`${install.command} failed (exit ${result.status ?? result.signal ?? "unknown"})`);
2295
+ } else if (needsInstall) {
2296
+ next.push(`Install it: ${install.command}`);
2297
+ }
2298
+ next.push(
2299
+ "Get an upload-only key (autter_rtu_\u2026) for this repository from Autter: it can only upload source maps, so it is the right key for CI. The full server key (autter_rt_\u2026) also works. Never paste it into chat or commit it.",
2300
+ `Store it as AUTTER_RUNTIME_KEY where the production build runs (see secret.where). Without it, builds still pass and upload nothing.`
2301
+ );
2302
+ if (detected.outputDir) {
2303
+ next.push(`After a production build with the key set, check it: npx autter-runtime sourcemaps verify ${shellArg(detected.outputDir)}`);
2304
+ }
2305
+ return {
2306
+ dir: detected.dir,
2307
+ dryRun: Boolean(options.dryRun),
2308
+ detected,
2309
+ actions,
2310
+ secret: {
2311
+ name: "AUTTER_RUNTIME_KEY",
2312
+ key: KEY_TEXT,
2313
+ where
2314
+ },
2315
+ next,
2316
+ exitCode: actions.some((a) => a.type === "manual") ? 2 : 0
2317
+ };
2318
+ }
2319
+ var LABEL = {
2320
+ edit: "edit",
2321
+ "add-dependency": "dependency",
2322
+ "add-script": "script",
2323
+ manual: "manual",
2324
+ "set-secret": "secret"
2325
+ };
2326
+ function formatSetup(result) {
2327
+ const d = result.detected;
2328
+ const lines = [`autter-runtime sourcemaps setup${result.dryRun ? " (dry run: nothing written)" : ""}: ${d.dir}`, "", "Detected"];
2329
+ const row = (label, value) => lines.push(` ${label.padEnd(16)} ${value}`);
2330
+ const tool = d.buildTool;
2331
+ row("build tool", `${tool.id}${tool.framework ? ` (${tool.framework})` : ""}${tool.config ? `, ${tool.config}` : ""}${tool.version ? `, ${tool.version}` : ""}`);
2332
+ row("package manager", `${d.packageManager.name}${d.packageManager.lockfile ? ` (${d.packageManager.lockfile})` : ""}`);
2333
+ row("CI", d.ci.length ? d.ci.map((c) => `${c.name} (${c.evidence}${c.repo ? `, ${c.repo}` : ""})`).join("; ") : "none found");
2334
+ row("output", `${d.outputDir ?? "unknown"}${d.urlPrefix !== "/" ? `, served from ${d.urlPrefix}` : ""}`);
2335
+ row("source maps", `${d.sourceMaps.state}${d.sourceMaps.setting ? ` (${d.sourceMaps.setting})` : ""}`);
2336
+ row(
2337
+ "browser SDK",
2338
+ d.sdk.package ? `${d.sdk.package} ${d.sdk.installed ?? d.sdk.range}${d.sdk.debugIds === false ? " (no debug ids: needs 1.6.0+)" : ""}` : "none in package.json"
2339
+ );
2340
+ row(
2341
+ "release",
2342
+ d.release.file ? `${d.release.file}: ${d.release.passed === true ? "passed" : d.release.passed === false ? "not passed" : "could not tell"}` : "no initAutterBrowser call found"
2343
+ );
2344
+ row("runtime-cli", d.cli.range ? `${d.cli.range}${d.cli.installed ? ` (installed ${d.cli.installed})` : " (not installed)"}` : "not in package.json");
2345
+ lines.push("", "Actions");
2346
+ if (!result.actions.length) lines.push(" nothing to do");
2347
+ for (const action of result.actions) {
2348
+ const status = action.type === "manual" ? "TODO" : action.type === "set-secret" ? "KEY " : result.dryRun ? "PLAN" : "DONE";
2349
+ lines.push(` ${status} ${LABEL[action.type].padEnd(10)} ${action.file ? `${action.file}: ` : ""}${action.description}`);
2350
+ if (action.command) lines.push(` run: ${action.command}`);
2351
+ if (action.snippet && action.type === "manual") for (const line of action.snippet.split("\n")) lines.push(` | ${line}`);
2352
+ }
2353
+ lines.push("", `Secret: ${result.secret.name}. ${result.secret.key}`);
2354
+ for (const target of result.secret.where) {
2355
+ lines.push(` ${target.name}: ${target.command ?? ""}${target.command ? "" : target.note}`);
2356
+ if (target.command) lines.push(` ${target.note}`);
2357
+ }
2358
+ lines.push("", "Next");
2359
+ result.next.forEach((item, i) => lines.push(` ${i + 1}. ${item.replace(/\n/g, "\n ")}`));
2360
+ return lines;
2361
+ }
2362
+
2363
+ // src/verify.ts
2364
+ import { existsSync as existsSync5 } from "fs";
2365
+ import { readdir as readdir2, readFile as readFile4 } from "fs/promises";
2366
+ import { dirname as dirname3, join as join4, relative as relative4, resolve as resolve3, sep as sep4 } from "path";
2367
+ var CHECK_BATCH2 = 500;
2368
+ var SNIPPET_ID_RE = /\(g\._autterDebugIds=g\._autterDebugIds\|\|\{\}\)\[s\]="([0-9a-f-]{36})"/;
2369
+ var NOT_BUNDLES = [
2370
+ [/(^|\/)_(build|ssg|middleware|clientMiddleware)Manifest\.js$/, "Next.js build manifest"],
2371
+ [/(^|\/)chunks\/polyfills-[\w-]+\.js$/, "Next.js polyfills, copied prebuilt without a source map"],
2372
+ [/(^|\/)(sw|service-worker|serviceWorker|firebase-messaging-sw|mockServiceWorker)\.js$/, "service worker (the browser SDK doesn't run there)"],
2373
+ [/(^|\/)workbox-[\w-]+\.js$/, "service worker runtime (the browser SDK doesn't run there)"]
2374
+ ];
2375
+ function globToRegExp(glob) {
2376
+ let re = "";
2377
+ for (let i = 0; i < glob.length; i++) {
2378
+ const ch = glob[i];
2379
+ if (ch === "*" && glob[i + 1] === "*") {
2380
+ re += glob[i + 2] === "/" ? "(?:.*/)?" : ".*";
2381
+ i += glob[i + 2] === "/" ? 2 : 1;
2382
+ } else if (ch === "*") re += "[^/]*";
2383
+ else if (ch === "?") re += "[^/]";
2384
+ else re += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
2385
+ }
2386
+ return new RegExp(glob.includes("/") ? `^${re.replace(/^\.\//, "")}$` : `(^|/)${re}$`);
2387
+ }
2388
+ async function walk3(dir, out) {
2389
+ let entries;
2390
+ try {
2391
+ entries = await readdir2(dir, { withFileTypes: true });
2392
+ } catch {
2393
+ return;
2394
+ }
2395
+ for (const entry of entries) {
2396
+ if (entry.name === "node_modules") continue;
2397
+ const full = join4(dir, entry.name);
2398
+ if (entry.isDirectory()) await walk3(full, out);
2399
+ else if (entry.isFile()) out.push(full);
2400
+ }
2401
+ }
2402
+ function staticRoots(dir) {
2403
+ for (let current = dirname3(dir); dirname3(current) !== current; current = dirname3(current)) {
2404
+ if (existsSync5(join4(current, "package.json"))) return ["public", "static", "src"].map((name) => join4(current, name));
2405
+ }
2406
+ return [];
2407
+ }
2408
+ async function verifyDir(dir, options = {}) {
2409
+ const root = resolve3(dir);
2410
+ const env = options.env ?? process.env;
2411
+ const files = [];
2412
+ await walk3(root, files);
2413
+ files.sort();
2414
+ const relPath = (path) => relative4(root, path).split(sep4).join("/");
2415
+ const ignore = (options.ignore ?? []).map(globToRegExp);
2416
+ const roots = staticRoots(root);
2417
+ const result = {
2418
+ dir: root,
2419
+ ok: true,
2420
+ bundles: [],
2421
+ maps: [],
2422
+ upload: { status: "unverified", endpoint: null, reason: null, checked: 0, missing: 0 },
2423
+ failures: []
2424
+ };
2425
+ const fail = (path, reason) => result.failures.push({ path, reason });
2426
+ for (const file of files) {
2427
+ const path = relPath(file);
2428
+ if (file.endsWith(".map")) {
2429
+ result.maps.push(path);
2430
+ if (!options.allowMaps) {
2431
+ fail(path, "source map in the deploy output: it exposes your original source. Delete it after upload (upload --delete-after-upload, or the bundler plugin) or keep it out of the deploy");
2432
+ }
2433
+ continue;
2434
+ }
2435
+ if (!BUNDLE_RE.test(file)) continue;
2436
+ const bytes = await readFile4(file);
2437
+ const bundle = { path, status: "ok", debugId: null, reasons: [], uploaded: null };
2438
+ result.bundles.push(bundle);
2439
+ const skip = (reason) => {
2440
+ bundle.status = "skipped";
2441
+ bundle.reasons.push(reason);
2442
+ };
2443
+ if (ignore.some((re) => re.test(path))) {
2444
+ skip("matches --ignore");
2445
+ continue;
2446
+ }
2447
+ const known = NOT_BUNDLES.find(([re]) => re.test(path));
2448
+ if (known) {
2449
+ skip(known[1]);
2450
+ continue;
2451
+ }
2452
+ let copied = null;
2453
+ for (const staticRoot of roots) {
2454
+ const candidate = join4(staticRoot, path);
2455
+ if (existsSync5(candidate) && (await readFile4(candidate)).equals(bytes)) copied = relative4(dirname3(staticRoot), candidate).split(sep4).join("/");
2456
+ }
2457
+ if (copied) {
2458
+ skip(`copied unchanged from ${copied}, not built`);
2459
+ continue;
2460
+ }
2461
+ const code = bytes.toString("utf8");
2462
+ const commentId = readDebugIdComment(code);
2463
+ const snippetId = SNIPPET_ID_RE.exec(code)?.[1] ?? null;
2464
+ bundle.debugId = commentId;
2465
+ if (!commentId && !hasDebugIdSnippet(code)) {
2466
+ bundle.reasons.push(
2467
+ "no debug id: its errors can't be matched to a source map. Run `autter-runtime sourcemaps inject` on the build output (or use the bundler plugin with AUTTER_RUNTIME_KEY set) before deploying"
2468
+ );
2469
+ } else if (!commentId) {
2470
+ bundle.reasons.push("has the debug id snippet but no //# debugId= comment: run `autter-runtime sourcemaps inject` on it again");
2471
+ } else if (!hasDebugIdSnippet(code)) {
2472
+ bundle.reasons.push(
2473
+ "has a //# debugId= comment but no registration snippet, so the browser SDK can't report its id: run `autter-runtime sourcemaps inject` on it"
2474
+ );
2475
+ } else if (snippetId && snippetId !== commentId) {
2476
+ bundle.reasons.push(`its snippet registers debug id ${snippetId} but its comment says ${commentId}: run inject on the original build output`);
2477
+ }
2478
+ const url = readSourceMappingUrl(code);
2479
+ if (url?.startsWith("data:")) {
2480
+ bundle.reasons.push("carries an inline source map (data: URL): your original source ships with the bundle. Use hidden or external maps");
2481
+ } else if (url && !/^[a-z][\w+.-]*:/i.test(url) && !url.startsWith("/")) {
2482
+ let target = url.split(/[?#]/)[0];
2483
+ try {
2484
+ target = decodeURIComponent(target);
2485
+ } catch {
2486
+ }
2487
+ if (!existsSync5(resolve3(dirname3(file), target))) {
2488
+ bundle.reasons.push(
2489
+ `its sourceMappingURL points at ${url}, which is not in the output (browsers' devtools request it and get a 404). Remove the comment: upload --delete-after-upload does`
2490
+ );
2491
+ }
2492
+ }
2493
+ if (bundle.reasons.length) {
2494
+ bundle.status = "failed";
2495
+ for (const reason of bundle.reasons) fail(path, reason);
2496
+ }
2497
+ }
2498
+ const counted = result.bundles.filter((b) => b.status !== "skipped");
2499
+ if (!counted.length) fail(".", `no .js/.mjs/.cjs bundles under ${dir}: is this the build output?`);
2500
+ await checkUploads(result, counted, options, env);
2501
+ result.ok = result.failures.length === 0;
2502
+ return result;
2503
+ }
2504
+ async function checkUploads(result, bundles, options, env) {
2505
+ const upload = result.upload;
2506
+ const key = (options.key ?? env.AUTTER_RUNTIME_KEY ?? "").trim();
2507
+ if (!key) {
2508
+ upload.reason = "AUTTER_RUNTIME_KEY is not set: only the local checks ran";
2509
+ return;
2510
+ }
2511
+ upload.endpoint = resolveEndpoint(options.endpoint, env);
2512
+ if (key.startsWith(CLIENT_KEY_PREFIX)) {
2513
+ upload.status = "error";
2514
+ upload.reason = "AUTTER_RUNTIME_KEY is a publishable client key (autter_rtc_\u2026), which can't check source maps: use an upload key (autter_rtu_\u2026) or the server key (autter_rt_\u2026)";
2515
+ result.failures.push({ path: ".", reason: upload.reason });
2516
+ return;
2517
+ }
2518
+ const withIds = bundles.filter((b) => b.debugId);
2519
+ if (!withIds.length) {
2520
+ upload.reason = "no bundle has a debug id to check";
2521
+ return;
2522
+ }
2523
+ let release = "verify";
2524
+ try {
2525
+ release = resolveRelease(options.release, env, options.cwd).release;
2526
+ } catch {
2527
+ }
2528
+ const doFetch = options.fetch ?? fetch;
2529
+ const missing = [];
2530
+ for (let start = 0; start < withIds.length; start += CHECK_BATCH2) {
2531
+ const batch = withIds.slice(start, start + CHECK_BATCH2);
2532
+ let res;
2533
+ try {
2534
+ res = await doFetch(`${upload.endpoint}/v1/sourcemaps/check`, {
2535
+ method: "POST",
2536
+ headers: { "content-type": "application/json", authorization: `Bearer ${key}` },
2537
+ body: JSON.stringify({ release, files: batch.map((b) => ({ filename: servedFilename(options.urlPrefix, b.path), debugId: b.debugId })) })
2538
+ });
2539
+ } catch (err) {
2540
+ upload.status = "error";
2541
+ upload.reason = `could not reach the ingester: ${err.message}`;
2542
+ result.failures.push({ path: ".", reason: upload.reason });
2543
+ return;
2544
+ }
2545
+ if (res.status === 404 || res.status === 400) {
2546
+ upload.status = "unverified";
2547
+ upload.reason = res.status === 404 ? "the ingester predates 1.6.0 (no /v1/sourcemaps/check), so uploads can't be checked: upgrade it" : "the ingester predates 1.7.0, which checks maps by debug id alone, so uploads can't be checked after the maps were deleted: upgrade it";
2548
+ return;
2549
+ }
2550
+ if (res.status === 401 || res.status === 403) {
2551
+ upload.status = "error";
2552
+ upload.reason = `the ingester rejected the key (${res.status}): ${(await res.text()).slice(0, 300)}` + (key.startsWith(UPLOAD_KEY_PREFIX) ? " (upload keys, autter_rtu_\u2026, need ingester 1.7.0 or later)" : "");
2553
+ result.failures.push({ path: ".", reason: upload.reason });
2554
+ return;
2555
+ }
2556
+ if (!res.ok) {
2557
+ upload.status = "error";
2558
+ upload.reason = `the source map check failed: HTTP ${res.status}`;
2559
+ result.failures.push({ path: ".", reason: upload.reason });
2560
+ return;
2561
+ }
2562
+ const body = await res.json();
2563
+ const present = new Set(Array.isArray(body.present) ? body.present : []);
2564
+ batch.forEach((bundle, index) => {
2565
+ bundle.uploaded = present.has(index);
2566
+ if (!bundle.uploaded) missing.push(bundle);
2567
+ });
2568
+ }
2569
+ upload.checked = withIds.length;
2570
+ upload.missing = missing.length;
2571
+ upload.status = missing.length ? "missing" : "verified";
2572
+ upload.reason = missing.length ? `${missing.length} of ${withIds.length} bundle(s) have no map on the ingester` : null;
2573
+ for (const bundle of missing) {
2574
+ const reason = `no map with debug id ${bundle.debugId} on the ingester for this key's repository: upload this build's maps (autter-runtime sourcemaps upload, or the bundler plugin with AUTTER_RUNTIME_KEY set)`;
2575
+ bundle.status = "failed";
2576
+ bundle.reasons.push(reason);
2577
+ result.failures.push({ path: bundle.path, reason });
2578
+ }
2579
+ }
2580
+ function formatVerify(result) {
2581
+ const lines = [];
2582
+ for (const bundle of result.bundles) {
2583
+ if (bundle.status === "ok") lines.push(`ok ${bundle.path} (debug id ${bundle.debugId}${bundle.uploaded ? ", map uploaded" : ""})`);
2584
+ else if (bundle.status === "skipped") lines.push(`skipped ${bundle.path}: ${bundle.reasons.join("; ")}`);
2585
+ else for (const reason of bundle.reasons) lines.push(`FAILED ${bundle.path}: ${reason}`);
2586
+ }
2587
+ for (const map of result.maps) {
2588
+ const failure = result.failures.find((f) => f.path === map);
2589
+ lines.push(failure ? `FAILED ${map}: ${failure.reason}` : `warning ${map}: source map in the output (allowed by --allow-maps; remove it before deploying)`);
2590
+ }
2591
+ for (const failure of result.failures.filter((f) => f.path === ".")) lines.push(`FAILED ${failure.reason}`);
2592
+ const u = result.upload;
2593
+ lines.push(
2594
+ u.status === "verified" ? `upload: verified, ${u.checked} bundle map(s) on the ingester (${u.endpoint})` : u.status === "missing" ? `upload: ${u.reason} (${u.endpoint})` : `upload: ${u.status}${u.reason ? `, ${u.reason}` : ""}`
2595
+ );
2596
+ const counted = result.bundles.filter((b) => b.status !== "skipped");
2597
+ lines.push(
2598
+ result.ok ? `ok: ${counted.length} bundle(s) with debug ids, ${result.maps.length ? `${result.maps.length} source map(s) left (--allow-maps)` : "no source maps left"}${u.status === "unverified" ? ", upload not verified" : ""}` : `${result.failures.length} problem(s) in ${result.dir}`
2599
+ );
2600
+ return lines;
2601
+ }
2602
+
867
2603
  // src/cli.ts
868
2604
  var USAGE = `autter-runtime \u2014 readable stack traces for Autter Runtime
869
2605
 
870
2606
  Usage:
871
- autter-runtime sourcemaps inject <dir> [--dry-run]
2607
+ autter-runtime sourcemaps setup [dir] [--dry-run] [--install] [--json]
2608
+ autter-runtime sourcemaps verify <dir> [--endpoint <url>] [--url-prefix <prefix>]
2609
+ [--ignore <glob>]... [--allow-maps] [--json]
2610
+ autter-runtime sourcemaps inject <dir> [--dry-run] [--json]
872
2611
  autter-runtime sourcemaps upload <dir> [--release <r>] [--url-prefix <prefix>]
873
- [--endpoint <url>] [--concurrency <n>] [--delete-after-upload] [--dry-run]
2612
+ [--endpoint <url>] [--concurrency <n>] [--delete-after-upload]
2613
+ [--best-effort] [--dry-run] [--json]
874
2614
  autter-runtime doctor ... (provided by @autter/runtime-node)
875
2615
 
2616
+ setup Wires source map upload into the project in [dir] (default "."):
2617
+ detects the build tool, package manager, CI and browser SDK, adds
2618
+ @autter/runtime-cli to devDependencies, adds the Vite/Rollup, webpack
2619
+ or Next.js plugin when the config is safe to edit (else a postbuild
2620
+ step), turns on hidden maps, and prints what is left to do by hand,
2621
+ including where to store AUTTER_RUNTIME_KEY. Never writes a key.
2622
+ Safe to re-run. Exit 0: all applied; 2: manual actions remain; 1: error.
2623
+ verify Checks a production build before deploy: every bundle has a debug
2624
+ id, no .map file is left, no sourceMappingURL points at a missing
2625
+ map, and (with AUTTER_RUNTIME_KEY) every bundle's map is on the
2626
+ ingester. Exit 0: ok (upload unverified without a key); 1: problems.
876
2627
  inject Gives every .js/.mjs/.cjs bundle that has a source map a debug id
877
2628
  derived from its bytes: a "//# debugId=" comment, a one-line snippet
878
2629
  that registers it for @autter/runtime-browser, and "debugId" in the
@@ -892,16 +2643,26 @@ Options:
892
2643
  then https://otlp.autter.dev
893
2644
  --concurrency <n> Parallel uploads (default 8)
894
2645
  --delete-after-upload Delete each .map once the ingester has it
2646
+ --best-effort For build scripts: never fail. Without a key, or when
2647
+ the upload fails, report it and exit 0; with
2648
+ --delete-after-upload, every .map under <dir> is then
2649
+ deleted anyway, so a build never ships maps
2650
+ --install setup: also run the package manager's install
2651
+ --ignore <glob> verify: JS files that are not bundles (repeatable)
2652
+ --allow-maps verify: report leftover .map files without failing
895
2653
  --dry-run Show what would change or be uploaded; write and send nothing
896
2654
  --json Print the result as JSON
897
2655
  -h, --help Show this help
898
2656
  -v, --version Print the version
899
2657
 
900
2658
  Environment:
901
- AUTTER_RUNTIME_KEY Server ingest key (autter_rt_\u2026), required by upload`;
902
- function ownVersion() {
2659
+ AUTTER_RUNTIME_KEY Upload-only key (autter_rtu_\u2026, recommended for CI: it
2660
+ can only upload source maps) or the server key
2661
+ (autter_rt_\u2026, also works). Required by upload; used by
2662
+ verify to check the ingester. Never a client key.`;
2663
+ function ownVersion2() {
903
2664
  try {
904
- return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
2665
+ return JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8")).version;
905
2666
  } catch {
906
2667
  return "unknown";
907
2668
  }
@@ -909,16 +2670,16 @@ function ownVersion() {
909
2670
  function delegate(pkg, args) {
910
2671
  let manifest;
911
2672
  try {
912
- manifest = createRequire(join2(process.cwd(), "package.json")).resolve(`${pkg}/package.json`);
2673
+ manifest = createRequire(join5(process.cwd(), "package.json")).resolve(`${pkg}/package.json`);
913
2674
  } catch {
914
2675
  return null;
915
2676
  }
916
- const bin = JSON.parse(readFileSync(manifest, "utf8")).bin;
917
- const relative3 = typeof bin === "string" ? bin : bin?.["autter-runtime"];
918
- if (!relative3) return null;
919
- const script = resolve2(dirname2(manifest), relative3);
920
- if (!existsSync3(script)) return null;
921
- const result = spawnSync(process.execPath, [script, ...args], { stdio: "inherit" });
2677
+ const bin = JSON.parse(readFileSync3(manifest, "utf8")).bin;
2678
+ const relative6 = typeof bin === "string" ? bin : bin?.["autter-runtime"];
2679
+ if (!relative6) return null;
2680
+ const script = resolve4(dirname4(manifest), relative6);
2681
+ if (!existsSync6(script)) return null;
2682
+ const result = spawnSync2(process.execPath, [script, ...args], { stdio: "inherit" });
922
2683
  return result.status ?? 1;
923
2684
  }
924
2685
  var SOURCEMAPS_OPTIONS = {
@@ -927,10 +2688,20 @@ var SOURCEMAPS_OPTIONS = {
927
2688
  endpoint: { type: "string" },
928
2689
  concurrency: { type: "string" },
929
2690
  "delete-after-upload": { type: "boolean" },
2691
+ "best-effort": { type: "boolean" },
2692
+ install: { type: "boolean" },
2693
+ ignore: { type: "string", multiple: true },
2694
+ "allow-maps": { type: "boolean" },
930
2695
  "dry-run": { type: "boolean" },
931
2696
  json: { type: "boolean" },
932
2697
  help: { type: "boolean", short: "h" }
933
2698
  };
2699
+ var ACTION_FLAGS = {
2700
+ setup: ["dry-run", "install"],
2701
+ inject: ["dry-run"],
2702
+ upload: ["release", "url-prefix", "endpoint", "concurrency", "delete-after-upload", "best-effort", "dry-run"],
2703
+ verify: ["endpoint", "url-prefix", "release", "ignore", "allow-maps"]
2704
+ };
934
2705
  async function sourcemaps(argv) {
935
2706
  let parsed;
936
2707
  try {
@@ -942,22 +2713,55 @@ ${USAGE}`);
942
2713
  return 64;
943
2714
  }
944
2715
  const { values, positionals } = parsed;
945
- const [action, dir, ...extra] = positionals;
2716
+ const [action, dirArg, ...extra] = positionals;
946
2717
  if (values.help) {
947
2718
  console.log(USAGE);
948
2719
  return 0;
949
2720
  }
950
- if (action !== "inject" && action !== "upload" || !dir || extra.length) {
951
- console.error(`${action && action !== "inject" && action !== "upload" ? `unknown command: sourcemaps ${action}
2721
+ const known = action !== void 0 && action in ACTION_FLAGS;
2722
+ const dir = dirArg ?? (action === "setup" ? "." : void 0);
2723
+ if (!known || !dir || extra.length) {
2724
+ console.error(`${action && !known ? `unknown command: sourcemaps ${action}
952
2725
 
953
2726
  ` : ""}${USAGE}`);
954
2727
  return 64;
955
2728
  }
956
- if (!existsSync3(dir)) {
957
- console.error(`autter-runtime: ${dir} does not exist`);
2729
+ const allowed = /* @__PURE__ */ new Set([...ACTION_FLAGS[action], "json", "help"]);
2730
+ const stray = Object.keys(values).filter((flag) => !allowed.has(flag));
2731
+ if (stray.length) {
2732
+ console.error(`autter-runtime: sourcemaps ${action} does not take --${stray.join(", --")}
2733
+
2734
+ ${USAGE}`);
958
2735
  return 64;
959
2736
  }
2737
+ if (!existsSync6(dir)) {
2738
+ console.error(`autter-runtime: ${dir} does not exist`);
2739
+ return action === "setup" ? 1 : 64;
2740
+ }
960
2741
  const dryRun = Boolean(values["dry-run"]);
2742
+ if (action === "setup") {
2743
+ try {
2744
+ const result = runSetup(dir, { dryRun, install: Boolean(values.install), installOutput: values.json ? "stderr" : "inherit" });
2745
+ if (values.json) console.log(JSON.stringify(result, null, 2));
2746
+ else for (const line of formatSetup(result)) console.log(line);
2747
+ return result.exitCode;
2748
+ } catch (err) {
2749
+ console.error(`autter-runtime: sourcemaps setup: ${err instanceof Error ? err.message : String(err)}`);
2750
+ return 1;
2751
+ }
2752
+ }
2753
+ if (action === "verify") {
2754
+ const result = await verifyDir(dir, {
2755
+ endpoint: values.endpoint,
2756
+ urlPrefix: values["url-prefix"],
2757
+ release: values.release,
2758
+ ignore: values.ignore,
2759
+ allowMaps: Boolean(values["allow-maps"])
2760
+ });
2761
+ if (values.json) console.log(JSON.stringify(result, null, 2));
2762
+ else for (const line of formatVerify(result)) console.log(line);
2763
+ return result.ok ? 0 : 1;
2764
+ }
961
2765
  if (action === "inject") {
962
2766
  const results = await injectDir(dir, { dryRun });
963
2767
  if (values.json) {
@@ -979,28 +2783,55 @@ ${USAGE}`);
979
2783
  console.error("autter-runtime: --concurrency must be a positive integer");
980
2784
  return 64;
981
2785
  }
982
- const summary = await uploadDir(dir, {
2786
+ const deleteAfterUpload = Boolean(values["delete-after-upload"]);
2787
+ const bestEffort = Boolean(values["best-effort"]);
2788
+ let summary = null;
2789
+ let error = null;
2790
+ const run = () => uploadDir(dir, {
983
2791
  release: values.release,
984
2792
  urlPrefix: values["url-prefix"],
985
2793
  endpoint: values.endpoint,
986
2794
  concurrency,
987
2795
  dryRun,
988
- deleteAfterUpload: Boolean(values["delete-after-upload"]),
2796
+ deleteAfterUpload,
989
2797
  log: (line) => console.error(line)
990
2798
  });
991
- if (values.json) console.log(JSON.stringify(summary, null, 2));
2799
+ if (!bestEffort) summary = await run();
2800
+ else if (!dryRun && !(process.env.AUTTER_RUNTIME_KEY ?? "").trim()) error = `AUTTER_RUNTIME_KEY is not set, so nothing was uploaded (${KEY_ADVICE})`;
992
2801
  else {
993
- if (!summary.uploaded.length && !summary.skipped.length && !summary.failed.length) {
994
- console.log(`no .js/.mjs/.cjs bundles with source maps under ${dir}`);
2802
+ try {
2803
+ summary = await run();
2804
+ } catch (err) {
2805
+ error = err instanceof Error ? err.message : String(err);
2806
+ }
2807
+ }
2808
+ let removedMaps = [];
2809
+ if (bestEffort && deleteAfterUpload && !dryRun) {
2810
+ const owners = new Map((await findBundles(dir)).map((bundle) => [bundle.map, bundle.js]));
2811
+ removedMaps = (await removeSourceMaps(await findMapFiles(dir), owners)).map((map) => relative5(resolve4(dir), map).split(sep5).join("/"));
2812
+ }
2813
+ if (values.json) {
2814
+ console.log(JSON.stringify({ ...summary ?? { uploaded: [], skipped: [], failed: [] }, ...bestEffort ? { error, removedMaps } : {} }, null, 2));
2815
+ } else {
2816
+ if (summary) {
2817
+ if (!summary.uploaded.length && !summary.skipped.length && !summary.failed.length) {
2818
+ console.log(`no .js/.mjs/.cjs bundles with source maps under ${dir}`);
2819
+ }
2820
+ for (const line of formatUploadSummary(summary)) console.log(line);
2821
+ }
2822
+ if (error) console.error(`autter-runtime: ${error}`);
2823
+ if (removedMaps.length) {
2824
+ const why = error ? "the upload did not run" : summary?.failed.length ? `${summary.failed.length} could not be uploaded` : "they were uploaded";
2825
+ console.log(`removed ${removedMaps.length} source map(s) under ${dir}, so they are not deployed: ${why}`);
995
2826
  }
996
- for (const line of formatUploadSummary(summary)) console.log(line);
2827
+ if (bestEffort && (error || summary?.failed.length)) console.error("autter-runtime: --best-effort: the build continues");
997
2828
  }
998
- return summary.failed.length ? 1 : 0;
2829
+ return bestEffort ? 0 : summary?.failed.length ? 1 : 0;
999
2830
  }
1000
2831
  async function main(argv) {
1001
2832
  const [command, ...rest] = argv;
1002
2833
  if (command === "-v" || command === "--version") {
1003
- console.log(ownVersion());
2834
+ console.log(ownVersion2());
1004
2835
  return 0;
1005
2836
  }
1006
2837
  if (command === void 0 || command === "-h" || command === "--help" || command === "help") {