@menapo/vi18n 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/LICENSE +49 -0
  2. package/README.md +110 -2
  3. package/dist/apply-Bre-lY9J.js +228 -0
  4. package/dist/captured-Bj8bO6vd.js +1034 -0
  5. package/dist/check-BIKEuu6B.js +201 -0
  6. package/dist/check-HsmmpJZg.js +1 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +41 -0
  9. package/dist/codec-DuvU2VQy.js +207 -0
  10. package/dist/collect-Cx4_SKOe.js +13 -0
  11. package/dist/collisions-CfpZU8e4.js +217 -0
  12. package/dist/compile-BaDh0cHG.js +245 -0
  13. package/dist/compile-Cc97JaW2.js +2502 -0
  14. package/dist/concurrency-C9UUWuDC.js +96 -0
  15. package/dist/concurrency-DONL7t4V.js +1 -0
  16. package/dist/config.d.ts +21 -0
  17. package/dist/config.js +39 -0
  18. package/dist/dir-zw6bGMKQ.js +290 -0
  19. package/dist/duplicates-yuRljQ6h.js +185 -0
  20. package/dist/engine-C2ENBRJe.js +531 -0
  21. package/dist/engine.d.ts +43 -0
  22. package/dist/engine.js +1241 -0
  23. package/dist/exclude-Du-Zd_qI.js +26 -0
  24. package/dist/export-C9Sy6UWv.js +103 -0
  25. package/dist/extract-C3S8btx0.js +298 -0
  26. package/dist/formats-9unqj_fZ.js +781 -0
  27. package/dist/i18next-9HAoxwUI.js +1 -0
  28. package/dist/i18next-BEJgv_S8.js +1596 -0
  29. package/dist/import-nANvQutw.js +225 -0
  30. package/dist/index.d.ts +156 -0
  31. package/dist/index.js +239 -0
  32. package/dist/init-C2ScuW6a.js +792 -0
  33. package/dist/library-D5CWvTQs.js +1882 -0
  34. package/dist/lingui-zUiCl9Um.js +833 -0
  35. package/dist/lint.d.ts +105 -0
  36. package/dist/lint.js +971 -0
  37. package/dist/load-CQRz4PpM.js +2950 -0
  38. package/dist/load-gW-oJt1Q.js +747 -0
  39. package/dist/locales-DX4MOBFE.js +827 -0
  40. package/dist/lsp-RMMMc88P.js +31 -0
  41. package/dist/match-BGr8EXB3.js +89 -0
  42. package/dist/mcp-CHVwBoRP.js +28 -0
  43. package/dist/merge-driver-BTX87aVm.js +422 -0
  44. package/dist/messages-BfjIExyB.js +166 -0
  45. package/dist/migrate-IpLDMXmq.js +1484 -0
  46. package/dist/migrate-rCQuOGsf.js +1 -0
  47. package/dist/openai-CTaMDcl-.js +346 -0
  48. package/dist/output-DWE3SIUA.js +164 -0
  49. package/dist/parse.d.ts +62 -0
  50. package/dist/parse.js +156 -0
  51. package/dist/pipeline-C9Dbotkh.js +2031 -0
  52. package/dist/pipeline-DbX6rlo5.js +1 -0
  53. package/dist/project-Bw9UPIdx.js +1046 -0
  54. package/dist/project-C4GpaN-n.js +381 -0
  55. package/dist/pseudo-DRv30iN5.js +145 -0
  56. package/dist/raw-C4Y6vyGY.js +4708 -0
  57. package/dist/react-intl-BP0u-D5m.js +519 -0
  58. package/dist/restore-CiYv-jxS.js +175 -0
  59. package/dist/rewrite-EsIw_lsq.js +803 -0
  60. package/dist/rich-DCUKl5HM.js +97 -0
  61. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  62. package/dist/run-BEcnBbu-.js +694 -0
  63. package/dist/runtime-DTMmliT1.js +64 -0
  64. package/dist/scan-worker.d.ts +62 -0
  65. package/dist/scan-worker.js +1 -0
  66. package/dist/scoped-BMYpSthj.js +44 -0
  67. package/dist/server-Da1Ot9IN.js +10485 -0
  68. package/dist/server-deBOq2vq.js +11823 -0
  69. package/dist/server.d.ts +169 -0
  70. package/dist/server.js +1234 -0
  71. package/dist/sitemap-DJXzKoza.js +159 -0
  72. package/dist/sources-D19czDSA.js +2242 -0
  73. package/dist/status-zr4jY59Y.js +149 -0
  74. package/dist/stdio-CVaJfxA7.js +29 -0
  75. package/dist/stdio-FPOKQAgu.js +95 -0
  76. package/dist/sync-B9V2KFwb.js +658 -0
  77. package/dist/t-DC-c9zE5.js +454 -0
  78. package/dist/test.d.ts +116 -0
  79. package/dist/test.js +319 -0
  80. package/dist/translate-CQnJfLjO.js +299 -0
  81. package/dist/ts-BaU5yOf0.js +4863 -0
  82. package/dist/types-CgUCkFU1.d.ts +707 -0
  83. package/dist/types-D3QGM8XO.js +146 -0
  84. package/dist/types-DsdWBG0b.d.ts +558 -0
  85. package/dist/types-DtvQ7UdD.js +1948 -0
  86. package/dist/types-wSh9fCBY.js +11 -0
  87. package/dist/units-CQ5Q99RC.js +150 -0
  88. package/dist/url-CRfCOhUI.js +172 -0
  89. package/dist/url-CsBkRaJy.js +225 -0
  90. package/dist/validate-BxpupdAf.js +2615 -0
  91. package/dist/validate-D6DJepZv.js +365 -0
  92. package/dist/version-Byqi2guX.js +28 -0
  93. package/dist/vite-scan-CT_vsp4g.js +242 -0
  94. package/dist/vite.d.ts +34 -0
  95. package/dist/vite.js +7558 -0
  96. package/dist/vue-i18n-g0iGy6CZ.js +651 -0
  97. package/dist/which-BOemZ5Q8.js +116 -0
  98. package/dist/worker-84U6uYB3.js +1656 -0
  99. package/package.json +97 -3
  100. package/schemas/catalog.json +57 -0
@@ -0,0 +1,792 @@
1
+ import { o as isDiagnosticError, t as CONFIG_FILE } from "./types-D3QGM8XO.js";
2
+ import { i as idScheme } from "./validate-BxpupdAf.js";
3
+ import { b as emptyCatalog, o as catalogLayout, p as serializeCatalog, s as catalogPath } from "./library-D5CWvTQs.js";
4
+ import { n as loadConfig, r as resolveConfig } from "./load-gW-oJt1Q.js";
5
+ import { t as VERSION } from "./version-Byqi2guX.js";
6
+ import { l as langFromId, u as parseModule } from "./worker-84U6uYB3.js";
7
+ import { a as writeGenFile, r as GEN_FILE } from "./sync-B9V2KFwb.js";
8
+ import { t as EXIT } from "./types-wSh9fCBY.js";
9
+ import { t as CliExit } from "./project-C4GpaN-n.js";
10
+ import { createRequire } from "node:module";
11
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
12
+ import { randomBytes } from "node:crypto";
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
15
+ //#region src/cli/scaffold.ts
16
+ /**
17
+ * The `<T>` components `vi18n init --rich` copies into a project (PLAN §4.1,
18
+ * §5.16; INTERNALS §6 D213): Vue (one scoped slot per tag) and Svelte 5
19
+ * (one snippet per tag). They are the user's code from then on, so nothing
20
+ * framework-specific is published; listing the file in
21
+ * `extraction.importSources` makes the scanner extract `<T message="…">`
22
+ * (§2.4 "Rich-text components"). Both call `rich*()` while they render, so
23
+ * the generated framework tracker re-renders them on a locale switch.
24
+ */
25
+ const VUE_IMPORTS = [`import { rich, type MessageKey } from "@menapo/vi18n";`, `import { Fragment, h, useSlots, type VNode, type VNodeArrayChildren } from "vue";`].join("\n");
26
+ const SVELTE_IMPORTS = [` import { richParts, type MessageKey, type RichPart, type Values } from "@menapo/vi18n";`, ` import type { Snippet } from "svelte";`].join("\n");
27
+ /** Vue 3.5: `<T message="Read the <link>docs</link>"><template #link="{ children }">…`. */
28
+ const T_VUE = `<script setup lang="ts">
29
+ // Scaffolded by \`vi18n init --rich\` (@menapo/vi18n). It is your code now: edit it freely.
30
+ // A named slot renders each tag; render its children with <component :is="children" />:
31
+ //
32
+ // <T message="Read the <link>docs</link>">
33
+ // <template #link="{ children }"><a href="/docs"><component :is="children" /></a></template>
34
+ // </T>
35
+ //
36
+ // The file is listed in extraction.importSources, so \`message\` (a literal) is extracted
37
+ // like a rich() call; \`id\`, \`context\` and \`maxLength\` are read at build time only.
38
+ ${VUE_IMPORTS}
39
+
40
+ // Vue checks props at runtime, so their types stay ones it can read: a message is the
41
+ // text you write, or the id or dev descriptor the plugin puts in its place.
42
+ const props = defineProps<{
43
+ message: string | object;
44
+ values?: Record<string, unknown>;
45
+ id?: string;
46
+ context?: string;
47
+ maxLength?: number;
48
+ }>();
49
+ const slots = useSlots();
50
+
51
+ // A component declared once: rich() runs in its render, which the plugin's Vue tracker
52
+ // re-runs on a locale switch, and an inline \`:is="() => …"\` would remount every render.
53
+ const Message = () =>
54
+ rich<VNode | VNode[] | undefined>(props.message as MessageKey, {
55
+ ...props.values,
56
+ ...Object.fromEntries(
57
+ Object.entries(slots).map(([tag, slot]) => [
58
+ tag,
59
+ (children: VNodeArrayChildren) => slot?.({ children: h(Fragment, children) }),
60
+ ]),
61
+ ),
62
+ });
63
+ <\/script>
64
+
65
+ <template><Message /></template>
66
+ `;
67
+ /** Svelte 5 (runes): \`<T message="…">{#snippet link(children)}…{/snippet}</T>\`. */
68
+ const T_SVELTE = `<script lang="ts">
69
+ // Scaffolded by \`vi18n init --rich\` (@menapo/vi18n). It is your code now: edit it freely.
70
+ // A snippet renders each tag and receives the tag's children as a snippet:
71
+ //
72
+ // <T message="Read the <link>docs</link>">
73
+ // {#snippet link(children)}<a href="/docs">{@render children()}</a>{/snippet}
74
+ // </T>
75
+ //
76
+ // The file is listed in extraction.importSources, so \`message\` (a literal) is extracted
77
+ // like a rich() call. In Svelte markup \`{\` starts an expression: write a message with
78
+ // arguments as a string, message={"Hello {name}"}, and pass values={{ name }}.
79
+ ${SVELTE_IMPORTS}
80
+
81
+ let {
82
+ message,
83
+ values,
84
+ ...tags
85
+ }: {
86
+ message: MessageKey;
87
+ values?: Values;
88
+ id?: string;
89
+ context?: string;
90
+ maxLength?: number;
91
+ [tag: string]: unknown;
92
+ } = $props();
93
+ <\/script>
94
+
95
+ <!-- richParts() runs in the template, so the plugin's Svelte tracker re-renders on a switch. -->
96
+ {#snippet render(parts: RichPart[])}
97
+ {#each parts as part}
98
+ {#if typeof part === "string"}{part}{:else}
99
+ {@const tag = tags[part.tag] as Snippet<[Snippet]> | undefined}
100
+ {#snippet children()}{@render render(part.children)}{/snippet}
101
+ {#if tag}{@render tag(children)}{:else}{@render children()}{/if}
102
+ {/if}
103
+ {/each}
104
+ {/snippet}
105
+
106
+ {@render render(richParts(message, values))}
107
+ `;
108
+ //#endregion
109
+ //#region src/cli/commands/init.ts
110
+ /**
111
+ * `vi18n init [--yes] [--format po|json] [--locales a,b] [--source-locale a] [--rich]`
112
+ * (INTERNALS §2.14, PLAN §5.19): the way into a project. Writes
113
+ * `vi18n.config.ts` (never over an existing one), adds the plugin to the
114
+ * exported config's `plugins` in `vite.config.*`, creates every locale's
115
+ * empty catalog, writes `vi18n.gen.ts` and lists it in the tsconfig that
116
+ * compiles the sources. `--rich` copies the `<T>` components for the
117
+ * project's Vue and Svelte and lists them in `extraction.importSources`
118
+ * (§6 D213); in a configured project it does only that. Each step
119
+ * that cannot edit a file safely prints the snippet instead.
120
+ */
121
+ const VITE_CONFIGS = [
122
+ "vite.config.ts",
123
+ "vite.config.mts",
124
+ "vite.config.js",
125
+ "vite.config.mjs",
126
+ "vite.config.cts",
127
+ "vite.config.cjs"
128
+ ];
129
+ const PLUGIN_IMPORT = "import vi18n from \"@menapo/vi18n/vite\";";
130
+ /** Source directories of the supported frameworks, in the order `init` looks for them. */
131
+ const SOURCE_DIRS = ["src", "app"];
132
+ /** Markers `findConfigFile` stops at; `init` does not look for a tsconfig above them either. */
133
+ const WORKSPACE_MARKERS = ["pnpm-workspace.yaml", ".git"];
134
+ async function init(ctx, flags) {
135
+ const dir = resolve(ctx.global.cwd);
136
+ const configFile = ctx.global.config === null ? join(dir, CONFIG_FILE) : resolve(dir, ctx.global.config);
137
+ const root = dirname(configFile);
138
+ if (flags.rich && existsSync(configFile)) return scaffoldOnly(ctx, root, configFile);
139
+ const input = configInput(await ask(ctx, flags), root);
140
+ const components = flags.rich ? richComponents(root) : [];
141
+ if (flags.rich && components.length === 0) throw noRichFramework();
142
+ if (components.length > 0) input.extraction = { importSources: components.map((c) => c.specifier) };
143
+ let config;
144
+ try {
145
+ config = resolveConfig(input, root, configFile);
146
+ } catch (error) {
147
+ if (isDiagnosticError(error)) throw new CliExit(EXIT.usage, [error.diagnostic]);
148
+ throw error;
149
+ }
150
+ const data = {
151
+ wrote: [],
152
+ skipped: [],
153
+ manual: [],
154
+ sourceLocale: config.sourceLocale,
155
+ locales: config.locales,
156
+ format: config.catalogs.format
157
+ };
158
+ const rel = (file) => relative(root, file).split(sep).join("/") || ".";
159
+ if (existsSync(configFile)) {
160
+ ctx.out.report([{
161
+ code: "config-invalid",
162
+ severity: "error",
163
+ message: `${rel(configFile)} already exists; vi18n init never overwrites it (edit it, or run vi18n extract)`,
164
+ file: configFile
165
+ }]);
166
+ return {
167
+ exitCode: EXIT.failure,
168
+ data
169
+ };
170
+ }
171
+ await atomicWrite(configFile, configSource(input));
172
+ data.wrote.push(rel(configFile));
173
+ if (flags.rich) await writeComponents(root, components, data, rel);
174
+ await addPlugin(root, data, rel);
175
+ const layout = catalogLayout(config);
176
+ const header = {
177
+ idScheme: idScheme(config.ids.length),
178
+ sourceLocale: config.sourceLocale
179
+ };
180
+ for (const locale of config.locales) {
181
+ const path = catalogPath(config, locale);
182
+ if (existsSync(path)) {
183
+ data.skipped.push(rel(path));
184
+ continue;
185
+ }
186
+ const isSourceLocale = locale === config.sourceLocale;
187
+ const text = serializeCatalog(emptyCatalog(locale, layout, header), {
188
+ isSourceLocale,
189
+ version: VERSION
190
+ });
191
+ await mkdir(dirname(path), { recursive: true });
192
+ await atomicWrite(path, text);
193
+ data.wrote.push(rel(path));
194
+ }
195
+ const gen = join(root, GEN_FILE);
196
+ if (await writeGenFile(config)) data.wrote.push(rel(gen));
197
+ await addToTsconfig(root, gen, data, rel);
198
+ printSummary(ctx, data);
199
+ ctx.out.result(`\nNext: wrap strings in t("…") and run vi18n extract.`);
200
+ return {
201
+ exitCode: EXIT.ok,
202
+ data
203
+ };
204
+ }
205
+ function printSummary(ctx, data) {
206
+ for (const file of data.wrote) ctx.out.result(`created ${file}`);
207
+ for (const file of data.skipped) ctx.out.result(`kept ${file}`);
208
+ for (const { file, snippet } of data.manual) ctx.out.result(`\nAdd to ${file} by hand:\n${snippet}`);
209
+ }
210
+ /**
211
+ * One component per framework the root `package.json` depends on, beside
212
+ * the project's other shared components: `<src>/lib` (SvelteKit's `$lib`)
213
+ * when it exists, else `<src>/components`. React, Preact and Solid get
214
+ * none: `rich()` returns what JSX renders, and the `<T>` macro is compiled
215
+ * away (§6 D213).
216
+ */
217
+ function richComponents(root) {
218
+ let deps = {};
219
+ try {
220
+ const pkg = JSON.parse(readFileSyncSafe(join(root, "package.json")));
221
+ deps = {
222
+ ...pkg.dependencies,
223
+ ...pkg.devDependencies
224
+ };
225
+ } catch {}
226
+ const sourceDir = SOURCE_DIRS.find((name) => existsSync(join(root, name)));
227
+ const base = sourceDir === void 0 ? "" : sourceDir + "/";
228
+ const dir = existsSync(join(root, base + "lib")) ? base + "lib" : base + "components";
229
+ const out = [];
230
+ if ("vue" in deps) out.push({
231
+ file: `${dir}/T.vue`,
232
+ specifier: `./${dir}/T.vue`,
233
+ source: T_VUE
234
+ });
235
+ if ("svelte" in deps) out.push({
236
+ file: `${dir}/T.svelte`,
237
+ specifier: `./${dir}/T.svelte`,
238
+ source: T_SVELTE
239
+ });
240
+ return out;
241
+ }
242
+ function noRichFramework() {
243
+ return usage("--rich copies a <T> component for Vue and Svelte, and package.json depends on neither; in JSX, rich() and the <T> macro need no component");
244
+ }
245
+ function readFileSyncSafe(file) {
246
+ return existsSync(file) ? readFileSync(file, "utf8") : "";
247
+ }
248
+ /** Copies the components, never over a file that exists. */
249
+ async function writeComponents(root, components, data, rel) {
250
+ for (const component of components) {
251
+ const path = join(root, component.file);
252
+ if (existsSync(path)) {
253
+ data.skipped.push(rel(path));
254
+ continue;
255
+ }
256
+ await mkdir(dirname(path), { recursive: true });
257
+ await atomicWrite(path, component.source);
258
+ data.wrote.push(rel(path));
259
+ }
260
+ }
261
+ /**
262
+ * `--rich` in a configured project: copy the components and, unless the
263
+ * config already names them, print the `importSources` entry to add (the
264
+ * config is the user's code, and `init` edits only what it can parse safely).
265
+ */
266
+ async function scaffoldOnly(ctx, root, configFile) {
267
+ const rel = (file) => relative(root, file).split(sep).join("/") || ".";
268
+ let config;
269
+ try {
270
+ config = await loadConfig({
271
+ cwd: root,
272
+ configFile
273
+ });
274
+ } catch (error) {
275
+ if (isDiagnosticError(error)) throw new CliExit(EXIT.usage, [error.diagnostic]);
276
+ throw error;
277
+ }
278
+ const data = {
279
+ wrote: [],
280
+ skipped: [],
281
+ manual: [],
282
+ sourceLocale: config.sourceLocale,
283
+ locales: config.locales,
284
+ format: config.catalogs.format
285
+ };
286
+ const components = richComponents(root);
287
+ if (components.length === 0) throw noRichFramework();
288
+ await writeComponents(root, components, data, rel);
289
+ const text = readFileSyncSafe(configFile);
290
+ const missing = components.filter((c) => !text.includes(c.specifier.slice(2)));
291
+ if (missing.length > 0) data.manual.push({
292
+ file: rel(configFile),
293
+ snippet: `extraction: { importSources: [${missing.map((c) => JSON.stringify(c.specifier)).join(", ")}] },`
294
+ });
295
+ printSummary(ctx, data);
296
+ return {
297
+ exitCode: EXIT.ok,
298
+ data
299
+ };
300
+ }
301
+ /**
302
+ * Flags first; questions only for what they leave open, only on a terminal,
303
+ * and never with `--yes` or `--json` (an agent or CI must not hang).
304
+ */
305
+ async function ask(ctx, flags) {
306
+ const prompt = flags.yes || ctx.global.json ? void 0 : ctx.io.prompt;
307
+ let locales = flags.locales;
308
+ let sourceLocale = flags.sourceLocale;
309
+ let format = flags.format;
310
+ if (prompt !== void 0) {
311
+ if (sourceLocale === null) sourceLocale = (await prompt("Source locale (the language of the strings in your code)", locales[0] ?? "en")).trim();
312
+ if (locales.length === 0) locales = (await prompt("Locales, comma-separated", sourceLocale || "en")).split(",").map((l) => l.trim()).filter((l) => l !== "");
313
+ if (format === null) {
314
+ const answer = (await prompt("Catalog format (po or json)", "po")).trim().toLowerCase();
315
+ if (answer !== "po" && answer !== "json") throw usage(`catalog format must be po or json, not "${answer}"`);
316
+ format = answer;
317
+ }
318
+ }
319
+ sourceLocale = sourceLocale || locales[0] || "en";
320
+ if (!locales.includes(sourceLocale)) locales = [sourceLocale, ...locales];
321
+ for (const locale of locales) {
322
+ let canonical;
323
+ try {
324
+ canonical = Intl.getCanonicalLocales(locale)[0];
325
+ } catch {
326
+ canonical = void 0;
327
+ }
328
+ if (canonical !== locale) throw usage(canonical === void 0 ? `"${locale}" is not a BCP 47 locale` : `"${locale}" is not in canonical form; use "${canonical}"`);
329
+ }
330
+ return {
331
+ sourceLocale,
332
+ locales: [...new Set(locales)],
333
+ format: format ?? "po"
334
+ };
335
+ }
336
+ function usage(message) {
337
+ return new CliExit(EXIT.usage, [{
338
+ code: "config-invalid",
339
+ severity: "error",
340
+ message
341
+ }]);
342
+ }
343
+ /** Only what differs from the defaults, so the file stays a short starting point. */
344
+ function configInput(answers, root) {
345
+ const input = {
346
+ sourceLocale: answers.sourceLocale,
347
+ locales: answers.locales
348
+ };
349
+ if (answers.format === "json") input.catalogs = { format: "json" };
350
+ const sourceDir = SOURCE_DIRS.find((name) => existsSync(join(root, name)));
351
+ if (sourceDir === void 0) input.project = { include: ["."] };
352
+ else if (sourceDir !== "src") input.project = { include: [sourceDir] };
353
+ return input;
354
+ }
355
+ function configSource(input) {
356
+ const lines = [
357
+ "import { defineConfig } from \"@menapo/vi18n/config\";",
358
+ "",
359
+ "export default defineConfig({",
360
+ ` sourceLocale: ${JSON.stringify(input.sourceLocale)},`,
361
+ ` locales: [${(input.locales ?? []).map((l) => JSON.stringify(l)).join(", ")}],`
362
+ ];
363
+ if (input.catalogs?.format === "json") lines.push(" catalogs: { format: \"json\" },");
364
+ if (input.project?.include !== void 0) lines.push(` project: { include: [${input.project.include.map((p) => JSON.stringify(p)).join(", ")}] },`);
365
+ const sources = input.extraction?.importSources;
366
+ if (sources !== void 0) lines.push(` extraction: { importSources: [${sources.map((p) => JSON.stringify(p)).join(", ")}] },`);
367
+ lines.push("});", "");
368
+ return lines.join("\n");
369
+ }
370
+ /**
371
+ * Adds the import after the last top-level import and `vi18n()` as the first
372
+ * element of the exported config's `plugins` array. Anything less certain
373
+ * (no array, a config built by a helper, CommonJS) prints the snippet
374
+ * instead of guessing.
375
+ */
376
+ async function addPlugin(root, data, rel) {
377
+ const file = VITE_CONFIGS.map((name) => join(root, name)).find((path) => existsSync(path));
378
+ const snippet = `${PLUGIN_IMPORT}\n\n// in defineConfig({ … }):\nplugins: [vi18n(), /* … */],`;
379
+ if (file === void 0) {
380
+ data.manual.push({
381
+ file: "vite.config.ts",
382
+ snippet
383
+ });
384
+ return;
385
+ }
386
+ const text = await readFile(file, "utf8");
387
+ if (text.includes("@menapo/vi18n/vite")) {
388
+ data.skipped.push(rel(file));
389
+ return;
390
+ }
391
+ const edited = withPlugin(text, file);
392
+ if (edited === null) {
393
+ data.manual.push({
394
+ file: rel(file),
395
+ snippet
396
+ });
397
+ return;
398
+ }
399
+ await atomicWrite(file, edited);
400
+ data.wrote.push(rel(file));
401
+ }
402
+ function unwrap(node) {
403
+ let current = node;
404
+ while (current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSNonNullExpression" || current.type === "ParenthesizedExpression") current = current.expression;
405
+ return current;
406
+ }
407
+ /**
408
+ * The object literal a config module exports: `export default {…}`,
409
+ * `defineConfig({…})`, `defineConfig(() => ({…}))`, a function returning
410
+ * one, or a top-level `const` holding any of these. Null for anything else.
411
+ */
412
+ function exportedConfig(body) {
413
+ const declared = (name) => {
414
+ for (const statement of body) {
415
+ const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
416
+ if (declaration?.type !== "VariableDeclaration") continue;
417
+ for (const declarator of declaration.declarations) {
418
+ const id = declarator.id;
419
+ if (id.type === "Identifier" && id.name === name && declarator.init != null) return declarator.init;
420
+ }
421
+ }
422
+ return null;
423
+ };
424
+ const returned = (block) => {
425
+ let found = null;
426
+ for (const statement of block.body) if (statement.type === "ReturnStatement" && statement.argument != null) found = statement.argument;
427
+ return found;
428
+ };
429
+ const resolveNode = (node, depth) => {
430
+ if (node === null || depth > 6) return null;
431
+ const n = unwrap(node);
432
+ switch (n.type) {
433
+ case "ObjectExpression": return n;
434
+ case "CallExpression": {
435
+ const first = n.arguments[0];
436
+ return first === void 0 || first.type === "SpreadElement" ? null : resolveNode(first, depth + 1);
437
+ }
438
+ case "ArrowFunctionExpression":
439
+ case "FunctionExpression":
440
+ case "FunctionDeclaration": {
441
+ const fnBody = n.body;
442
+ return fnBody.type === "BlockStatement" ? resolveNode(returned(fnBody), depth + 1) : resolveNode(fnBody, depth + 1);
443
+ }
444
+ case "Identifier": return resolveNode(declared(n.name), depth + 1);
445
+ default: return null;
446
+ }
447
+ };
448
+ for (const statement of body) if (statement.type === "ExportDefaultDeclaration") return resolveNode(statement.declaration, 0);
449
+ return null;
450
+ }
451
+ /** Where an import goes in a module without one: after a hashbang and leading comments. */
452
+ function leadingEnd(text) {
453
+ let at = text.startsWith("#!") ? text.indexOf("\n") + 1 || text.length : 0;
454
+ for (;;) {
455
+ const space = /^\s*/.exec(text.slice(at))?.[0].length ?? 0;
456
+ const next = at + space;
457
+ if (text.startsWith("//", next)) {
458
+ const end = text.indexOf("\n", next);
459
+ if (end === -1) return text.length;
460
+ at = end + 1;
461
+ } else if (text.startsWith("/*", next)) {
462
+ const end = text.indexOf("*/", next + 2);
463
+ if (end === -1) return at;
464
+ at = end + 2;
465
+ if (text[at] === "\r") at++;
466
+ if (text[at] === "\n") at++;
467
+ } else return at;
468
+ }
469
+ }
470
+ /**
471
+ * The edited `vite.config.*` text, or null when the exported config has no
472
+ * `plugins` array to extend. Parsed, so a `plugins: [` in a comment, a string
473
+ * or a nested object (`build.rollupOptions.plugins`) is never the one edited;
474
+ * CommonJS configs are left alone, since an `import` would not run there.
475
+ */
476
+ function withPlugin(text, file = "vite.config.ts") {
477
+ const ext = extname(file);
478
+ if (ext === ".cjs" || ext === ".cts") return null;
479
+ let program;
480
+ try {
481
+ program = parseModule(text, langFromId(file), basename(file)).program;
482
+ } catch (error) {
483
+ if (isDiagnosticError(error)) return null;
484
+ throw error;
485
+ }
486
+ const body = program.body;
487
+ const config = exportedConfig(body);
488
+ if (config === null) return null;
489
+ const property = config.properties.find((p) => {
490
+ if (p.type !== "Property" || p.computed === true) return false;
491
+ const key = p.key;
492
+ return (key.type === "Identifier" ? key.name : key.value) === "plugins";
493
+ });
494
+ if (property === void 0) return null;
495
+ const array = unwrap(property.value);
496
+ if (array.type !== "ArrayExpression") return null;
497
+ const at = array.start + 1;
498
+ const rest = text.slice(at);
499
+ let plugin;
500
+ if (array.elements.length === 0 && text.slice(at, array.end - 1).trim() === "") plugin = {
501
+ at,
502
+ text: "vi18n()"
503
+ };
504
+ else {
505
+ const multiline = /^[ \t]*\r?\n([ \t]*)/.exec(rest);
506
+ plugin = multiline === null ? {
507
+ at,
508
+ text: "vi18n(), "
509
+ } : {
510
+ at,
511
+ text: "\n" + multiline[1] + "vi18n(),"
512
+ };
513
+ }
514
+ let out = text.slice(0, plugin.at) + plugin.text;
515
+ out += plugin.text === "vi18n()" ? text.slice(array.end - 1) : rest;
516
+ let lastImport = null;
517
+ for (const statement of body) if (statement.type === "ImportDeclaration") lastImport = statement;
518
+ if (lastImport !== null && lastImport.end <= plugin.at) return `${out.slice(0, lastImport.end)}\n${PLUGIN_IMPORT}${out.slice(lastImport.end)}`;
519
+ if (lastImport !== null) return null;
520
+ const insertAt = leadingEnd(text);
521
+ return `${out.slice(0, insertAt)}${PLUGIN_IMPORT}\n${out.slice(insertAt)}`;
522
+ }
523
+ /**
524
+ * Lists `vi18n.gen.ts` in the `include` of the tsconfig that compiles the
525
+ * sources, when that file is plain JSON (a JSONC file with comments is left
526
+ * alone: rewriting it would drop them). Without `include` or `files` of its
527
+ * own or through `extends`, TypeScript already sees every file; with only
528
+ * inherited ones, the snippet to add is printed. A config that has `files` but no
529
+ * `include` never gets an `include` added (the snippet is printed), and a
530
+ * solution-style config (`references` and nothing of its own to compile, as
531
+ * create-vite's) is never edited: adding `include` there would make it a
532
+ * project of its own, which breaks `tsc -b`. Its reference whose `include`
533
+ * covers the source directory is the one edited instead.
534
+ */
535
+ async function addToTsconfig(root, gen, data, rel) {
536
+ const file = await nearestTsconfig(root);
537
+ if (file === null) return;
538
+ const read = await readTsconfig(file);
539
+ if (read === null) return;
540
+ const sourceDir = SOURCE_DIRS.map((name) => join(root, name)).find((dir) => existsSync(dir)) ?? root;
541
+ const json = read.json;
542
+ if (json === null) {
543
+ const entry = relative(dirname(file), gen).split(sep).join("/");
544
+ data.manual.push({
545
+ file: rel(file),
546
+ snippet: `"include": [/* … */ ${JSON.stringify(entry)}]`
547
+ });
548
+ return;
549
+ }
550
+ if (typeof json !== "object" || Array.isArray(json)) return;
551
+ const references = Array.isArray(json.references) ? json.references : [];
552
+ const ownFiles = Array.isArray(json.files) ? json.files : null;
553
+ if (!(references.length > 0 && json.include === void 0 && (json.files === void 0 || ownFiles?.length === 0))) {
554
+ await listGenFile(file, read, gen, data, rel);
555
+ return;
556
+ }
557
+ let first = null;
558
+ for (const reference of references) {
559
+ const path = reference?.path;
560
+ if (typeof path !== "string") continue;
561
+ let target = resolve(dirname(file), path);
562
+ try {
563
+ if ((await stat(target)).isDirectory()) target = join(target, "tsconfig.json");
564
+ } catch {
565
+ continue;
566
+ }
567
+ const referenced = await readTsconfig(target);
568
+ if (referenced === null) continue;
569
+ first ??= target;
570
+ const include = referenced.json?.include;
571
+ if (Array.isArray(include) && covers(include, dirname(target), sourceDir)) {
572
+ await listGenFile(target, referenced, gen, data, rel);
573
+ return;
574
+ }
575
+ }
576
+ const entry = relative(dirname(first ?? file), gen).split(sep).join("/");
577
+ data.manual.push({
578
+ file: rel(first ?? file),
579
+ snippet: `"include": [/* … */ ${JSON.stringify(entry)}]`
580
+ });
581
+ }
582
+ /** Adds the entry to one config's `include`, or prints the snippet when that cannot be done safely. */
583
+ async function listGenFile(file, read, gen, data, rel) {
584
+ const entry = relative(dirname(file), gen).split(sep).join("/");
585
+ const json = read.json;
586
+ if (typeof json !== "object" || json === null || Array.isArray(json)) return;
587
+ if (json.include === void 0 && json.files === void 0) {
588
+ const snippet = await inheritedSnippet(file, json, gen, entry);
589
+ if (snippet !== null) data.manual.push({
590
+ file: rel(file),
591
+ snippet
592
+ });
593
+ return;
594
+ }
595
+ if (json.include === void 0) {
596
+ data.manual.push({
597
+ file: rel(file),
598
+ snippet: `"files": [/* … */ ${JSON.stringify(entry)}]`
599
+ });
600
+ return;
601
+ }
602
+ const include = Array.isArray(json.include) ? json.include : [];
603
+ if (include.includes(entry) || include.includes("./" + entry)) {
604
+ data.skipped.push(rel(file));
605
+ return;
606
+ }
607
+ if (!read.plain) {
608
+ data.manual.push({
609
+ file: rel(file),
610
+ snippet: `"include": [/* … */ ${JSON.stringify(entry)}]`
611
+ });
612
+ return;
613
+ }
614
+ json.include = [...include, entry];
615
+ const text = read.text;
616
+ const indent = /\n([ \t]+)\S/.exec(text.slice(text.indexOf("{") + 1))?.[1] ?? " ";
617
+ await atomicWrite(file, JSON.stringify(json, null, indent) + (text.endsWith("\n") ? "\n" : ""));
618
+ data.wrote.push(rel(file));
619
+ }
620
+ /**
621
+ * The `include` snippet for a tsconfig with no `include` or `files` of its
622
+ * own, or null when TypeScript already compiles the generated file. Such a
623
+ * config takes both from `extends` (SvelteKit's `.svelte-kit/tsconfig.json`
624
+ * lists globs under `../src` and a few files), and an `include` written here
625
+ * replaces the inherited one, so the snippet repeats its entries, rebased to
626
+ * this file. Inherited `files` stay: TypeScript adds an own `include` to them.
627
+ */
628
+ async function inheritedSnippet(file, json, gen, entry) {
629
+ if (json.extends === void 0) return null;
630
+ const include = await inherited(file, json, "include", /* @__PURE__ */ new Set([file]));
631
+ const files = await inherited(file, json, "files", /* @__PURE__ */ new Set([file]));
632
+ if (include === void 0 || files === void 0) return `"include": [/* every entry "extends" gives it (none given: nothing to add) */ ${JSON.stringify(entry)}]`;
633
+ if (include === null && files === null) return null;
634
+ const target = toSlash(gen);
635
+ if (files !== null && files.list.some((f) => toSlash(resolve(files.base, f)) === target)) return null;
636
+ if (include !== null && include.list.some((p) => matchesInclude(p, include.base, target))) return null;
637
+ return `"include": [${[...include === null ? [] : include.list.map((p) => toSlash(relative(dirname(file), resolve(include.base, p))) || "."), entry].map((p) => JSON.stringify(p)).join(", ")}]`;
638
+ }
639
+ /**
640
+ * `key` as `file` inherits it through `extends` (later bases override
641
+ * earlier ones, as in TypeScript), with the directory its paths are relative
642
+ * to; null when no base has it; undefined when a base cannot be read.
643
+ */
644
+ async function inherited(file, json, key, seen) {
645
+ const bases = typeof json.extends === "string" ? [json.extends] : Array.isArray(json.extends) ? json.extends : [];
646
+ let found = null;
647
+ for (const spec of bases) {
648
+ const target = typeof spec === "string" ? resolveExtends(file, spec) : null;
649
+ if (target === null || seen.has(target)) return void 0;
650
+ const base = (await readTsconfig(target))?.json;
651
+ if (typeof base !== "object" || base === null || Array.isArray(base)) return void 0;
652
+ const value = base[key];
653
+ let result;
654
+ if (value === void 0) result = await inherited(target, base, key, /* @__PURE__ */ new Set([...seen, target]));
655
+ else if (Array.isArray(value) && value.every((v) => typeof v === "string")) result = {
656
+ list: value,
657
+ base: dirname(target)
658
+ };
659
+ else return void 0;
660
+ if (result === void 0) return void 0;
661
+ if (result !== null) found = result;
662
+ }
663
+ return found;
664
+ }
665
+ /** The file an `extends` entry names, as TypeScript finds it; null when it cannot be found. */
666
+ function resolveExtends(file, spec) {
667
+ if (spec.startsWith("./") || spec.startsWith("../") || isAbsolute(spec)) {
668
+ const path = resolve(dirname(file), spec);
669
+ if (existsSync(path)) return path;
670
+ return !path.endsWith(".json") && existsSync(path + ".json") ? path + ".json" : null;
671
+ }
672
+ const require = createRequire(file);
673
+ const candidates = spec.endsWith(".json") ? [spec] : [`${spec}.json`, `${spec}/tsconfig.json`];
674
+ for (const candidate of candidates) try {
675
+ return require.resolve(candidate);
676
+ } catch {}
677
+ return null;
678
+ }
679
+ function toSlash(path) {
680
+ return path.split(sep).join("/");
681
+ }
682
+ /**
683
+ * Whether a tsconfig `include` pattern (relative to `base`) takes `file`
684
+ * (absolute, `/`-separated): `*` and `?` within a segment, `**` across
685
+ * directories, and a pattern without wildcards as a file or a directory.
686
+ */
687
+ function matchesInclude(pattern, base, file) {
688
+ const absolute = toSlash(resolve(base, pattern));
689
+ if (!/[*?]/.test(pattern)) return file === absolute || file.startsWith(absolute + "/");
690
+ let regex = "";
691
+ for (let i = 0; i < absolute.length; i++) {
692
+ const c = absolute[i];
693
+ if (c === "*" && absolute[i + 1] === "*") {
694
+ const slash = absolute[i + 2] === "/";
695
+ regex += slash ? "(?:[^/]*/)*" : ".*";
696
+ i += slash ? 2 : 1;
697
+ } else if (c === "*") regex += "[^/]*";
698
+ else if (c === "?") regex += "[^/]";
699
+ else regex += /[\\^$.+()[\]{}|]/.test(c) ? "\\" + c : c;
700
+ }
701
+ return new RegExp(`^${regex}$`).test(file);
702
+ }
703
+ /**
704
+ * A tsconfig as JSON when it is plain JSON (`plain`: safe to rewrite), else
705
+ * read leniently past comments and trailing commas (only to look at it);
706
+ * null when it cannot be read at all.
707
+ */
708
+ async function readTsconfig(file) {
709
+ let text;
710
+ try {
711
+ text = await readFile(file, "utf8");
712
+ } catch {
713
+ return null;
714
+ }
715
+ try {
716
+ return {
717
+ json: JSON.parse(text),
718
+ text,
719
+ plain: true
720
+ };
721
+ } catch {}
722
+ try {
723
+ return {
724
+ json: JSON.parse(stripJsonc(text)),
725
+ text,
726
+ plain: false
727
+ };
728
+ } catch {
729
+ return {
730
+ json: null,
731
+ text,
732
+ plain: false
733
+ };
734
+ }
735
+ }
736
+ /** JSONC → JSON: comments and trailing commas outside strings removed. */
737
+ function stripJsonc(text) {
738
+ let out = "";
739
+ for (let i = 0; i < text.length; i++) {
740
+ const c = text[i];
741
+ if (c === "\"") {
742
+ let j = i + 1;
743
+ while (j < text.length && text[j] !== "\"") j += text[j] === "\\" ? 2 : 1;
744
+ out += text.slice(i, j + 1);
745
+ i = j;
746
+ } else if (c === "/" && text[i + 1] === "/") {
747
+ while (i < text.length && text[i] !== "\n") i++;
748
+ out += "\n";
749
+ } else if (c === "/" && text[i + 1] === "*") {
750
+ const end = text.indexOf("*/", i + 2);
751
+ i = end === -1 ? text.length : end + 1;
752
+ } else out += c;
753
+ }
754
+ return out.replace(/,(\s*[}\]])/g, "$1");
755
+ }
756
+ /** Whether an `include` list reaches `dir`: a pattern's static prefix contains it. */
757
+ function covers(include, base, dir) {
758
+ for (const pattern of include) {
759
+ if (typeof pattern !== "string") continue;
760
+ const segments = [];
761
+ for (const segment of pattern.split("/")) {
762
+ if (/[*?[{]/.test(segment)) break;
763
+ segments.push(segment);
764
+ }
765
+ const prefix = resolve(base, ...segments);
766
+ const rel = relative(prefix, dir);
767
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute(rel)) return true;
768
+ }
769
+ return false;
770
+ }
771
+ async function nearestTsconfig(from) {
772
+ for (let dir = from;; dir = dirname(dir)) {
773
+ const file = join(dir, "tsconfig.json");
774
+ try {
775
+ if ((await stat(file)).isFile()) return file;
776
+ } catch {}
777
+ if (WORKSPACE_MARKERS.some((marker) => existsSync(join(dir, marker)))) return null;
778
+ if (dirname(dir) === dir) return null;
779
+ }
780
+ }
781
+ async function atomicWrite(path, content) {
782
+ const temp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
783
+ try {
784
+ await writeFile(temp, content);
785
+ await rename(temp, path);
786
+ } catch (error) {
787
+ await rm(temp, { force: true });
788
+ throw error;
789
+ }
790
+ }
791
+ //#endregion
792
+ export { init };