@antepod/linguini-vite 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/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # @linguini/vite
2
+
3
+ Vite plugin for Linguini projects.
4
+
5
+ It watches `linguini.toml`, `.lgs`, and `.lgl` files, runs `linguini build`
6
+ after changes, invalidates generated Linguini modules, and emits a
7
+ `linguini:update` HMR event.
8
+
9
+ ```js
10
+ import { defineConfig } from "vite";
11
+ import linguini from "@linguini/vite";
12
+
13
+ export default defineConfig({
14
+ plugins: [linguini()]
15
+ });
16
+ ```
17
+
18
+ Options:
19
+
20
+ - `root`: project root. Defaults to Vite root.
21
+ - `configFile`: config path relative to root. Defaults to `linguini.toml`.
22
+ - `command`: Linguini executable. Defaults to `linguini`.
23
+ - `args`: build command arguments. Defaults to `["build"]`.
24
+ - `buildOnStart`: run codegen during Vite startup. Defaults to `true`.
25
+ - `generatedModulePatterns`: substrings used to invalidate generated modules.
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@antepod/linguini-vite",
3
+ "version": "0.1.0",
4
+ "description": "Vite plugin for Linguini source watching and TypeScript codegen",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./src/index.d.ts",
10
+ "default": "./src/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "src"
15
+ ],
16
+ "peerDependencies": {
17
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
18
+ },
19
+ "scripts": {
20
+ "test": "node --test"
21
+ }
22
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { Plugin, ViteDevServer } from "vite";
2
+
3
+ export interface LinguiniBuildContext {
4
+ root: string;
5
+ reason: "build-start" | "hot-update" | string;
6
+ }
7
+
8
+ export interface LinguiniViteOptions {
9
+ root?: string;
10
+ configFile?: string;
11
+ command?: string;
12
+ args?: string[];
13
+ buildOnStart?: boolean;
14
+ generatedModulePatterns?: string[];
15
+ build?: (context: LinguiniBuildContext) => void | Promise<void>;
16
+ }
17
+
18
+ export declare function linguini(options?: LinguiniViteOptions): Plugin;
19
+ export default linguini;
20
+
21
+ export declare function discoverLinguiniFiles(
22
+ root: string,
23
+ configFile?: string
24
+ ): Promise<string[]>;
25
+
26
+ export declare function isLinguiniSource(
27
+ file: string,
28
+ root?: string,
29
+ configFile?: string
30
+ ): boolean;
31
+
32
+ export type { ViteDevServer };
package/src/index.js ADDED
@@ -0,0 +1,192 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readdir, readFile, stat } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ const DEFAULT_CONFIG_FILE = "linguini.toml";
6
+ const DEFAULT_SCHEMA_DIR = "linguini/schema";
7
+ const DEFAULT_LOCALE_DIR = "linguini/locale";
8
+ const DEFAULT_GENERATED_PATTERNS = [
9
+ "/generated/linguini/",
10
+ "\\generated\\linguini\\",
11
+ "/linguini/generated/",
12
+ "\\linguini\\generated\\"
13
+ ];
14
+
15
+ export function linguini(options = {}) {
16
+ let viteConfig;
17
+ let projectRoot = process.cwd();
18
+ let pendingBuild;
19
+
20
+ async function runBuild(reason) {
21
+ if (pendingBuild) {
22
+ return pendingBuild;
23
+ }
24
+ pendingBuild = Promise.resolve()
25
+ .then(() => buildProject(projectRoot, options, reason))
26
+ .finally(() => {
27
+ pendingBuild = undefined;
28
+ });
29
+ return pendingBuild;
30
+ }
31
+
32
+ async function watchSources(server) {
33
+ const files = await discoverLinguiniFiles(projectRoot, options.configFile);
34
+ for (const file of files) {
35
+ server.watcher.add(file);
36
+ }
37
+ }
38
+
39
+ function invalidateGeneratedModules(server) {
40
+ const patterns = options.generatedModulePatterns ?? DEFAULT_GENERATED_PATTERNS;
41
+ for (const module of server.moduleGraph.idToModuleMap.values()) {
42
+ if (module.id && patterns.some((pattern) => module.id.includes(pattern))) {
43
+ server.moduleGraph.invalidateModule(module);
44
+ }
45
+ }
46
+ }
47
+
48
+ return {
49
+ name: "vite-plugin-linguini",
50
+ enforce: "pre",
51
+ configResolved(config) {
52
+ viteConfig = config;
53
+ projectRoot = path.resolve(options.root ?? config.root ?? process.cwd());
54
+ },
55
+ async buildStart() {
56
+ if (!viteConfig) {
57
+ projectRoot = path.resolve(options.root ?? process.cwd());
58
+ }
59
+ if (options.buildOnStart ?? true) {
60
+ await runBuild("build-start");
61
+ }
62
+ for (const file of await discoverLinguiniFiles(projectRoot, options.configFile)) {
63
+ this.addWatchFile(file);
64
+ }
65
+ },
66
+ async configureServer(server) {
67
+ await watchSources(server);
68
+ server.watcher.on("add", async (file) => {
69
+ if (isLinguiniSource(file, projectRoot, options.configFile)) {
70
+ await watchSources(server);
71
+ }
72
+ });
73
+ },
74
+ async handleHotUpdate(ctx) {
75
+ if (!isLinguiniSource(ctx.file, projectRoot, options.configFile)) {
76
+ return;
77
+ }
78
+ await runBuild("hot-update");
79
+ await watchSources(ctx.server);
80
+ invalidateGeneratedModules(ctx.server);
81
+ ctx.server.ws.send({
82
+ type: "custom",
83
+ event: "linguini:update",
84
+ data: { file: ctx.file }
85
+ });
86
+ return [];
87
+ }
88
+ };
89
+ }
90
+
91
+ export default linguini;
92
+
93
+ export async function discoverLinguiniFiles(root, configFile = DEFAULT_CONFIG_FILE) {
94
+ const configPath = path.resolve(root, configFile);
95
+ const paths = await readProjectPaths(configPath);
96
+ const files = [configPath];
97
+ files.push(...(await collectFiles(path.resolve(root, paths.schema), ".lgs")));
98
+ files.push(...(await collectFiles(path.resolve(root, paths.locale), ".lgl")));
99
+ return files;
100
+ }
101
+
102
+ export function isLinguiniSource(file, root = process.cwd(), configFile = DEFAULT_CONFIG_FILE) {
103
+ const absolute = path.resolve(file);
104
+ const configPath = path.resolve(root, configFile);
105
+ return absolute === configPath || absolute.endsWith(".lgs") || absolute.endsWith(".lgl");
106
+ }
107
+
108
+ async function buildProject(root, options, reason) {
109
+ if (options.build) {
110
+ await options.build({ root, reason });
111
+ return;
112
+ }
113
+ const command = options.command ?? "linguini";
114
+ const args = options.args ?? ["build"];
115
+ await execFilePromise(command, args, { cwd: root });
116
+ }
117
+
118
+ function execFilePromise(command, args, options) {
119
+ return new Promise((resolve, reject) => {
120
+ execFile(command, args, options, (error, stdout, stderr) => {
121
+ if (error) {
122
+ error.message = [error.message, stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
123
+ reject(error);
124
+ } else {
125
+ resolve({ stdout, stderr });
126
+ }
127
+ });
128
+ });
129
+ }
130
+
131
+ async function readProjectPaths(configPath) {
132
+ try {
133
+ const source = await readFile(configPath, "utf8");
134
+ return {
135
+ schema: readTomlString(source, "paths", "schema") ?? DEFAULT_SCHEMA_DIR,
136
+ locale: readTomlString(source, "paths", "locale") ?? DEFAULT_LOCALE_DIR
137
+ };
138
+ } catch (error) {
139
+ if (error && error.code === "ENOENT") {
140
+ return { schema: DEFAULT_SCHEMA_DIR, locale: DEFAULT_LOCALE_DIR };
141
+ }
142
+ throw error;
143
+ }
144
+ }
145
+
146
+ function readTomlString(source, section, key) {
147
+ const lines = source.split(/\r?\n/);
148
+ let currentSection = "";
149
+ for (const line of lines) {
150
+ const trimmed = line.trim();
151
+ const sectionMatch = /^\[([^\]]+)\]$/.exec(trimmed);
152
+ if (sectionMatch) {
153
+ currentSection = sectionMatch[1];
154
+ continue;
155
+ }
156
+ if (currentSection !== section || trimmed.startsWith("#")) {
157
+ continue;
158
+ }
159
+ const valueMatch = new RegExp(`^${key}\\s*=\\s*"([^"]+)"`).exec(trimmed);
160
+ if (valueMatch) {
161
+ return valueMatch[1];
162
+ }
163
+ }
164
+ return undefined;
165
+ }
166
+
167
+ async function collectFiles(root, extension) {
168
+ let metadata;
169
+ try {
170
+ metadata = await stat(root);
171
+ } catch (error) {
172
+ if (error && error.code === "ENOENT") {
173
+ return [];
174
+ }
175
+ throw error;
176
+ }
177
+ if (!metadata.isDirectory()) {
178
+ return [];
179
+ }
180
+
181
+ const entries = await readdir(root, { withFileTypes: true });
182
+ const files = [];
183
+ for (const entry of entries) {
184
+ const entryPath = path.join(root, entry.name);
185
+ if (entry.isDirectory()) {
186
+ files.push(...(await collectFiles(entryPath, extension)));
187
+ } else if (entry.isFile() && entry.name.endsWith(extension)) {
188
+ files.push(entryPath);
189
+ }
190
+ }
191
+ return files.sort();
192
+ }