vite-plugin-fvtt 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 MatyeusM
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # **vite-plugin-fvtt**
2
+
3
+ A powerful [Vite](https://vitejs.dev/) plugin to **streamline and automate** the development of Foundry VTT modules and systems. It handles manifest resolution, asset copying, language file composition, and template handling with **minimal setup**, letting you focus on your code.
4
+
5
+ ## **🚀 Key Features**
6
+
7
+ The primary advantage of this plugin is the ability to develop your module in a modern, isolated environment. You no longer need to work directly inside your Foundry VTT data folder, which can be messy and inefficient. This allows you to leverage Vite's Hot Module Replacement (HMR) and other developer-friendly features without polluting your local installation.
8
+
9
+ ## **Getting Started**
10
+
11
+ ### **1. Setup a Foundry VTT Project**
12
+
13
+ Create a standard [Foundry VTT module or system](https://foundryvtt.com/article/module-development/).
14
+ Place your `module.json` or `system.json` manifest in either your **project root** or your **public/** directory.
15
+
16
+ ### **2. Add the Plugin to your Vite Config**
17
+
18
+ Install the plugin with `npm i -D vite-plugin-fvtt`.
19
+
20
+ Add the plugin to your vite.config.js. The **build.lib.entry** field is required, most of the other settings are infer'd by the plugin from your foundry manifest.
21
+
22
+ ```js
23
+ // vite.config.js
24
+ import { defineConfig } from 'vite';
25
+ import foundryVTT from 'vite-plugin-fvtt';
26
+
27
+ export default defineConfig({
28
+ plugins: [foundryVTT()],
29
+ build: {
30
+ // ⚠️ Required: The entry point for your module/system.
31
+ // This file should import your main CSS/SCSS/LESS file.
32
+ lib: {
33
+ entry: './src/main.js',
34
+ },
35
+ sourcemap: true,
36
+ },
37
+ });
38
+ ```
39
+
40
+ ## **⚙️ How it Works**
41
+
42
+ ### **Manifest & Asset Resolution**
43
+
44
+ * The plugin automatically detects your manifest file (`module.json` or `system.json`) in the project **root** or `public/` folder.
45
+ * Assets referenced in the manifest (styles, esmodules, scripts) are **automatically generated to the build output**, simplifying your build process.
46
+
47
+ ### **Template Handling**
48
+
49
+ * The plugin automatically detects templates in common locations:
50
+ * Your public folder (e.g., `public/handlebars/`).
51
+ * The project root (e.g., `templates/`).
52
+ * A templates folder directly under your entry file's directory (e.g., `src/tpl/`).
53
+ * **Note:** Only templates located in the **public folder** are copied to the build output.
54
+
55
+ ### **Language File Merging**
56
+
57
+ The plugin offers a powerful feature for managing translations.
58
+
59
+ * **Complete Language Files:** Place a complete JSON file (e.g., `public/lang/en.json`) and the plugin will copy it as-is.
60
+ * **Partial Language Files:** To modularize your translations, place multiple JSON files in a subdirectory (e.g., `src/lang/en/`). The plugin will automatically **merge them into a single file** (`lang/en.json`) during the build, as specified in your manifest. The plugin looks in **root** or your **source directory** for the paths as specified in your foundry manifest file.
61
+
62
+ ## **Example Project Structure**
63
+ ```
64
+ my-module/
65
+ ├─ src/
66
+ │ ├─ main.js # The primary module entry file (required by Vite).
67
+ │ ├─ style.css # Your project's main stylesheet.
68
+ │ └─ lang/en/ # Directory for partial, merged translation files.
69
+ │ ├─ spells.json
70
+ │ ├─ abilities.json
71
+ │ └─ general.json
72
+ ├─ public/
73
+ │ ├─ module.json # Your module's manifest file (or system.json).
74
+ │ └─ templates/ # HTML template files for your module.
75
+ ├─ vite.config.js # Your Vite configuration file.
76
+ ```
77
+
78
+ ## **🐛 Known Issues & Troubleshooting**
79
+
80
+ * **HMR:** Hot Module Replacement may be inconsistent for non-English language files. A full page refresh or server restart might be needed.
81
+ * **App V2:** HMR for Foundry's new App V2 has not been fully tested. If you encounter issues, please open a GitHub issue.
82
+ * **General Issues:** If you face unexpected behavior, the first step is always to **restart your dev server (npm run dev) or run a fresh build**. This often resolves caching or HMR-related glitches.
83
+
84
+ ---
85
+
86
+ License: MIT
@@ -0,0 +1,6 @@
1
+ import { Plugin } from "vite";
2
+
3
+ //#region src/index.d.ts
4
+ declare function foundryVTTPlugin(): Plugin;
5
+ //#endregion
6
+ export { foundryVTTPlugin as default };
package/dist/index.js ADDED
@@ -0,0 +1,534 @@
1
+ import fs from "fs-extra";
2
+ import posix from "path/posix";
3
+ import dotenv from "dotenv";
4
+ import { sync } from "glob";
5
+ import { Server } from "socket.io";
6
+ import { io } from "socket.io-client";
7
+
8
+ //#region src/context.ts
9
+ const context = {};
10
+
11
+ //#endregion
12
+ //#region src/config/env.ts
13
+ function loadEnv() {
14
+ const envPath = posix.resolve(process.cwd(), ".env.foundryvtt.local");
15
+ const { parsed } = dotenv.config({
16
+ path: envPath,
17
+ quiet: true
18
+ });
19
+ return {
20
+ foundryUrl: parsed?.FOUNDRY_URL ?? "localhost",
21
+ foundryPort: parseInt(parsed?.FOUNDRY_PORT ?? "30000", 10)
22
+ };
23
+ }
24
+
25
+ //#endregion
26
+ //#region src/config/foundryvtt-manifest.ts
27
+ function loadManifest(config) {
28
+ if (context?.manifest) return context.manifest;
29
+ const publicDir = config.publicDir || "public";
30
+ const MANIFEST_LOCATIONS = [
31
+ "system.json",
32
+ "module.json",
33
+ `${publicDir}/system.json`,
34
+ `${publicDir}/module.json`
35
+ ];
36
+ const foundPath = MANIFEST_LOCATIONS.map((relPath) => posix.resolve(process.cwd(), relPath)).find((absPath) => fs.pathExistsSync(absPath));
37
+ if (!foundPath) throw new Error(`Could not find a manifest file (system.json or module.json) in project root or ${publicDir}/.`);
38
+ try {
39
+ const data = fs.readJsonSync(foundPath);
40
+ data.manifestType = foundPath.includes("module.json") ? "module" : "system";
41
+ return data;
42
+ } catch (err) {
43
+ throw new Error(`Failed to read manifest at ${foundPath}: ${err?.message || err}`);
44
+ }
45
+ }
46
+
47
+ //#endregion
48
+ //#region src/utils/logger.ts
49
+ var Logger = class {
50
+ namespace;
51
+ colors = {
52
+ info: "\x1B[32m",
53
+ warn: "\x1B[33m",
54
+ error: "\x1B[31m"
55
+ };
56
+ constructor({ namespace = "vite-plugin-foundryvtt" } = {}) {
57
+ this.namespace = namespace;
58
+ }
59
+ format(level, message) {
60
+ const color = this.colors[level] ?? "";
61
+ const reset = "\x1B[0m";
62
+ return `${color}[${this.namespace}] [${level.toUpperCase()}]${reset} ${message}`;
63
+ }
64
+ info(message) {
65
+ console.log(this.format("info", message));
66
+ }
67
+ warn(message) {
68
+ console.warn(this.format("warn", message));
69
+ }
70
+ error(message) {
71
+ console.error(this.format("error", message));
72
+ }
73
+ fail(message) {
74
+ this.error(message);
75
+ throw new Error(typeof message === "string" ? message : JSON.stringify(message, null, 2));
76
+ }
77
+ };
78
+ var logger_default = new Logger();
79
+
80
+ //#endregion
81
+ //#region src/config/vite-options.ts
82
+ function createPartialViteConfig(config) {
83
+ const base = config.base ?? `/${context.manifest?.manifestType}s/${context.manifest?.id}/`;
84
+ const useEsModules = context.manifest?.esmodules.length === 1;
85
+ const formats = useEsModules ? ["es"] : ["umd"];
86
+ const fileName = (useEsModules ? context.manifest?.esmodules[0] : context.manifest?.scripts?.[0]) ?? "bundle";
87
+ if (fileName === "bundle") logger_default.warn("No output file specified in manifest, using default \"bundle\"");
88
+ if (!context.manifest?.styles?.length) logger_default.warn("No CSS file found in manifest");
89
+ const cssFileName = posix.parse(context.manifest?.styles[0] ?? "").name;
90
+ const foundryPort = context.env?.foundryPort ?? 3e4;
91
+ const foundryUrl = context.env?.foundryUrl ?? "localhost";
92
+ const entry = (config.build?.lib)?.entry;
93
+ if (!entry) logger_default.fail("Entry must be specified in lib");
94
+ if (typeof entry !== "string") logger_default.fail("Only a singular string entry is supported for build.lib.entry");
95
+ return {
96
+ base,
97
+ esbuild: config.esbuild ?? {
98
+ minifyIdentifiers: false,
99
+ minifySyntax: true,
100
+ minifyWhitespace: true,
101
+ keepNames: true
102
+ },
103
+ server: {
104
+ port: foundryPort + 1,
105
+ proxy: { [`^(?!${base})`]: `http://${foundryUrl}:${foundryPort}` }
106
+ },
107
+ build: {
108
+ minify: "esbuild",
109
+ lib: {
110
+ cssFileName,
111
+ entry,
112
+ fileName,
113
+ formats,
114
+ name: context.manifest?.id
115
+ }
116
+ }
117
+ };
118
+ }
119
+
120
+ //#endregion
121
+ //#region src/server/hmr-client.ts
122
+ var hmr_client_default = `
123
+ if (import.meta.hot) {
124
+ function refreshApplications() {
125
+ // AppV1 refresh
126
+ Object.values(foundry.ui.windows).forEach(app => app.render(true))
127
+ // AppV2 refresh
128
+ // TODO: Can we filter out to only refresh the correct apps?
129
+ foundry.applications.instances.forEach(appV2 => appV2.render(true))
130
+ }
131
+
132
+ import.meta.hot.on('foundryvtt-template-update', async ({ path }) => {
133
+ console.log('Vite | Force reload template', path)
134
+ Handlebars.unregisterPartial(path)
135
+ await foundry.applications.handlebars.getTemplate(path)
136
+ refreshApplications()
137
+ })
138
+
139
+ import.meta.hot.on('foundryvtt-language-update', async () => {
140
+ console.log('Vite | Force reassigning language')
141
+ await game.i18n.setLanguage(game.i18n.lang)
142
+ refreshApplications()
143
+ })
144
+ } else console.error('Vite | HMR is disabled')
145
+ `;
146
+
147
+ //#endregion
148
+ //#region src/server/trackers/abstract-file-tracker.ts
149
+ var AbstractFileTracker = class {
150
+ initialized = false;
151
+ tracked = /* @__PURE__ */ new Map();
152
+ watcher = null;
153
+ config;
154
+ constructor(config) {
155
+ this.config = config;
156
+ }
157
+ initialize(server) {
158
+ if (this.initialized) return;
159
+ this.initialized = true;
160
+ this.watcher = server.watcher;
161
+ this.watcher.on("change", (changedPath) => {
162
+ const value = this.tracked.get(changedPath);
163
+ if (!value) return;
164
+ logger_default.info(`Attempting to hot reload ${changedPath}`);
165
+ const eventData = this.getEventData(changedPath, value);
166
+ server.ws.send({
167
+ type: "custom",
168
+ event: this.updateEvent,
169
+ data: eventData
170
+ });
171
+ });
172
+ }
173
+ addFile(value, filePath) {
174
+ const absPath = posix.resolve(filePath);
175
+ if (!this.tracked.has(absPath)) {
176
+ this.tracked.set(absPath, value);
177
+ this.watcher?.add(absPath);
178
+ }
179
+ }
180
+ };
181
+
182
+ //#endregion
183
+ //#region src/server/trackers/handlebars-tracker.ts
184
+ var HandlebarsTracker = class extends AbstractFileTracker {
185
+ updateEvent = "foundryvtt-template-update";
186
+ constructor() {
187
+ super(context.config);
188
+ }
189
+ getEventData(changedPath, value) {
190
+ return { path: value };
191
+ }
192
+ };
193
+ const handlebarsTracker = new HandlebarsTracker();
194
+
195
+ //#endregion
196
+ //#region src/server/trackers/language-tracker.ts
197
+ var LanguageTracker = class extends AbstractFileTracker {
198
+ updateEvent = "foundryvtt-language-update";
199
+ constructor() {
200
+ super({});
201
+ }
202
+ getEventData() {
203
+ return {};
204
+ }
205
+ };
206
+ const languageTracker = new LanguageTracker();
207
+
208
+ //#endregion
209
+ //#region src/utils/path-utils.ts
210
+ var PathUtils = class PathUtils {
211
+ static _config = null;
212
+ static _sourceDirectory = null;
213
+ static _decodedBase = null;
214
+ static _publicDir = null;
215
+ static _outDir = null;
216
+ static _root = null;
217
+ static getConfig() {
218
+ if (!PathUtils._config) {
219
+ const config = context.config;
220
+ if (!config) logger_default.fail("Path utils can only be called after vite has resolved the config");
221
+ PathUtils._config = config;
222
+ }
223
+ return PathUtils._config;
224
+ }
225
+ static getDecodedBase() {
226
+ if (!PathUtils._decodedBase) {
227
+ const config = PathUtils.getConfig();
228
+ PathUtils._decodedBase = PathUtils.normalize(decodeURI(config.base));
229
+ }
230
+ return PathUtils._decodedBase;
231
+ }
232
+ static getSourceDirectory() {
233
+ if (!PathUtils._sourceDirectory) {
234
+ const config = PathUtils.getConfig();
235
+ const normalizedEntry = PathUtils.normalize(config.build.lib.entry.toString());
236
+ const segments = normalizedEntry.split(posix.sep).filter(Boolean).filter((s) => s !== ".");
237
+ const firstFolder = segments.length > 0 ? segments[0] : ".";
238
+ PathUtils._sourceDirectory = posix.join(config.root, firstFolder);
239
+ }
240
+ return PathUtils._sourceDirectory;
241
+ }
242
+ static getPublicDir() {
243
+ if (!PathUtils._publicDir) {
244
+ const config = PathUtils.getConfig();
245
+ PathUtils._publicDir = PathUtils.normalize(posix.resolve(config.publicDir));
246
+ }
247
+ return PathUtils._publicDir;
248
+ }
249
+ static getOutDir() {
250
+ if (!PathUtils._outDir) {
251
+ const config = PathUtils.getConfig();
252
+ PathUtils._outDir = PathUtils.normalize(posix.resolve(config.build.outDir));
253
+ }
254
+ return PathUtils._outDir;
255
+ }
256
+ static getRoot() {
257
+ if (!PathUtils._root) {
258
+ const config = PathUtils.getConfig();
259
+ PathUtils._root = PathUtils.normalize(config.root);
260
+ }
261
+ return PathUtils._root;
262
+ }
263
+ static getOutDirFile(p) {
264
+ const file = posix.join(PathUtils.getOutDir(), p);
265
+ return fs.existsSync(file) ? file : "";
266
+ }
267
+ static getPublicDirFile(p) {
268
+ const file = posix.join(PathUtils.getPublicDir(), p);
269
+ return fs.existsSync(file) ? file : "";
270
+ }
271
+ static normalize(p) {
272
+ return posix.normalize(p);
273
+ }
274
+ static findLocalFilePath(p) {
275
+ const fileCandidates = [
276
+ PathUtils.getPublicDir(),
277
+ PathUtils.getSourceDirectory(),
278
+ PathUtils.getRoot()
279
+ ].map((pth) => posix.join(pth, p));
280
+ return fileCandidates.find((pth) => fs.existsSync(pth)) ?? null;
281
+ }
282
+ static isFoundryVTTUrl(p) {
283
+ const decodedBase = PathUtils.getDecodedBase();
284
+ const pathToCheck = PathUtils.normalize(p);
285
+ return pathToCheck.startsWith(decodedBase);
286
+ }
287
+ static foundryVTTUrlToLocal(p) {
288
+ const decodedBase = PathUtils.getDecodedBase();
289
+ let pathToTransform = PathUtils.normalize("/" + p);
290
+ if (!pathToTransform.startsWith(decodedBase)) return null;
291
+ pathToTransform = posix.relative(decodedBase, pathToTransform);
292
+ return PathUtils.findLocalFilePath(pathToTransform);
293
+ }
294
+ static localToFoundryVTTUrl(p) {
295
+ const decodedBase = PathUtils.getDecodedBase();
296
+ let pathToTransform = PathUtils.normalize(p);
297
+ [
298
+ PathUtils.getPublicDir(),
299
+ PathUtils.getSourceDirectory(),
300
+ PathUtils.getRoot()
301
+ ].forEach((pth) => {
302
+ if (pathToTransform.startsWith(pth)) pathToTransform = pathToTransform.slice(pth.length);
303
+ });
304
+ return posix.join(decodedBase, pathToTransform);
305
+ }
306
+ static getLanguageSourcePath(p, lang) {
307
+ const dir = posix.parse(p).dir;
308
+ const lastDirName = posix.basename(dir);
309
+ const finalSegments = lastDirName === lang ? dir : posix.join(dir, lang);
310
+ return posix.join(PathUtils.getSourceDirectory(), finalSegments);
311
+ }
312
+ };
313
+ var path_utils_default = PathUtils;
314
+
315
+ //#endregion
316
+ //#region src/language/loader.ts
317
+ function getLocalLanguageFiles(lang, outDir = false) {
318
+ const manifest = context.manifest;
319
+ const language = manifest.languages.find((l) => l.lang === lang);
320
+ if (!language) logger_default.fail(`Cannot find language "${lang}"`);
321
+ const langPath = language?.path ?? "";
322
+ if (outDir) {
323
+ const languageFile = path_utils_default.getOutDirFile(langPath);
324
+ return [languageFile];
325
+ }
326
+ const publicDirFile = path_utils_default.getPublicDirFile(langPath);
327
+ if (publicDirFile !== "") return [publicDirFile];
328
+ const sourcePath = path_utils_default.getLanguageSourcePath(langPath, lang);
329
+ if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isDirectory()) {
330
+ logger_default.warn(`No language folder found at: ${sourcePath}`);
331
+ return [];
332
+ }
333
+ return sync(posix.join(sourcePath, "**/*.json"));
334
+ }
335
+ function loadLanguage(lang, outDir = false) {
336
+ const files = getLocalLanguageFiles(lang, outDir);
337
+ const result = /* @__PURE__ */ new Map();
338
+ for (const file of files) try {
339
+ result.set(file, fs.readJSONSync(file));
340
+ languageTracker.addFile(lang, file);
341
+ } catch (e) {
342
+ logger_default.warn(e);
343
+ }
344
+ return result;
345
+ }
346
+
347
+ //#endregion
348
+ //#region src/language/transformer.ts
349
+ function flattenKeys(obj, prefix = "") {
350
+ const result = {};
351
+ for (const [key, val] of Object.entries(obj)) {
352
+ const fullKey = prefix ? `${prefix}.${key}` : key;
353
+ if (val && typeof val === "object" && !Array.isArray(val)) Object.assign(result, flattenKeys(val, fullKey));
354
+ else result[fullKey] = val;
355
+ }
356
+ return result;
357
+ }
358
+ function expandDotNotationKeys(target, source, depth = 0) {
359
+ if (depth > 32) logger_default.fail("Max object expansion depth exceeded.");
360
+ if (!source || typeof source !== "object" || Array.isArray(source)) return source;
361
+ for (const [key, value] of Object.entries(source)) {
362
+ let current = target;
363
+ const parts = key.split(".");
364
+ const lastKey = parts.pop();
365
+ for (const part of parts) {
366
+ if (!(part in current)) current[part] = {};
367
+ current = current[part];
368
+ }
369
+ if (lastKey in current) console.warn(`Warning: Overwriting key "${lastKey}" during transformation.`);
370
+ current[lastKey] = expandDotNotationKeys({}, value, depth + 1);
371
+ }
372
+ return target;
373
+ }
374
+ function transform(dataMap) {
375
+ const mergedData = {};
376
+ for (const data of dataMap.values()) expandDotNotationKeys(mergedData, data);
377
+ return mergedData;
378
+ }
379
+
380
+ //#endregion
381
+ //#region src/server/http-middleware.ts
382
+ function httpMiddlewareHook(server) {
383
+ server.middlewares.use((req, res, next) => {
384
+ const config = context.config;
385
+ if (!path_utils_default.isFoundryVTTUrl(req.url ?? "")) {
386
+ next();
387
+ return;
388
+ }
389
+ const cssFileName = config.build.lib.cssFileName;
390
+ const cssEntry = cssFileName ? path_utils_default.localToFoundryVTTUrl(`${cssFileName}.css`) : null;
391
+ if (path_utils_default.normalize(req.url ?? "") === cssEntry) {
392
+ logger_default.info(`Blocking CSS entry to ${req.url}`);
393
+ res.setHeader("Content-Type", "text/css");
394
+ res.end("/* The cake is in another castle. */");
395
+ return;
396
+ }
397
+ const languages = context.manifest.languages.filter((lang) => path_utils_default.localToFoundryVTTUrl(lang.path) === path_utils_default.normalize(req.url ?? ""));
398
+ if (languages.length === 1) {
399
+ const lang = languages[0].lang;
400
+ const language = loadLanguage(lang);
401
+ const jsonData = transform(language);
402
+ res.setHeader("Content-Type", "application/json");
403
+ res.end(JSON.stringify(jsonData, null, 2));
404
+ return;
405
+ }
406
+ next();
407
+ });
408
+ }
409
+
410
+ //#endregion
411
+ //#region src/server/socket-proxy.ts
412
+ function socketProxy(server) {
413
+ const env = context.env;
414
+ const ioProxy = new Server(server.httpServer, { path: "/socket.io" });
415
+ ioProxy.on("connection", (socket) => {
416
+ const upstream = io(`http://${env.foundryUrl}:${env.foundryPort}`, {
417
+ transports: ["websocket"],
418
+ upgrade: false,
419
+ query: socket.handshake.query
420
+ });
421
+ socket.onAny((event, ...args) => {
422
+ const maybeAck = typeof args[args.length - 1] === "function" ? args.pop() : null;
423
+ if (event === "template") {
424
+ const localPath = path_utils_default.foundryVTTUrlToLocal(args[0]);
425
+ if (localPath) {
426
+ if (maybeAck) maybeAck({
427
+ html: fs.readFileSync(localPath, "utf8"),
428
+ success: true
429
+ });
430
+ handlebarsTracker.addFile(args[0], localPath);
431
+ return;
432
+ }
433
+ }
434
+ upstream.emit(event, ...args, (response) => {
435
+ if (maybeAck) maybeAck(response);
436
+ });
437
+ });
438
+ upstream.onAny((event, ...args) => {
439
+ const lastArg = args[args.length - 1];
440
+ const maybeAck = typeof lastArg === "function" ? args.pop() : null;
441
+ socket.emit(event, ...args, (response) => {
442
+ if (maybeAck) maybeAck(response);
443
+ });
444
+ });
445
+ });
446
+ }
447
+
448
+ //#endregion
449
+ //#region src/server/index.ts
450
+ function setupDevServer(server) {
451
+ handlebarsTracker.initialize(server);
452
+ languageTracker.initialize(server);
453
+ httpMiddlewareHook(server);
454
+ socketProxy(server);
455
+ }
456
+
457
+ //#endregion
458
+ //#region src/language/validator.ts
459
+ function validator() {
460
+ const manifest = context.manifest;
461
+ const baseLanguageData = loadLanguage("en", true);
462
+ if (baseLanguageData.size === 0) {
463
+ logger_default.error("Base language \"en\" not found or could not be loaded.");
464
+ return;
465
+ }
466
+ const base = flattenKeys(baseLanguageData.values().next().value);
467
+ for (const lang of manifest.languages) {
468
+ if (lang.lang === "en") continue;
469
+ const currentLanguageData = loadLanguage(lang.lang, true);
470
+ if (currentLanguageData.size === 0) {
471
+ console.warn(`Summary for language [${lang.lang}]: Could not be loaded.`);
472
+ continue;
473
+ }
474
+ const current = flattenKeys(currentLanguageData.values().next().value);
475
+ const missing = Object.keys(base).filter((key) => !(key in current));
476
+ const extra = Object.keys(current).filter((key) => !(key in base));
477
+ console.log(`Summary for language [${lang.lang}]:`);
478
+ if (missing.length) console.warn(`\tMissing keys: ${missing.length}`, missing.slice(0, 5));
479
+ if (extra.length) console.warn(`\tExtra keys: ${extra.length}`, extra.slice(0, 5));
480
+ if (!missing.length && !extra.length) console.log(" ✅ All keys match.");
481
+ }
482
+ }
483
+
484
+ //#endregion
485
+ //#region src/index.ts
486
+ function foundryVTTPlugin() {
487
+ context.env = loadEnv();
488
+ return {
489
+ name: "vite-plugin-foundryvtt",
490
+ config(config) {
491
+ context.manifest = loadManifest(config);
492
+ return createPartialViteConfig(config);
493
+ },
494
+ configResolved(config) {
495
+ context.config = config;
496
+ },
497
+ async closeBundle() {
498
+ if (context.config?.mode !== "production") return;
499
+ const outDir = posix.resolve(process.cwd(), context.config.build.outDir);
500
+ const candidates = ["system.json", "module.json"];
501
+ for (const file of candidates) {
502
+ const src = posix.resolve(process.cwd(), file);
503
+ if (await fs.pathExists(src)) {
504
+ const dest = posix.join(outDir, file);
505
+ await fs.copy(src, dest);
506
+ logger_default.info(`Copied ${file} >>> ${dest}`);
507
+ }
508
+ }
509
+ const languages = context.manifest?.languages ?? [];
510
+ if (languages.length > 0) {
511
+ for (const language of languages) {
512
+ if (path_utils_default.getOutDirFile(language.path) !== "") continue;
513
+ const languageDataRaw = loadLanguage(language.lang);
514
+ const languageData = transform(languageDataRaw);
515
+ fs.writeJSONSync(posix.join(path_utils_default.getOutDir(), language.path), languageData);
516
+ }
517
+ validator();
518
+ }
519
+ },
520
+ load(id) {
521
+ const config = context.config;
522
+ const jsFileName = config.build.lib.fileName;
523
+ if (id === jsFileName || id === `/${jsFileName}`) {
524
+ const entryPath = posix.resolve(config.build.lib.entry);
525
+ const viteId = `/@fs/${entryPath}`;
526
+ return `import '${viteId}';\n${hmr_client_default}`;
527
+ }
528
+ },
529
+ configureServer: setupDevServer
530
+ };
531
+ }
532
+
533
+ //#endregion
534
+ export { foundryVTTPlugin as default };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "vite-plugin-fvtt",
3
+ "version": "0.1.1",
4
+ "description": "A Vite plugin for module and system development for Foundry VTT",
5
+ "keywords": [
6
+ "vite",
7
+ "vite-plugin",
8
+ "foundryvtt"
9
+ ],
10
+ "homepage": "https://github.com/MatyeusM/vite-plugin-fvtt#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/MatyeusM/vite-plugin-fvtt/issues"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/MatyeusM/vite-plugin-fvtt.git"
17
+ },
18
+ "license": "MIT",
19
+ "author": "MatyeusM (https://github.com/MatyeusM)",
20
+ "type": "module",
21
+ "main": "dist/index.js",
22
+ "types": "dist/index.d.ts",
23
+ "files": [
24
+ "dist",
25
+ "LICENSE",
26
+ "README.md"
27
+ ],
28
+ "scripts": {
29
+ "dev": "tsdown --watch",
30
+ "build": "tsdown"
31
+ },
32
+ "devDependencies": {
33
+ "@eslint/js": "^9.34.0",
34
+ "@types/fs-extra": "^11.0.4",
35
+ "@types/node": "^24.3.0",
36
+ "eslint": "^9.34.0",
37
+ "prettier": "^3.6.2",
38
+ "tsdown": "^0.14.2",
39
+ "typescript": "^5.9.2",
40
+ "typescript-eslint": "^8.41.0",
41
+ "vite": "*"
42
+ },
43
+ "dependencies": {
44
+ "dotenv": "^17.2.1",
45
+ "fs-extra": "^11.3.1",
46
+ "glob": "^11.0.3",
47
+ "socket.io": "^4.8.1",
48
+ "socket.io-client": "^4.8.1"
49
+ },
50
+ "peerDependencies": {
51
+ "vite": "^7.0.0"
52
+ }
53
+ }