vite-plugin-fvtt 0.1.2 β†’ 0.1.4

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 (3) hide show
  1. package/README.md +45 -31
  2. package/dist/index.js +161 -92
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,23 +1,23 @@
1
1
  # **vite-plugin-fvtt**
2
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.
3
+ A [Vite](https://vitejs.dev/) plugin to **streamline and automate** the development of Foundry VTT modules and systems.
4
4
 
5
- ## **πŸš€ Key Features**
5
+ It handles manifest resolution, asset copying, language file composition, and template handling with **minimal setup**, letting you focus on your code.
6
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.
7
+ The plugin's core goal is to enable a robust HMR workflow via Vite's development server, freeing you from Foundry VTT's native HMR and build watch commands.
8
8
 
9
- ## **Getting Started**
9
+ ## **πŸš€ Getting Started**
10
10
 
11
- ### **1. Setup a Foundry VTT Project**
11
+ ### **Step 1. Setup a Foundry VTT Project**
12
12
 
13
13
  Create a standard [Foundry VTT module or system](https://foundryvtt.com/article/module-development/).
14
14
  Place your `module.json` or `system.json` manifest in either your **project root** or your **public/** directory.
15
15
 
16
- ### **2. Add the Plugin to your Vite Config**
16
+ ### **Step 2. Add the Plugin to your Vite Config**
17
17
 
18
18
  Install the plugin with `npm i -D vite-plugin-fvtt`.
19
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.
20
+ Add the plugin to your vite.config.js. The **build.lib.entry** field is required; most of the other settings are inferred by the plugin from your Foundry VTT manifest.
21
21
 
22
22
  ```js
23
23
  // vite.config.js
@@ -37,50 +37,64 @@ export default defineConfig({
37
37
  });
38
38
  ```
39
39
 
40
- ## **βš™οΈ How it Works**
40
+ ## **βš™οΈ Features**
41
41
 
42
- ### **Manifest & Asset Resolution**
42
+ ### **1. Configuration**
43
+ The plugin needs to know where your Foundry VTT instance is running to proxy and serve assets correctly. If you want to change anything from the defaults `http://localhost:30000`, create a `.env.foundryvtt.local` file in your project.
44
+ ```ini
45
+ FOUNDRY_URL=localhost
46
+ FOUNDRY_PORT=30000
47
+ ```
48
+
49
+ The Vite dev server will run on `FOUNDRY_PORT + 1`, where you will need to open your browser manually to.
50
+
51
+ ### **2. Manifest & Asset Resolution**
52
+
53
+ The plugin automatically detects your manifest file (`module.json` or `system.json`) in the project **root** or `public/` folder.
54
+
55
+ This plugin shapes the output depending on your manifest; it tries to automatically discover the relevant files in the `root`, `source`, and `public` folders to build the output files. The `public` folder is defined by the Vite config file. The plugin determines the `source` directory based on your `lib.entry` path. For example, if your `lib.entry` is './mysource/package/main.js', the `mysource/` directory is considered your source directory.
56
+
57
+ πŸ’‘ Your entry file should always import your main stylesheet; the manifest dictates how everything is named and output.
58
+
59
+ ### **3. ESModules, Scripts & Styles**
43
60
 
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.
61
+ `esmodules` and `scripts` declared in your manifest are automatically created from your `lib.entry`. Since Vite compiles the module, the plugin expects the `esmodules` or `scripts` entry in your manifest to only point to a single JavaScript file.
46
62
 
47
- ### **Template Handling**
63
+ Stylesheets (CSS/SCSS/LESS) should be imported in your entry file; the plugin ensures they are outputted as the correct file.
48
64
 
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.
65
+ ### **4. Template Handling**
54
66
 
55
- ### **Language File Merging**
67
+ Templates work in HMR properly on the development server; they are autodiscovered as discussed in [2. Manifest & Asset Resolution](#2-manifest--asset-resolution). The development server intercepts the websocket traffic and sends the local templates instead of Foundry VTT's, if present. e.g., a template request to `/systems/mysystem/tpl/character-header.hbs` might be rerouted to `public/tpl/character-header.hbs`. Folder structure inside your project is mirrored, apart from the `system`/`module` specific prefix.
56
68
 
57
- The plugin offers a powerful feature for managing translations.
69
+ ### **5. Language File Merging**
58
70
 
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.
71
+ Supports both complete and partial translation workflows:
61
72
 
62
- ## **Example Project Structure**
73
+ * **Complete files:** Place a complete JSON file (e.g., `public/lang/en.json`) and the plugin will copy it as-is.
74
+ * **Partial files:** Place multiple JSONs inside `src/lang/en/` and the plugin merges them into one `lang/en.json` at build.
75
+
76
+ Merging follows your manifest’s declared language paths, searching in root or source directories.
77
+
78
+ ⚠️ **Note:** HMR works for language files, but non-English locales may not reload as expected.
79
+
80
+ ### **Example Project Structure**
63
81
  ```
64
82
  my-module/
65
83
  β”œβ”€ src/
66
84
  β”‚ β”œβ”€ main.js # The primary module entry file (required by Vite).
67
- β”‚ β”œβ”€ style.css # Your project's main stylesheet.
85
+ β”‚ β”œβ”€ style.css # Your project's main stylesheet, imported by main.js.
68
86
  β”‚ └─ lang/en/ # Directory for partial, merged translation files.
69
87
  β”‚ β”œβ”€ spells.json
70
88
  β”‚ β”œβ”€ abilities.json
71
89
  β”‚ └─ general.json
72
- β”œβ”€ public/
90
+ β”œβ”€ public/ # For static assets (templates, images)
73
91
  β”‚ β”œβ”€ module.json # Your module's manifest file (or system.json).
74
92
  β”‚ └─ templates/ # HTML template files for your module.
75
93
  β”œβ”€ vite.config.js # Your Vite configuration file.
76
94
  ```
77
95
 
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
96
  ---
85
97
 
86
- License: MIT
98
+ ## πŸ“„ License
99
+
100
+ [MIT](LICENSE)
package/dist/index.js CHANGED
@@ -95,17 +95,41 @@ function createPartialViteConfig(config) {
95
95
  const base = config.base ?? `/${context.manifest?.manifestType}s/${context.manifest?.id}/`;
96
96
  const useEsModules = context.manifest?.esmodules.length === 1;
97
97
  const formats = useEsModules ? ["es"] : ["umd"];
98
- const fileName = (useEsModules ? context.manifest?.esmodules[0] : context.manifest?.scripts?.[0]) ?? "bundle";
99
- if (fileName === "bundle") logger_default.warn("No output file specified in manifest, using default \"bundle\"");
98
+ const fileName = (useEsModules ? context.manifest?.esmodules[0] : context.manifest?.scripts?.[0]) ?? "scripts/bundle.js";
99
+ if (!(useEsModules || context.manifest?.scripts?.[0])) logger_default.warn("No output file specified in manifest, using default \"bundle\" in the \"scripts/\" folder");
100
100
  if (!context.manifest?.styles?.length) logger_default.warn("No CSS file found in manifest");
101
- const cssFileName = posix.parse(context.manifest?.styles[0] ?? "bundle.css").name;
101
+ const cssFileName = context.manifest?.styles[0] ?? "styles/bundle.css";
102
+ if (!context.manifest?.styles[0]) logger_default.warn("No output css file specified in manifest, using default \"bundle\" in the \"styles/\" folder");
102
103
  const foundryPort = context.env?.foundryPort ?? 3e4;
103
104
  const foundryUrl = context.env?.foundryUrl ?? "localhost";
104
105
  const entry = (config.build?.lib)?.entry;
105
106
  if (!entry) logger_default.fail("Entry must be specified in lib");
106
107
  if (typeof entry !== "string") logger_default.fail("Only a singular string entry is supported for build.lib.entry");
108
+ const isWatch = process.argv.includes("--watch") || !!config.build?.watch;
107
109
  return {
108
110
  base,
111
+ build: {
112
+ emptyOutDir: config.build?.emptyOutDir ?? !isWatch,
113
+ lib: {
114
+ entry,
115
+ formats,
116
+ name: context.manifest?.id ?? "bundle",
117
+ cssFileName: "bundle"
118
+ },
119
+ minify: "esbuild",
120
+ rollupOptions: { output: {
121
+ entryFileNames: fileName,
122
+ assetFileNames: (assetInfo) => {
123
+ const names = assetInfo.names ?? [];
124
+ if (names.some((n) => n.endsWith(".css"))) return cssFileName;
125
+ return "[name][extname]";
126
+ }
127
+ } }
128
+ },
129
+ define: { __FVTT_PLUGIN__: {
130
+ id: context.manifest?.id,
131
+ isSystem: context.manifest?.manifestType === "system"
132
+ } },
109
133
  esbuild: config.esbuild ?? {
110
134
  minifyIdentifiers: false,
111
135
  minifySyntax: true,
@@ -115,47 +139,10 @@ function createPartialViteConfig(config) {
115
139
  server: {
116
140
  port: foundryPort + 1,
117
141
  proxy: { [`^(?!${base})`]: `http://${foundryUrl}:${foundryPort}` }
118
- },
119
- build: {
120
- minify: "esbuild",
121
- lib: {
122
- cssFileName,
123
- entry,
124
- fileName,
125
- formats,
126
- name: context.manifest?.id
127
- }
128
142
  }
129
143
  };
130
144
  }
131
145
 
132
- //#endregion
133
- //#region src/server/hmr-client.ts
134
- var hmr_client_default = `
135
- if (import.meta.hot) {
136
- function refreshApplications() {
137
- // AppV1 refresh
138
- Object.values(foundry.ui.windows).forEach(app => app.render(true))
139
- // AppV2 refresh
140
- // TODO: Can we filter out to only refresh the correct apps?
141
- foundry.applications.instances.forEach(appV2 => appV2.render(true))
142
- }
143
-
144
- import.meta.hot.on('foundryvtt-template-update', async ({ path }) => {
145
- console.log('Vite | Force reload template', path)
146
- Handlebars.unregisterPartial(path)
147
- await foundry.applications.handlebars.getTemplate(path)
148
- refreshApplications()
149
- })
150
-
151
- import.meta.hot.on('foundryvtt-language-update', async () => {
152
- console.log('Vite | Force reassigning language')
153
- await game.i18n.setLanguage(game.i18n.lang)
154
- refreshApplications()
155
- })
156
- } else console.error('Vite | HMR is disabled')
157
- `;
158
-
159
146
  //#endregion
160
147
  //#region src/server/trackers/abstract-file-tracker.ts
161
148
  var AbstractFileTracker = class {
@@ -191,19 +178,6 @@ var AbstractFileTracker = class {
191
178
  }
192
179
  };
193
180
 
194
- //#endregion
195
- //#region src/server/trackers/handlebars-tracker.ts
196
- var HandlebarsTracker = class extends AbstractFileTracker {
197
- updateEvent = "foundryvtt-template-update";
198
- constructor() {
199
- super(context.config);
200
- }
201
- getEventData(changedPath, value) {
202
- return { path: value };
203
- }
204
- };
205
- const handlebarsTracker = new HandlebarsTracker();
206
-
207
181
  //#endregion
208
182
  //#region src/server/trackers/language-tracker.ts
209
183
  var LanguageTracker = class extends AbstractFileTracker {
@@ -389,6 +363,46 @@ function transform(dataMap) {
389
363
  return mergedData;
390
364
  }
391
365
 
366
+ //#endregion
367
+ //#region src/language/validator.ts
368
+ function validator() {
369
+ const manifest = context.manifest;
370
+ const baseLanguageData = loadLanguage("en", true);
371
+ if (baseLanguageData.size === 0) {
372
+ logger_default.error("Base language \"en\" not found or could not be loaded.");
373
+ return;
374
+ }
375
+ const base = flattenKeys(baseLanguageData.values().next().value);
376
+ for (const lang of manifest.languages) {
377
+ if (lang.lang === "en") continue;
378
+ const currentLanguageData = loadLanguage(lang.lang, true);
379
+ if (currentLanguageData.size === 0) {
380
+ console.warn(`Summary for language [${lang.lang}]: Could not be loaded.`);
381
+ continue;
382
+ }
383
+ const current = flattenKeys(currentLanguageData.values().next().value);
384
+ const missing = Object.keys(base).filter((key) => !(key in current));
385
+ const extra = Object.keys(current).filter((key) => !(key in base));
386
+ console.log(`Summary for language [${lang.lang}]:`);
387
+ if (missing.length) console.warn(`\tMissing keys: ${missing.length}`, missing.slice(0, 5));
388
+ if (extra.length) console.warn(`\tExtra keys: ${extra.length}`, extra.slice(0, 5));
389
+ if (!missing.length && !extra.length) console.log(" βœ… All keys match.");
390
+ }
391
+ }
392
+
393
+ //#endregion
394
+ //#region src/server/trackers/handlebars-tracker.ts
395
+ var HandlebarsTracker = class extends AbstractFileTracker {
396
+ updateEvent = "foundryvtt-template-update";
397
+ constructor() {
398
+ super(context.config);
399
+ }
400
+ getEventData(changedPath, value) {
401
+ return { path: value };
402
+ }
403
+ };
404
+ const handlebarsTracker = new HandlebarsTracker();
405
+
392
406
  //#endregion
393
407
  //#region src/server/http-middleware.ts
394
408
  function httpMiddlewareHook(server) {
@@ -467,31 +481,83 @@ function setupDevServer(server) {
467
481
  }
468
482
 
469
483
  //#endregion
470
- //#region src/language/validator.ts
471
- function validator() {
472
- const manifest = context.manifest;
473
- const baseLanguageData = loadLanguage("en", true);
474
- if (baseLanguageData.size === 0) {
475
- logger_default.error("Base language \"en\" not found or could not be loaded.");
476
- return;
477
- }
478
- const base = flattenKeys(baseLanguageData.values().next().value);
479
- for (const lang of manifest.languages) {
480
- if (lang.lang === "en") continue;
481
- const currentLanguageData = loadLanguage(lang.lang, true);
482
- if (currentLanguageData.size === 0) {
483
- console.warn(`Summary for language [${lang.lang}]: Could not be loaded.`);
484
- continue;
485
- }
486
- const current = flattenKeys(currentLanguageData.values().next().value);
487
- const missing = Object.keys(base).filter((key) => !(key in current));
488
- const extra = Object.keys(current).filter((key) => !(key in base));
489
- console.log(`Summary for language [${lang.lang}]:`);
490
- if (missing.length) console.warn(`\tMissing keys: ${missing.length}`, missing.slice(0, 5));
491
- if (extra.length) console.warn(`\tExtra keys: ${extra.length}`, extra.slice(0, 5));
492
- if (!missing.length && !extra.length) console.log(" βœ… All keys match.");
493
- }
494
- }
484
+ //#region src/server/hmr-client.ts
485
+ var hmr_client_default = `
486
+ if (import.meta.hot) {
487
+ const FVTT_PLUGIN = __FVTT_PLUGIN__
488
+
489
+ function refreshApplications(path = null) {
490
+ // AppV1 refresh
491
+ Object.values(foundry.ui.windows).forEach(app => app.render(true))
492
+ // AppV2 refresh
493
+ if (path)
494
+ foundry.applications.instances.forEach(appV2 => {
495
+ Object.values(appV2.constructor.PARTS ?? {}).forEach(part => {
496
+ const templates = Array.isArray(part.templates) ? part.templates : []
497
+ if (part.template === path || templates.includes(path)) appV2.render(true)
498
+ })
499
+ })
500
+ else foundry.applications.instances.forEach(appV2 => appV2.render(true))
501
+ }
502
+
503
+ import.meta.hot.on('foundryvtt-template-update', ({ path }) => {
504
+ game.socket.emit('template', path, response => {
505
+ if (response.error) new Error(response.error)
506
+ let template = undefined
507
+ try {
508
+ template = Handlebars.compile(response.html)
509
+ } catch (error) {
510
+ console.error(error)
511
+ return
512
+ }
513
+ Handlebars.registerPartial(path, template)
514
+ console.log(\`Vite | Retrieved and compiled template \${path}\`)
515
+ refreshApplications(path)
516
+ })
517
+ })
518
+
519
+ async function hmrLanguage(lang, targetObject = game.i18n.translations) {
520
+ try {
521
+ const languages = FVTT_PLUGIN.isSystem
522
+ ? game.system.languages
523
+ : game.modules.get(FVTT_PLUGIN.id)?.languages
524
+ if (!languages) {
525
+ console.warn(
526
+ 'Vite | Got a HMR request to reload languages, however no languages were found.',
527
+ )
528
+ return
529
+ }
530
+ const langEntry = languages.find(l => l.lang === lang)
531
+ if (!langEntry) {
532
+ console.warn('Vite | Got an HMR request for an undefined language')
533
+ return
534
+ }
535
+
536
+ const url = langEntry.path
537
+ const resp = await fetch(url)
538
+ if (!resp.ok) throw new Error('Failed to fetch language file!')
539
+
540
+ const json = await resp.json()
541
+
542
+ foundry.utils.mergeObject(targetObject, json)
543
+ console.log(\`Vite | HMR: Reloaded language '\${lang}'\`)
544
+ } catch (error) {
545
+ console.error(\`Vite | HMR: Error reloading language '\${lang}' for \${FVTT_PLUGIN.id}\`, error);
546
+ }
547
+ }
548
+
549
+ import.meta.hot.on('foundryvtt-language-update', async () => {
550
+ const currentLang = game.i18n.lang
551
+ const promises = []
552
+ if (currentLang !== 'en') {
553
+ promises.push(hmrLanguage('en', game.i18n._fallback))
554
+ }
555
+ promises.push(hmrLanguage(currentLang))
556
+ await Promise.all(promises)
557
+ refreshApplications()
558
+ })
559
+ } else console.error('Vite | HMR is disabled')
560
+ //`;
495
561
 
496
562
  //#endregion
497
563
  //#region src/index.ts
@@ -506,32 +572,35 @@ function foundryVTTPlugin() {
506
572
  configResolved(config) {
507
573
  context.config = config;
508
574
  },
509
- async closeBundle() {
510
- if (context.config?.mode !== "production") return;
511
- const outDir = posix.resolve(process.cwd(), context.config.build.outDir);
575
+ async writeBundle() {
576
+ if (!context.config) return;
577
+ const outDir = path_utils_default.getOutDir();
512
578
  const candidates = ["system.json", "module.json"];
513
579
  for (const file of candidates) {
514
- const src = posix.resolve(process.cwd(), file);
515
- if (await fs.pathExists(src)) {
580
+ const src = posix.resolve(file);
581
+ if (!path_utils_default.getOutDirFile(file) && fs.existsSync(src)) {
582
+ this.addWatchFile(src);
516
583
  const dest = posix.join(outDir, file);
517
584
  await fs.copy(src, dest);
518
585
  logger_default.info(`Copied ${file} >>> ${dest}`);
519
586
  }
520
587
  }
521
588
  const languages = context.manifest?.languages ?? [];
522
- if (languages.length > 0) {
523
- for (const language of languages) {
524
- if (path_utils_default.getOutDirFile(language.path) !== "") continue;
525
- const languageDataRaw = loadLanguage(language.lang);
526
- const languageData = transform(languageDataRaw);
527
- fs.writeJSONSync(posix.join(path_utils_default.getOutDir(), language.path), languageData);
528
- }
529
- validator();
589
+ if (languages.length > 0) for (const language of languages) {
590
+ if (path_utils_default.getOutDirFile(language.path)) continue;
591
+ getLocalLanguageFiles(language.lang).forEach((langFile) => this.addWatchFile(langFile));
592
+ const languageDataRaw = loadLanguage(language.lang);
593
+ const languageData = transform(languageDataRaw);
594
+ fs.writeJSONSync(posix.join(outDir, language.path), languageData);
530
595
  }
531
596
  },
597
+ closeBundle() {
598
+ const languages = context.manifest?.languages ?? [];
599
+ if (languages.length > 0) validator();
600
+ },
532
601
  load(id) {
533
602
  const config = context.config;
534
- const jsFileName = config.build.lib.fileName;
603
+ const jsFileName = (config.build.rollupOptions?.output).entryFileNames;
535
604
  if (id === jsFileName || id === `/${jsFileName}`) {
536
605
  const entryPath = posix.resolve(config.build.lib.entry);
537
606
  const viteId = `/@fs/${entryPath}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-fvtt",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "A Vite plugin for module and system development for Foundry VTT",
5
5
  "keywords": [
6
6
  "vite",