@dualkit/convert 0.4.0-beta.0 → 0.5.0-beta.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.
Files changed (3) hide show
  1. package/dist/index.cjs +241 -32
  2. package/dist/index.js +238 -29
  3. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -177,8 +177,8 @@ function detectProject(root) {
177
177
  }
178
178
 
179
179
  // src/convert.ts
180
- var import_lint12 = require("@dualkit/lint");
181
- var import_node_path17 = require("path");
180
+ var import_lint13 = require("@dualkit/lint");
181
+ var import_node_path18 = require("path");
182
182
 
183
183
  // src/apply.ts
184
184
  var import_lint2 = require("@dualkit/lint");
@@ -274,11 +274,11 @@ var RefusedError = class extends Error {
274
274
  };
275
275
  var NO_GIT = "git is not installed or not on PATH";
276
276
  function git(cwd, args) {
277
- return new Promise((resolve3, reject) => {
277
+ return new Promise((resolve4, reject) => {
278
278
  (0, import_node_child_process.execFile)("git", ["--literal-pathspecs", ...args], { cwd, maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => {
279
279
  if (error && error.code === "ENOENT") return reject(new Error((0, import_node_fs3.existsSync)(cwd) ? NO_GIT : `The folder ${cwd} does not exist.`));
280
280
  const code = error ? typeof error.code === "number" ? error.code : 1 : 0;
281
- resolve3({ code, out: `${stdout}${stderr}`.trim(), stdout });
281
+ resolve4({ code, out: `${stdout}${stderr}`.trim(), stdout });
282
282
  });
283
283
  });
284
284
  }
@@ -644,15 +644,15 @@ var ServerError = class extends Error {
644
644
  var OUTPUT_LINES = 20;
645
645
  var POLL_MS = 250;
646
646
  var KILL_GRACE_MS = 5e3;
647
- var sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
647
+ var sleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
648
648
  function portTaken(port) {
649
- return new Promise((resolve3) => {
649
+ return new Promise((resolve4) => {
650
650
  const socket = (0, import_node_net.connect)({ port, host: "localhost" });
651
651
  socket.once("connect", () => {
652
652
  socket.destroy();
653
- resolve3(true);
653
+ resolve4(true);
654
654
  });
655
- socket.once("error", () => resolve3(false));
655
+ socket.once("error", () => resolve4(false));
656
656
  });
657
657
  }
658
658
  async function answers(url) {
@@ -692,7 +692,7 @@ async function startServer(cmd, port, cwd, timeoutMs = 12e4) {
692
692
  });
693
693
  const pid = child.pid;
694
694
  if (pid === void 0) {
695
- await new Promise((resolve3) => setImmediate(resolve3));
695
+ await new Promise((resolve4) => setImmediate(resolve4));
696
696
  throw new ServerError(`Could not start "${cmd}": ${spawnError?.message ?? "no process id"}`);
697
697
  }
698
698
  const collect = (chunk) => {
@@ -1655,12 +1655,12 @@ function routeMoves(appDir, paths) {
1655
1655
  }
1656
1656
  return { moves, keptPages };
1657
1657
  }
1658
- function eachNode(node, visit) {
1659
- if (Array.isArray(node)) return node.forEach((child) => eachNode(child, visit));
1658
+ function eachNode(node, visit2) {
1659
+ if (Array.isArray(node)) return node.forEach((child) => eachNode(child, visit2));
1660
1660
  if (!node || typeof node !== "object") return;
1661
1661
  const n = node;
1662
- if (typeof n.type === "string") visit(n);
1663
- for (const [key, child] of Object.entries(n)) if (!SKIP3.has(key)) eachNode(child, visit);
1662
+ if (typeof n.type === "string") visit2(n);
1663
+ for (const [key, child] of Object.entries(n)) if (!SKIP3.has(key)) eachNode(child, visit2);
1664
1664
  }
1665
1665
  var RELATIVE = /^\.\.?\//;
1666
1666
  var stringLiteral = (node) => node?.type === "StringLiteral" ? node : null;
@@ -2962,9 +2962,208 @@ function runPasses(path, text, abs, ctx) {
2962
2962
  return { kind: "done", text: file.text, edits: edits2, byKind, flags, entries };
2963
2963
  }
2964
2964
 
2965
+ // src/transforms/data.ts
2966
+ var import_node_path16 = require("path");
2967
+ var import_lint12 = require("@dualkit/lint");
2968
+ var SKIP8 = /* @__PURE__ */ new Set(["loc", "leadingComments", "trailingComments", "innerComments", "extra"]);
2969
+ var TEXT_KEYS = /* @__PURE__ */ new Set(["title", "label", "description", "placeholder", "subtitle", "message", "text", "alt", "heading", "caption"]);
2970
+ var NAV_KEYS = /* @__PURE__ */ new Set(["href", "url", "path", "to", "icon"]);
2971
+ var RESERVED = /* @__PURE__ */ new Set(["metadata", "viewport", "config", "routing"]);
2972
+ var PROSE2 = /^\p{Lu}\p{Ll}/u;
2973
+ var NOT_PROSE2 = /[/\\_=<>{}@#|]/;
2974
+ var EXTENSIONS3 = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js", "/index.jsx"];
2975
+ var ALIASES = /^[@~]\//;
2976
+ var unwrap = (node) => node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" ? unwrap(node.expression) : node;
2977
+ var children = (node) => Object.entries(node).filter(([key]) => !SKIP8.has(key));
2978
+ function visit(node, ancestors, fn) {
2979
+ if (Array.isArray(node)) return node.forEach((child) => visit(child, ancestors, fn));
2980
+ if (!node || typeof node !== "object") return;
2981
+ const n = node;
2982
+ if (typeof n.type === "string") fn(n, ancestors);
2983
+ const next = typeof n.type === "string" ? [...ancestors, n] : ancestors;
2984
+ for (const [, child] of children(n)) visit(child, next, fn);
2985
+ }
2986
+ var keyName = (property) => {
2987
+ const key = property.key;
2988
+ if (property.computed) return null;
2989
+ return key.type === "Identifier" ? String(key.name) : key.type === "StringLiteral" ? String(key.value) : null;
2990
+ };
2991
+ var isProse = (value) => PROSE2.test(value) && !NOT_PROSE2.test(value);
2992
+ function textProperty(property, parent) {
2993
+ const key = keyName(property);
2994
+ if (key === null) return false;
2995
+ if (TEXT_KEYS.has(key)) return true;
2996
+ if (key !== "name" || parent?.type !== "ObjectExpression") return false;
2997
+ return (parent.properties ?? []).some((p) => p.type === "ObjectProperty" && NAV_KEYS.has(keyName(p) ?? ""));
2998
+ }
2999
+ function textsIn(init, reserved) {
3000
+ const strings = [];
3001
+ let clash = false;
3002
+ visit(init, [], (node, above) => {
3003
+ if (FUNCTIONS.has(node.type) && scopeNames(node).includes("t")) clash = true;
3004
+ if (node.type !== "ObjectProperty" || above.some((a) => FUNCTIONS.has(a.type) || a.type?.startsWith("JSX"))) return;
3005
+ const value = node.value;
3006
+ if (value.type === "StringLiteral" && isProse(String(value.value)) && !reserved.has(String(value.value)) && textProperty(node, above[above.length - 1])) strings.push(value);
3007
+ });
3008
+ return clash ? null : strings;
3009
+ }
3010
+ function dataIn(module2, reserved) {
3011
+ const found = [];
3012
+ for (const statement of module2.program.body ?? []) {
3013
+ const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
3014
+ if (declaration?.type !== "VariableDeclaration" || declaration.kind !== "const") continue;
3015
+ for (const declarator of declaration.declarations ?? []) {
3016
+ const id = declarator.id;
3017
+ const init = declarator.init;
3018
+ if (id.type !== "Identifier" || !init || RESERVED.has(String(id.name))) continue;
3019
+ if (!["ArrayExpression", "ObjectExpression"].includes(unwrap(init).type)) continue;
3020
+ const strings = textsIn(init, reserved);
3021
+ if (strings?.length) found.push({ module: module2, name: String(id.name), declarator, init, strings });
3022
+ }
3023
+ }
3024
+ return found;
3025
+ }
3026
+ function literalTypes(modules) {
3027
+ const found = /* @__PURE__ */ new Set();
3028
+ for (const module2 of modules) {
3029
+ visit(module2.program, [], (node) => {
3030
+ const literal = node.type === "TSLiteralType" ? node.literal : null;
3031
+ if (literal?.type === "StringLiteral") found.add(String(literal.value));
3032
+ });
3033
+ }
3034
+ return found;
3035
+ }
3036
+ function resolve3(from, source, known) {
3037
+ const bases = source.startsWith(".") ? [(0, import_node_path16.join)((0, import_node_path16.dirname)(from), source)] : ALIASES.test(source) ? [source.slice(2), (0, import_node_path16.join)("src", source.slice(2))] : [];
3038
+ for (const base of bases) {
3039
+ for (const extension of EXTENSIONS3) {
3040
+ const path = (0, import_node_path16.normalize)(`${base}${extension}`);
3041
+ if (known.has(path)) return path;
3042
+ }
3043
+ }
3044
+ return null;
3045
+ }
3046
+ function opaque(module2, known) {
3047
+ const paths = [];
3048
+ for (const statement of module2.program.body ?? []) {
3049
+ const source = statement.source;
3050
+ if (!source) continue;
3051
+ const path = resolve3(module2.path, String(source.value), known);
3052
+ if (!path) continue;
3053
+ const star = (statement.specifiers ?? []).some((s) => s.type === "ImportNamespaceSpecifier");
3054
+ if (statement.type !== "ImportDeclaration" || star) paths.push(path);
3055
+ }
3056
+ return paths;
3057
+ }
3058
+ function usesOf(module2, local, skip, lib) {
3059
+ const uses = [];
3060
+ let blocked = false;
3061
+ visit(module2.program, [], (node, above) => {
3062
+ if (blocked || node.type !== "Identifier" || node.name !== local || node === skip) return;
3063
+ const parent = above[above.length - 1];
3064
+ if (parent.type?.startsWith("Import") || parent.type === "TSTypeReference" || parent.type === "TSQualifiedName") return;
3065
+ if (parent.type === "MemberExpression" && parent.property === node && !parent.computed) return;
3066
+ if (parent.type === "OptionalMemberExpression" && parent.property === node && !parent.computed) return;
3067
+ if (parent.type === "ObjectProperty" && parent.key === node && !parent.computed && !parent.shorthand) return;
3068
+ const inner = above.slice(1);
3069
+ const shadowed = inner.some((a) => scopeNames(a).includes(local));
3070
+ const component = componentOf(above);
3071
+ const stuck = parent.type === "TSTypeQuery" || parent.type?.startsWith("Export") || Boolean(parent.shorthand);
3072
+ const inParams = component !== null && node.start < component.body.start;
3073
+ if (shadowed || stuck || inParams || !component || tInScope(inner) || lib === "react-i18next" && component.async) blocked = true;
3074
+ else uses.push({ module: module2, node, local, component });
3075
+ });
3076
+ return blocked ? null : uses;
3077
+ }
3078
+ function allUses(data, modules, known, lib) {
3079
+ const own = usesOf(data.module, data.name, data.declarator.id, lib);
3080
+ if (!own) return null;
3081
+ const uses = [...own];
3082
+ for (const module2 of modules) {
3083
+ if (module2 === data.module) continue;
3084
+ for (const statement of module2.program.body ?? []) {
3085
+ if (statement.type !== "ImportDeclaration" || resolve3(module2.path, String(statement.source.value), known) !== data.module.path) continue;
3086
+ const specifier2 = (statement.specifiers ?? []).find((s) => s.type === "ImportSpecifier" && s.imported.name === data.name);
3087
+ if (!specifier2) continue;
3088
+ const found = usesOf(module2, String(specifier2.local.name), null, lib);
3089
+ if (!found) return null;
3090
+ uses.push(...found);
3091
+ }
3092
+ }
3093
+ return uses.length ? uses : null;
3094
+ }
3095
+ function parseModule({ path, text }) {
3096
+ if (text.includes("use server")) return null;
3097
+ const outcome = (0, import_lint12.parseSourceOutcome)(path, text, path);
3098
+ const block = "file" in outcome ? outcome.file.js[0] : void 0;
3099
+ return block ? { path, text, base: block.offset, program: block.ast.program } : null;
3100
+ }
3101
+ function keyFor2(keys, stem, en) {
3102
+ const known = keys.byText.get(`${stem}
3103
+ ${en}`);
3104
+ if (known) return { key: known, isNew: false };
3105
+ const base = `${stem}.${keyWords(en)}`;
3106
+ let key = base;
3107
+ for (let n = 2; keys.used.has(key); n++) key = `${base}-${n}`;
3108
+ keys.used.add(key);
3109
+ keys.byText.set(`${stem}
3110
+ ${en}`, key);
3111
+ return { key, isNew: true };
3112
+ }
3113
+ function dataEdits(data, keys, entries) {
3114
+ const { module: module2, init, declarator, strings } = data;
3115
+ const { base, text, path } = module2;
3116
+ const annotation = declarator.id.typeAnnotation;
3117
+ const returns = annotation ? text.slice(base + annotation.start, base + annotation.end) : "";
3118
+ const edits2 = [
3119
+ { start: base + init.start, end: base + init.start, text: `(t: (key: string) => string)${returns} => (` },
3120
+ { start: base + init.end, end: base + init.end, text: ")" },
3121
+ ...annotation ? [{ start: base + annotation.start, end: base + annotation.end, text: "" }] : []
3122
+ ];
3123
+ const stem = stemOf(path);
3124
+ for (const literal of strings) {
3125
+ const en = String(literal.value);
3126
+ const { key, isNew } = keyFor2(keys, stem, en);
3127
+ if (isNew) entries.push({ key, en, ar: "", status: "pending", file: path, line: lineOf(text, base + literal.start) });
3128
+ edits2.push({ start: base + literal.start, end: base + literal.end, text: `t('${key}')` });
3129
+ }
3130
+ return edits2;
3131
+ }
3132
+ function liftData(inputs, lib, taken) {
3133
+ const modules = inputs.map(parseModule).filter((m) => m !== null);
3134
+ const known = new Set(inputs.map((i) => (0, import_node_path16.normalize)(i.path)));
3135
+ const barred = new Set(modules.flatMap((m) => opaque(m, known)));
3136
+ const reserved = literalTypes(modules);
3137
+ const edits2 = /* @__PURE__ */ new Map();
3138
+ const components = /* @__PURE__ */ new Map();
3139
+ const entries = [];
3140
+ const keys = { used: new Set(taken), byText: /* @__PURE__ */ new Map() };
3141
+ const push = (module2, more) => {
3142
+ edits2.set(module2, [...edits2.get(module2) ?? [], ...more]);
3143
+ };
3144
+ let moved = 0;
3145
+ for (const data of modules.filter((m) => !barred.has((0, import_node_path16.normalize)(m.path))).flatMap((m) => dataIn(m, reserved))) {
3146
+ const uses = allUses(data, modules, known, lib);
3147
+ if (!uses) continue;
3148
+ push(data.module, dataEdits(data, keys, entries));
3149
+ moved += data.strings.length;
3150
+ for (const use of uses) {
3151
+ push(use.module, [{ start: use.module.base + use.node.end, end: use.module.base + use.node.end, text: "(t)" }]);
3152
+ components.set(use.module, (components.get(use.module) ?? /* @__PURE__ */ new Set()).add(use.component));
3153
+ }
3154
+ }
3155
+ for (const [module2, found] of components) {
3156
+ const style = styleOf(module2.program, module2.text, module2.base);
3157
+ const hooks = [...found].map((fn) => hookEdits(fn, lib, module2.text, module2.base, style));
3158
+ push(module2, [...hooks.flatMap((h) => h.edits), ...importEdits(module2.program, hooks.flatMap((h) => h.imports), module2.base, style)]);
3159
+ }
3160
+ const texts = new Map([...edits2].map(([module2, list]) => [module2.path, applyEdits(module2.text, list)]));
3161
+ return { texts, entries, moved };
3162
+ }
3163
+
2965
3164
  // src/rollback.ts
2966
3165
  var import_node_fs9 = require("fs");
2967
- var import_node_path16 = require("path");
3166
+ var import_node_path17 = require("path");
2968
3167
  var ConvertFailedError = class extends Error {
2969
3168
  constructor(message, cause) {
2970
3169
  super(message);
@@ -2983,7 +3182,7 @@ async function rollBack(target, step, error) {
2983
3182
  const failed = `convert failed at ${step}: ${reasonOf(error)}.`;
2984
3183
  try {
2985
3184
  await resetHard(target.root);
2986
- for (const path of [...target.created].reverse()) (0, import_node_fs9.rmSync)((0, import_node_path16.join)(target.root, path), { recursive: true, force: true });
3185
+ for (const path of [...target.created].reverse()) (0, import_node_fs9.rmSync)((0, import_node_path17.join)(target.root, path), { recursive: true, force: true });
2987
3186
  if (target.madeBranch) {
2988
3187
  await checkout(target.root, target.base);
2989
3188
  await deleteBranch(target.root, target.branch);
@@ -3063,7 +3262,7 @@ var branchFor = (mode) => `dualkit/${mode}`;
3063
3262
  var problems = (flags) => flags.filter((f) => f.kind !== "info");
3064
3263
  function transformAll(project, generated, taken) {
3065
3264
  const { root } = project;
3066
- const walked = (0, import_lint12.walk)(["."], root);
3265
+ const walked = (0, import_lint13.walk)(["."], root);
3067
3266
  const used = new Set(taken);
3068
3267
  const ctx = {
3069
3268
  tailwind: project.tailwind,
@@ -3072,18 +3271,21 @@ function transformAll(project, generated, taken) {
3072
3271
  css: physicalContext(root)
3073
3272
  };
3074
3273
  const out = { texts: /* @__PURE__ */ new Map(), edits: 0, byKind: { tailwind: 0, css: 0, markup: 0, strings: 0, formats: 0 }, perFile: /* @__PURE__ */ new Map(), flags: [], entries: [], skipped: [...walked.skipped] };
3274
+ const lifted = lift(project, walked.files.filter((path) => !generated.has(path)), ctx.lib, used);
3275
+ out.entries.push(...lifted.entries);
3276
+ for (const entry of lifted.entries) used.add(entry.key);
3075
3277
  for (const path of walked.files) {
3076
3278
  if (generated.has(path)) continue;
3077
- const raw = readText(root, path);
3279
+ const raw = lifted.texts.get(path) ?? readText(root, path);
3078
3280
  if (raw === null) {
3079
3281
  out.skipped.push({ file: path, reason: "unreadable" });
3080
3282
  continue;
3081
3283
  }
3082
- if ((0, import_lint12.isMinified)(raw)) {
3284
+ if ((0, import_lint13.isMinified)(raw)) {
3083
3285
  out.skipped.push({ file: path, reason: "minified" });
3084
3286
  continue;
3085
3287
  }
3086
- const result = runPasses(path, raw, (0, import_node_path17.join)(root, path), { ...ctx, taken: used });
3288
+ const result = runPasses(path, raw, (0, import_node_path18.join)(root, path), { ...ctx, taken: used });
3087
3289
  if (result.kind === "skipped") {
3088
3290
  out.skipped.push({ file: path, reason: result.reason });
3089
3291
  continue;
@@ -3091,7 +3293,7 @@ function transformAll(project, generated, taken) {
3091
3293
  out.flags.push(...result.flags);
3092
3294
  out.entries.push(...result.entries);
3093
3295
  for (const entry of result.entries) used.add(entry.key);
3094
- if (result.edits === 0) continue;
3296
+ if (result.edits === 0 && !lifted.texts.has(path)) continue;
3095
3297
  out.edits += result.edits;
3096
3298
  for (const kind of Object.keys(out.byKind)) out.byKind[kind] += result.byKind[kind];
3097
3299
  out.perFile.set(path, result.edits);
@@ -3099,6 +3301,13 @@ function transformAll(project, generated, taken) {
3099
3301
  }
3100
3302
  return out;
3101
3303
  }
3304
+ function lift(project, paths, lib, taken) {
3305
+ const inputs = paths.flatMap((path) => {
3306
+ const text = /\.[jt]sx?$/.test(path) ? readText(project.root, path) : null;
3307
+ return text !== null && !text.startsWith(BOM) && !(0, import_lint13.isMinified)(text) ? [{ path, text }] : [];
3308
+ });
3309
+ return liftData(inputs, lib, taken);
3310
+ }
3102
3311
  function changedFiles(perFile, moved, wiring) {
3103
3312
  const total = /* @__PURE__ */ new Map();
3104
3313
  const add = (file, n) => {
@@ -3188,7 +3397,7 @@ async function convert(options, ioIn) {
3188
3397
 
3189
3398
  // src/finish.ts
3190
3399
  var import_node_fs11 = require("fs");
3191
- var import_node_path19 = require("path");
3400
+ var import_node_path20 = require("path");
3192
3401
 
3193
3402
  // src/match.ts
3194
3403
  var import_pngjs = require("pngjs");
@@ -3219,7 +3428,7 @@ function compare(a, b) {
3219
3428
 
3220
3429
  // src/finish-undo.ts
3221
3430
  var import_node_fs10 = require("fs");
3222
- var import_node_path18 = require("path");
3431
+ var import_node_path19 = require("path");
3223
3432
  async function snapshot(root, paths) {
3224
3433
  const clean = new Set(await cleanAtHead(root, paths));
3225
3434
  return paths.map((path) => ({ path, clean: clean.has(path), text: clean.has(path) ? null : readText(root, path) }));
@@ -3228,8 +3437,8 @@ async function undo(root, saved, staged) {
3228
3437
  await restoreFromHead(root, saved.filter((s) => s.clean).map((s) => s.path));
3229
3438
  const dirty = saved.filter((s) => !s.clean);
3230
3439
  for (const { path, text } of dirty) {
3231
- if (text === null) (0, import_node_fs10.rmSync)((0, import_node_path18.join)(root, path), { force: true });
3232
- else (0, import_node_fs10.writeFileSync)((0, import_node_path18.join)(root, path), text);
3440
+ if (text === null) (0, import_node_fs10.rmSync)((0, import_node_path19.join)(root, path), { force: true });
3441
+ else (0, import_node_fs10.writeFileSync)((0, import_node_path19.join)(root, path), text);
3233
3442
  }
3234
3443
  await unstage(root, [.../* @__PURE__ */ new Set([...dirty.map((s) => s.path), ...staged])]);
3235
3444
  }
@@ -3408,7 +3617,7 @@ function writeMessages(root, ledger) {
3408
3617
  for (const [path, text] of Object.entries(messagesFiles(ledger))) writeText(disk, path, mergedMessages(root, path, text));
3409
3618
  }
3410
3619
  function mergedMessages(root, path, generated) {
3411
- const full = (0, import_node_path19.join)(root, path);
3620
+ const full = (0, import_node_path20.join)(root, path);
3412
3621
  if (!(0, import_node_fs11.existsSync)(full)) return generated;
3413
3622
  try {
3414
3623
  const existing = JSON.parse((0, import_node_fs11.readFileSync)(full, "utf8"));
@@ -3423,7 +3632,7 @@ async function englishChecks(root, base, routes, shoot) {
3423
3632
  const noBaseline = [];
3424
3633
  for (const route of routes) {
3425
3634
  for (const width of WIDTHS) {
3426
- const file = (0, import_node_path19.join)(root, baselinePath(route, width));
3635
+ const file = (0, import_node_path20.join)(root, baselinePath(route, width));
3427
3636
  if (!(0, import_node_fs11.existsSync)(file)) {
3428
3637
  noBaseline.push({ route, width });
3429
3638
  continue;
@@ -3433,8 +3642,8 @@ async function englishChecks(root, base, routes, shoot) {
3433
3642
  let diffPath;
3434
3643
  if (ratio >= MAX_DIFF_RATIO) {
3435
3644
  diffPath = `${DIFF_DIR}/${routeSlug(route)}-${width}.png`;
3436
- (0, import_node_fs11.mkdirSync)((0, import_node_path19.join)(root, DIFF_DIR), { recursive: true });
3437
- (0, import_node_fs11.writeFileSync)((0, import_node_path19.join)(root, diffPath), diff);
3645
+ (0, import_node_fs11.mkdirSync)((0, import_node_path20.join)(root, DIFF_DIR), { recursive: true });
3646
+ (0, import_node_fs11.writeFileSync)((0, import_node_path20.join)(root, diffPath), diff);
3438
3647
  }
3439
3648
  english2.push({ route, width, ratio, ...diffPath ? { diffPath } : {} });
3440
3649
  }
@@ -3455,7 +3664,7 @@ async function arabicChecks(project, state, base, routes, audit) {
3455
3664
  return arabic2;
3456
3665
  }
3457
3666
  function closedAtBaseline(root) {
3458
- const path = (0, import_node_path19.join)(root, CLOSED_FILE);
3667
+ const path = (0, import_node_path20.join)(root, CLOSED_FILE);
3459
3668
  if (!(0, import_node_fs11.existsSync)(path)) return /* @__PURE__ */ new Set();
3460
3669
  try {
3461
3670
  const routes = JSON.parse((0, import_node_fs11.readFileSync)(path, "utf8"));
@@ -3525,17 +3734,17 @@ async function run(ready, step) {
3525
3734
  remainingLint,
3526
3735
  ...tests2 ? { tests: { code: tests2.code, tail: tail(tests2.out) } } : {}
3527
3736
  };
3528
- (0, import_node_fs11.writeFileSync)((0, import_node_path19.join)(root, REPORT_FILE), renderConvertReport(data));
3737
+ (0, import_node_fs11.writeFileSync)((0, import_node_path20.join)(root, REPORT_FILE), renderConvertReport(data));
3529
3738
  step.name = "saving the state";
3530
3739
  saveState(root, { ...state, phase: "finished" });
3531
3740
  step.name = "saving your answers";
3532
3741
  if (options.configAnswers) writeConfigAnswers({ root, created: [] }, options.configAnswers, configFile);
3533
3742
  step.name = "committing";
3534
- await stage(root, own.filter((p) => (p !== configFile || options.configAnswers) && (0, import_node_fs11.existsSync)((0, import_node_path19.join)(root, p))));
3743
+ await stage(root, own.filter((p) => (p !== configFile || options.configAnswers) && (0, import_node_fs11.existsSync)((0, import_node_path20.join)(root, p))));
3535
3744
  if ((await stagedFiles(root)).length) await commit(root, `dualkit: finish ${state.mode} conversion`);
3536
3745
  const checked = checks.arabic.length > 0;
3537
3746
  const passed = checked && checks.english.every((p) => p.ratio < MAX_DIFF_RATIO) && checks.arabic.every((p) => p.errors === 0) && (tests2?.code ?? 0) === 0;
3538
- return { reportPath: (0, import_node_path19.join)(root, REPORT_FILE), passed };
3747
+ return { reportPath: (0, import_node_path20.join)(root, REPORT_FILE), passed };
3539
3748
  }
3540
3749
  async function finish(options, ioIn) {
3541
3750
  const ready = await prepare(options, ioIn);
package/dist/index.js CHANGED
@@ -143,7 +143,7 @@ function detectProject(root) {
143
143
 
144
144
  // src/convert.ts
145
145
  import { isMinified, walk as walk4 } from "@dualkit/lint";
146
- import { join as join9 } from "path";
146
+ import { join as join10 } from "path";
147
147
 
148
148
  // src/apply.ts
149
149
  import { parseSourceOutcome } from "@dualkit/lint";
@@ -239,11 +239,11 @@ var RefusedError = class extends Error {
239
239
  };
240
240
  var NO_GIT = "git is not installed or not on PATH";
241
241
  function git(cwd, args) {
242
- return new Promise((resolve3, reject) => {
242
+ return new Promise((resolve4, reject) => {
243
243
  execFile("git", ["--literal-pathspecs", ...args], { cwd, maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => {
244
244
  if (error && error.code === "ENOENT") return reject(new Error(existsSync3(cwd) ? NO_GIT : `The folder ${cwd} does not exist.`));
245
245
  const code = error ? typeof error.code === "number" ? error.code : 1 : 0;
246
- resolve3({ code, out: `${stdout}${stderr}`.trim(), stdout });
246
+ resolve4({ code, out: `${stdout}${stderr}`.trim(), stdout });
247
247
  });
248
248
  });
249
249
  }
@@ -609,15 +609,15 @@ var ServerError = class extends Error {
609
609
  var OUTPUT_LINES = 20;
610
610
  var POLL_MS = 250;
611
611
  var KILL_GRACE_MS = 5e3;
612
- var sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
612
+ var sleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
613
613
  function portTaken(port) {
614
- return new Promise((resolve3) => {
614
+ return new Promise((resolve4) => {
615
615
  const socket = connect({ port, host: "localhost" });
616
616
  socket.once("connect", () => {
617
617
  socket.destroy();
618
- resolve3(true);
618
+ resolve4(true);
619
619
  });
620
- socket.once("error", () => resolve3(false));
620
+ socket.once("error", () => resolve4(false));
621
621
  });
622
622
  }
623
623
  async function answers(url) {
@@ -657,7 +657,7 @@ async function startServer(cmd, port, cwd, timeoutMs = 12e4) {
657
657
  });
658
658
  const pid = child.pid;
659
659
  if (pid === void 0) {
660
- await new Promise((resolve3) => setImmediate(resolve3));
660
+ await new Promise((resolve4) => setImmediate(resolve4));
661
661
  throw new ServerError(`Could not start "${cmd}": ${spawnError?.message ?? "no process id"}`);
662
662
  }
663
663
  const collect = (chunk) => {
@@ -1620,12 +1620,12 @@ function routeMoves(appDir, paths) {
1620
1620
  }
1621
1621
  return { moves, keptPages };
1622
1622
  }
1623
- function eachNode(node, visit) {
1624
- if (Array.isArray(node)) return node.forEach((child) => eachNode(child, visit));
1623
+ function eachNode(node, visit2) {
1624
+ if (Array.isArray(node)) return node.forEach((child) => eachNode(child, visit2));
1625
1625
  if (!node || typeof node !== "object") return;
1626
1626
  const n = node;
1627
- if (typeof n.type === "string") visit(n);
1628
- for (const [key, child] of Object.entries(n)) if (!SKIP3.has(key)) eachNode(child, visit);
1627
+ if (typeof n.type === "string") visit2(n);
1628
+ for (const [key, child] of Object.entries(n)) if (!SKIP3.has(key)) eachNode(child, visit2);
1629
1629
  }
1630
1630
  var RELATIVE = /^\.\.?\//;
1631
1631
  var stringLiteral = (node) => node?.type === "StringLiteral" ? node : null;
@@ -2927,9 +2927,208 @@ function runPasses(path, text, abs, ctx) {
2927
2927
  return { kind: "done", text: file.text, edits: edits2, byKind, flags, entries };
2928
2928
  }
2929
2929
 
2930
+ // src/transforms/data.ts
2931
+ import { dirname as dirname4, join as join8, normalize } from "path";
2932
+ import { parseSourceOutcome as parseSourceOutcome7 } from "@dualkit/lint";
2933
+ var SKIP8 = /* @__PURE__ */ new Set(["loc", "leadingComments", "trailingComments", "innerComments", "extra"]);
2934
+ var TEXT_KEYS = /* @__PURE__ */ new Set(["title", "label", "description", "placeholder", "subtitle", "message", "text", "alt", "heading", "caption"]);
2935
+ var NAV_KEYS = /* @__PURE__ */ new Set(["href", "url", "path", "to", "icon"]);
2936
+ var RESERVED = /* @__PURE__ */ new Set(["metadata", "viewport", "config", "routing"]);
2937
+ var PROSE2 = /^\p{Lu}\p{Ll}/u;
2938
+ var NOT_PROSE2 = /[/\\_=<>{}@#|]/;
2939
+ var EXTENSIONS3 = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js", "/index.jsx"];
2940
+ var ALIASES = /^[@~]\//;
2941
+ var unwrap = (node) => node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" ? unwrap(node.expression) : node;
2942
+ var children = (node) => Object.entries(node).filter(([key]) => !SKIP8.has(key));
2943
+ function visit(node, ancestors, fn) {
2944
+ if (Array.isArray(node)) return node.forEach((child) => visit(child, ancestors, fn));
2945
+ if (!node || typeof node !== "object") return;
2946
+ const n = node;
2947
+ if (typeof n.type === "string") fn(n, ancestors);
2948
+ const next = typeof n.type === "string" ? [...ancestors, n] : ancestors;
2949
+ for (const [, child] of children(n)) visit(child, next, fn);
2950
+ }
2951
+ var keyName = (property) => {
2952
+ const key = property.key;
2953
+ if (property.computed) return null;
2954
+ return key.type === "Identifier" ? String(key.name) : key.type === "StringLiteral" ? String(key.value) : null;
2955
+ };
2956
+ var isProse = (value) => PROSE2.test(value) && !NOT_PROSE2.test(value);
2957
+ function textProperty(property, parent) {
2958
+ const key = keyName(property);
2959
+ if (key === null) return false;
2960
+ if (TEXT_KEYS.has(key)) return true;
2961
+ if (key !== "name" || parent?.type !== "ObjectExpression") return false;
2962
+ return (parent.properties ?? []).some((p) => p.type === "ObjectProperty" && NAV_KEYS.has(keyName(p) ?? ""));
2963
+ }
2964
+ function textsIn(init, reserved) {
2965
+ const strings = [];
2966
+ let clash = false;
2967
+ visit(init, [], (node, above) => {
2968
+ if (FUNCTIONS.has(node.type) && scopeNames(node).includes("t")) clash = true;
2969
+ if (node.type !== "ObjectProperty" || above.some((a) => FUNCTIONS.has(a.type) || a.type?.startsWith("JSX"))) return;
2970
+ const value = node.value;
2971
+ if (value.type === "StringLiteral" && isProse(String(value.value)) && !reserved.has(String(value.value)) && textProperty(node, above[above.length - 1])) strings.push(value);
2972
+ });
2973
+ return clash ? null : strings;
2974
+ }
2975
+ function dataIn(module, reserved) {
2976
+ const found = [];
2977
+ for (const statement of module.program.body ?? []) {
2978
+ const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
2979
+ if (declaration?.type !== "VariableDeclaration" || declaration.kind !== "const") continue;
2980
+ for (const declarator of declaration.declarations ?? []) {
2981
+ const id = declarator.id;
2982
+ const init = declarator.init;
2983
+ if (id.type !== "Identifier" || !init || RESERVED.has(String(id.name))) continue;
2984
+ if (!["ArrayExpression", "ObjectExpression"].includes(unwrap(init).type)) continue;
2985
+ const strings = textsIn(init, reserved);
2986
+ if (strings?.length) found.push({ module, name: String(id.name), declarator, init, strings });
2987
+ }
2988
+ }
2989
+ return found;
2990
+ }
2991
+ function literalTypes(modules) {
2992
+ const found = /* @__PURE__ */ new Set();
2993
+ for (const module of modules) {
2994
+ visit(module.program, [], (node) => {
2995
+ const literal = node.type === "TSLiteralType" ? node.literal : null;
2996
+ if (literal?.type === "StringLiteral") found.add(String(literal.value));
2997
+ });
2998
+ }
2999
+ return found;
3000
+ }
3001
+ function resolve3(from, source, known) {
3002
+ const bases = source.startsWith(".") ? [join8(dirname4(from), source)] : ALIASES.test(source) ? [source.slice(2), join8("src", source.slice(2))] : [];
3003
+ for (const base of bases) {
3004
+ for (const extension of EXTENSIONS3) {
3005
+ const path = normalize(`${base}${extension}`);
3006
+ if (known.has(path)) return path;
3007
+ }
3008
+ }
3009
+ return null;
3010
+ }
3011
+ function opaque(module, known) {
3012
+ const paths = [];
3013
+ for (const statement of module.program.body ?? []) {
3014
+ const source = statement.source;
3015
+ if (!source) continue;
3016
+ const path = resolve3(module.path, String(source.value), known);
3017
+ if (!path) continue;
3018
+ const star = (statement.specifiers ?? []).some((s) => s.type === "ImportNamespaceSpecifier");
3019
+ if (statement.type !== "ImportDeclaration" || star) paths.push(path);
3020
+ }
3021
+ return paths;
3022
+ }
3023
+ function usesOf(module, local, skip, lib) {
3024
+ const uses = [];
3025
+ let blocked = false;
3026
+ visit(module.program, [], (node, above) => {
3027
+ if (blocked || node.type !== "Identifier" || node.name !== local || node === skip) return;
3028
+ const parent = above[above.length - 1];
3029
+ if (parent.type?.startsWith("Import") || parent.type === "TSTypeReference" || parent.type === "TSQualifiedName") return;
3030
+ if (parent.type === "MemberExpression" && parent.property === node && !parent.computed) return;
3031
+ if (parent.type === "OptionalMemberExpression" && parent.property === node && !parent.computed) return;
3032
+ if (parent.type === "ObjectProperty" && parent.key === node && !parent.computed && !parent.shorthand) return;
3033
+ const inner = above.slice(1);
3034
+ const shadowed = inner.some((a) => scopeNames(a).includes(local));
3035
+ const component = componentOf(above);
3036
+ const stuck = parent.type === "TSTypeQuery" || parent.type?.startsWith("Export") || Boolean(parent.shorthand);
3037
+ const inParams = component !== null && node.start < component.body.start;
3038
+ if (shadowed || stuck || inParams || !component || tInScope(inner) || lib === "react-i18next" && component.async) blocked = true;
3039
+ else uses.push({ module, node, local, component });
3040
+ });
3041
+ return blocked ? null : uses;
3042
+ }
3043
+ function allUses(data, modules, known, lib) {
3044
+ const own = usesOf(data.module, data.name, data.declarator.id, lib);
3045
+ if (!own) return null;
3046
+ const uses = [...own];
3047
+ for (const module of modules) {
3048
+ if (module === data.module) continue;
3049
+ for (const statement of module.program.body ?? []) {
3050
+ if (statement.type !== "ImportDeclaration" || resolve3(module.path, String(statement.source.value), known) !== data.module.path) continue;
3051
+ const specifier2 = (statement.specifiers ?? []).find((s) => s.type === "ImportSpecifier" && s.imported.name === data.name);
3052
+ if (!specifier2) continue;
3053
+ const found = usesOf(module, String(specifier2.local.name), null, lib);
3054
+ if (!found) return null;
3055
+ uses.push(...found);
3056
+ }
3057
+ }
3058
+ return uses.length ? uses : null;
3059
+ }
3060
+ function parseModule({ path, text }) {
3061
+ if (text.includes("use server")) return null;
3062
+ const outcome = parseSourceOutcome7(path, text, path);
3063
+ const block = "file" in outcome ? outcome.file.js[0] : void 0;
3064
+ return block ? { path, text, base: block.offset, program: block.ast.program } : null;
3065
+ }
3066
+ function keyFor2(keys, stem, en) {
3067
+ const known = keys.byText.get(`${stem}
3068
+ ${en}`);
3069
+ if (known) return { key: known, isNew: false };
3070
+ const base = `${stem}.${keyWords(en)}`;
3071
+ let key = base;
3072
+ for (let n = 2; keys.used.has(key); n++) key = `${base}-${n}`;
3073
+ keys.used.add(key);
3074
+ keys.byText.set(`${stem}
3075
+ ${en}`, key);
3076
+ return { key, isNew: true };
3077
+ }
3078
+ function dataEdits(data, keys, entries) {
3079
+ const { module, init, declarator, strings } = data;
3080
+ const { base, text, path } = module;
3081
+ const annotation = declarator.id.typeAnnotation;
3082
+ const returns = annotation ? text.slice(base + annotation.start, base + annotation.end) : "";
3083
+ const edits2 = [
3084
+ { start: base + init.start, end: base + init.start, text: `(t: (key: string) => string)${returns} => (` },
3085
+ { start: base + init.end, end: base + init.end, text: ")" },
3086
+ ...annotation ? [{ start: base + annotation.start, end: base + annotation.end, text: "" }] : []
3087
+ ];
3088
+ const stem = stemOf(path);
3089
+ for (const literal of strings) {
3090
+ const en = String(literal.value);
3091
+ const { key, isNew } = keyFor2(keys, stem, en);
3092
+ if (isNew) entries.push({ key, en, ar: "", status: "pending", file: path, line: lineOf(text, base + literal.start) });
3093
+ edits2.push({ start: base + literal.start, end: base + literal.end, text: `t('${key}')` });
3094
+ }
3095
+ return edits2;
3096
+ }
3097
+ function liftData(inputs, lib, taken) {
3098
+ const modules = inputs.map(parseModule).filter((m) => m !== null);
3099
+ const known = new Set(inputs.map((i) => normalize(i.path)));
3100
+ const barred = new Set(modules.flatMap((m) => opaque(m, known)));
3101
+ const reserved = literalTypes(modules);
3102
+ const edits2 = /* @__PURE__ */ new Map();
3103
+ const components = /* @__PURE__ */ new Map();
3104
+ const entries = [];
3105
+ const keys = { used: new Set(taken), byText: /* @__PURE__ */ new Map() };
3106
+ const push = (module, more) => {
3107
+ edits2.set(module, [...edits2.get(module) ?? [], ...more]);
3108
+ };
3109
+ let moved = 0;
3110
+ for (const data of modules.filter((m) => !barred.has(normalize(m.path))).flatMap((m) => dataIn(m, reserved))) {
3111
+ const uses = allUses(data, modules, known, lib);
3112
+ if (!uses) continue;
3113
+ push(data.module, dataEdits(data, keys, entries));
3114
+ moved += data.strings.length;
3115
+ for (const use of uses) {
3116
+ push(use.module, [{ start: use.module.base + use.node.end, end: use.module.base + use.node.end, text: "(t)" }]);
3117
+ components.set(use.module, (components.get(use.module) ?? /* @__PURE__ */ new Set()).add(use.component));
3118
+ }
3119
+ }
3120
+ for (const [module, found] of components) {
3121
+ const style = styleOf(module.program, module.text, module.base);
3122
+ const hooks = [...found].map((fn) => hookEdits(fn, lib, module.text, module.base, style));
3123
+ push(module, [...hooks.flatMap((h) => h.edits), ...importEdits(module.program, hooks.flatMap((h) => h.imports), module.base, style)]);
3124
+ }
3125
+ const texts = new Map([...edits2].map(([module, list]) => [module.path, applyEdits(module.text, list)]));
3126
+ return { texts, entries, moved };
3127
+ }
3128
+
2930
3129
  // src/rollback.ts
2931
3130
  import { rmSync } from "fs";
2932
- import { join as join8 } from "path";
3131
+ import { join as join9 } from "path";
2933
3132
  var ConvertFailedError = class extends Error {
2934
3133
  constructor(message, cause) {
2935
3134
  super(message);
@@ -2948,7 +3147,7 @@ async function rollBack(target, step, error) {
2948
3147
  const failed = `convert failed at ${step}: ${reasonOf(error)}.`;
2949
3148
  try {
2950
3149
  await resetHard(target.root);
2951
- for (const path of [...target.created].reverse()) rmSync(join8(target.root, path), { recursive: true, force: true });
3150
+ for (const path of [...target.created].reverse()) rmSync(join9(target.root, path), { recursive: true, force: true });
2952
3151
  if (target.madeBranch) {
2953
3152
  await checkout(target.root, target.base);
2954
3153
  await deleteBranch(target.root, target.branch);
@@ -3037,9 +3236,12 @@ function transformAll(project, generated, taken) {
3037
3236
  css: physicalContext(root)
3038
3237
  };
3039
3238
  const out = { texts: /* @__PURE__ */ new Map(), edits: 0, byKind: { tailwind: 0, css: 0, markup: 0, strings: 0, formats: 0 }, perFile: /* @__PURE__ */ new Map(), flags: [], entries: [], skipped: [...walked.skipped] };
3239
+ const lifted = lift(project, walked.files.filter((path) => !generated.has(path)), ctx.lib, used);
3240
+ out.entries.push(...lifted.entries);
3241
+ for (const entry of lifted.entries) used.add(entry.key);
3040
3242
  for (const path of walked.files) {
3041
3243
  if (generated.has(path)) continue;
3042
- const raw = readText(root, path);
3244
+ const raw = lifted.texts.get(path) ?? readText(root, path);
3043
3245
  if (raw === null) {
3044
3246
  out.skipped.push({ file: path, reason: "unreadable" });
3045
3247
  continue;
@@ -3048,7 +3250,7 @@ function transformAll(project, generated, taken) {
3048
3250
  out.skipped.push({ file: path, reason: "minified" });
3049
3251
  continue;
3050
3252
  }
3051
- const result = runPasses(path, raw, join9(root, path), { ...ctx, taken: used });
3253
+ const result = runPasses(path, raw, join10(root, path), { ...ctx, taken: used });
3052
3254
  if (result.kind === "skipped") {
3053
3255
  out.skipped.push({ file: path, reason: result.reason });
3054
3256
  continue;
@@ -3056,7 +3258,7 @@ function transformAll(project, generated, taken) {
3056
3258
  out.flags.push(...result.flags);
3057
3259
  out.entries.push(...result.entries);
3058
3260
  for (const entry of result.entries) used.add(entry.key);
3059
- if (result.edits === 0) continue;
3261
+ if (result.edits === 0 && !lifted.texts.has(path)) continue;
3060
3262
  out.edits += result.edits;
3061
3263
  for (const kind of Object.keys(out.byKind)) out.byKind[kind] += result.byKind[kind];
3062
3264
  out.perFile.set(path, result.edits);
@@ -3064,6 +3266,13 @@ function transformAll(project, generated, taken) {
3064
3266
  }
3065
3267
  return out;
3066
3268
  }
3269
+ function lift(project, paths, lib, taken) {
3270
+ const inputs = paths.flatMap((path) => {
3271
+ const text = /\.[jt]sx?$/.test(path) ? readText(project.root, path) : null;
3272
+ return text !== null && !text.startsWith(BOM) && !isMinified(text) ? [{ path, text }] : [];
3273
+ });
3274
+ return liftData(inputs, lib, taken);
3275
+ }
3067
3276
  function changedFiles(perFile, moved, wiring) {
3068
3277
  const total = /* @__PURE__ */ new Map();
3069
3278
  const add = (file, n) => {
@@ -3153,7 +3362,7 @@ async function convert(options, ioIn) {
3153
3362
 
3154
3363
  // src/finish.ts
3155
3364
  import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
3156
- import { join as join11 } from "path";
3365
+ import { join as join12 } from "path";
3157
3366
 
3158
3367
  // src/match.ts
3159
3368
  import { PNG } from "pngjs";
@@ -3184,7 +3393,7 @@ function compare(a, b) {
3184
3393
 
3185
3394
  // src/finish-undo.ts
3186
3395
  import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
3187
- import { join as join10 } from "path";
3396
+ import { join as join11 } from "path";
3188
3397
  async function snapshot(root, paths) {
3189
3398
  const clean = new Set(await cleanAtHead(root, paths));
3190
3399
  return paths.map((path) => ({ path, clean: clean.has(path), text: clean.has(path) ? null : readText(root, path) }));
@@ -3193,8 +3402,8 @@ async function undo(root, saved, staged) {
3193
3402
  await restoreFromHead(root, saved.filter((s) => s.clean).map((s) => s.path));
3194
3403
  const dirty = saved.filter((s) => !s.clean);
3195
3404
  for (const { path, text } of dirty) {
3196
- if (text === null) rmSync2(join10(root, path), { force: true });
3197
- else writeFileSync6(join10(root, path), text);
3405
+ if (text === null) rmSync2(join11(root, path), { force: true });
3406
+ else writeFileSync6(join11(root, path), text);
3198
3407
  }
3199
3408
  await unstage(root, [.../* @__PURE__ */ new Set([...dirty.map((s) => s.path), ...staged])]);
3200
3409
  }
@@ -3373,7 +3582,7 @@ function writeMessages(root, ledger) {
3373
3582
  for (const [path, text] of Object.entries(messagesFiles(ledger))) writeText(disk, path, mergedMessages(root, path, text));
3374
3583
  }
3375
3584
  function mergedMessages(root, path, generated) {
3376
- const full = join11(root, path);
3585
+ const full = join12(root, path);
3377
3586
  if (!existsSync8(full)) return generated;
3378
3587
  try {
3379
3588
  const existing = JSON.parse(readFileSync7(full, "utf8"));
@@ -3388,7 +3597,7 @@ async function englishChecks(root, base, routes, shoot) {
3388
3597
  const noBaseline = [];
3389
3598
  for (const route of routes) {
3390
3599
  for (const width of WIDTHS) {
3391
- const file = join11(root, baselinePath(route, width));
3600
+ const file = join12(root, baselinePath(route, width));
3392
3601
  if (!existsSync8(file)) {
3393
3602
  noBaseline.push({ route, width });
3394
3603
  continue;
@@ -3398,8 +3607,8 @@ async function englishChecks(root, base, routes, shoot) {
3398
3607
  let diffPath;
3399
3608
  if (ratio >= MAX_DIFF_RATIO) {
3400
3609
  diffPath = `${DIFF_DIR}/${routeSlug(route)}-${width}.png`;
3401
- mkdirSync5(join11(root, DIFF_DIR), { recursive: true });
3402
- writeFileSync7(join11(root, diffPath), diff);
3610
+ mkdirSync5(join12(root, DIFF_DIR), { recursive: true });
3611
+ writeFileSync7(join12(root, diffPath), diff);
3403
3612
  }
3404
3613
  english2.push({ route, width, ratio, ...diffPath ? { diffPath } : {} });
3405
3614
  }
@@ -3420,7 +3629,7 @@ async function arabicChecks(project, state, base, routes, audit) {
3420
3629
  return arabic2;
3421
3630
  }
3422
3631
  function closedAtBaseline(root) {
3423
- const path = join11(root, CLOSED_FILE);
3632
+ const path = join12(root, CLOSED_FILE);
3424
3633
  if (!existsSync8(path)) return /* @__PURE__ */ new Set();
3425
3634
  try {
3426
3635
  const routes = JSON.parse(readFileSync7(path, "utf8"));
@@ -3490,17 +3699,17 @@ async function run(ready, step) {
3490
3699
  remainingLint,
3491
3700
  ...tests2 ? { tests: { code: tests2.code, tail: tail(tests2.out) } } : {}
3492
3701
  };
3493
- writeFileSync7(join11(root, REPORT_FILE), renderConvertReport(data));
3702
+ writeFileSync7(join12(root, REPORT_FILE), renderConvertReport(data));
3494
3703
  step.name = "saving the state";
3495
3704
  saveState(root, { ...state, phase: "finished" });
3496
3705
  step.name = "saving your answers";
3497
3706
  if (options.configAnswers) writeConfigAnswers({ root, created: [] }, options.configAnswers, configFile);
3498
3707
  step.name = "committing";
3499
- await stage(root, own.filter((p) => (p !== configFile || options.configAnswers) && existsSync8(join11(root, p))));
3708
+ await stage(root, own.filter((p) => (p !== configFile || options.configAnswers) && existsSync8(join12(root, p))));
3500
3709
  if ((await stagedFiles(root)).length) await commit(root, `dualkit: finish ${state.mode} conversion`);
3501
3710
  const checked = checks.arabic.length > 0;
3502
3711
  const passed = checked && checks.english.every((p) => p.ratio < MAX_DIFF_RATIO) && checks.arabic.every((p) => p.errors === 0) && (tests2?.code ?? 0) === 0;
3503
- return { reportPath: join11(root, REPORT_FILE), passed };
3712
+ return { reportPath: join12(root, REPORT_FILE), passed };
3504
3713
  }
3505
3714
  async function finish(options, ioIn) {
3506
3715
  const ready = await prepare(options, ioIn);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dualkit/convert",
3
- "version": "0.4.0-beta.0",
3
+ "version": "0.5.0-beta.0",
4
4
  "description": "Converts an English React or Next.js app to Arabic on a new git branch.",
5
5
  "license": "MIT",
6
6
  "type": "module",