prosebook 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -32
- package/dist/cli.js +3 -3
- package/dist/{config-CxVMn19f.js → config-D--RITk2.js} +76 -14
- package/dist/config-D--RITk2.js.map +1 -0
- package/dist/{config-CF7HziDa.d.ts → config-_4HfnJQu.d.ts} +25 -6
- package/dist/config-_4HfnJQu.d.ts.map +1 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/{sarif-PkPvsB07.js → sarif-CI8HOokm.js} +3 -3
- package/dist/{sarif-PkPvsB07.js.map → sarif-CI8HOokm.js.map} +1 -1
- package/dist/{scan-B0tZ7y-D.js → scan-Ch2BfIUD.js} +178 -64
- package/dist/scan-Ch2BfIUD.js.map +1 -0
- package/dist/tools.d.ts +28 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +4 -4
- package/dist/vite.d.ts +13 -4
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +47 -21
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
- package/src/tools/compile.ts +14 -3
- package/src/tools/config.ts +19 -3
- package/src/tools/generate.ts +30 -32
- package/src/tools/icu.ts +30 -1
- package/src/tools/index.ts +14 -1
- package/src/tools/lint.ts +49 -12
- package/src/tools/scan.ts +134 -4
- package/src/vite.ts +62 -18
- package/dist/config-CF7HziDa.d.ts.map +0 -1
- package/dist/config-CxVMn19f.js.map +0 -1
- package/dist/scan-B0tZ7y-D.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as numberOptions, D as walk, E as tagsOf, S as formatsOf, T as staticLiteral, b as dateOptions, p as lint, u as applyEdits, v as TYPE$1, w as parseMessage$1, x as formatProblems, y as argsOf } from "./config-
|
|
1
|
+
import { C as numberOptions, D as walk, E as tagsOf, S as formatsOf, T as staticLiteral, b as dateOptions, p as lint, u as applyEdits, v as TYPE$1, w as parseMessage$1, x as formatProblems, y as argsOf } from "./config-D--RITk2.js";
|
|
2
2
|
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { LineCounter, Scalar, isMap, isScalar, parse, parseDocument } from "yaml";
|
|
@@ -1569,7 +1569,8 @@ function previewOf(message) {
|
|
|
1569
1569
|
*
|
|
1570
1570
|
* A message with no placeholders becomes its string. Any other becomes `(a, h) => …`, where `a` holds
|
|
1571
1571
|
* the arguments and `h` the locale's helpers (see `runtime/helpers.ts`): a plural is a call picking an
|
|
1572
|
-
* arm, a number is a call to a cached `Intl.NumberFormat`.
|
|
1572
|
+
* arm, a number is a call to a cached `Intl.NumberFormat`. One that calls no helper is `(a) => …`, so
|
|
1573
|
+
* the generated TypeScript passes `noUnusedParameters`. Formatting options are resolved here and
|
|
1573
1574
|
* hoisted to module constants, so a skeleton costs nothing at runtime and its formatter is built once.
|
|
1574
1575
|
*/
|
|
1575
1576
|
/** Module-level constants for formatting options, one per distinct object. */
|
|
@@ -1615,9 +1616,18 @@ function node(el, hoisted, pound) {
|
|
|
1615
1616
|
default: throw new Error(`prosebook: no compiler for ICU element type ${el.type}`);
|
|
1616
1617
|
}
|
|
1617
1618
|
}
|
|
1619
|
+
/** Whether the message calls a helper: everything but literals and plain placeholders does. */
|
|
1620
|
+
function usesHelpers(ast) {
|
|
1621
|
+
let uses = false;
|
|
1622
|
+
walk(ast, (el) => {
|
|
1623
|
+
if (el.type !== TYPE$1.literal && el.type !== TYPE$1.argument) uses = true;
|
|
1624
|
+
});
|
|
1625
|
+
return uses;
|
|
1626
|
+
}
|
|
1618
1627
|
function message(ast, hoisted) {
|
|
1619
1628
|
const literal = staticLiteral(ast);
|
|
1620
|
-
|
|
1629
|
+
if (literal !== void 0) return JSON.stringify(literal);
|
|
1630
|
+
return `(${usesHelpers(ast) ? "a, h" : "a"}) => ${nodes(ast, hoisted, null)}`;
|
|
1621
1631
|
}
|
|
1622
1632
|
/** One locale's messages as code, keeping only `keep` when given. */
|
|
1623
1633
|
function compileLocale(entries, keep) {
|
|
@@ -2143,67 +2153,69 @@ function catalogLiteral(config, locales, ext) {
|
|
|
2143
2153
|
}
|
|
2144
2154
|
/** The JSON Schema an editor reads to complete and explain catalog entries. */
|
|
2145
2155
|
function catalogSchema(config) {
|
|
2156
|
+
const shorthand = {
|
|
2157
|
+
type: "string",
|
|
2158
|
+
description: `The message, in ${SYNTAX_NAME[config.messageFormat]}, with nothing said about it.`
|
|
2159
|
+
};
|
|
2160
|
+
const entry = {
|
|
2161
|
+
type: "object",
|
|
2162
|
+
required: [config.source, ...config.require],
|
|
2163
|
+
additionalProperties: false,
|
|
2164
|
+
properties: {
|
|
2165
|
+
[config.source]: {
|
|
2166
|
+
type: "string",
|
|
2167
|
+
description: `The message, in ${SYNTAX_NAME[config.messageFormat]}.`
|
|
2168
|
+
},
|
|
2169
|
+
kind: {
|
|
2170
|
+
description: "What sort of string it is.",
|
|
2171
|
+
oneOf: Object.entries(config.kinds).map(([k, r]) => ({
|
|
2172
|
+
const: k,
|
|
2173
|
+
description: r.doc
|
|
2174
|
+
}))
|
|
2175
|
+
},
|
|
2176
|
+
surface: {
|
|
2177
|
+
description: "Where the string sits.",
|
|
2178
|
+
oneOf: Object.entries(config.surfaces).map(([k, d]) => ({
|
|
2179
|
+
const: k,
|
|
2180
|
+
description: d
|
|
2181
|
+
}))
|
|
2182
|
+
},
|
|
2183
|
+
context: {
|
|
2184
|
+
type: "string",
|
|
2185
|
+
description: "One sentence telling a translator where the string appears and what it answers to."
|
|
2186
|
+
},
|
|
2187
|
+
args: {
|
|
2188
|
+
type: "object",
|
|
2189
|
+
description: "What each placeholder holds.",
|
|
2190
|
+
additionalProperties: { type: "string" }
|
|
2191
|
+
},
|
|
2192
|
+
fragment: {
|
|
2193
|
+
type: "boolean",
|
|
2194
|
+
description: "Completes something beside it rather than standing as a sentence."
|
|
2195
|
+
},
|
|
2196
|
+
comment: {
|
|
2197
|
+
type: "string",
|
|
2198
|
+
description: "A change asked of whoever edits the entry next. Make it, then delete the comment."
|
|
2199
|
+
},
|
|
2200
|
+
remove: {
|
|
2201
|
+
type: "boolean",
|
|
2202
|
+
description: "Asks for the entry to go: delete it, its translations and the code that reads it."
|
|
2203
|
+
},
|
|
2204
|
+
allow: {
|
|
2205
|
+
type: "object",
|
|
2206
|
+
description: "Lint rules this entry is exempt from, each with the reason.",
|
|
2207
|
+
propertyNames: { enum: Object.keys(config.lint.rules) },
|
|
2208
|
+
additionalProperties: { type: "string" }
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
};
|
|
2146
2212
|
return {
|
|
2147
2213
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
2148
2214
|
title: "prosebook catalog",
|
|
2149
2215
|
description: "Generated by `prosebook generate` from the config. Do not edit.",
|
|
2150
2216
|
type: "object",
|
|
2151
2217
|
propertyNames: { pattern: "^[a-z0-9]+(?:[-_][a-z0-9]+)*(?:\\.[a-z0-9]+(?:[-_][a-z0-9]+)*)+$" },
|
|
2152
|
-
additionalProperties: { oneOf: [
|
|
2153
|
-
type: "string",
|
|
2154
|
-
description: `The message, in ${SYNTAX_NAME[config.messageFormat]}, with nothing said about it.`
|
|
2155
|
-
}, {
|
|
2156
|
-
type: "object",
|
|
2157
|
-
required: [config.source, ...config.require],
|
|
2158
|
-
additionalProperties: false,
|
|
2159
|
-
properties: {
|
|
2160
|
-
[config.source]: {
|
|
2161
|
-
type: "string",
|
|
2162
|
-
description: `The message, in ${SYNTAX_NAME[config.messageFormat]}.`
|
|
2163
|
-
},
|
|
2164
|
-
kind: {
|
|
2165
|
-
description: "What sort of string it is.",
|
|
2166
|
-
oneOf: Object.entries(config.kinds).map(([k, r]) => ({
|
|
2167
|
-
const: k,
|
|
2168
|
-
description: r.doc
|
|
2169
|
-
}))
|
|
2170
|
-
},
|
|
2171
|
-
surface: {
|
|
2172
|
-
description: "Where the string sits.",
|
|
2173
|
-
oneOf: Object.entries(config.surfaces).map(([k, d]) => ({
|
|
2174
|
-
const: k,
|
|
2175
|
-
description: d
|
|
2176
|
-
}))
|
|
2177
|
-
},
|
|
2178
|
-
context: {
|
|
2179
|
-
type: "string",
|
|
2180
|
-
description: "One sentence telling a translator where the string appears and what it answers to."
|
|
2181
|
-
},
|
|
2182
|
-
args: {
|
|
2183
|
-
type: "object",
|
|
2184
|
-
description: "What each placeholder holds.",
|
|
2185
|
-
additionalProperties: { type: "string" }
|
|
2186
|
-
},
|
|
2187
|
-
fragment: {
|
|
2188
|
-
type: "boolean",
|
|
2189
|
-
description: "Completes something beside it rather than standing as a sentence."
|
|
2190
|
-
},
|
|
2191
|
-
comment: {
|
|
2192
|
-
type: "string",
|
|
2193
|
-
description: "A change asked of whoever edits the entry next. Make it, then delete the comment."
|
|
2194
|
-
},
|
|
2195
|
-
remove: {
|
|
2196
|
-
type: "boolean",
|
|
2197
|
-
description: "Asks for the entry to go: delete it, its translations and the code that reads it."
|
|
2198
|
-
},
|
|
2199
|
-
allow: {
|
|
2200
|
-
type: "object",
|
|
2201
|
-
description: "Lint rules this entry is exempt from, each with the reason.",
|
|
2202
|
-
propertyNames: { enum: Object.keys(config.lint.rules) },
|
|
2203
|
-
additionalProperties: { type: "string" }
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
}] }
|
|
2218
|
+
additionalProperties: config.require.length === 0 ? { oneOf: [shorthand, entry] } : entry
|
|
2207
2219
|
};
|
|
2208
2220
|
}
|
|
2209
2221
|
/** Everything `generate` writes, in memory. */
|
|
@@ -2352,10 +2364,8 @@ const NON_PROSE_ELEMENTS = /* @__PURE__ */ new Set([
|
|
|
2352
2364
|
"path"
|
|
2353
2365
|
]);
|
|
2354
2366
|
const LETTER = /\p{L}/u;
|
|
2355
|
-
/**
|
|
2356
|
-
function
|
|
2357
|
-
const { program } = parseSync(file, code);
|
|
2358
|
-
const found = [];
|
|
2367
|
+
/** Offsets in `code` as lines and columns, and whether a line is marked `prosebook-ignore`, on it or the line above. */
|
|
2368
|
+
function lineIndex(code) {
|
|
2359
2369
|
const lines = code.split("\n");
|
|
2360
2370
|
const lineStarts = [];
|
|
2361
2371
|
let offset = 0;
|
|
@@ -2377,6 +2387,16 @@ function hardcodedText(file, code, attributes) {
|
|
|
2377
2387
|
};
|
|
2378
2388
|
};
|
|
2379
2389
|
const ignored = (line) => (lines[line - 1] ?? "").includes("prosebook-ignore") || (lines[line - 2] ?? "").includes("prosebook-ignore");
|
|
2390
|
+
return {
|
|
2391
|
+
position,
|
|
2392
|
+
ignored
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
/** Literal text in JSX that a person reads, where it should be a catalog entry. */
|
|
2396
|
+
function hardcodedText(file, code, attributes) {
|
|
2397
|
+
const { program } = parseSync(file, code);
|
|
2398
|
+
const found = [];
|
|
2399
|
+
const { position, ignored } = lineIndex(code);
|
|
2380
2400
|
const report = (node, text, where) => {
|
|
2381
2401
|
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
2382
2402
|
if (!LETTER.test(trimmed) || looksLikeCode(trimmed)) return;
|
|
@@ -2442,6 +2462,100 @@ function scan(dirs, extensions, attributes, root, skip = []) {
|
|
|
2442
2462
|
files: files.length
|
|
2443
2463
|
};
|
|
2444
2464
|
}
|
|
2445
|
-
|
|
2465
|
+
/** A copy's methods that take a key first. */
|
|
2466
|
+
const READERS = /* @__PURE__ */ new Set(["t", "rich"]);
|
|
2467
|
+
const PREFIX = /^[a-z0-9][\w-]*(?:\.[\w-]+)*\.$/;
|
|
2468
|
+
const WRAPPERS = /* @__PURE__ */ new Set([
|
|
2469
|
+
"ParenthesizedExpression",
|
|
2470
|
+
"TSAsExpression",
|
|
2471
|
+
"TSSatisfiesExpression",
|
|
2472
|
+
"TSNonNullExpression",
|
|
2473
|
+
"TSTypeAssertion"
|
|
2474
|
+
]);
|
|
2475
|
+
/** Whether `keyRefs` finds every key an expression can be: a literal, or a template or sum after a key prefix. */
|
|
2476
|
+
function keySeen(node) {
|
|
2477
|
+
if (node === void 0) return false;
|
|
2478
|
+
if (WRAPPERS.has(node.type)) return keySeen(node.expression);
|
|
2479
|
+
switch (node.type) {
|
|
2480
|
+
case "Literal": return typeof node.value === "string";
|
|
2481
|
+
case "TemplateLiteral": {
|
|
2482
|
+
const [head] = node.quasis;
|
|
2483
|
+
return node.expressions.length === 0 || PREFIX.test(head?.value.cooked ?? "");
|
|
2484
|
+
}
|
|
2485
|
+
case "BinaryExpression": {
|
|
2486
|
+
const left = node.left;
|
|
2487
|
+
return node.operator === "+" && left.type === "Literal" && typeof left.value === "string" && PREFIX.test(left.value);
|
|
2488
|
+
}
|
|
2489
|
+
case "ConditionalExpression": return keySeen(node.consequent) && keySeen(node.alternate);
|
|
2490
|
+
case "LogicalExpression": return keySeen(node.left) && keySeen(node.right);
|
|
2491
|
+
default: return false;
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
const nameOf = (node) => node?.type === "Identifier" || node?.type === "JSXIdentifier" ? node.name : node?.type === "Literal" ? String(node.value) : void 0;
|
|
2495
|
+
/**
|
|
2496
|
+
* The reads in a module whose key a pruned build can't see, as in `t(item.key)` or `<Text id={id} />`:
|
|
2497
|
+
* the build would leave the message out, and the key would show in its place. A build has no types,
|
|
2498
|
+
* so readers are known by name: `t` and `rich` called alone or as methods, and any name imported here,
|
|
2499
|
+
* which may be bound to one elsewhere (`aliases`); the caller keeps those whose name turns out to be a
|
|
2500
|
+
* reader. `<Text>` counts where it is imported from `prosebook/react`. Lines marked `prosebook-ignore`
|
|
2501
|
+
* are skipped.
|
|
2502
|
+
*/
|
|
2503
|
+
function unseenReads(file, code) {
|
|
2504
|
+
const { program } = parseSync(file, code);
|
|
2505
|
+
const { position, ignored } = lineIndex(code);
|
|
2506
|
+
const unseen = [];
|
|
2507
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
2508
|
+
/** Local name to the name it was imported by. */
|
|
2509
|
+
const imported = /* @__PURE__ */ new Map();
|
|
2510
|
+
const texts = /* @__PURE__ */ new Set();
|
|
2511
|
+
for (const statement of program.body) {
|
|
2512
|
+
if (statement.type !== "ImportDeclaration") continue;
|
|
2513
|
+
for (const s of statement.specifiers) {
|
|
2514
|
+
const name = s.type === "ImportSpecifier" ? nameOf(s.imported) : void 0;
|
|
2515
|
+
const local = nameOf(s.local);
|
|
2516
|
+
if (name === void 0 || local === void 0) continue;
|
|
2517
|
+
if (name === "Text" && statement.source.value === "prosebook/react") texts.add(local);
|
|
2518
|
+
else imported.set(local, name);
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
const report = (node, name) => {
|
|
2522
|
+
const at = position(node.start);
|
|
2523
|
+
if (ignored(at.line)) return;
|
|
2524
|
+
const text = code.slice(node.start, node.end).replace(/\s+/g, " ");
|
|
2525
|
+
unseen.push({
|
|
2526
|
+
file,
|
|
2527
|
+
...at,
|
|
2528
|
+
name,
|
|
2529
|
+
code: text.length > 80 ? `${text.slice(0, 77)}...` : text
|
|
2530
|
+
});
|
|
2531
|
+
};
|
|
2532
|
+
const calls = [];
|
|
2533
|
+
const visit = (node) => {
|
|
2534
|
+
if (node.type === "ObjectPattern") {
|
|
2535
|
+
for (const p of node.properties) if (p.type === "Property" && READERS.has(nameOf(p.key) ?? "") && p.value.type === "Identifier") aliases.add(nameOf(p.value));
|
|
2536
|
+
}
|
|
2537
|
+
if (node.type === "CallExpression" && !keySeen(node.arguments[0])) {
|
|
2538
|
+
const callee = node.callee;
|
|
2539
|
+
if (callee.type === "Identifier") calls.push([node, callee.name]);
|
|
2540
|
+
else if (callee.type === "MemberExpression" && !callee.computed && READERS.has(nameOf(callee.property) ?? "")) report(node, nameOf(callee.property));
|
|
2541
|
+
}
|
|
2542
|
+
if (node.type === "JSXOpeningElement" && texts.has(nameOf(node.name) ?? "")) {
|
|
2543
|
+
const value = node.attributes.find((a) => a.type === "JSXAttribute" && nameOf(a.name) === "id")?.value;
|
|
2544
|
+
if (value !== null && value !== void 0 && !keySeen(value.type === "JSXExpressionContainer" ? value.expression : value)) report(node, "Text");
|
|
2545
|
+
}
|
|
2546
|
+
for (const value of Object.values(node)) if (Array.isArray(value)) {
|
|
2547
|
+
for (const child of value) if (isNode(child)) visit(child);
|
|
2548
|
+
} else if (isNode(value)) visit(value);
|
|
2549
|
+
};
|
|
2550
|
+
visit(program);
|
|
2551
|
+
for (const [node, local] of calls) if (READERS.has(local) || aliases.has(local)) report(node, local);
|
|
2552
|
+
else if (imported.has(local)) report(node, imported.get(local));
|
|
2553
|
+
unseen.sort((a, b) => a.line === b.line ? a.column - b.column : a.line - b.line);
|
|
2554
|
+
return {
|
|
2555
|
+
unseen,
|
|
2556
|
+
aliases
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
export { entryLocation as A, parseMf2 as B, editEntry as C, localeModule as D, localeExpression as E, SYNTAX_NAME as F, toMf1 as H, readMessage as I, writeMessage as L, loadCatalog as M, previewOf as N, CatalogError as O, whereOf as P, intlDate as R, applyFixes as S, compileLocale as T, toMf2 as V, emptyLock as _, looksLikeCode as a, reconcile as b, unseenReads as c, localeEntries as d, localesOf as f, pseudoText as g, pseudoAst as h, keyRefs as i, entryLocator as j, KEY_PATTERN as k, catalogSchema as l, writeFiles as m, hardcodedText as n, scan as o, render as p, isReferenced as r, sourceFiles as s, READERS as t, generate as u, hash as v, readEntry as w, serializeLock as x, readLock as y, intlNumber as z };
|
|
2446
2560
|
|
|
2447
|
-
//# sourceMappingURL=scan-
|
|
2561
|
+
//# sourceMappingURL=scan-Ch2BfIUD.js.map
|