@menapo/vi18n 0.0.0-stage → 0.1.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/LICENSE +49 -0
- package/README.md +110 -2
- package/dist/apply-Bre-lY9J.js +228 -0
- package/dist/captured-Bj8bO6vd.js +1034 -0
- package/dist/check-D34P-S--.js +1 -0
- package/dist/check-DyrAbv73.js +201 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +41 -0
- package/dist/codec-DuvU2VQy.js +207 -0
- package/dist/collect-Cx4_SKOe.js +13 -0
- package/dist/collisions-CfpZU8e4.js +217 -0
- package/dist/compile-B5Mz6WJm.js +245 -0
- package/dist/compile-Cc97JaW2.js +2502 -0
- package/dist/concurrency-C9UUWuDC.js +96 -0
- package/dist/concurrency-DONL7t4V.js +1 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +39 -0
- package/dist/dir-zw6bGMKQ.js +290 -0
- package/dist/duplicates-yuRljQ6h.js +185 -0
- package/dist/engine-B7p40M__.js +531 -0
- package/dist/engine.d.ts +43 -0
- package/dist/engine.js +1241 -0
- package/dist/exclude-Du-Zd_qI.js +26 -0
- package/dist/export-DGsMbcTZ.js +103 -0
- package/dist/extract-Dujlsgnk.js +298 -0
- package/dist/formats-KPIg-08V.js +781 -0
- package/dist/i18next-CrUnKFfh.js +1596 -0
- package/dist/i18next-Rgo-v9l0.js +1 -0
- package/dist/import-Cwdhcywt.js +225 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.js +239 -0
- package/dist/init-BecCTvKi.js +792 -0
- package/dist/library-D5CWvTQs.js +1882 -0
- package/dist/lingui--PcOOYry.js +833 -0
- package/dist/lint.d.ts +105 -0
- package/dist/lint.js +971 -0
- package/dist/load-CQRz4PpM.js +2950 -0
- package/dist/load-gW-oJt1Q.js +747 -0
- package/dist/locales-DX4MOBFE.js +827 -0
- package/dist/lsp-Cq1Nuu-Q.js +31 -0
- package/dist/match-BGr8EXB3.js +89 -0
- package/dist/mcp-Cf0T7qPW.js +28 -0
- package/dist/merge-driver-CpiiSVIc.js +422 -0
- package/dist/messages-Cxa-5tR0.js +166 -0
- package/dist/migrate-CntqvrW4.js +1 -0
- package/dist/migrate-dPgq9uzy.js +1484 -0
- package/dist/openai-CTaMDcl-.js +346 -0
- package/dist/output-DsaHKHEt.js +164 -0
- package/dist/parse.d.ts +62 -0
- package/dist/parse.js +156 -0
- package/dist/pipeline-C7g11THL.js +2031 -0
- package/dist/pipeline-wxS0SApG.js +1 -0
- package/dist/project-Dwx7iPm6.js +381 -0
- package/dist/project-pXipT8iG.js +1046 -0
- package/dist/pseudo-DRv30iN5.js +145 -0
- package/dist/raw-C4Y6vyGY.js +4708 -0
- package/dist/react-intl-CaBVwJ9u.js +519 -0
- package/dist/restore-Btq66Tid.js +175 -0
- package/dist/rewrite-CiTRAICl.js +803 -0
- package/dist/rich-DCUKl5HM.js +97 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/run-DYD_VXKy.js +694 -0
- package/dist/runtime-DTMmliT1.js +64 -0
- package/dist/scan-worker.d.ts +62 -0
- package/dist/scan-worker.js +1 -0
- package/dist/scoped-BMYpSthj.js +44 -0
- package/dist/server-BDkykMjb.js +11823 -0
- package/dist/server-qv3mz70d.js +10485 -0
- package/dist/server.d.ts +169 -0
- package/dist/server.js +1234 -0
- package/dist/sitemap-DV3o8QD7.js +159 -0
- package/dist/sources-D19czDSA.js +2242 -0
- package/dist/status-DZArW5aE.js +149 -0
- package/dist/stdio-CVaJfxA7.js +29 -0
- package/dist/stdio-FPOKQAgu.js +95 -0
- package/dist/sync-CcVzger_.js +658 -0
- package/dist/t-DC-c9zE5.js +454 -0
- package/dist/test.d.ts +116 -0
- package/dist/test.js +319 -0
- package/dist/translate-DZR3MkSA.js +299 -0
- package/dist/ts-BaU5yOf0.js +4863 -0
- package/dist/types-CgUCkFU1.d.ts +707 -0
- package/dist/types-D3QGM8XO.js +146 -0
- package/dist/types-DsdWBG0b.d.ts +558 -0
- package/dist/types-DtvQ7UdD.js +1948 -0
- package/dist/types-wSh9fCBY.js +11 -0
- package/dist/units-CQ5Q99RC.js +150 -0
- package/dist/url-CRfCOhUI.js +172 -0
- package/dist/url-CsBkRaJy.js +225 -0
- package/dist/validate-BxpupdAf.js +2615 -0
- package/dist/validate-D6DJepZv.js +365 -0
- package/dist/version-BlyLGlOa.js +28 -0
- package/dist/vite-scan-B_YW4X_a.js +242 -0
- package/dist/vite.d.ts +34 -0
- package/dist/vite.js +7558 -0
- package/dist/vue-i18n-CEMnpv7k.js +651 -0
- package/dist/which-C6PFI1Dm.js +116 -0
- package/dist/worker-84U6uYB3.js +1656 -0
- package/package.json +97 -3
- 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-BlyLGlOa.js";
|
|
6
|
+
import { l as langFromId, u as parseModule } from "./worker-84U6uYB3.js";
|
|
7
|
+
import { a as writeGenFile, r as GEN_FILE } from "./sync-CcVzger_.js";
|
|
8
|
+
import { t as EXIT } from "./types-wSh9fCBY.js";
|
|
9
|
+
import { t as CliExit } from "./project-Dwx7iPm6.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 };
|