prosebook 0.4.0 → 0.5.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 +288 -28
- package/dist/bind-BIZNv8nU.js +62 -0
- package/dist/bind-BIZNv8nU.js.map +1 -0
- package/dist/bind-ZS86jHXk.d.ts +31 -0
- package/dist/bind-ZS86jHXk.d.ts.map +1 -0
- package/dist/bun.d.ts +11 -2
- package/dist/bun.d.ts.map +1 -1
- package/dist/bun.js +77 -26
- package/dist/bun.js.map +1 -1
- package/dist/{bundle-CpUIWb9j.d.ts → bundle-CLHY_97D.d.ts} +2 -2
- package/dist/bundle-CLHY_97D.d.ts.map +1 -0
- package/dist/bundle-Ut6Y2X6N.js +217 -0
- package/dist/bundle-Ut6Y2X6N.js.map +1 -0
- package/dist/cli.js +55 -12
- package/dist/cli.js.map +1 -1
- package/dist/{config-nWZf9QmF.js → config-7T4hX_f7.js} +5 -3
- package/dist/{config-nWZf9QmF.js.map → config-7T4hX_f7.js.map} +1 -1
- package/dist/config-CzaJJQ60.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/{copy-B3V_QRkT.d.ts → copy-BXn0aYQn.d.ts} +37 -3
- package/dist/copy-BXn0aYQn.d.ts.map +1 -0
- package/dist/copy-Bv6S-Hr2.js +235 -0
- package/dist/copy-Bv6S-Hr2.js.map +1 -0
- package/dist/endpoints-Dhgowovz.js +217 -0
- package/dist/endpoints-Dhgowovz.js.map +1 -0
- package/dist/generate-1BeQAd1O.d.ts +72 -0
- package/dist/generate-1BeQAd1O.d.ts.map +1 -0
- package/dist/{generate-BW3N8EGb.js → generate-CKv86Iki.js} +1091 -309
- package/dist/generate-CKv86Iki.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/next-loader.d.ts +9 -2
- package/dist/next-loader.d.ts.map +1 -1
- package/dist/next-loader.js +12 -3
- package/dist/next-loader.js.map +1 -1
- package/dist/next.d.ts +10 -2
- package/dist/next.d.ts.map +1 -1
- package/dist/next.js +94 -21
- package/dist/next.js.map +1 -1
- package/dist/preact/index.d.ts +34 -0
- package/dist/preact/index.d.ts.map +1 -0
- package/dist/preact/index.js +72 -0
- package/dist/preact/index.js.map +1 -0
- package/dist/react/index.d.ts +5 -14
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +9 -33
- package/dist/react/index.js.map +1 -1
- package/dist/react/server.d.ts +1 -1
- package/dist/react/server.js +1 -1
- package/dist/{sarif-B5nrMqrT.js → sarif-BnwYC78_.js} +8 -98
- package/dist/sarif-BnwYC78_.js.map +1 -0
- package/dist/{scan-Bx0_yDSi.js → scan-C0ap4Mpp.js} +145 -20
- package/dist/scan-C0ap4Mpp.js.map +1 -0
- package/dist/solid/index.d.ts +29 -0
- package/dist/solid/index.d.ts.map +1 -0
- package/dist/solid/index.js +48 -0
- package/dist/solid/index.js.map +1 -0
- package/dist/svelte/Text.svelte +13 -0
- package/dist/svelte/Text.svelte.d.ts +20 -0
- package/dist/svelte/index.d.ts +31 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +35 -0
- package/dist/svelte/index.js.map +1 -0
- package/dist/tools.d.ts +43 -64
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +5 -5
- package/dist/vite.d.ts +12 -2
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +82 -25
- package/dist/vite.js.map +1 -1
- package/dist/vue/index.d.ts +43 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/index.js +76 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +63 -7
- package/src/bun.ts +77 -20
- package/src/cli.ts +62 -8
- package/src/index.ts +1 -1
- package/src/next-loader.ts +17 -5
- package/src/next.ts +120 -16
- package/src/preact/index.ts +91 -0
- package/src/react/index.tsx +12 -42
- package/src/runtime/bind.ts +96 -0
- package/src/runtime/copy.ts +148 -24
- package/src/solid/index.ts +67 -0
- package/src/svelte/Text.svelte +13 -0
- package/src/svelte/Text.svelte.d.ts +20 -0
- package/src/svelte/index.ts +65 -0
- package/src/tools/bundle.ts +143 -2
- package/src/tools/catalog.ts +10 -1
- package/src/tools/chunks.ts +142 -0
- package/src/tools/config.ts +1 -1
- package/src/tools/entry.ts +19 -20
- package/src/tools/exchange.ts +275 -0
- package/src/tools/extract.ts +2 -2
- package/src/tools/generate.ts +7 -1
- package/src/tools/index.ts +2 -1
- package/src/tools/native/android.ts +10 -25
- package/src/tools/native/apple.ts +70 -8
- package/src/tools/native/date.ts +219 -0
- package/src/tools/native/kotlin.ts +47 -6
- package/src/tools/native/model.ts +42 -16
- package/src/tools/native/printf.ts +46 -4
- package/src/tools/native/rust.ts +86 -5
- package/src/tools/native/swift.ts +62 -6
- package/src/tools/scan.ts +146 -25
- package/src/tools/todo.ts +6 -1
- package/src/vite.ts +121 -28
- package/src/vue/index.ts +117 -0
- package/dist/bundle-CpUIWb9j.d.ts.map +0 -1
- package/dist/bundle-DboItAre.js +0 -111
- package/dist/bundle-DboItAre.js.map +0 -1
- package/dist/compile-ClK9htKr.d.ts +0 -16
- package/dist/compile-ClK9htKr.d.ts.map +0 -1
- package/dist/copy-B3V_QRkT.d.ts.map +0 -1
- package/dist/copy-Bo_FIoLy.js +0 -154
- package/dist/copy-Bo_FIoLy.js.map +0 -1
- package/dist/endpoints-BgG_5Vpm.js +0 -96
- package/dist/endpoints-BgG_5Vpm.js.map +0 -1
- package/dist/generate-BW3N8EGb.js.map +0 -1
- package/dist/sarif-B5nrMqrT.js.map +0 -1
- package/dist/scan-Bx0_yDSi.js.map +0 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* message, and a file-private enum holding what formats them.
|
|
4
4
|
*/
|
|
5
5
|
import type { NativeTarget } from '#src/tools/config.ts';
|
|
6
|
+
import { pieceExpr, type DateSpec } from './date.ts';
|
|
6
7
|
import { camel, checkNames, conditionCode, float, leading, stringLiteral, wordsOf } from './emit.ts';
|
|
7
8
|
import type { NativeMessage, NativeModel, Part } from './model.ts';
|
|
8
9
|
import type { NumberSpec } from './number.ts';
|
|
@@ -36,6 +37,7 @@ function value(part: Exclude<Part, { type: 'text' }>, indent: string, rt: string
|
|
|
36
37
|
const of = part.offset === 0 ? argName(part.arg) : `${argName(part.arg)} - ${float(part.offset)}`;
|
|
37
38
|
return `${rt}.number(${of}, ${rt}.f${part.format}, ${lit(part.fallback)})`;
|
|
38
39
|
}
|
|
40
|
+
if (part.type === 'date') return `${rt}.d${part.format}(${argName(part.arg)})`;
|
|
39
41
|
if (part.type === 'select') {
|
|
40
42
|
const arms = part.arms.map((a) => `${inner}case ${lit(a.key)}: return ${expr(a.parts, inner, rt)}\n`).join('');
|
|
41
43
|
return `{ () -> String in\n${inner}switch ${argName(part.arg)} {\n${arms}${inner}default: return ${expr(part.other, inner, rt)}\n${inner}}\n${indent}}()`;
|
|
@@ -66,10 +68,17 @@ function member(m: NativeMessage, name: string, rt: string, number: NumberProtoc
|
|
|
66
68
|
const head = doc(argDocs.length > 0 ? [...m.doc, '', ...argDocs] : m.doc, ' ');
|
|
67
69
|
const body = ` switch self {\n${cases((parts) => expr(parts, ' ', rt))} }\n`;
|
|
68
70
|
if (m.args.length === 0) return `${head} public var ${name}: String {\n${body} }\n`;
|
|
69
|
-
const params = m.args
|
|
71
|
+
const params = m.args
|
|
72
|
+
.map((a) => `${argName(a.name)}: ${a.type === 'number' ? `some ${number.type}` : a.type === 'date' ? 'Date' : 'String'}`)
|
|
73
|
+
.join(', ');
|
|
70
74
|
const numbers = m.args
|
|
71
|
-
.
|
|
72
|
-
|
|
75
|
+
.map((a) =>
|
|
76
|
+
a.type === 'number'
|
|
77
|
+
? ` let ${argName(a.name)} = ${argName(a.name)}.${number.value}\n`
|
|
78
|
+
: a.type === 'date'
|
|
79
|
+
? ` let ${argName(a.name)} = ${rt}.clock(${argName(a.name)})\n`
|
|
80
|
+
: '',
|
|
81
|
+
)
|
|
73
82
|
.join('');
|
|
74
83
|
return `${head} public func ${name}(${params}) -> String {\n${numbers}${body} }\n`;
|
|
75
84
|
}
|
|
@@ -97,6 +106,51 @@ function formatConst(spec: NumberSpec, i: number): string {
|
|
|
97
106
|
return ` static let f${i} = Format(\n${fields.map((f) => ` ${f}`).join(',\n')}\n )\n`;
|
|
98
107
|
}
|
|
99
108
|
|
|
109
|
+
/** A date format as a function of the clock's fields. */
|
|
110
|
+
function dateFn(spec: DateSpec, i: number): string {
|
|
111
|
+
const digits = spec.digits === undefined ? 'nil' : `g${i}`;
|
|
112
|
+
const at = (field: string) => `t.${field}`;
|
|
113
|
+
const lines = spec.pieces.map((piece) => {
|
|
114
|
+
if (piece.type === 'text') return `s += ${lit(piece.value)}`;
|
|
115
|
+
if (piece.type === 'name') return `s += [${piece.names.map(lit).join(', ')}][${pieceExpr(piece, at)}]`;
|
|
116
|
+
return `s += pad(${pieceExpr(piece, at)}, ${piece.pad}, ${digits})`;
|
|
117
|
+
});
|
|
118
|
+
const table = spec.digits === undefined ? '' : ` static let g${i} = [${spec.digits.map(lit).join(', ')}]\n`;
|
|
119
|
+
const body = lines.map((l) => ` ${l}\n`).join('');
|
|
120
|
+
return `${table} static func d${i}(_ t: Clock) -> String {\n var s = ""\n${body} return s\n }\n`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const DATE = `
|
|
124
|
+
/// A date as the clock shows it in the device's time zone; \`weekday\` is 0 for Sunday.
|
|
125
|
+
struct Clock {
|
|
126
|
+
let year: Int
|
|
127
|
+
let month: Int
|
|
128
|
+
let day: Int
|
|
129
|
+
let hour: Int
|
|
130
|
+
let minute: Int
|
|
131
|
+
let second: Int
|
|
132
|
+
let weekday: Int
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
static func clock(_ date: Date) -> Clock {
|
|
136
|
+
var calendar = Calendar(identifier: .gregorian)
|
|
137
|
+
calendar.timeZone = .current
|
|
138
|
+
let c = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second, .weekday], from: date)
|
|
139
|
+
return Clock(
|
|
140
|
+
year: c.year ?? 0, month: c.month ?? 1, day: c.day ?? 1, hour: c.hour ?? 0, minute: c.minute ?? 0,
|
|
141
|
+
second: c.second ?? 0, weekday: (c.weekday ?? 1) - 1
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// \`value\` written with at least \`width\` digits, in \`digits\` where the locale has its own.
|
|
146
|
+
private static func pad(_ value: Int, _ width: Int, _ digits: [String]?) -> String {
|
|
147
|
+
var text = String(value)
|
|
148
|
+
while text.count < width { text = "0" + text }
|
|
149
|
+
guard let digits else { return text }
|
|
150
|
+
return text.map { c in c.wholeNumberValue.map { digits[$0] } ?? String(c) }.joined()
|
|
151
|
+
}
|
|
152
|
+
`;
|
|
153
|
+
|
|
100
154
|
const NUMBER = `
|
|
101
155
|
/// A number format, taken apart when the file was generated.
|
|
102
156
|
struct Format {
|
|
@@ -265,6 +319,8 @@ export function swiftFile(model: NativeModel, target: NativeTarget & { name: str
|
|
|
265
319
|
const number = { type: `${name}Number`, value: `${name.charAt(0).toLowerCase()}${name.slice(1)}Value` };
|
|
266
320
|
const members = model.messages.map((m, i) => member(m, names[i]![1], rt, number)).join('\n');
|
|
267
321
|
const usesNumbers = model.messages.some((m) => m.args.some((a) => a.type === 'number'));
|
|
322
|
+
const usesDates = model.dates.length > 0;
|
|
323
|
+
const usesFormats = model.formats.length + model.rules.length > 0;
|
|
268
324
|
const numberProtocol = !usesNumbers
|
|
269
325
|
? ''
|
|
270
326
|
: `
|
|
@@ -285,12 +341,12 @@ ${NUMBER_TYPES.map((t) => `extension ${t}: ${number.type} { public var ${number.
|
|
|
285
341
|
})
|
|
286
342
|
.join('\n');
|
|
287
343
|
const runtime =
|
|
288
|
-
|
|
344
|
+
!usesFormats && !usesDates
|
|
289
345
|
? ''
|
|
290
346
|
: `
|
|
291
347
|
/// What formats the messages, generated with them.
|
|
292
|
-
private enum ${rt} {${NUMBER}${model.rules.length > 0 ? PLURAL : ''}
|
|
293
|
-
${model.formats.map(formatConst).join('\n')}${rules === '' ? '' : `\n${rules}`}}
|
|
348
|
+
private enum ${rt} {${usesFormats ? NUMBER : ''}${model.rules.length > 0 ? PLURAL : ''}${usesDates ? DATE : ''}
|
|
349
|
+
${[...model.formats.map(formatConst), ...model.dates.map(dateFn)].join('\n')}${rules === '' ? '' : `\n${rules}`}}
|
|
294
350
|
`;
|
|
295
351
|
|
|
296
352
|
return `${banner}
|
package/src/tools/scan.ts
CHANGED
|
@@ -232,41 +232,73 @@ const nameOf = (node: Node | undefined): string | undefined =>
|
|
|
232
232
|
? String(node.value)
|
|
233
233
|
: undefined;
|
|
234
234
|
|
|
235
|
+
/** Where `<Text>` comes from: each framework binding's. */
|
|
236
|
+
const TEXT_SOURCES = new Set(['prosebook/react', 'prosebook/preact', 'prosebook/solid', 'prosebook/vue', 'prosebook/svelte']);
|
|
237
|
+
|
|
238
|
+
/** Single-file components, whose reads are in their scripts and their markup. */
|
|
239
|
+
export const SFC = /\.(?:vue|svelte)$/;
|
|
240
|
+
|
|
241
|
+
/** Code inside a file, parsed apart: a script block, or an expression in markup, at its offset. */
|
|
242
|
+
interface Piece {
|
|
243
|
+
code: string;
|
|
244
|
+
at: number;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** A single-file component's script blocks, each at its offset. */
|
|
248
|
+
export const scriptBlocks = (code: string): Piece[] =>
|
|
249
|
+
[...code.matchAll(/(<script\b[^>]*>)([\s\S]*?)<\/script>/g)].map((m) => ({ code: m[2]!, at: m.index + m[1]!.length }));
|
|
250
|
+
|
|
235
251
|
/**
|
|
236
252
|
* The reads in a module whose key a pruned build can't see, as in `t(item.key)` or `<Text id={id} />`:
|
|
237
253
|
* the build would leave the message out, and the key would show in its place. A build has no types,
|
|
238
254
|
* so readers are known by name: `t` and `rich` called alone or as methods, and any name imported here,
|
|
239
255
|
* which may be bound to one elsewhere (`aliases`); the caller keeps those whose name turns out to be a
|
|
240
|
-
* reader. `<Text>` counts where it is imported from
|
|
241
|
-
* are skipped.
|
|
256
|
+
* reader. `<Text>` counts where it is imported from a prosebook binding. In a `.vue` or `.svelte` file,
|
|
257
|
+
* its scripts and the expressions in its markup are read. Lines marked `prosebook-ignore` are skipped.
|
|
242
258
|
*/
|
|
243
259
|
export function unseenReads(file: string, code: string): Reads {
|
|
244
|
-
|
|
260
|
+
if (!SFC.test(file)) return readsIn(file, code, file, [{ code, at: 0 }]);
|
|
261
|
+
const vue = file.endsWith('.vue');
|
|
262
|
+
const markup = blank(code, /<script\b[\s\S]*?<\/script>|<style\b[\s\S]*?<\/style>|<!--[\s\S]*?-->/g);
|
|
263
|
+
const scripts = scriptBlocks(code);
|
|
264
|
+
const expressions = vue ? vueExpressions(markup) : svelteExpressions(markup);
|
|
265
|
+
// Scripts and markup alike may be TypeScript.
|
|
266
|
+
return readsIn(file, code, 'component.ts', [...scripts, ...expressions], (names, report) => {
|
|
267
|
+
for (const tag of markupTexts(markup, names, vue)) report(tag.at, tag.end, 'Text');
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
type Report = (start: number, end: number, name: string) => void;
|
|
272
|
+
|
|
273
|
+
function readsIn(file: string, code: string, as: string, pieces: Piece[], markup?: (texts: Set<string>, report: Report) => void): Reads {
|
|
274
|
+
const programs = pieces.map((piece) => ({ at: piece.at, program: parseSync(as, piece.code).program as unknown as Node }));
|
|
245
275
|
const { position, ignored } = lineIndex(code);
|
|
246
276
|
const unseen: UnseenRead[] = [];
|
|
247
277
|
const aliases = new Set<string>();
|
|
248
278
|
/** Local name to the name it was imported by. */
|
|
249
279
|
const imported = new Map<string, string>();
|
|
250
280
|
const texts = new Set<string>();
|
|
251
|
-
for (const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
281
|
+
for (const { program } of programs) {
|
|
282
|
+
for (const statement of program.body as Node[]) {
|
|
283
|
+
if (statement.type !== 'ImportDeclaration') continue;
|
|
284
|
+
for (const s of statement.specifiers as Node[]) {
|
|
285
|
+
const name = s.type === 'ImportSpecifier' ? nameOf(s.imported as Node) : undefined;
|
|
286
|
+
const local = nameOf(s.local as Node);
|
|
287
|
+
if (name === undefined || local === undefined) continue;
|
|
288
|
+
if (name === 'Text' && TEXT_SOURCES.has(String((statement.source as Node).value))) texts.add(local);
|
|
289
|
+
else imported.set(local, name);
|
|
290
|
+
}
|
|
259
291
|
}
|
|
260
292
|
}
|
|
261
|
-
const report = (
|
|
262
|
-
const at = position(
|
|
293
|
+
const report: Report = (start, end, name) => {
|
|
294
|
+
const at = position(start);
|
|
263
295
|
if (ignored(at.line)) return;
|
|
264
|
-
const text = code.slice(
|
|
296
|
+
const text = code.slice(start, end).replace(/\s+/g, ' ');
|
|
265
297
|
unseen.push({ file, ...at, name, code: text.length > 80 ? `${text.slice(0, 77)}...` : text });
|
|
266
298
|
};
|
|
267
299
|
// Calls by a plain name, kept until every alias in the module is known.
|
|
268
|
-
const calls: [
|
|
269
|
-
const visit = (node: Node) => {
|
|
300
|
+
const calls: [start: number, end: number, name: string][] = [];
|
|
301
|
+
const visit = (node: Node, at: number) => {
|
|
270
302
|
if (node.type === 'ObjectPattern') {
|
|
271
303
|
for (const p of node.properties as Node[]) {
|
|
272
304
|
if (p.type === 'Property' && READERS.has(nameOf(p.key as Node) ?? '') && (p.value as Node).type === 'Identifier')
|
|
@@ -275,29 +307,118 @@ export function unseenReads(file: string, code: string): Reads {
|
|
|
275
307
|
}
|
|
276
308
|
if (node.type === 'CallExpression' && !keySeen((node.arguments as Node[])[0])) {
|
|
277
309
|
const callee = node.callee as Node;
|
|
278
|
-
if (callee.type === 'Identifier') calls.push([node, callee.name as string]);
|
|
310
|
+
if (callee.type === 'Identifier') calls.push([at + node.start, at + node.end, callee.name as string]);
|
|
279
311
|
else if (callee.type === 'MemberExpression' && !callee.computed && READERS.has(nameOf(callee.property as Node) ?? ''))
|
|
280
|
-
report(node, nameOf(callee.property as Node)!);
|
|
312
|
+
report(at + node.start, at + node.end, nameOf(callee.property as Node)!);
|
|
281
313
|
}
|
|
282
314
|
if (node.type === 'JSXOpeningElement' && texts.has(nameOf(node.name as Node) ?? '')) {
|
|
283
315
|
const id = (node.attributes as Node[]).find((a) => a.type === 'JSXAttribute' && nameOf(a.name as Node) === 'id');
|
|
284
316
|
const value = id?.value as Node | null | undefined;
|
|
285
317
|
if (value !== null && value !== undefined && !keySeen(value.type === 'JSXExpressionContainer' ? (value.expression as Node) : value))
|
|
286
|
-
report(node, 'Text');
|
|
318
|
+
report(at + node.start, at + node.end, 'Text');
|
|
287
319
|
}
|
|
288
320
|
for (const value of Object.values(node)) {
|
|
289
321
|
if (Array.isArray(value)) {
|
|
290
|
-
for (const child of value) if (isNode(child)) visit(child);
|
|
322
|
+
for (const child of value) if (isNode(child)) visit(child, at);
|
|
291
323
|
} else if (isNode(value)) {
|
|
292
|
-
visit(value);
|
|
324
|
+
visit(value, at);
|
|
293
325
|
}
|
|
294
326
|
}
|
|
295
327
|
};
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
328
|
+
for (const { program, at } of programs) visit(program, at);
|
|
329
|
+
markup?.(texts, report);
|
|
330
|
+
for (const [start, end, local] of calls) {
|
|
331
|
+
if (READERS.has(local) || aliases.has(local)) report(start, end, local);
|
|
332
|
+
else if (imported.has(local)) report(start, end, imported.get(local)!);
|
|
300
333
|
}
|
|
301
334
|
unseen.sort((a, b) => (a.line === b.line ? a.column - b.column : a.line - b.line));
|
|
302
335
|
return { unseen, aliases };
|
|
303
336
|
}
|
|
337
|
+
|
|
338
|
+
/** `code` with each match of `pattern` blanked to spaces, its lines kept, so offsets still point into the file. */
|
|
339
|
+
const blank = (code: string, pattern: RegExp) => code.replace(pattern, (m) => m.replace(/[^\n]/g, ' '));
|
|
340
|
+
|
|
341
|
+
/** A Vue template's expressions: `{{ ... }}`, and the values of bound attributes and directives. */
|
|
342
|
+
function vueExpressions(markup: string): Piece[] {
|
|
343
|
+
const pieces: Piece[] = [];
|
|
344
|
+
for (const m of markup.matchAll(/\{\{([\s\S]*?)\}\}/g)) pieces.push({ code: m[1]!, at: m.index + 2 });
|
|
345
|
+
for (const m of markup.matchAll(/\s((?::|@|v-)[^\s=>/"']*)\s*=\s*(["'])([\s\S]*?)\2/g)) {
|
|
346
|
+
// A loop's and a slot's values bind names; they read nothing.
|
|
347
|
+
if (/^v-(?:for|slot)\b/.test(m[1]!)) continue;
|
|
348
|
+
pieces.push({ code: m[3]!, at: m.index + m[0].length - m[3]!.length - 1 });
|
|
349
|
+
}
|
|
350
|
+
return pieces;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Where the `}` closing the `{` at `start` is, past strings and nested braces; -1 if none does. */
|
|
354
|
+
function closing(text: string, start: number): number {
|
|
355
|
+
let depth = 0;
|
|
356
|
+
for (let i = start; i < text.length; i++) {
|
|
357
|
+
const c = text[i]!;
|
|
358
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
359
|
+
const end = text.indexOf(c, i + 1);
|
|
360
|
+
if (end === -1) return -1;
|
|
361
|
+
i = end;
|
|
362
|
+
} else if (c === '{') {
|
|
363
|
+
depth++;
|
|
364
|
+
} else if (c === '}' && --depth === 0) {
|
|
365
|
+
return i;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return -1;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** A Svelte template's expressions: each `{...}`, a block's or tag's past its keyword. */
|
|
372
|
+
function svelteExpressions(markup: string): Piece[] {
|
|
373
|
+
const pieces: Piece[] = [];
|
|
374
|
+
for (let start = markup.indexOf('{'); start !== -1;) {
|
|
375
|
+
const end = closing(markup, start);
|
|
376
|
+
if (end === -1) break;
|
|
377
|
+
const at = start + 1;
|
|
378
|
+
const inner = markup.slice(at, end);
|
|
379
|
+
const tag = /^\s*([#:@/])(\w*)(\s+if\b)?/.exec(inner);
|
|
380
|
+
if (tag === null) {
|
|
381
|
+
// A spread, `{...props}`, reads as the object it spreads.
|
|
382
|
+
pieces.push({ code: inner.replace('...', ' '), at });
|
|
383
|
+
} else {
|
|
384
|
+
const [head, sigil, word, elseIf] = tag;
|
|
385
|
+
// `{/if}`, `{:else}` and `{:then x}` hold no expression, and `{#snippet}` names parameters.
|
|
386
|
+
const reads = sigil !== '/' && word !== 'snippet' && word !== 'then' && word !== 'catch' && (word !== 'else' || elseIf !== undefined);
|
|
387
|
+
// `{@const x = ...}` declares; `{#each xs as x}` and `{#await p then x}` bind names after the expression.
|
|
388
|
+
if (reads && word === 'const') pieces.push({ code: `var${inner.slice(head.length)}`, at: at + head.length - 3 });
|
|
389
|
+
else if (reads) pieces.push({ code: inner.slice(head.length).split(/\s+(?:as|then)\s/)[0]!, at: at + head.length });
|
|
390
|
+
}
|
|
391
|
+
start = markup.indexOf('{', end + 1);
|
|
392
|
+
}
|
|
393
|
+
return pieces;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Each `<Text>` in markup whose key the build can't see: Vue's `:id="..."`, Svelte's `id={...}`,
|
|
398
|
+
* `{id}` or `id="text {with} a placeholder"`. An `id` written as plain text is a key `keyRefs` reads.
|
|
399
|
+
*/
|
|
400
|
+
function markupTexts(markup: string, names: Set<string>, vue: boolean): { at: number; end: number }[] {
|
|
401
|
+
if (names.size === 0) return [];
|
|
402
|
+
const found: { at: number; end: number }[] = [];
|
|
403
|
+
const tags = new RegExp(`<(?:${[...names].join('|')})(?=[\\s/>])((?:[^>"']|"[^"]*"|'[^']*')*)>`, 'g');
|
|
404
|
+
for (const m of markup.matchAll(tags)) {
|
|
405
|
+
const attributes = m[1]!;
|
|
406
|
+
let id: string | undefined;
|
|
407
|
+
if (vue) {
|
|
408
|
+
id = /(?:^|\s)(?::|v-bind:)id\s*=\s*(["'])([\s\S]*?)\1/.exec(attributes)?.[2];
|
|
409
|
+
} else {
|
|
410
|
+
const bound = /(?:^|\s)id\s*=\s*\{/.exec(attributes);
|
|
411
|
+
if (bound !== null) {
|
|
412
|
+
const open = bound.index + bound[0].length - 1;
|
|
413
|
+
id = attributes.slice(open + 1, closing(attributes, open));
|
|
414
|
+
} else if (/(?:^|\s)\{\s*id\s*\}/.test(attributes) || /(?:^|\s)id\s*=\s*(["'])[^"']*\{/.test(attributes)) {
|
|
415
|
+
id = 'id';
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (id === undefined) continue;
|
|
419
|
+
const [statement] = parseSync('id.ts', `(${id})`).program.body as unknown as Node[];
|
|
420
|
+
if (!keySeen(statement?.type === 'ExpressionStatement' ? (statement.expression as Node) : undefined))
|
|
421
|
+
found.push({ at: m.index, end: m.index + m[0].length });
|
|
422
|
+
}
|
|
423
|
+
return found;
|
|
424
|
+
}
|
package/src/tools/todo.ts
CHANGED
|
@@ -27,7 +27,7 @@ export function todoOf(
|
|
|
27
27
|
locale: string,
|
|
28
28
|
prompt = false,
|
|
29
29
|
): Todo {
|
|
30
|
-
const wanted = catalog
|
|
30
|
+
const wanted = todoMessages(catalog, stale, locale);
|
|
31
31
|
if (wanted.length === 0) return { count: 0, text: `# ${locale} is complete` };
|
|
32
32
|
const byArea = new Map<string, string[]>();
|
|
33
33
|
for (const m of wanted) {
|
|
@@ -58,6 +58,11 @@ export function todoOf(
|
|
|
58
58
|
return { count, text: `${head}\n${entries}` };
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/** The entries a locale wants a translation of: those it lacks, and those gone stale. */
|
|
62
|
+
export function todoMessages(catalog: Catalog, stale: ReadonlyMap<string, StaleTranslation>, locale: string): Message[] {
|
|
63
|
+
return catalog.messages.filter((m) => !m.translations.has(locale) || stale.has(m.key));
|
|
64
|
+
}
|
|
65
|
+
|
|
61
66
|
function promptFor(config: ResolvedConfig, wanted: Message[], locale: string, anyStale: boolean): string {
|
|
62
67
|
const mf2 = config.messageFormat === 2;
|
|
63
68
|
const [target, source] = [nameOf(locale), nameOf(config.source)];
|
package/src/vite.ts
CHANGED
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
* code names: each locale module is swapped for its messages less the keys no module references
|
|
7
7
|
* (see `tools/scan.ts` for how a reference is found, and why it errs towards keeping).
|
|
8
8
|
*
|
|
9
|
+
* With `split`, a client's catalog has no messages of its own: each module naming an area's keys
|
|
10
|
+
* imports a module for that area, which the bundler places beside it, and which registers the area
|
|
11
|
+
* (`addArea`) and waits until every copy holds it in its locale. Areas are found by the same
|
|
12
|
+
* references pruning reads, so a read pruning can't see is one splitting can't place either. A lazy
|
|
13
|
+
* locale's part is a file per group of areas the bundler will put in one chunk (`tools/chunks.ts`),
|
|
14
|
+
* read from the source when the build starts, as an area loads before every module naming it has.
|
|
15
|
+
* The client build writes down the file each area loads from per lazy locale, and the server build
|
|
16
|
+
* after it gives its catalog that list (`areaFiles`), so a server render can link what it read.
|
|
17
|
+
*
|
|
9
18
|
* In dev an edit doesn't reload the page: each generated locale module accepts its own update and
|
|
10
19
|
* hands the new messages to every copy (`HotHook`), which re-renders in the locale and state it was in.
|
|
11
20
|
* The index accepts its own too, since its doc comments change with the source text, and reloads the
|
|
@@ -15,18 +24,39 @@
|
|
|
15
24
|
* inspector asks for through Vite's own `/__open-in-editor`, so the editor is whichever Vite already
|
|
16
25
|
* launches.
|
|
17
26
|
*/
|
|
18
|
-
import {
|
|
27
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
28
|
+
import { join, relative, resolve } from 'node:path';
|
|
19
29
|
import { fileURLToPath } from 'node:url';
|
|
20
30
|
|
|
21
31
|
import type { Plugin } from 'vite';
|
|
22
32
|
|
|
23
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
AREA,
|
|
35
|
+
areaModule,
|
|
36
|
+
areasNamed,
|
|
37
|
+
lazyAreas,
|
|
38
|
+
beside,
|
|
39
|
+
HOT_INDEX,
|
|
40
|
+
HOT_SET,
|
|
41
|
+
hotLocale,
|
|
42
|
+
importingAreas,
|
|
43
|
+
inAreas,
|
|
44
|
+
installInspector,
|
|
45
|
+
keeper,
|
|
46
|
+
membersOf,
|
|
47
|
+
splitIndex,
|
|
48
|
+
unseenMessage,
|
|
49
|
+
unseenOf,
|
|
50
|
+
type Groups,
|
|
51
|
+
type Prune,
|
|
52
|
+
} from './tools/bundle.ts';
|
|
24
53
|
import { CatalogError, type EntryLocation } from './tools/catalog.ts';
|
|
54
|
+
import { areaGroups } from './tools/chunks.ts';
|
|
25
55
|
import { compileLocale, localeExpression } from './tools/compile.ts';
|
|
26
56
|
import { loadConfig, type ProsebookConfig, type ResolvedConfig } from './tools/config.ts';
|
|
27
57
|
import { inspectorEndpoints, nodeMiddleware, redirect } from './tools/endpoints.ts';
|
|
28
|
-
import { generate, type Generated } from './tools/generate.ts';
|
|
29
|
-
import { keyRefs, unseenReads, type KeyRefs, type Reads } from './tools/scan.ts';
|
|
58
|
+
import { generate, writeFiles, type Generated } from './tools/generate.ts';
|
|
59
|
+
import { keyRefs, SFC, unseenReads, type KeyRefs, type Reads } from './tools/scan.ts';
|
|
30
60
|
|
|
31
61
|
export interface ProsebookPluginOptions {
|
|
32
62
|
/** The config, inline. Without it, `prosebook.config.*` at `root` is read. */
|
|
@@ -42,19 +72,37 @@ export interface ProsebookPluginOptions {
|
|
|
42
72
|
* pruned. `loose` prunes without that check; `false` ships every message.
|
|
43
73
|
*/
|
|
44
74
|
prune?: Prune;
|
|
45
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Keys a build always keeps, exactly or as `prefix.*`: for keys built at runtime from data. With
|
|
77
|
+
* `split`, their areas load with the catalog.
|
|
78
|
+
*/
|
|
46
79
|
keep?: string[];
|
|
80
|
+
/**
|
|
81
|
+
* Split each locale by area, so a lazy route loads only the messages it reads: a module that names
|
|
82
|
+
* an area's keys imports that area, which the bundler places beside it, and a lazy locale loads
|
|
83
|
+
* each area as it is needed. In the browser only: a server render loads whole locales, and its
|
|
84
|
+
* catalog lists each area's files, which a copy's `preload` hands on as the render reads them.
|
|
85
|
+
*/
|
|
86
|
+
split?: boolean;
|
|
47
87
|
/** In dev, Alt-click a string on the page to open its entry in the editor. On by default. */
|
|
48
88
|
inspect?: boolean;
|
|
49
89
|
}
|
|
50
90
|
|
|
51
91
|
const PLACEHOLDER = /__PROSEBOOK_LOCALE_(\d+)__/g;
|
|
52
|
-
const SCRIPT = /\.[cm]?[jt]sx
|
|
92
|
+
const SCRIPT = /\.(?:[cm]?[jt]sx?|vue|svelte)$/;
|
|
53
93
|
const DEV = 'virtual:prosebook/dev';
|
|
54
94
|
const DEV_ID = '\0prosebook/dev';
|
|
95
|
+
const AREA_ID = '\0prosebook/area/';
|
|
55
96
|
|
|
56
97
|
const normalize = (path: string) => resolve(path).replaceAll('\\', '/');
|
|
57
98
|
|
|
99
|
+
/** Where a split client build leaves its area files for the server build: apart from the source and the output. */
|
|
100
|
+
const areaFilesPath = (root: string) => join(root, 'node_modules', '.prosebook', 'area-files.json');
|
|
101
|
+
|
|
102
|
+
/** The index with `areaFiles` among its catalog's fields. */
|
|
103
|
+
const withAreaFiles = (index: string, files: unknown) =>
|
|
104
|
+
index.replace(/defineCatalog(?:<[^>]*>)?\(\{/, (call) => `${call}\n areaFiles: ${JSON.stringify(files)},`);
|
|
105
|
+
|
|
58
106
|
const inspectorPath = beside(import.meta.url, 'inspector');
|
|
59
107
|
// prosebook's own modules, which read keys handed to them: `<Text>` calls `rich(props.id)`.
|
|
60
108
|
const ownDir = normalize(fileURLToPath(new URL('.', import.meta.url)));
|
|
@@ -69,7 +117,9 @@ export function prosebook(options: ProsebookPluginOptions = {}): Plugin {
|
|
|
69
117
|
let refs: KeyRefs = { exact: new Set(), prefixes: new Set() };
|
|
70
118
|
const reads = new Map<string, Reads>();
|
|
71
119
|
const prune = options.prune ?? 'strict';
|
|
72
|
-
|
|
120
|
+
// What each placeholder stands for: a locale's messages, or with `split`, some areas' of them.
|
|
121
|
+
const placeholders: { locale: string; areas?: string[] }[] = [];
|
|
122
|
+
let groups: Groups = new Map();
|
|
73
123
|
const shipped = { total: 0, kept: 0 };
|
|
74
124
|
|
|
75
125
|
const describe = (e: unknown) => (e instanceof CatalogError ? e.message : e instanceof Error ? (e.stack ?? e.message) : String(e));
|
|
@@ -88,6 +138,10 @@ export function prosebook(options: ProsebookPluginOptions = {}): Plugin {
|
|
|
88
138
|
return path === `${out}/index.ts` || path === `${out}/index.js`;
|
|
89
139
|
};
|
|
90
140
|
|
|
141
|
+
// Split in the browser only: a server render's copies, one per request, load whole locales.
|
|
142
|
+
const splitting = (ssr: boolean | undefined) => options.split === true && ssr !== true && generated !== undefined;
|
|
143
|
+
const placeholder = (locale: string, areas?: string[]) => `__PROSEBOOK_LOCALE_${placeholders.push({ locale, areas }) - 1}__`;
|
|
144
|
+
|
|
91
145
|
return {
|
|
92
146
|
name: 'prosebook',
|
|
93
147
|
|
|
@@ -99,7 +153,9 @@ export function prosebook(options: ProsebookPluginOptions = {}): Plugin {
|
|
|
99
153
|
},
|
|
100
154
|
|
|
101
155
|
resolveId(id) {
|
|
102
|
-
|
|
156
|
+
if (id === DEV) return DEV_ID;
|
|
157
|
+
if (id.startsWith(AREA)) return AREA_ID + id.slice(AREA.length);
|
|
158
|
+
return null;
|
|
103
159
|
},
|
|
104
160
|
|
|
105
161
|
buildStart() {
|
|
@@ -109,8 +165,9 @@ export function prosebook(options: ProsebookPluginOptions = {}): Plugin {
|
|
|
109
165
|
this.error(describe(e));
|
|
110
166
|
}
|
|
111
167
|
refs = { exact: new Set(), prefixes: new Set() };
|
|
112
|
-
kept = keeper(refs, options.keep);
|
|
168
|
+
kept = prune === false ? () => true : keeper(refs, options.keep);
|
|
113
169
|
reads.clear();
|
|
170
|
+
groups = building && options.split === true && generated !== undefined ? areaGroups(config, generated, options.keep) : new Map();
|
|
114
171
|
placeholders.length = 0;
|
|
115
172
|
shipped.total = shipped.kept = 0;
|
|
116
173
|
},
|
|
@@ -123,6 +180,8 @@ export function prosebook(options: ProsebookPluginOptions = {}): Plugin {
|
|
|
123
180
|
if (!path.endsWith('.yaml') || !dirs.some((d) => path.startsWith(`${d}/`))) return;
|
|
124
181
|
try {
|
|
125
182
|
generated = generate(config);
|
|
183
|
+
// An area's module names the locales, which an edit may have added to.
|
|
184
|
+
for (const [id, mod] of server.moduleGraph.idToModuleMap) if (id.startsWith(AREA_ID)) server.moduleGraph.invalidateModule(mod);
|
|
126
185
|
server.config.logger.info(`prosebook: regenerated from ${relative(config.root, path).replaceAll('\\', '/')}`, {
|
|
127
186
|
timestamp: true,
|
|
128
187
|
});
|
|
@@ -142,16 +201,26 @@ export function prosebook(options: ProsebookPluginOptions = {}): Plugin {
|
|
|
142
201
|
server.middlewares.use(nodeMiddleware(inspectorEndpoints(config, { base, open: editor })));
|
|
143
202
|
},
|
|
144
203
|
|
|
145
|
-
load(id) {
|
|
204
|
+
load(id, loadOptions) {
|
|
146
205
|
if (id === DEV_ID) {
|
|
147
206
|
if (options.inspect === false) return HOT_SET;
|
|
148
207
|
return HOT_SET + installInspector(inspectorPath, base);
|
|
149
208
|
}
|
|
209
|
+
if (id.startsWith(AREA_ID) && generated !== undefined) {
|
|
210
|
+
const [area = '', locale] = id.slice(AREA_ID.length).split('/');
|
|
211
|
+
if (locale !== undefined) return `export default ${placeholder(locale, membersOf(groups, area))};\n`;
|
|
212
|
+
return areaModule(config, generated, area, building ? (l) => placeholder(l, [area]) : undefined, undefined, groups);
|
|
213
|
+
}
|
|
214
|
+
if (splitting(loadOptions?.ssr) && isIndex(id)) return splitIndex(config, generated!, options.keep);
|
|
215
|
+
if (building && options.split === true && loadOptions?.ssr === true && isIndex(id)) {
|
|
216
|
+
const path = areaFilesPath(viteRoot);
|
|
217
|
+
if (existsSync(path)) return withAreaFiles(readFileSync(id.split('?', 1)[0]!, 'utf8'), JSON.parse(readFileSync(path, 'utf8')));
|
|
218
|
+
this.warn('no split client build to link area files from: build the client before the server, or `preload` is handed nothing');
|
|
219
|
+
}
|
|
150
220
|
if (!building || prune === false || generated === undefined) return null;
|
|
151
221
|
const locale = localeOf(id);
|
|
152
222
|
if (locale === undefined) return null;
|
|
153
|
-
|
|
154
|
-
return `export default __PROSEBOOK_LOCALE_${placeholders.length - 1}__;\n`;
|
|
223
|
+
return `export default ${placeholder(locale)};\n`;
|
|
155
224
|
},
|
|
156
225
|
|
|
157
226
|
// Before other plugins, so a read is found in the source as written: `<Text id>` still JSX, and
|
|
@@ -159,21 +228,31 @@ export function prosebook(options: ProsebookPluginOptions = {}): Plugin {
|
|
|
159
228
|
transform: {
|
|
160
229
|
order: 'pre',
|
|
161
230
|
handler(code, id, transformOptions) {
|
|
231
|
+
const ssr = transformOptions?.ssr;
|
|
162
232
|
if (!building) {
|
|
163
233
|
const locale = localeOf(id);
|
|
164
234
|
if (locale !== undefined) return { code: code + hotLocale(locale), map: null };
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
235
|
+
if (isIndex(id)) {
|
|
236
|
+
// A server render has no page to inspect, and its copies are made per request.
|
|
237
|
+
const dev = ssr === true ? '' : `import ${JSON.stringify(DEV)};\n`;
|
|
238
|
+
return { code: dev + code + HOT_INDEX, map: null };
|
|
239
|
+
}
|
|
240
|
+
// The index's `Messages` type names every key, and is read here before its types are stripped.
|
|
241
|
+
} else if (localeOf(id) !== undefined || isIndex(id)) {
|
|
242
|
+
return null;
|
|
169
243
|
}
|
|
170
|
-
if (
|
|
171
|
-
// The index's `Messages` type names every key, and is read here before its types are stripped.
|
|
172
|
-
if (localeOf(id) !== undefined || isIndex(id)) return null;
|
|
173
|
-
keyRefs(code, refs);
|
|
244
|
+
if (id.includes('/node_modules/') || id.startsWith('\0')) return null;
|
|
174
245
|
const path = id.split('?', 1)[0]!;
|
|
175
|
-
|
|
176
|
-
|
|
246
|
+
// A component file is read whole, as written; the parts its compiler splits off (`?vue&type=script`) are not again.
|
|
247
|
+
const part = SFC.test(path) && path !== id;
|
|
248
|
+
const own = normalize(path).startsWith(`${ownDir}/`);
|
|
249
|
+
if (building && prune !== false) {
|
|
250
|
+
keyRefs(code, refs);
|
|
251
|
+
if (prune === 'strict' && SCRIPT.test(path) && !part && !own) reads.set(id, unseenReads(path, code));
|
|
252
|
+
}
|
|
253
|
+
if (!splitting(ssr) || !SCRIPT.test(path) || part || own) return null;
|
|
254
|
+
const areas = areasNamed(code, lazyAreas(generated!, options.keep));
|
|
255
|
+
return areas.length === 0 ? null : { code: importingAreas(code, path, areas), map: null };
|
|
177
256
|
},
|
|
178
257
|
},
|
|
179
258
|
|
|
@@ -188,8 +267,9 @@ export function prosebook(options: ProsebookPluginOptions = {}): Plugin {
|
|
|
188
267
|
if (!code.includes('__PROSEBOOK_LOCALE_') || generated === undefined) return null;
|
|
189
268
|
const entries = generated.entries;
|
|
190
269
|
const replaced = code.replace(PLACEHOLDER, (_match, index: string) => {
|
|
191
|
-
const locale = placeholders[Number(index)]!;
|
|
192
|
-
const
|
|
270
|
+
const { locale, areas } = placeholders[Number(index)]!;
|
|
271
|
+
const all = entries.get(locale) ?? [];
|
|
272
|
+
const list = areas === undefined ? all : inAreas(all, areas);
|
|
193
273
|
const compiled = compileLocale(list, kept);
|
|
194
274
|
shipped.total += list.length;
|
|
195
275
|
shipped.kept += compiled.rows.length;
|
|
@@ -198,11 +278,24 @@ export function prosebook(options: ProsebookPluginOptions = {}): Plugin {
|
|
|
198
278
|
return { code: replaced, map: null };
|
|
199
279
|
},
|
|
200
280
|
|
|
201
|
-
generateBundle() {
|
|
281
|
+
generateBundle(_output, bundle) {
|
|
282
|
+
if (building && options.split === true && this.environment.config.consumer === 'client') {
|
|
283
|
+
// Each lazy locale's part of a group, for the areas in it that locale translates.
|
|
284
|
+
const files: Record<string, Record<string, string>> = {};
|
|
285
|
+
for (const chunk of Object.values(bundle)) {
|
|
286
|
+
if (chunk.type !== 'chunk' || chunk.facadeModuleId?.startsWith(AREA_ID) !== true) continue;
|
|
287
|
+
const [lead = '', locale] = chunk.facadeModuleId.slice(AREA_ID.length).split('/');
|
|
288
|
+
if (locale === undefined) continue;
|
|
289
|
+
const own = generated?.entries.get(locale) ?? [];
|
|
290
|
+
for (const area of membersOf(groups, lead))
|
|
291
|
+
if (inAreas(own, [area]).length > 0) (files[area] ??= {})[locale] = base + chunk.fileName;
|
|
292
|
+
}
|
|
293
|
+
writeFiles(new Map([[areaFilesPath(viteRoot), `${JSON.stringify(files, null, 2)}\n`]]));
|
|
294
|
+
}
|
|
202
295
|
if (shipped.total === 0) return;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
);
|
|
296
|
+
const locales = new Set(placeholders.map((p) => p.locale)).size;
|
|
297
|
+
const pruned = prune === false ? '' : `; pruned ${shipped.total - shipped.kept} no code names`;
|
|
298
|
+
this.info(`shipped ${shipped.kept} of ${shipped.total} messages across ${locales} locale(s)${pruned}`);
|
|
206
299
|
},
|
|
207
300
|
};
|
|
208
301
|
}
|