@dualkit/convert 0.3.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.
- package/dist/index.cjs +327 -39
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +324 -36
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -151,6 +151,11 @@ function detectPackageManager(root) {
|
|
|
151
151
|
if (top || (0, import_node_path.dirname)(dir) === dir) return "npm";
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
+
var I18N_LIBRARIES = ["next-intl", "react-i18next", "i18next", "next-i18next", "react-intl", "@lingui/core", "next-international", "@inlang/paraglide-js"];
|
|
155
|
+
function existingI18n(root) {
|
|
156
|
+
const deps = readDeps(root);
|
|
157
|
+
return I18N_LIBRARIES.find((name) => name in deps) ?? null;
|
|
158
|
+
}
|
|
154
159
|
function detectI18n(deps) {
|
|
155
160
|
if ("next-intl" in deps) return "next-intl";
|
|
156
161
|
if ("react-i18next" in deps) return "react-i18next";
|
|
@@ -172,8 +177,8 @@ function detectProject(root) {
|
|
|
172
177
|
}
|
|
173
178
|
|
|
174
179
|
// src/convert.ts
|
|
175
|
-
var
|
|
176
|
-
var
|
|
180
|
+
var import_lint13 = require("@dualkit/lint");
|
|
181
|
+
var import_node_path18 = require("path");
|
|
177
182
|
|
|
178
183
|
// src/apply.ts
|
|
179
184
|
var import_lint2 = require("@dualkit/lint");
|
|
@@ -269,11 +274,11 @@ var RefusedError = class extends Error {
|
|
|
269
274
|
};
|
|
270
275
|
var NO_GIT = "git is not installed or not on PATH";
|
|
271
276
|
function git(cwd, args) {
|
|
272
|
-
return new Promise((
|
|
277
|
+
return new Promise((resolve4, reject) => {
|
|
273
278
|
(0, import_node_child_process.execFile)("git", ["--literal-pathspecs", ...args], { cwd, maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
274
279
|
if (error && error.code === "ENOENT") return reject(new Error((0, import_node_fs3.existsSync)(cwd) ? NO_GIT : `The folder ${cwd} does not exist.`));
|
|
275
280
|
const code = error ? typeof error.code === "number" ? error.code : 1 : 0;
|
|
276
|
-
|
|
281
|
+
resolve4({ code, out: `${stdout}${stderr}`.trim(), stdout });
|
|
277
282
|
});
|
|
278
283
|
});
|
|
279
284
|
}
|
|
@@ -327,7 +332,7 @@ async function stagedFiles(root) {
|
|
|
327
332
|
return out.split("\0").filter(Boolean);
|
|
328
333
|
}
|
|
329
334
|
async function commit(root, message) {
|
|
330
|
-
await must(root, ["commit", "-q", "-m", message]);
|
|
335
|
+
await must(root, ["commit", "-q", "--no-verify", "-m", message]);
|
|
331
336
|
}
|
|
332
337
|
async function trackedFiles(root) {
|
|
333
338
|
const out = await must(root, ["ls-files", "-z"]);
|
|
@@ -562,6 +567,26 @@ function writeConfigAnswers(disk, answers2, file = CONFIG_FILE) {
|
|
|
562
567
|
return writeText(disk, file, mergedConfigText(readText(disk.root, file), answers2, file));
|
|
563
568
|
}
|
|
564
569
|
|
|
570
|
+
// src/closed-routes.ts
|
|
571
|
+
var CLOSED = /* @__PURE__ */ new Set([401, 403, 404]);
|
|
572
|
+
var STATUS_TIMEOUT_MS = 6e4;
|
|
573
|
+
var closedStatus = async (url) => {
|
|
574
|
+
try {
|
|
575
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(STATUS_TIMEOUT_MS), redirect: "manual" });
|
|
576
|
+
return CLOSED.has(res.status) ? res.status : null;
|
|
577
|
+
} catch {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
async function closedRoutes(base, routes) {
|
|
582
|
+
const found = [];
|
|
583
|
+
for (const route of routes) {
|
|
584
|
+
const status = await closedStatus(`${base}${route}`);
|
|
585
|
+
if (status) found.push({ route, reason: `the English page returns ${status}, so its Arabic page was not checked` });
|
|
586
|
+
}
|
|
587
|
+
return found;
|
|
588
|
+
}
|
|
589
|
+
|
|
565
590
|
// src/baseline.ts
|
|
566
591
|
var import_node_fs8 = require("fs");
|
|
567
592
|
var import_node_path9 = require("path");
|
|
@@ -619,15 +644,15 @@ var ServerError = class extends Error {
|
|
|
619
644
|
var OUTPUT_LINES = 20;
|
|
620
645
|
var POLL_MS = 250;
|
|
621
646
|
var KILL_GRACE_MS = 5e3;
|
|
622
|
-
var sleep = (ms) => new Promise((
|
|
647
|
+
var sleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
623
648
|
function portTaken(port) {
|
|
624
|
-
return new Promise((
|
|
649
|
+
return new Promise((resolve4) => {
|
|
625
650
|
const socket = (0, import_node_net.connect)({ port, host: "localhost" });
|
|
626
651
|
socket.once("connect", () => {
|
|
627
652
|
socket.destroy();
|
|
628
|
-
|
|
653
|
+
resolve4(true);
|
|
629
654
|
});
|
|
630
|
-
socket.once("error", () =>
|
|
655
|
+
socket.once("error", () => resolve4(false));
|
|
631
656
|
});
|
|
632
657
|
}
|
|
633
658
|
async function answers(url) {
|
|
@@ -667,7 +692,7 @@ async function startServer(cmd, port, cwd, timeoutMs = 12e4) {
|
|
|
667
692
|
});
|
|
668
693
|
const pid = child.pid;
|
|
669
694
|
if (pid === void 0) {
|
|
670
|
-
await new Promise((
|
|
695
|
+
await new Promise((resolve4) => setImmediate(resolve4));
|
|
671
696
|
throw new ServerError(`Could not start "${cmd}": ${spawnError?.message ?? "no process id"}`);
|
|
672
697
|
}
|
|
673
698
|
const collect = (chunk) => {
|
|
@@ -706,6 +731,7 @@ ${tail2()}`);
|
|
|
706
731
|
|
|
707
732
|
// src/baseline.ts
|
|
708
733
|
var BASELINE_DIR = `${STATE_DIR}/baseline`;
|
|
734
|
+
var CLOSED_FILE = `${BASELINE_DIR}/closed.json`;
|
|
709
735
|
var WIDTHS = [390, 1280];
|
|
710
736
|
var DEFAULT_PORTS = { "next-app": 3e3, "vite-react": 5173 };
|
|
711
737
|
var routeSlug = (route) => route.replace(/^\/+|\/+$/g, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "") || "home";
|
|
@@ -717,6 +743,7 @@ async function shootBaseline(project, config, shoot, log) {
|
|
|
717
743
|
const server = await startServer(config.start, port, project.root);
|
|
718
744
|
const shots = [];
|
|
719
745
|
try {
|
|
746
|
+
(0, import_node_fs8.writeFileSync)((0, import_node_path9.join)(project.root, CLOSED_FILE), JSON.stringify((await closedRoutes(server.url, routes)).map((c) => c.route)));
|
|
720
747
|
for (const route of routes) {
|
|
721
748
|
for (const width of WIDTHS) {
|
|
722
749
|
const path = baselinePath(route, width);
|
|
@@ -1093,6 +1120,7 @@ function pieceOf(child, ctx) {
|
|
|
1093
1120
|
const source = ctx.file.text.slice(ctx.base + expression.start, ctx.base + expression.end);
|
|
1094
1121
|
return { kind: "value", name, source: mayBeEmpty(expression) ? `${source} ?? ''` : source, start: start2, end };
|
|
1095
1122
|
}
|
|
1123
|
+
var OWN_DIRECTION = /setAttribute\(\s*['"]dir['"]|documentElement\.dir\s*=[^=]/;
|
|
1096
1124
|
var NODE_NAMES = /^(children|icon|avatar|logo|image|element|node)$/i;
|
|
1097
1125
|
var mayBeEmpty = (expression) => /^Optional/.test(expression.type) || expression.type === "CallExpression" && !textMethodTarget(expression);
|
|
1098
1126
|
var jsxText = (value) => value.replace(/^\s*\n\s*/, "").replace(/\s*\n\s*$/, "");
|
|
@@ -1342,6 +1370,15 @@ function extractStrings(file, lib, taken) {
|
|
|
1342
1370
|
for (const block of file.js) {
|
|
1343
1371
|
const program = block.ast.program;
|
|
1344
1372
|
if (isServerFile(program)) continue;
|
|
1373
|
+
if (OWN_DIRECTION.test(file.text.slice(block.offset))) {
|
|
1374
|
+
flag(
|
|
1375
|
+
{ file, flags },
|
|
1376
|
+
block.offset,
|
|
1377
|
+
"unsupported",
|
|
1378
|
+
"This file sets the page direction itself, which can clash with the Arabic layout.",
|
|
1379
|
+
"Check the Arabic pages, and drive this direction from the locale so components read it too."
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1345
1382
|
const ctx = { file, base: block.offset, lib, stem: stemOf(file.path), used, byText: /* @__PURE__ */ new Map(), entries, edits: edits2, flags, components: /* @__PURE__ */ new Set(), sawJsx: false, consumed: /* @__PURE__ */ new Set(), usedTrans: false };
|
|
1346
1383
|
walk(program, ctx, []);
|
|
1347
1384
|
if (ctx.sawJsx) flagModuleStrings(program, ctx);
|
|
@@ -1618,12 +1655,12 @@ function routeMoves(appDir, paths) {
|
|
|
1618
1655
|
}
|
|
1619
1656
|
return { moves, keptPages };
|
|
1620
1657
|
}
|
|
1621
|
-
function eachNode(node,
|
|
1622
|
-
if (Array.isArray(node)) return node.forEach((child) => eachNode(child,
|
|
1658
|
+
function eachNode(node, visit2) {
|
|
1659
|
+
if (Array.isArray(node)) return node.forEach((child) => eachNode(child, visit2));
|
|
1623
1660
|
if (!node || typeof node !== "object") return;
|
|
1624
1661
|
const n = node;
|
|
1625
|
-
if (typeof n.type === "string")
|
|
1626
|
-
for (const [key, child] of Object.entries(n)) if (!SKIP3.has(key)) eachNode(child,
|
|
1662
|
+
if (typeof n.type === "string") visit2(n);
|
|
1663
|
+
for (const [key, child] of Object.entries(n)) if (!SKIP3.has(key)) eachNode(child, visit2);
|
|
1627
1664
|
}
|
|
1628
1665
|
var RELATIVE = /^\.\.?\//;
|
|
1629
1666
|
var stringLiteral = (node) => node?.type === "StringLiteral" ? node : null;
|
|
@@ -2096,7 +2133,7 @@ export function DualkitLanguageSwitch() {
|
|
|
2096
2133
|
)
|
|
2097
2134
|
}
|
|
2098
2135
|
`;
|
|
2099
|
-
var INLINE_SCRIPT = `<script>(function(){var m=document.cookie.match(${COOKIE_PATTERN});var l=m&&m[1]==='ar'?'ar':'en';var h=document.documentElement;h.lang=l;h.dir=l==='ar'?'rtl':'ltr'})()</script>`;
|
|
2136
|
+
var INLINE_SCRIPT = `<script>(function(){var m=document.cookie.match(${COOKIE_PATTERN});var l=m&&m[1]==='ar'?'ar':'en';var h=document.documentElement;h.lang=l;h.dir=l==='ar'?'rtl':'ltr';if(l==='ar')new MutationObserver(function(){if(h.dir!=='rtl')h.dir='rtl'}).observe(h,{attributes:true,attributeFilter:['dir']})})()</script>`;
|
|
2100
2137
|
function htmlEdits2(html, mode) {
|
|
2101
2138
|
if (mode === "bilingual") {
|
|
2102
2139
|
if (html.includes(LOCALE_COOKIE)) return [];
|
|
@@ -2692,12 +2729,27 @@ function visitExpression(container, ancestors, ctx) {
|
|
|
2692
2729
|
ctx.edits.push({ start: ctx.base + container.start, end: ctx.base + container.start, text: "<bdi>" });
|
|
2693
2730
|
ctx.edits.push({ start: ctx.base + container.end, end: ctx.base + container.end, text: "</bdi>" });
|
|
2694
2731
|
}
|
|
2732
|
+
var SIGNED_NUMBER = /^(?:[+\-\u2212]\d[\d,.]*\s?%?|\d[\d,.]*\s?%)$/;
|
|
2733
|
+
var NO_BDI = /* @__PURE__ */ new Set(["svg", "text", "option", "title", "textarea", "style", "script"]);
|
|
2734
|
+
function visitText(text, ancestors, ctx) {
|
|
2735
|
+
const parent = ancestors[ancestors.length - 1];
|
|
2736
|
+
if (parent.type !== "JSXElement" && parent.type !== "JSXFragment") return;
|
|
2737
|
+
const value = String(text.value);
|
|
2738
|
+
const trimmed = value.trim();
|
|
2739
|
+
if (!SIGNED_NUMBER.test(trimmed)) return;
|
|
2740
|
+
const elements = ancestors.filter((a) => a.type === "JSXElement");
|
|
2741
|
+
if (elements.some((a) => isolates(a) || NO_BDI.has(tagName2(a.openingElement) ?? ""))) return;
|
|
2742
|
+
const start2 = ctx.base + text.start + value.indexOf(trimmed);
|
|
2743
|
+
ctx.edits.push({ start: start2, end: start2, text: "<bdi>" });
|
|
2744
|
+
ctx.edits.push({ start: start2 + trimmed.length, end: start2 + trimmed.length, text: "</bdi>" });
|
|
2745
|
+
}
|
|
2695
2746
|
function walk3(node, ctx, ancestors) {
|
|
2696
2747
|
if (Array.isArray(node)) return node.forEach((child) => walk3(child, ctx, ancestors));
|
|
2697
2748
|
if (!node || typeof node !== "object") return;
|
|
2698
2749
|
const n = node;
|
|
2699
2750
|
if (n.type === "JSXOpeningElement") visitOpening(n, ctx, ancestors);
|
|
2700
2751
|
if (n.type === "JSXExpressionContainer" && ancestors.length) visitExpression(n, ancestors, ctx);
|
|
2752
|
+
if (n.type === "JSXText" && ancestors.length) visitText(n, ancestors, ctx);
|
|
2701
2753
|
const next = typeof n.type === "string" ? [...ancestors, n] : ancestors;
|
|
2702
2754
|
for (const [key, child] of Object.entries(n)) if (!SKIP6.has(key)) walk3(child, ctx, next);
|
|
2703
2755
|
}
|
|
@@ -2839,6 +2891,15 @@ function groups(fragments) {
|
|
|
2839
2891
|
for (const f of fragments) byGroup.set(f.group, [...byGroup.get(f.group) ?? [], f]);
|
|
2840
2892
|
return [...byGroup.values()];
|
|
2841
2893
|
}
|
|
2894
|
+
var TRACKED = /^tracking-(?!normal$)/;
|
|
2895
|
+
function trackingEdits(fragments) {
|
|
2896
|
+
const tokens = tokensOf(fragments);
|
|
2897
|
+
if (tokens.some((t) => t.parsed.core.startsWith("tracking-") && t.parsed.variants.includes("rtl"))) return [];
|
|
2898
|
+
const first = tokens.find((t) => TRACKED.test(t.parsed.core) && KEPT(t));
|
|
2899
|
+
if (!first) return [];
|
|
2900
|
+
const end = first.at + first.text.length;
|
|
2901
|
+
return [{ start: end, end, text: " rtl:tracking-normal" }];
|
|
2902
|
+
}
|
|
2842
2903
|
function tailwindEdits(file, version) {
|
|
2843
2904
|
const parsed = parseVersion(version);
|
|
2844
2905
|
const fragments = (0, import_lint10.classStrings)(file);
|
|
@@ -2851,7 +2912,7 @@ function tailwindEdits(file, version) {
|
|
|
2851
2912
|
return { edits: [], flags: [{ file: file.path, line: lineOf(file.text, first.offset), kind: "unsupported", message, suggestion }] };
|
|
2852
2913
|
}
|
|
2853
2914
|
const sources = groups(fragments);
|
|
2854
|
-
const edits2 = sources.flatMap((g) => [...physicalEdits(g, parsed), ...parsed[0] < 4 ? reverseEdits(g) : []]);
|
|
2915
|
+
const edits2 = sources.flatMap((g) => [...physicalEdits(g, parsed), ...parsed[0] < 4 ? reverseEdits(g) : [], ...trackingEdits(g)]);
|
|
2855
2916
|
const flags = [...sources.flatMap((g) => formFlags(file, g)), ...computedFlags(file)];
|
|
2856
2917
|
return { edits: edits2, flags: flags.sort((a, b) => a.line - b.line) };
|
|
2857
2918
|
}
|
|
@@ -2901,9 +2962,208 @@ function runPasses(path, text, abs, ctx) {
|
|
|
2901
2962
|
return { kind: "done", text: file.text, edits: edits2, byKind, flags, entries };
|
|
2902
2963
|
}
|
|
2903
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
|
+
|
|
2904
3164
|
// src/rollback.ts
|
|
2905
3165
|
var import_node_fs9 = require("fs");
|
|
2906
|
-
var
|
|
3166
|
+
var import_node_path17 = require("path");
|
|
2907
3167
|
var ConvertFailedError = class extends Error {
|
|
2908
3168
|
constructor(message, cause) {
|
|
2909
3169
|
super(message);
|
|
@@ -2922,7 +3182,7 @@ async function rollBack(target, step, error) {
|
|
|
2922
3182
|
const failed = `convert failed at ${step}: ${reasonOf(error)}.`;
|
|
2923
3183
|
try {
|
|
2924
3184
|
await resetHard(target.root);
|
|
2925
|
-
for (const path of [...target.created].reverse()) (0, import_node_fs9.rmSync)((0,
|
|
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 });
|
|
2926
3186
|
if (target.madeBranch) {
|
|
2927
3187
|
await checkout(target.root, target.base);
|
|
2928
3188
|
await deleteBranch(target.root, target.branch);
|
|
@@ -3002,7 +3262,7 @@ var branchFor = (mode) => `dualkit/${mode}`;
|
|
|
3002
3262
|
var problems = (flags) => flags.filter((f) => f.kind !== "info");
|
|
3003
3263
|
function transformAll(project, generated, taken) {
|
|
3004
3264
|
const { root } = project;
|
|
3005
|
-
const walked = (0,
|
|
3265
|
+
const walked = (0, import_lint13.walk)(["."], root);
|
|
3006
3266
|
const used = new Set(taken);
|
|
3007
3267
|
const ctx = {
|
|
3008
3268
|
tailwind: project.tailwind,
|
|
@@ -3011,18 +3271,21 @@ function transformAll(project, generated, taken) {
|
|
|
3011
3271
|
css: physicalContext(root)
|
|
3012
3272
|
};
|
|
3013
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);
|
|
3014
3277
|
for (const path of walked.files) {
|
|
3015
3278
|
if (generated.has(path)) continue;
|
|
3016
|
-
const raw = readText(root, path);
|
|
3279
|
+
const raw = lifted.texts.get(path) ?? readText(root, path);
|
|
3017
3280
|
if (raw === null) {
|
|
3018
3281
|
out.skipped.push({ file: path, reason: "unreadable" });
|
|
3019
3282
|
continue;
|
|
3020
3283
|
}
|
|
3021
|
-
if ((0,
|
|
3284
|
+
if ((0, import_lint13.isMinified)(raw)) {
|
|
3022
3285
|
out.skipped.push({ file: path, reason: "minified" });
|
|
3023
3286
|
continue;
|
|
3024
3287
|
}
|
|
3025
|
-
const result = runPasses(path, raw, (0,
|
|
3288
|
+
const result = runPasses(path, raw, (0, import_node_path18.join)(root, path), { ...ctx, taken: used });
|
|
3026
3289
|
if (result.kind === "skipped") {
|
|
3027
3290
|
out.skipped.push({ file: path, reason: result.reason });
|
|
3028
3291
|
continue;
|
|
@@ -3030,7 +3293,7 @@ function transformAll(project, generated, taken) {
|
|
|
3030
3293
|
out.flags.push(...result.flags);
|
|
3031
3294
|
out.entries.push(...result.entries);
|
|
3032
3295
|
for (const entry of result.entries) used.add(entry.key);
|
|
3033
|
-
if (result.edits === 0) continue;
|
|
3296
|
+
if (result.edits === 0 && !lifted.texts.has(path)) continue;
|
|
3034
3297
|
out.edits += result.edits;
|
|
3035
3298
|
for (const kind of Object.keys(out.byKind)) out.byKind[kind] += result.byKind[kind];
|
|
3036
3299
|
out.perFile.set(path, result.edits);
|
|
@@ -3038,6 +3301,13 @@ function transformAll(project, generated, taken) {
|
|
|
3038
3301
|
}
|
|
3039
3302
|
return out;
|
|
3040
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
|
+
}
|
|
3041
3311
|
function changedFiles(perFile, moved, wiring) {
|
|
3042
3312
|
const total = /* @__PURE__ */ new Map();
|
|
3043
3313
|
const add = (file, n) => {
|
|
@@ -3096,6 +3366,8 @@ async function convert(options, ioIn) {
|
|
|
3096
3366
|
if (options.configAnswers) checkConfigAnswers(root, options.configFile);
|
|
3097
3367
|
const io = options.configAnswers ? { ...ioIn, config: { ...ioIn.config, ...options.configAnswers } } : ioIn;
|
|
3098
3368
|
const project = detectProject(root);
|
|
3369
|
+
const library = rerun ? null : existingI18n(root);
|
|
3370
|
+
if (library) throw new UnsupportedError(`This app already uses ${library}. Convert adds its own translation setup and the two collide, so nothing was changed. Convert supports apps with no translation library yet.`);
|
|
3099
3371
|
const branch = branchFor(mode);
|
|
3100
3372
|
const generated = new Set(state?.generated ?? []);
|
|
3101
3373
|
const existing = readLedger(root);
|
|
@@ -3125,7 +3397,7 @@ async function convert(options, ioIn) {
|
|
|
3125
3397
|
|
|
3126
3398
|
// src/finish.ts
|
|
3127
3399
|
var import_node_fs11 = require("fs");
|
|
3128
|
-
var
|
|
3400
|
+
var import_node_path20 = require("path");
|
|
3129
3401
|
|
|
3130
3402
|
// src/match.ts
|
|
3131
3403
|
var import_pngjs = require("pngjs");
|
|
@@ -3156,7 +3428,7 @@ function compare(a, b) {
|
|
|
3156
3428
|
|
|
3157
3429
|
// src/finish-undo.ts
|
|
3158
3430
|
var import_node_fs10 = require("fs");
|
|
3159
|
-
var
|
|
3431
|
+
var import_node_path19 = require("path");
|
|
3160
3432
|
async function snapshot(root, paths) {
|
|
3161
3433
|
const clean = new Set(await cleanAtHead(root, paths));
|
|
3162
3434
|
return paths.map((path) => ({ path, clean: clean.has(path), text: clean.has(path) ? null : readText(root, path) }));
|
|
@@ -3165,8 +3437,8 @@ async function undo(root, saved, staged) {
|
|
|
3165
3437
|
await restoreFromHead(root, saved.filter((s) => s.clean).map((s) => s.path));
|
|
3166
3438
|
const dirty = saved.filter((s) => !s.clean);
|
|
3167
3439
|
for (const { path, text } of dirty) {
|
|
3168
|
-
if (text === null) (0, import_node_fs10.rmSync)((0,
|
|
3169
|
-
else (0, import_node_fs10.writeFileSync)((0,
|
|
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);
|
|
3170
3442
|
}
|
|
3171
3443
|
await unstage(root, [.../* @__PURE__ */ new Set([...dirty.map((s) => s.path), ...staged])]);
|
|
3172
3444
|
}
|
|
@@ -3269,7 +3541,8 @@ function english(data) {
|
|
|
3269
3541
|
}
|
|
3270
3542
|
function arabic(data) {
|
|
3271
3543
|
const lines = data.arabic.map(
|
|
3272
|
-
(p) => !Number.isFinite(p.errors) ? `- ${esc(p.route)}: the check returned no result` : `- ${esc(p.route)}: score ${p.score}, ${plural(p.errors, "error", "errors")}, ${plural(p.warnings, "warning", "warnings")}`
|
|
3544
|
+
(p) => !Number.isFinite(p.errors) ? `- ${esc(p.route)}: ${esc(p.problems?.[0] ?? "the check returned no result")}` : `- ${esc(p.route)}: score ${p.score}, ${plural(p.errors, "error", "errors")}, ${plural(p.warnings, "warning", "warnings")}${(p.problems ?? []).map((m) => `
|
|
3545
|
+
- ${esc(m)}`).join("")}`
|
|
3273
3546
|
);
|
|
3274
3547
|
return section("Arabic pages", lines, "No pages were checked.");
|
|
3275
3548
|
}
|
|
@@ -3344,7 +3617,7 @@ function writeMessages(root, ledger) {
|
|
|
3344
3617
|
for (const [path, text] of Object.entries(messagesFiles(ledger))) writeText(disk, path, mergedMessages(root, path, text));
|
|
3345
3618
|
}
|
|
3346
3619
|
function mergedMessages(root, path, generated) {
|
|
3347
|
-
const full = (0,
|
|
3620
|
+
const full = (0, import_node_path20.join)(root, path);
|
|
3348
3621
|
if (!(0, import_node_fs11.existsSync)(full)) return generated;
|
|
3349
3622
|
try {
|
|
3350
3623
|
const existing = JSON.parse((0, import_node_fs11.readFileSync)(full, "utf8"));
|
|
@@ -3359,7 +3632,7 @@ async function englishChecks(root, base, routes, shoot) {
|
|
|
3359
3632
|
const noBaseline = [];
|
|
3360
3633
|
for (const route of routes) {
|
|
3361
3634
|
for (const width of WIDTHS) {
|
|
3362
|
-
const file = (0,
|
|
3635
|
+
const file = (0, import_node_path20.join)(root, baselinePath(route, width));
|
|
3363
3636
|
if (!(0, import_node_fs11.existsSync)(file)) {
|
|
3364
3637
|
noBaseline.push({ route, width });
|
|
3365
3638
|
continue;
|
|
@@ -3369,8 +3642,8 @@ async function englishChecks(root, base, routes, shoot) {
|
|
|
3369
3642
|
let diffPath;
|
|
3370
3643
|
if (ratio >= MAX_DIFF_RATIO) {
|
|
3371
3644
|
diffPath = `${DIFF_DIR}/${routeSlug(route)}-${width}.png`;
|
|
3372
|
-
(0, import_node_fs11.mkdirSync)((0,
|
|
3373
|
-
(0, import_node_fs11.writeFileSync)((0,
|
|
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);
|
|
3374
3647
|
}
|
|
3375
3648
|
english2.push({ route, width, ratio, ...diffPath ? { diffPath } : {} });
|
|
3376
3649
|
}
|
|
@@ -3385,11 +3658,21 @@ async function arabicChecks(project, state, base, routes, audit) {
|
|
|
3385
3658
|
const result = await audit(`${base}${path}`, width, options);
|
|
3386
3659
|
const usable = result && Number.isFinite(result.errors);
|
|
3387
3660
|
const failed = { score: Number.NaN, errors: Number.NaN, warnings: Number.NaN };
|
|
3388
|
-
arabic2.push({ route: `${path} at ${width}px`, ...usable ? result : failed });
|
|
3661
|
+
arabic2.push({ route: `${path} at ${width}px`, ...usable ? result : { ...failed, ...result?.problems ? { problems: result.problems } : {} } });
|
|
3389
3662
|
}
|
|
3390
3663
|
}
|
|
3391
3664
|
return arabic2;
|
|
3392
3665
|
}
|
|
3666
|
+
function closedAtBaseline(root) {
|
|
3667
|
+
const path = (0, import_node_path20.join)(root, CLOSED_FILE);
|
|
3668
|
+
if (!(0, import_node_fs11.existsSync)(path)) return /* @__PURE__ */ new Set();
|
|
3669
|
+
try {
|
|
3670
|
+
const routes = JSON.parse((0, import_node_fs11.readFileSync)(path, "utf8"));
|
|
3671
|
+
return new Set(Array.isArray(routes) ? routes.filter((r) => typeof r === "string") : []);
|
|
3672
|
+
} catch {
|
|
3673
|
+
return /* @__PURE__ */ new Set();
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3393
3676
|
async function runChecks(project, state, routes, { config, shoot, audit }, step) {
|
|
3394
3677
|
const { start: cmd } = config;
|
|
3395
3678
|
if (!cmd || !shoot) throw new RefusedError(NEEDS_BROWSER);
|
|
@@ -3400,8 +3683,13 @@ async function runChecks(project, state, routes, { config, shoot, audit }, step)
|
|
|
3400
3683
|
});
|
|
3401
3684
|
try {
|
|
3402
3685
|
step.name = "checking the pages";
|
|
3403
|
-
const
|
|
3404
|
-
|
|
3686
|
+
const closed = await closedRoutes(server.url, routes);
|
|
3687
|
+
const shut = new Set(closed.map((c) => c.route));
|
|
3688
|
+
const open = routes.filter((route) => !shut.has(route));
|
|
3689
|
+
const before = closedAtBaseline(project.root);
|
|
3690
|
+
const compared = routes.filter((route) => !(shut.has(route) && before.has(route)));
|
|
3691
|
+
const english2 = state.mode === "bilingual" ? await englishChecks(project.root, server.url, compared, shoot) : { english: [], noBaseline: [] };
|
|
3692
|
+
return { ...english2, closed, arabic: await arabicChecks(project, state, server.url, open, audit) };
|
|
3405
3693
|
} finally {
|
|
3406
3694
|
await server.stop();
|
|
3407
3695
|
}
|
|
@@ -3442,21 +3730,21 @@ async function run(ready, step) {
|
|
|
3442
3730
|
ledger,
|
|
3443
3731
|
...checks,
|
|
3444
3732
|
...earlier ? {} : { summaryMissing: true },
|
|
3445
|
-
unreachable,
|
|
3733
|
+
unreachable: [...unreachable, ...checks.closed],
|
|
3446
3734
|
remainingLint,
|
|
3447
3735
|
...tests2 ? { tests: { code: tests2.code, tail: tail(tests2.out) } } : {}
|
|
3448
3736
|
};
|
|
3449
|
-
(0, import_node_fs11.writeFileSync)((0,
|
|
3737
|
+
(0, import_node_fs11.writeFileSync)((0, import_node_path20.join)(root, REPORT_FILE), renderConvertReport(data));
|
|
3450
3738
|
step.name = "saving the state";
|
|
3451
3739
|
saveState(root, { ...state, phase: "finished" });
|
|
3452
3740
|
step.name = "saving your answers";
|
|
3453
3741
|
if (options.configAnswers) writeConfigAnswers({ root, created: [] }, options.configAnswers, configFile);
|
|
3454
3742
|
step.name = "committing";
|
|
3455
|
-
await stage(root, own.filter((p) => (p !== configFile || options.configAnswers) && (0, import_node_fs11.existsSync)((0,
|
|
3743
|
+
await stage(root, own.filter((p) => (p !== configFile || options.configAnswers) && (0, import_node_fs11.existsSync)((0, import_node_path20.join)(root, p))));
|
|
3456
3744
|
if ((await stagedFiles(root)).length) await commit(root, `dualkit: finish ${state.mode} conversion`);
|
|
3457
3745
|
const checked = checks.arabic.length > 0;
|
|
3458
3746
|
const passed = checked && checks.english.every((p) => p.ratio < MAX_DIFF_RATIO) && checks.arabic.every((p) => p.errors === 0) && (tests2?.code ?? 0) === 0;
|
|
3459
|
-
return { reportPath: (0,
|
|
3747
|
+
return { reportPath: (0, import_node_path20.join)(root, REPORT_FILE), passed };
|
|
3460
3748
|
}
|
|
3461
3749
|
async function finish(options, ioIn) {
|
|
3462
3750
|
const ready = await prepare(options, ioIn);
|
package/dist/index.d.cts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -116,6 +116,11 @@ function detectPackageManager(root) {
|
|
|
116
116
|
if (top || dirname(dir) === dir) return "npm";
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
+
var I18N_LIBRARIES = ["next-intl", "react-i18next", "i18next", "next-i18next", "react-intl", "@lingui/core", "next-international", "@inlang/paraglide-js"];
|
|
120
|
+
function existingI18n(root) {
|
|
121
|
+
const deps = readDeps(root);
|
|
122
|
+
return I18N_LIBRARIES.find((name) => name in deps) ?? null;
|
|
123
|
+
}
|
|
119
124
|
function detectI18n(deps) {
|
|
120
125
|
if ("next-intl" in deps) return "next-intl";
|
|
121
126
|
if ("react-i18next" in deps) return "react-i18next";
|
|
@@ -138,7 +143,7 @@ function detectProject(root) {
|
|
|
138
143
|
|
|
139
144
|
// src/convert.ts
|
|
140
145
|
import { isMinified, walk as walk4 } from "@dualkit/lint";
|
|
141
|
-
import { join as
|
|
146
|
+
import { join as join10 } from "path";
|
|
142
147
|
|
|
143
148
|
// src/apply.ts
|
|
144
149
|
import { parseSourceOutcome } from "@dualkit/lint";
|
|
@@ -234,11 +239,11 @@ var RefusedError = class extends Error {
|
|
|
234
239
|
};
|
|
235
240
|
var NO_GIT = "git is not installed or not on PATH";
|
|
236
241
|
function git(cwd, args) {
|
|
237
|
-
return new Promise((
|
|
242
|
+
return new Promise((resolve4, reject) => {
|
|
238
243
|
execFile("git", ["--literal-pathspecs", ...args], { cwd, maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
239
244
|
if (error && error.code === "ENOENT") return reject(new Error(existsSync3(cwd) ? NO_GIT : `The folder ${cwd} does not exist.`));
|
|
240
245
|
const code = error ? typeof error.code === "number" ? error.code : 1 : 0;
|
|
241
|
-
|
|
246
|
+
resolve4({ code, out: `${stdout}${stderr}`.trim(), stdout });
|
|
242
247
|
});
|
|
243
248
|
});
|
|
244
249
|
}
|
|
@@ -292,7 +297,7 @@ async function stagedFiles(root) {
|
|
|
292
297
|
return out.split("\0").filter(Boolean);
|
|
293
298
|
}
|
|
294
299
|
async function commit(root, message) {
|
|
295
|
-
await must(root, ["commit", "-q", "-m", message]);
|
|
300
|
+
await must(root, ["commit", "-q", "--no-verify", "-m", message]);
|
|
296
301
|
}
|
|
297
302
|
async function trackedFiles(root) {
|
|
298
303
|
const out = await must(root, ["ls-files", "-z"]);
|
|
@@ -527,6 +532,26 @@ function writeConfigAnswers(disk, answers2, file = CONFIG_FILE) {
|
|
|
527
532
|
return writeText(disk, file, mergedConfigText(readText(disk.root, file), answers2, file));
|
|
528
533
|
}
|
|
529
534
|
|
|
535
|
+
// src/closed-routes.ts
|
|
536
|
+
var CLOSED = /* @__PURE__ */ new Set([401, 403, 404]);
|
|
537
|
+
var STATUS_TIMEOUT_MS = 6e4;
|
|
538
|
+
var closedStatus = async (url) => {
|
|
539
|
+
try {
|
|
540
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(STATUS_TIMEOUT_MS), redirect: "manual" });
|
|
541
|
+
return CLOSED.has(res.status) ? res.status : null;
|
|
542
|
+
} catch {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
async function closedRoutes(base, routes) {
|
|
547
|
+
const found = [];
|
|
548
|
+
for (const route of routes) {
|
|
549
|
+
const status = await closedStatus(`${base}${route}`);
|
|
550
|
+
if (status) found.push({ route, reason: `the English page returns ${status}, so its Arabic page was not checked` });
|
|
551
|
+
}
|
|
552
|
+
return found;
|
|
553
|
+
}
|
|
554
|
+
|
|
530
555
|
// src/baseline.ts
|
|
531
556
|
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
532
557
|
import { join as join7 } from "path";
|
|
@@ -584,15 +609,15 @@ var ServerError = class extends Error {
|
|
|
584
609
|
var OUTPUT_LINES = 20;
|
|
585
610
|
var POLL_MS = 250;
|
|
586
611
|
var KILL_GRACE_MS = 5e3;
|
|
587
|
-
var sleep = (ms) => new Promise((
|
|
612
|
+
var sleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
588
613
|
function portTaken(port) {
|
|
589
|
-
return new Promise((
|
|
614
|
+
return new Promise((resolve4) => {
|
|
590
615
|
const socket = connect({ port, host: "localhost" });
|
|
591
616
|
socket.once("connect", () => {
|
|
592
617
|
socket.destroy();
|
|
593
|
-
|
|
618
|
+
resolve4(true);
|
|
594
619
|
});
|
|
595
|
-
socket.once("error", () =>
|
|
620
|
+
socket.once("error", () => resolve4(false));
|
|
596
621
|
});
|
|
597
622
|
}
|
|
598
623
|
async function answers(url) {
|
|
@@ -632,7 +657,7 @@ async function startServer(cmd, port, cwd, timeoutMs = 12e4) {
|
|
|
632
657
|
});
|
|
633
658
|
const pid = child.pid;
|
|
634
659
|
if (pid === void 0) {
|
|
635
|
-
await new Promise((
|
|
660
|
+
await new Promise((resolve4) => setImmediate(resolve4));
|
|
636
661
|
throw new ServerError(`Could not start "${cmd}": ${spawnError?.message ?? "no process id"}`);
|
|
637
662
|
}
|
|
638
663
|
const collect = (chunk) => {
|
|
@@ -671,6 +696,7 @@ ${tail2()}`);
|
|
|
671
696
|
|
|
672
697
|
// src/baseline.ts
|
|
673
698
|
var BASELINE_DIR = `${STATE_DIR}/baseline`;
|
|
699
|
+
var CLOSED_FILE = `${BASELINE_DIR}/closed.json`;
|
|
674
700
|
var WIDTHS = [390, 1280];
|
|
675
701
|
var DEFAULT_PORTS = { "next-app": 3e3, "vite-react": 5173 };
|
|
676
702
|
var routeSlug = (route) => route.replace(/^\/+|\/+$/g, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "") || "home";
|
|
@@ -682,6 +708,7 @@ async function shootBaseline(project, config, shoot, log) {
|
|
|
682
708
|
const server = await startServer(config.start, port, project.root);
|
|
683
709
|
const shots = [];
|
|
684
710
|
try {
|
|
711
|
+
writeFileSync5(join7(project.root, CLOSED_FILE), JSON.stringify((await closedRoutes(server.url, routes)).map((c) => c.route)));
|
|
685
712
|
for (const route of routes) {
|
|
686
713
|
for (const width of WIDTHS) {
|
|
687
714
|
const path = baselinePath(route, width);
|
|
@@ -1058,6 +1085,7 @@ function pieceOf(child, ctx) {
|
|
|
1058
1085
|
const source = ctx.file.text.slice(ctx.base + expression.start, ctx.base + expression.end);
|
|
1059
1086
|
return { kind: "value", name, source: mayBeEmpty(expression) ? `${source} ?? ''` : source, start: start2, end };
|
|
1060
1087
|
}
|
|
1088
|
+
var OWN_DIRECTION = /setAttribute\(\s*['"]dir['"]|documentElement\.dir\s*=[^=]/;
|
|
1061
1089
|
var NODE_NAMES = /^(children|icon|avatar|logo|image|element|node)$/i;
|
|
1062
1090
|
var mayBeEmpty = (expression) => /^Optional/.test(expression.type) || expression.type === "CallExpression" && !textMethodTarget(expression);
|
|
1063
1091
|
var jsxText = (value) => value.replace(/^\s*\n\s*/, "").replace(/\s*\n\s*$/, "");
|
|
@@ -1307,6 +1335,15 @@ function extractStrings(file, lib, taken) {
|
|
|
1307
1335
|
for (const block of file.js) {
|
|
1308
1336
|
const program = block.ast.program;
|
|
1309
1337
|
if (isServerFile(program)) continue;
|
|
1338
|
+
if (OWN_DIRECTION.test(file.text.slice(block.offset))) {
|
|
1339
|
+
flag(
|
|
1340
|
+
{ file, flags },
|
|
1341
|
+
block.offset,
|
|
1342
|
+
"unsupported",
|
|
1343
|
+
"This file sets the page direction itself, which can clash with the Arabic layout.",
|
|
1344
|
+
"Check the Arabic pages, and drive this direction from the locale so components read it too."
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1310
1347
|
const ctx = { file, base: block.offset, lib, stem: stemOf(file.path), used, byText: /* @__PURE__ */ new Map(), entries, edits: edits2, flags, components: /* @__PURE__ */ new Set(), sawJsx: false, consumed: /* @__PURE__ */ new Set(), usedTrans: false };
|
|
1311
1348
|
walk(program, ctx, []);
|
|
1312
1349
|
if (ctx.sawJsx) flagModuleStrings(program, ctx);
|
|
@@ -1583,12 +1620,12 @@ function routeMoves(appDir, paths) {
|
|
|
1583
1620
|
}
|
|
1584
1621
|
return { moves, keptPages };
|
|
1585
1622
|
}
|
|
1586
|
-
function eachNode(node,
|
|
1587
|
-
if (Array.isArray(node)) return node.forEach((child) => eachNode(child,
|
|
1623
|
+
function eachNode(node, visit2) {
|
|
1624
|
+
if (Array.isArray(node)) return node.forEach((child) => eachNode(child, visit2));
|
|
1588
1625
|
if (!node || typeof node !== "object") return;
|
|
1589
1626
|
const n = node;
|
|
1590
|
-
if (typeof n.type === "string")
|
|
1591
|
-
for (const [key, child] of Object.entries(n)) if (!SKIP3.has(key)) eachNode(child,
|
|
1627
|
+
if (typeof n.type === "string") visit2(n);
|
|
1628
|
+
for (const [key, child] of Object.entries(n)) if (!SKIP3.has(key)) eachNode(child, visit2);
|
|
1592
1629
|
}
|
|
1593
1630
|
var RELATIVE = /^\.\.?\//;
|
|
1594
1631
|
var stringLiteral = (node) => node?.type === "StringLiteral" ? node : null;
|
|
@@ -2061,7 +2098,7 @@ export function DualkitLanguageSwitch() {
|
|
|
2061
2098
|
)
|
|
2062
2099
|
}
|
|
2063
2100
|
`;
|
|
2064
|
-
var INLINE_SCRIPT = `<script>(function(){var m=document.cookie.match(${COOKIE_PATTERN});var l=m&&m[1]==='ar'?'ar':'en';var h=document.documentElement;h.lang=l;h.dir=l==='ar'?'rtl':'ltr'})()</script>`;
|
|
2101
|
+
var INLINE_SCRIPT = `<script>(function(){var m=document.cookie.match(${COOKIE_PATTERN});var l=m&&m[1]==='ar'?'ar':'en';var h=document.documentElement;h.lang=l;h.dir=l==='ar'?'rtl':'ltr';if(l==='ar')new MutationObserver(function(){if(h.dir!=='rtl')h.dir='rtl'}).observe(h,{attributes:true,attributeFilter:['dir']})})()</script>`;
|
|
2065
2102
|
function htmlEdits2(html, mode) {
|
|
2066
2103
|
if (mode === "bilingual") {
|
|
2067
2104
|
if (html.includes(LOCALE_COOKIE)) return [];
|
|
@@ -2657,12 +2694,27 @@ function visitExpression(container, ancestors, ctx) {
|
|
|
2657
2694
|
ctx.edits.push({ start: ctx.base + container.start, end: ctx.base + container.start, text: "<bdi>" });
|
|
2658
2695
|
ctx.edits.push({ start: ctx.base + container.end, end: ctx.base + container.end, text: "</bdi>" });
|
|
2659
2696
|
}
|
|
2697
|
+
var SIGNED_NUMBER = /^(?:[+\-\u2212]\d[\d,.]*\s?%?|\d[\d,.]*\s?%)$/;
|
|
2698
|
+
var NO_BDI = /* @__PURE__ */ new Set(["svg", "text", "option", "title", "textarea", "style", "script"]);
|
|
2699
|
+
function visitText(text, ancestors, ctx) {
|
|
2700
|
+
const parent = ancestors[ancestors.length - 1];
|
|
2701
|
+
if (parent.type !== "JSXElement" && parent.type !== "JSXFragment") return;
|
|
2702
|
+
const value = String(text.value);
|
|
2703
|
+
const trimmed = value.trim();
|
|
2704
|
+
if (!SIGNED_NUMBER.test(trimmed)) return;
|
|
2705
|
+
const elements = ancestors.filter((a) => a.type === "JSXElement");
|
|
2706
|
+
if (elements.some((a) => isolates(a) || NO_BDI.has(tagName2(a.openingElement) ?? ""))) return;
|
|
2707
|
+
const start2 = ctx.base + text.start + value.indexOf(trimmed);
|
|
2708
|
+
ctx.edits.push({ start: start2, end: start2, text: "<bdi>" });
|
|
2709
|
+
ctx.edits.push({ start: start2 + trimmed.length, end: start2 + trimmed.length, text: "</bdi>" });
|
|
2710
|
+
}
|
|
2660
2711
|
function walk3(node, ctx, ancestors) {
|
|
2661
2712
|
if (Array.isArray(node)) return node.forEach((child) => walk3(child, ctx, ancestors));
|
|
2662
2713
|
if (!node || typeof node !== "object") return;
|
|
2663
2714
|
const n = node;
|
|
2664
2715
|
if (n.type === "JSXOpeningElement") visitOpening(n, ctx, ancestors);
|
|
2665
2716
|
if (n.type === "JSXExpressionContainer" && ancestors.length) visitExpression(n, ancestors, ctx);
|
|
2717
|
+
if (n.type === "JSXText" && ancestors.length) visitText(n, ancestors, ctx);
|
|
2666
2718
|
const next = typeof n.type === "string" ? [...ancestors, n] : ancestors;
|
|
2667
2719
|
for (const [key, child] of Object.entries(n)) if (!SKIP6.has(key)) walk3(child, ctx, next);
|
|
2668
2720
|
}
|
|
@@ -2804,6 +2856,15 @@ function groups(fragments) {
|
|
|
2804
2856
|
for (const f of fragments) byGroup.set(f.group, [...byGroup.get(f.group) ?? [], f]);
|
|
2805
2857
|
return [...byGroup.values()];
|
|
2806
2858
|
}
|
|
2859
|
+
var TRACKED = /^tracking-(?!normal$)/;
|
|
2860
|
+
function trackingEdits(fragments) {
|
|
2861
|
+
const tokens = tokensOf(fragments);
|
|
2862
|
+
if (tokens.some((t) => t.parsed.core.startsWith("tracking-") && t.parsed.variants.includes("rtl"))) return [];
|
|
2863
|
+
const first = tokens.find((t) => TRACKED.test(t.parsed.core) && KEPT(t));
|
|
2864
|
+
if (!first) return [];
|
|
2865
|
+
const end = first.at + first.text.length;
|
|
2866
|
+
return [{ start: end, end, text: " rtl:tracking-normal" }];
|
|
2867
|
+
}
|
|
2807
2868
|
function tailwindEdits(file, version) {
|
|
2808
2869
|
const parsed = parseVersion(version);
|
|
2809
2870
|
const fragments = classStrings(file);
|
|
@@ -2816,7 +2877,7 @@ function tailwindEdits(file, version) {
|
|
|
2816
2877
|
return { edits: [], flags: [{ file: file.path, line: lineOf(file.text, first.offset), kind: "unsupported", message, suggestion }] };
|
|
2817
2878
|
}
|
|
2818
2879
|
const sources = groups(fragments);
|
|
2819
|
-
const edits2 = sources.flatMap((g) => [...physicalEdits(g, parsed), ...parsed[0] < 4 ? reverseEdits(g) : []]);
|
|
2880
|
+
const edits2 = sources.flatMap((g) => [...physicalEdits(g, parsed), ...parsed[0] < 4 ? reverseEdits(g) : [], ...trackingEdits(g)]);
|
|
2820
2881
|
const flags = [...sources.flatMap((g) => formFlags(file, g)), ...computedFlags(file)];
|
|
2821
2882
|
return { edits: edits2, flags: flags.sort((a, b) => a.line - b.line) };
|
|
2822
2883
|
}
|
|
@@ -2866,9 +2927,208 @@ function runPasses(path, text, abs, ctx) {
|
|
|
2866
2927
|
return { kind: "done", text: file.text, edits: edits2, byKind, flags, entries };
|
|
2867
2928
|
}
|
|
2868
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
|
+
|
|
2869
3129
|
// src/rollback.ts
|
|
2870
3130
|
import { rmSync } from "fs";
|
|
2871
|
-
import { join as
|
|
3131
|
+
import { join as join9 } from "path";
|
|
2872
3132
|
var ConvertFailedError = class extends Error {
|
|
2873
3133
|
constructor(message, cause) {
|
|
2874
3134
|
super(message);
|
|
@@ -2887,7 +3147,7 @@ async function rollBack(target, step, error) {
|
|
|
2887
3147
|
const failed = `convert failed at ${step}: ${reasonOf(error)}.`;
|
|
2888
3148
|
try {
|
|
2889
3149
|
await resetHard(target.root);
|
|
2890
|
-
for (const path of [...target.created].reverse()) rmSync(
|
|
3150
|
+
for (const path of [...target.created].reverse()) rmSync(join9(target.root, path), { recursive: true, force: true });
|
|
2891
3151
|
if (target.madeBranch) {
|
|
2892
3152
|
await checkout(target.root, target.base);
|
|
2893
3153
|
await deleteBranch(target.root, target.branch);
|
|
@@ -2976,9 +3236,12 @@ function transformAll(project, generated, taken) {
|
|
|
2976
3236
|
css: physicalContext(root)
|
|
2977
3237
|
};
|
|
2978
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);
|
|
2979
3242
|
for (const path of walked.files) {
|
|
2980
3243
|
if (generated.has(path)) continue;
|
|
2981
|
-
const raw = readText(root, path);
|
|
3244
|
+
const raw = lifted.texts.get(path) ?? readText(root, path);
|
|
2982
3245
|
if (raw === null) {
|
|
2983
3246
|
out.skipped.push({ file: path, reason: "unreadable" });
|
|
2984
3247
|
continue;
|
|
@@ -2987,7 +3250,7 @@ function transformAll(project, generated, taken) {
|
|
|
2987
3250
|
out.skipped.push({ file: path, reason: "minified" });
|
|
2988
3251
|
continue;
|
|
2989
3252
|
}
|
|
2990
|
-
const result = runPasses(path, raw,
|
|
3253
|
+
const result = runPasses(path, raw, join10(root, path), { ...ctx, taken: used });
|
|
2991
3254
|
if (result.kind === "skipped") {
|
|
2992
3255
|
out.skipped.push({ file: path, reason: result.reason });
|
|
2993
3256
|
continue;
|
|
@@ -2995,7 +3258,7 @@ function transformAll(project, generated, taken) {
|
|
|
2995
3258
|
out.flags.push(...result.flags);
|
|
2996
3259
|
out.entries.push(...result.entries);
|
|
2997
3260
|
for (const entry of result.entries) used.add(entry.key);
|
|
2998
|
-
if (result.edits === 0) continue;
|
|
3261
|
+
if (result.edits === 0 && !lifted.texts.has(path)) continue;
|
|
2999
3262
|
out.edits += result.edits;
|
|
3000
3263
|
for (const kind of Object.keys(out.byKind)) out.byKind[kind] += result.byKind[kind];
|
|
3001
3264
|
out.perFile.set(path, result.edits);
|
|
@@ -3003,6 +3266,13 @@ function transformAll(project, generated, taken) {
|
|
|
3003
3266
|
}
|
|
3004
3267
|
return out;
|
|
3005
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
|
+
}
|
|
3006
3276
|
function changedFiles(perFile, moved, wiring) {
|
|
3007
3277
|
const total = /* @__PURE__ */ new Map();
|
|
3008
3278
|
const add = (file, n) => {
|
|
@@ -3061,6 +3331,8 @@ async function convert(options, ioIn) {
|
|
|
3061
3331
|
if (options.configAnswers) checkConfigAnswers(root, options.configFile);
|
|
3062
3332
|
const io = options.configAnswers ? { ...ioIn, config: { ...ioIn.config, ...options.configAnswers } } : ioIn;
|
|
3063
3333
|
const project = detectProject(root);
|
|
3334
|
+
const library = rerun ? null : existingI18n(root);
|
|
3335
|
+
if (library) throw new UnsupportedError(`This app already uses ${library}. Convert adds its own translation setup and the two collide, so nothing was changed. Convert supports apps with no translation library yet.`);
|
|
3064
3336
|
const branch = branchFor(mode);
|
|
3065
3337
|
const generated = new Set(state?.generated ?? []);
|
|
3066
3338
|
const existing = readLedger(root);
|
|
@@ -3090,7 +3362,7 @@ async function convert(options, ioIn) {
|
|
|
3090
3362
|
|
|
3091
3363
|
// src/finish.ts
|
|
3092
3364
|
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
3093
|
-
import { join as
|
|
3365
|
+
import { join as join12 } from "path";
|
|
3094
3366
|
|
|
3095
3367
|
// src/match.ts
|
|
3096
3368
|
import { PNG } from "pngjs";
|
|
@@ -3121,7 +3393,7 @@ function compare(a, b) {
|
|
|
3121
3393
|
|
|
3122
3394
|
// src/finish-undo.ts
|
|
3123
3395
|
import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
3124
|
-
import { join as
|
|
3396
|
+
import { join as join11 } from "path";
|
|
3125
3397
|
async function snapshot(root, paths) {
|
|
3126
3398
|
const clean = new Set(await cleanAtHead(root, paths));
|
|
3127
3399
|
return paths.map((path) => ({ path, clean: clean.has(path), text: clean.has(path) ? null : readText(root, path) }));
|
|
@@ -3130,8 +3402,8 @@ async function undo(root, saved, staged) {
|
|
|
3130
3402
|
await restoreFromHead(root, saved.filter((s) => s.clean).map((s) => s.path));
|
|
3131
3403
|
const dirty = saved.filter((s) => !s.clean);
|
|
3132
3404
|
for (const { path, text } of dirty) {
|
|
3133
|
-
if (text === null) rmSync2(
|
|
3134
|
-
else writeFileSync6(
|
|
3405
|
+
if (text === null) rmSync2(join11(root, path), { force: true });
|
|
3406
|
+
else writeFileSync6(join11(root, path), text);
|
|
3135
3407
|
}
|
|
3136
3408
|
await unstage(root, [.../* @__PURE__ */ new Set([...dirty.map((s) => s.path), ...staged])]);
|
|
3137
3409
|
}
|
|
@@ -3234,7 +3506,8 @@ function english(data) {
|
|
|
3234
3506
|
}
|
|
3235
3507
|
function arabic(data) {
|
|
3236
3508
|
const lines = data.arabic.map(
|
|
3237
|
-
(p) => !Number.isFinite(p.errors) ? `- ${esc(p.route)}: the check returned no result` : `- ${esc(p.route)}: score ${p.score}, ${plural(p.errors, "error", "errors")}, ${plural(p.warnings, "warning", "warnings")}`
|
|
3509
|
+
(p) => !Number.isFinite(p.errors) ? `- ${esc(p.route)}: ${esc(p.problems?.[0] ?? "the check returned no result")}` : `- ${esc(p.route)}: score ${p.score}, ${plural(p.errors, "error", "errors")}, ${plural(p.warnings, "warning", "warnings")}${(p.problems ?? []).map((m) => `
|
|
3510
|
+
- ${esc(m)}`).join("")}`
|
|
3238
3511
|
);
|
|
3239
3512
|
return section("Arabic pages", lines, "No pages were checked.");
|
|
3240
3513
|
}
|
|
@@ -3309,7 +3582,7 @@ function writeMessages(root, ledger) {
|
|
|
3309
3582
|
for (const [path, text] of Object.entries(messagesFiles(ledger))) writeText(disk, path, mergedMessages(root, path, text));
|
|
3310
3583
|
}
|
|
3311
3584
|
function mergedMessages(root, path, generated) {
|
|
3312
|
-
const full =
|
|
3585
|
+
const full = join12(root, path);
|
|
3313
3586
|
if (!existsSync8(full)) return generated;
|
|
3314
3587
|
try {
|
|
3315
3588
|
const existing = JSON.parse(readFileSync7(full, "utf8"));
|
|
@@ -3324,7 +3597,7 @@ async function englishChecks(root, base, routes, shoot) {
|
|
|
3324
3597
|
const noBaseline = [];
|
|
3325
3598
|
for (const route of routes) {
|
|
3326
3599
|
for (const width of WIDTHS) {
|
|
3327
|
-
const file =
|
|
3600
|
+
const file = join12(root, baselinePath(route, width));
|
|
3328
3601
|
if (!existsSync8(file)) {
|
|
3329
3602
|
noBaseline.push({ route, width });
|
|
3330
3603
|
continue;
|
|
@@ -3334,8 +3607,8 @@ async function englishChecks(root, base, routes, shoot) {
|
|
|
3334
3607
|
let diffPath;
|
|
3335
3608
|
if (ratio >= MAX_DIFF_RATIO) {
|
|
3336
3609
|
diffPath = `${DIFF_DIR}/${routeSlug(route)}-${width}.png`;
|
|
3337
|
-
mkdirSync5(
|
|
3338
|
-
writeFileSync7(
|
|
3610
|
+
mkdirSync5(join12(root, DIFF_DIR), { recursive: true });
|
|
3611
|
+
writeFileSync7(join12(root, diffPath), diff);
|
|
3339
3612
|
}
|
|
3340
3613
|
english2.push({ route, width, ratio, ...diffPath ? { diffPath } : {} });
|
|
3341
3614
|
}
|
|
@@ -3350,11 +3623,21 @@ async function arabicChecks(project, state, base, routes, audit) {
|
|
|
3350
3623
|
const result = await audit(`${base}${path}`, width, options);
|
|
3351
3624
|
const usable = result && Number.isFinite(result.errors);
|
|
3352
3625
|
const failed = { score: Number.NaN, errors: Number.NaN, warnings: Number.NaN };
|
|
3353
|
-
arabic2.push({ route: `${path} at ${width}px`, ...usable ? result : failed });
|
|
3626
|
+
arabic2.push({ route: `${path} at ${width}px`, ...usable ? result : { ...failed, ...result?.problems ? { problems: result.problems } : {} } });
|
|
3354
3627
|
}
|
|
3355
3628
|
}
|
|
3356
3629
|
return arabic2;
|
|
3357
3630
|
}
|
|
3631
|
+
function closedAtBaseline(root) {
|
|
3632
|
+
const path = join12(root, CLOSED_FILE);
|
|
3633
|
+
if (!existsSync8(path)) return /* @__PURE__ */ new Set();
|
|
3634
|
+
try {
|
|
3635
|
+
const routes = JSON.parse(readFileSync7(path, "utf8"));
|
|
3636
|
+
return new Set(Array.isArray(routes) ? routes.filter((r) => typeof r === "string") : []);
|
|
3637
|
+
} catch {
|
|
3638
|
+
return /* @__PURE__ */ new Set();
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3358
3641
|
async function runChecks(project, state, routes, { config, shoot, audit }, step) {
|
|
3359
3642
|
const { start: cmd } = config;
|
|
3360
3643
|
if (!cmd || !shoot) throw new RefusedError(NEEDS_BROWSER);
|
|
@@ -3365,8 +3648,13 @@ async function runChecks(project, state, routes, { config, shoot, audit }, step)
|
|
|
3365
3648
|
});
|
|
3366
3649
|
try {
|
|
3367
3650
|
step.name = "checking the pages";
|
|
3368
|
-
const
|
|
3369
|
-
|
|
3651
|
+
const closed = await closedRoutes(server.url, routes);
|
|
3652
|
+
const shut = new Set(closed.map((c) => c.route));
|
|
3653
|
+
const open = routes.filter((route) => !shut.has(route));
|
|
3654
|
+
const before = closedAtBaseline(project.root);
|
|
3655
|
+
const compared = routes.filter((route) => !(shut.has(route) && before.has(route)));
|
|
3656
|
+
const english2 = state.mode === "bilingual" ? await englishChecks(project.root, server.url, compared, shoot) : { english: [], noBaseline: [] };
|
|
3657
|
+
return { ...english2, closed, arabic: await arabicChecks(project, state, server.url, open, audit) };
|
|
3370
3658
|
} finally {
|
|
3371
3659
|
await server.stop();
|
|
3372
3660
|
}
|
|
@@ -3407,21 +3695,21 @@ async function run(ready, step) {
|
|
|
3407
3695
|
ledger,
|
|
3408
3696
|
...checks,
|
|
3409
3697
|
...earlier ? {} : { summaryMissing: true },
|
|
3410
|
-
unreachable,
|
|
3698
|
+
unreachable: [...unreachable, ...checks.closed],
|
|
3411
3699
|
remainingLint,
|
|
3412
3700
|
...tests2 ? { tests: { code: tests2.code, tail: tail(tests2.out) } } : {}
|
|
3413
3701
|
};
|
|
3414
|
-
writeFileSync7(
|
|
3702
|
+
writeFileSync7(join12(root, REPORT_FILE), renderConvertReport(data));
|
|
3415
3703
|
step.name = "saving the state";
|
|
3416
3704
|
saveState(root, { ...state, phase: "finished" });
|
|
3417
3705
|
step.name = "saving your answers";
|
|
3418
3706
|
if (options.configAnswers) writeConfigAnswers({ root, created: [] }, options.configAnswers, configFile);
|
|
3419
3707
|
step.name = "committing";
|
|
3420
|
-
await stage(root, own.filter((p) => (p !== configFile || options.configAnswers) && existsSync8(
|
|
3708
|
+
await stage(root, own.filter((p) => (p !== configFile || options.configAnswers) && existsSync8(join12(root, p))));
|
|
3421
3709
|
if ((await stagedFiles(root)).length) await commit(root, `dualkit: finish ${state.mode} conversion`);
|
|
3422
3710
|
const checked = checks.arabic.length > 0;
|
|
3423
3711
|
const passed = checked && checks.english.every((p) => p.ratio < MAX_DIFF_RATIO) && checks.arabic.every((p) => p.errors === 0) && (tests2?.code ?? 0) === 0;
|
|
3424
|
-
return { reportPath:
|
|
3712
|
+
return { reportPath: join12(root, REPORT_FILE), passed };
|
|
3425
3713
|
}
|
|
3426
3714
|
async function finish(options, ioIn) {
|
|
3427
3715
|
const ready = await prepare(options, ioIn);
|