vite-plugin-fvtt 0.2.5 → 0.2.6

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/CHANGELOG.md CHANGED
@@ -1,17 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ [0.2.6] - 2025-10-01
4
+
5
+ ### Added
6
+
7
+ - CI now tests against Node.js Latest, 20 LTS, and 22 LTS, ensuring Foundry projects compile across
8
+ supported environments.
9
+ - Badges were added to the README for improved project visibility.
10
+ - Dependabot now tracks GitHub Actions, not just NPM dependencies.
11
+
12
+ ### Changed
13
+
14
+ - HMR logic updated to mirror Foundry V14's internal implementation, with a full fallback to V13
15
+ behavior for templates and JSON language files. _(If Foundry doesn't end up relying on the new
16
+ data shape in V14, this will have been an over-engineered no-op; but future-proofing beats
17
+ regret.)_
18
+ - ESLint configuration significantly tightened:
19
+ - Added sonarjs and unicorn plugins for deeper static analysis.
20
+ - Upgraded TypeScript ESLint rules from recommended to strict, increasing development signal
21
+ accuracy.
22
+ - Test suite refactored to reduce duplication and simplify onboarding for future test additions.
23
+
3
24
  ## [0.2.5] - 2025-09-24
4
25
 
5
26
  ### Fixed
6
27
 
7
- - `system.json` or `module.json` in root due to missing wait condition for the check not properly copying.
28
+ - `system.json` or `module.json` in root due to missing wait condition for the check not properly
29
+ copying.
8
30
 
9
31
  ## [0.2.4] - 2025-09-23
10
32
 
11
33
  ### Changed
12
34
 
13
35
  - Removed dependencies of `fs-extra` and `dotenv` to shrink the dependencies.
14
- - Async file loading should improve the performance for a large number of language files significantly.
36
+ - Async file loading should improve the performance for a large number of language files
37
+ significantly.
15
38
 
16
39
  ## [0.2.3] - 2025-09-20
17
40
 
@@ -101,7 +124,10 @@
101
124
 
102
125
  - Initial Release
103
126
 
104
- [unreleased]: https://github.com/MatyeusM/vite-plugin-fvtt/compare/v0.2.3...HEAD
127
+ [unreleased]: https://github.com/MatyeusM/vite-plugin-fvtt/compare/v0.2.6...HEAD
128
+ [0.2.6]: https://github.com/MatyeusM/vite-plugin-fvtt/compare/v0.2.5...v0.2.6
129
+ [0.2.5]: https://github.com/MatyeusM/vite-plugin-fvtt/compare/v0.2.4...v0.2.5
130
+ [0.2.4]: https://github.com/MatyeusM/vite-plugin-fvtt/compare/v0.2.3...v0.2.4
105
131
  [0.2.3]: https://github.com/MatyeusM/vite-plugin-fvtt/compare/v0.2.2...v0.2.3
106
132
  [0.2.2]: https://github.com/MatyeusM/vite-plugin-fvtt/compare/v0.2.1...v0.2.2
107
133
  [0.2.1]: https://github.com/MatyeusM/vite-plugin-fvtt/compare/v0.2.0...v0.2.1
package/README.md CHANGED
@@ -1,4 +1,14 @@
1
- # **vite-plugin-fvtt**
1
+ <h1 align="center">vite-plugin-fvtt</h1>
2
+
3
+ <div align="center">
4
+
5
+ ![NPM Version](https://img.shields.io/npm/v/vite-plugin-fvtt?style=for-the-badge&labelColor=1a1c23&color=a9cbae)
6
+ ![GitHub License](https://img.shields.io/github/license/MatyeusM/vite-plugin-fvtt?style=for-the-badge&labelColor=1a1c23&color=97cdcc)
7
+ ![GitHub last commit](https://img.shields.io/github/last-commit/MatyeusM/vite-plugin-fvtt?style=for-the-badge&labelColor=1a1c23&color=a1c6e1)
8
+ ![GitHub repo size](https://img.shields.io/github/repo-size/MatyeusM/vite-plugin-fvtt?style=for-the-badge&labelColor=1a1c23&color=bdbde4)
9
+ ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/MatyeusM/vite-plugin-fvtt/ci.yml?style=for-the-badge&labelColor=1a1c23&color=d6b5d2)
10
+
11
+ </div>
2
12
 
3
13
  A [Vite](https://vitejs.dev/) plugin to **streamline and automate** the development of Foundry VTT
4
14
  modules and systems.
@@ -45,7 +55,7 @@ export default defineConfig({
45
55
 
46
56
  ## **⚙️ Features**
47
57
 
48
- ### **1. Configuration**
58
+ ### **1. Configuration (Optional)**
49
59
 
50
60
  The plugin needs to know where your Foundry VTT instance is running to proxy and serve assets
51
61
  correctly. If you want to change anything from the defaults `http://localhost:30000`, create a
package/dist/index.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import { Plugin } from "vite";
2
2
 
3
3
  //#region src/index.d.ts
4
- declare function foundryVTTPlugin(options?: {
5
- buildPacks: boolean;
4
+ declare function foundryVTTPlugin({
5
+ buildPacks
6
+ }?: {
7
+ buildPacks?: boolean | undefined;
6
8
  }): Promise<Plugin>;
7
9
  //#endregion
8
10
  export { foundryVTTPlugin as default };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import path from "path";
1
+ import path from "node:path";
2
2
  import { glob } from "tinyglobby";
3
- import fs from "fs/promises";
3
+ import fs from "node:fs/promises";
4
4
  import { compilePack } from "@foundryvtt/foundryvtt-cli";
5
5
  import { Server } from "socket.io";
6
6
  import { io } from "socket.io-client";
@@ -9,39 +9,36 @@ import { io } from "socket.io-client";
9
9
  const context = {};
10
10
 
11
11
  //#endregion
12
- //#region src/utils/fs-utils.ts
13
- var FsUtils = class FsUtils {
14
- static async checkType(p, check) {
15
- try {
16
- const stats = await fs.stat(p);
17
- return check(stats);
18
- } catch {
19
- return false;
20
- }
21
- }
22
- static async fileExists(p) {
23
- return FsUtils.checkType(p, (s) => s.isFile());
24
- }
25
- static async dirExists(p) {
26
- return FsUtils.checkType(p, (s) => s.isDirectory());
27
- }
28
- static async readFile(filePath, encoding = "utf-8") {
29
- return fs.readFile(filePath, { encoding });
12
+ //#region src/utils/fs-utilities.ts
13
+ async function checkType(p, check) {
14
+ try {
15
+ const stats = await fs.stat(p);
16
+ return check(stats);
17
+ } catch {
18
+ return false;
30
19
  }
31
- static async readJson(filePath) {
32
- try {
33
- const content = await FsUtils.readFile(filePath);
34
- return JSON.parse(content);
35
- } catch {
36
- return null;
37
- }
20
+ }
21
+ async function fileExists(p) {
22
+ return checkType(p, (s) => s.isFile());
23
+ }
24
+ async function directoryExists(p) {
25
+ return checkType(p, (s) => s.isDirectory());
26
+ }
27
+ async function readFile(filePath, encoding = "utf8") {
28
+ return fs.readFile(filePath, { encoding });
29
+ }
30
+ async function readJson(filePath) {
31
+ try {
32
+ const content = await readFile(filePath);
33
+ return JSON.parse(content);
34
+ } catch {
35
+ return;
38
36
  }
39
- };
40
- var fs_utils_default = FsUtils;
37
+ }
41
38
 
42
39
  //#endregion
43
- //#region src/config/env.ts
44
- function parseEnv(content) {
40
+ //#region src/config/environment.ts
41
+ function parseEnvironment(content) {
45
42
  const result = {};
46
43
  for (const line of content.split(/\r?\n/)) {
47
44
  const trimmed = line.trim();
@@ -51,93 +48,105 @@ function parseEnv(content) {
51
48
  }
52
49
  return result;
53
50
  }
54
- async function loadEnv() {
55
- const envPaths = await glob(".env.foundryvtt*", { absolute: true });
51
+ async function loadEnvironment() {
52
+ const environmentPaths = await glob(".env.foundryvtt*", { absolute: true });
56
53
  let merged = {
57
54
  FOUNDRY_URL: "localhost",
58
55
  FOUNDRY_PORT: "30000"
59
56
  };
60
- for (const file of envPaths) {
61
- const content = await fs_utils_default.readFile(file, "utf-8");
57
+ for (const file of environmentPaths) {
58
+ const content = await readFile(file);
62
59
  merged = {
63
60
  ...merged,
64
- ...parseEnv(content)
61
+ ...parseEnvironment(content)
65
62
  };
66
63
  }
67
64
  return {
68
65
  foundryUrl: merged.FOUNDRY_URL,
69
- foundryPort: parseInt(merged.FOUNDRY_PORT, 10)
66
+ foundryPort: Number.parseInt(merged.FOUNDRY_PORT, 10)
70
67
  };
71
68
  }
72
69
 
73
70
  //#endregion
74
71
  //#region src/utils/logger.ts
75
- var Logger = class Logger {
76
- static namespace = "vite-plugin-fvtt";
77
- static colors = {
78
- info: "\x1B[36m",
79
- warn: "\x1B[33m",
80
- error: "\x1B[31m"
81
- };
82
- static reset = "\x1B[0m";
83
- initialize(namespace = "vite-plugin-fvtt") {
84
- Logger.namespace = namespace;
85
- }
86
- static format(level, message) {
87
- return `${Logger.colors[level] ?? ""}[${Logger.namespace}] [${level.toUpperCase()}]${Logger.reset} ${message}`;
88
- }
89
- static info(message) {
90
- console.log(Logger.format("info", message));
91
- }
92
- static warn(message) {
93
- console.warn(Logger.format("warn", message));
94
- }
95
- static error(message) {
96
- console.error(Logger.format("error", message));
97
- }
98
- static fail(message) {
99
- const formatted = Logger.format("error", Logger.stringify(message));
100
- console.error(formatted);
101
- throw new Error(formatted);
102
- }
103
- static stringify(message) {
104
- if (message instanceof Error) return message.stack ?? message.message;
105
- return typeof message === "string" ? message : JSON.stringify(message, null, 2);
106
- }
72
+ let loggerNamespace = "vite-plugin-fvtt";
73
+ const colors = {
74
+ info: "\x1B[36m",
75
+ warn: "\x1B[33m",
76
+ error: "\x1B[31m"
107
77
  };
108
- var logger_default = Logger;
78
+ const reset = "\x1B[0m";
79
+ function format(level, message) {
80
+ return `${colors[level] ?? ""}[${loggerNamespace}] [${level.toUpperCase()}]${reset} ${message}`;
81
+ }
82
+ function info(message) {
83
+ console.log(format("info", message));
84
+ }
85
+ function warn(message) {
86
+ console.warn(format("warn", message));
87
+ }
88
+ function error(message) {
89
+ console.error(format("error", message));
90
+ }
91
+ function fail(message) {
92
+ const formatted = format("error", stringify(message));
93
+ console.error(formatted);
94
+ throw new Error(formatted);
95
+ }
96
+ function stringify(message) {
97
+ if (message instanceof Error) return message.stack ?? message.message;
98
+ return typeof message === "string" ? message : JSON.stringify(message, void 0, 2);
99
+ }
109
100
 
110
101
  //#endregion
111
102
  //#region src/config/foundryvtt-manifest.ts
112
- async function loadManifest(config) {
113
- if (context?.manifest) return context.manifest;
114
- const publicDir = config.publicDir || "public";
103
+ async function resolveManifestPath(publicDirectory) {
115
104
  const paths = [
116
105
  "system.json",
117
106
  "module.json",
118
- `${publicDir}/system.json`,
119
- `${publicDir}/module.json`
107
+ `${publicDirectory}/system.json`,
108
+ `${publicDirectory}/module.json`
120
109
  ].map((f) => path.resolve(process.cwd(), f));
121
- const idx = (await Promise.all(paths.map((p) => fs_utils_default.fileExists(p)))).findIndex(Boolean);
122
- const foundPath = idx !== -1 ? paths[idx] : void 0;
123
- if (!foundPath) logger_default.fail(`Could not find a manifest file (system.json or module.json) in project root or ${publicDir}/.`);
110
+ const index = (await Promise.all(paths.map((p) => fileExists(p)))).findIndex(Boolean);
111
+ return index === -1 ? void 0 : paths[index];
112
+ }
113
+ function isLanguageEntry(object) {
114
+ return typeof object === "object" && object !== null && "lang" in object && "path" in object && typeof object.lang === "string" && typeof object.path === "string";
115
+ }
116
+ function isPackEntry(object) {
117
+ return typeof object === "object" && object !== null && "path" in object && typeof object.path === "string";
118
+ }
119
+ function validateManifest(rawData, foundPath) {
120
+ if (typeof rawData !== "object" || rawData === null) fail(`Manifest at ${foundPath} is not a valid JSON object.`);
121
+ const data = rawData;
122
+ if (typeof data.id !== "string") fail("Manifest is missing required \"id\" field or it is not a string");
123
+ const esmodules = Array.isArray(data.esmodules) ? data.esmodules.map(String) : [];
124
+ const scripts = Array.isArray(data.scripts) ? data.scripts.map(String) : [];
125
+ if (esmodules.length > 0 === scripts.length > 0) fail("Manifest must define exactly one of \"esmodules\" or \"scripts\"");
126
+ const styles = Array.isArray(data.styles) ? data.styles.map(String) : [];
127
+ const languages = Array.isArray(data.languages) && data.languages.every((entry) => isLanguageEntry(entry)) ? data.languages : [];
128
+ const packs = Array.isArray(data.packs) && data.packs.every((entry) => isPackEntry(entry)) ? data.packs : [];
129
+ return {
130
+ manifestType: foundPath.includes("module.json") ? "module" : "system",
131
+ id: data.id,
132
+ esmodules,
133
+ scripts,
134
+ styles,
135
+ languages,
136
+ packs
137
+ };
138
+ }
139
+ async function loadManifest(config) {
140
+ if (context?.manifest) return context.manifest;
141
+ const publicDirectory = config.publicDir || "public";
142
+ const foundPath = await resolveManifestPath(publicDirectory);
143
+ if (!foundPath) fail(`Could not find a manifest file (system.json or module.json) in project root or ${publicDirectory}/.`);
124
144
  try {
125
- const data = await fs_utils_default.readJson(foundPath);
126
- if (!data.id || typeof data.id !== "string") logger_default.fail(`Manifest at ${foundPath} is missing required "id" field.`);
127
- const hasEsmodules = Array.isArray(data.esmodules) && data.esmodules.length > 0;
128
- const hasScripts = Array.isArray(data.scripts) && data.scripts.length > 0;
129
- if (hasEsmodules === hasScripts) logger_default.fail(`Manifest at ${foundPath} must define exactly one of "esmodules" or "scripts".`);
130
- return {
131
- manifestType: foundPath.includes("module.json") ? "module" : "system",
132
- id: data.id,
133
- esmodules: Array.isArray(data.esmodules) ? data.esmodules : [],
134
- scripts: Array.isArray(data.scripts) ? data.scripts : [],
135
- styles: Array.isArray(data.styles) ? data.styles : [],
136
- languages: Array.isArray(data.languages) ? data.languages : [],
137
- packs: Array.isArray(data.packs) ? data.packs : []
138
- };
139
- } catch (err) {
140
- logger_default.fail(`Failed to read manifest at ${foundPath}: ${err?.message || err}`);
145
+ const rawData = await readJson(foundPath);
146
+ return validateManifest(rawData, foundPath);
147
+ } catch (error$1) {
148
+ if (error$1 instanceof Error) fail(`Failed to read manifest at ${foundPath}: ${error$1.message}`);
149
+ fail(`Failed to read manifest at ${foundPath}: ${String(error$1)}`);
141
150
  }
142
151
  }
143
152
 
@@ -148,15 +157,17 @@ function createPartialViteConfig(config) {
148
157
  const useEsModules = context.manifest?.esmodules.length === 1;
149
158
  const formats = useEsModules ? ["es"] : ["umd"];
150
159
  const fileName = (useEsModules ? context.manifest?.esmodules[0] : context.manifest?.scripts?.[0]) ?? "scripts/bundle.js";
151
- if (!(useEsModules || context.manifest?.scripts?.[0])) logger_default.warn("No output file specified in manifest, using default \"bundle\" in the \"scripts/\" folder");
152
- if (!context.manifest?.styles?.length) logger_default.warn("No CSS file found in manifest");
160
+ if (!(useEsModules || context.manifest?.scripts?.[0])) warn("No output file specified in manifest, using default \"bundle\" in the \"scripts/\" folder");
161
+ if (!context.manifest?.styles?.length) warn("No CSS file found in manifest");
153
162
  const cssFileName = context.manifest?.styles[0] ?? "styles/bundle.css";
154
- if (!context.manifest?.styles[0]) logger_default.warn("No output css file specified in manifest, using default \"bundle\" in the \"styles/\" folder");
163
+ if (!context.manifest?.styles[0]) warn("No output css file specified in manifest, using default \"bundle\" in the \"styles/\" folder");
155
164
  const foundryPort = context.env?.foundryPort ?? 3e4;
156
165
  const foundryUrl = context.env?.foundryUrl ?? "localhost";
157
- const entry = (config.build?.lib)?.entry;
158
- if (!entry) logger_default.fail("Entry must be specified in lib");
159
- if (typeof entry !== "string") logger_default.fail("Only a singular string entry is supported for build.lib.entry");
166
+ const library = config.build?.lib;
167
+ if (!library || typeof library !== "object") fail("This plugin needs a configured build.lib");
168
+ const entry = library.entry;
169
+ if (!entry) fail("Entry must be specified in lib");
170
+ if (typeof entry !== "string") fail("Only a singular string entry is supported for build.lib.entry");
160
171
  const isWatch = process.argv.includes("--watch") || !!config.build?.watch;
161
172
  return {
162
173
  base,
@@ -199,7 +210,7 @@ function createPartialViteConfig(config) {
199
210
  var AbstractFileTracker = class {
200
211
  initialized = false;
201
212
  tracked = /* @__PURE__ */ new Map();
202
- watcher = null;
213
+ watcher = void 0;
203
214
  config;
204
215
  constructor(config) {
205
216
  this.config = config;
@@ -211,7 +222,7 @@ var AbstractFileTracker = class {
211
222
  this.watcher.on("change", (changedPath) => {
212
223
  const value = this.tracked.get(changedPath);
213
224
  if (!value) return;
214
- logger_default.info(`Attempting to hot reload ${changedPath}`);
225
+ info(`Attempting to hot reload ${changedPath}`);
215
226
  const eventData = this.getEventData(changedPath, value);
216
227
  server.ws.send({
217
228
  type: "custom",
@@ -243,132 +254,128 @@ var LanguageTracker = class extends AbstractFileTracker {
243
254
  const languageTracker = new LanguageTracker();
244
255
 
245
256
  //#endregion
246
- //#region src/utils/path-utils.ts
247
- var PathUtils = class PathUtils {
248
- static _config = null;
249
- static _sourceDirectory = null;
250
- static _decodedBase = null;
251
- static _publicDir = null;
252
- static _outDir = null;
253
- static _root = null;
254
- static getConfig() {
255
- if (!PathUtils._config) {
256
- const config = context.config;
257
- if (!config) logger_default.fail("Path utils can only be called after vite has resolved the config");
258
- PathUtils._config = config;
259
- }
260
- return PathUtils._config;
261
- }
262
- static getDecodedBase() {
263
- if (!PathUtils._decodedBase) {
264
- const config = PathUtils.getConfig();
265
- PathUtils._decodedBase = path.posix.normalize(decodeURI(config.base));
266
- }
267
- return PathUtils._decodedBase;
268
- }
269
- static getSourceDirectory() {
270
- if (!PathUtils._sourceDirectory) {
271
- const config = PathUtils.getConfig();
272
- const segments = path.normalize(config.build.lib.entry.toString()).split(path.sep).filter(Boolean).filter((s) => s !== ".");
273
- const firstFolder = segments.length > 0 ? segments[0] : ".";
274
- PathUtils._sourceDirectory = path.join(config.root, firstFolder);
275
- }
276
- return PathUtils._sourceDirectory;
277
- }
278
- static getPublicDir() {
279
- if (!PathUtils._publicDir) {
280
- const config = PathUtils.getConfig();
281
- PathUtils._publicDir = path.resolve(config.publicDir);
282
- }
283
- return PathUtils._publicDir;
284
- }
285
- static getOutDir() {
286
- if (!PathUtils._outDir) {
287
- const config = PathUtils.getConfig();
288
- PathUtils._outDir = path.resolve(config.build.outDir);
289
- }
290
- return PathUtils._outDir;
291
- }
292
- static getRoot() {
293
- if (!PathUtils._root) {
294
- const config = PathUtils.getConfig();
295
- PathUtils._root = path.resolve(config.root);
296
- }
297
- return PathUtils._root;
298
- }
299
- static async getOutDirFile(p) {
300
- const file = path.join(PathUtils.getOutDir(), p);
301
- return await fs_utils_default.fileExists(file) ? file : "";
302
- }
303
- static async getPublicDirFile(p) {
304
- const file = path.join(PathUtils.getPublicDir(), p);
305
- return await fs_utils_default.fileExists(file) ? file : "";
306
- }
307
- static async findLocalFilePath(p) {
308
- const fileCandidates = [
309
- PathUtils.getPublicDir(),
310
- PathUtils.getSourceDirectory(),
311
- PathUtils.getRoot()
312
- ].map((pth) => path.join(pth, p));
313
- const idx = (await Promise.all(fileCandidates.map(fs_utils_default.fileExists))).findIndex(Boolean);
314
- return idx !== -1 ? fileCandidates[idx] : null;
257
+ //#region src/utils/path-utilities.ts
258
+ let _config;
259
+ let _sourceDirectory;
260
+ let _decodedBase;
261
+ let _publicDirectory;
262
+ let _outDirectory;
263
+ let _root;
264
+ function getConfig() {
265
+ if (!_config) {
266
+ const config = context.config;
267
+ if (!config) fail("Path utils can only be called after vite has resolved the config");
268
+ _config = config;
315
269
  }
316
- static isFoundryVTTUrl(p) {
317
- const decodedBase = PathUtils.getDecodedBase();
318
- return path.posix.normalize(p).startsWith(decodedBase);
270
+ return _config;
271
+ }
272
+ function getDecodedBase() {
273
+ if (!_decodedBase) {
274
+ const config = getConfig();
275
+ _decodedBase = path.posix.normalize(decodeURI(config.base));
319
276
  }
320
- static async foundryVTTUrlToLocal(p) {
321
- const decodedBase = PathUtils.getDecodedBase();
322
- let pathToTransform = path.posix.normalize("/" + p);
323
- if (!pathToTransform.startsWith(decodedBase)) return null;
324
- pathToTransform = path.relative(decodedBase, pathToTransform);
325
- return PathUtils.findLocalFilePath(pathToTransform);
277
+ return _decodedBase;
278
+ }
279
+ function getSourceDirectory() {
280
+ if (!_sourceDirectory) {
281
+ const config = getConfig();
282
+ const segments = path.normalize(config.build.lib.entry.toString()).split(path.sep).filter(Boolean).filter((s) => s !== ".");
283
+ const firstFolder = segments.length > 0 ? segments[0] : ".";
284
+ _sourceDirectory = path.join(config.root, firstFolder);
285
+ }
286
+ return _sourceDirectory;
287
+ }
288
+ function getPublicDirectory() {
289
+ if (!_publicDirectory) {
290
+ const config = getConfig();
291
+ _publicDirectory = path.resolve(config.publicDir);
326
292
  }
327
- static localToFoundryVTTUrl(p) {
328
- const decodedBase = PathUtils.getDecodedBase();
329
- let pathToTransform = path.normalize(p);
330
- [
331
- PathUtils.getPublicDir(),
332
- PathUtils.getSourceDirectory(),
333
- PathUtils.getRoot()
334
- ].forEach((pth) => {
335
- if (pathToTransform.startsWith(pth)) pathToTransform = pathToTransform.slice(pth.length);
336
- });
337
- return path.join(decodedBase, pathToTransform);
293
+ return _publicDirectory;
294
+ }
295
+ function getOutDirectory() {
296
+ if (!_outDirectory) {
297
+ const config = getConfig();
298
+ _outDirectory = path.resolve(config.build.outDir);
338
299
  }
339
- static getLanguageSourcePath(p, lang) {
340
- const dir = path.parse(p).dir;
341
- const finalSegments = path.basename(dir) === lang ? dir : path.join(dir, lang);
342
- return path.join(PathUtils.getSourceDirectory(), finalSegments);
300
+ return _outDirectory;
301
+ }
302
+ function getRoot() {
303
+ if (!_root) {
304
+ const config = getConfig();
305
+ _root = path.resolve(config.root);
343
306
  }
344
- };
345
- var path_utils_default = PathUtils;
307
+ return _root;
308
+ }
309
+ async function getOutDirectoryFile(p) {
310
+ const file = path.join(getOutDirectory(), p);
311
+ return await fileExists(file) ? file : "";
312
+ }
313
+ async function getPublicDirectoryFile(p) {
314
+ const file = path.join(getPublicDirectory(), p);
315
+ return await fileExists(file) ? file : "";
316
+ }
317
+ async function findLocalFilePath(p) {
318
+ const fileCandidates = [
319
+ getPublicDirectory(),
320
+ getSourceDirectory(),
321
+ getRoot()
322
+ ].map((pth) => path.join(pth, p));
323
+ const index = (await Promise.all(fileCandidates.map((file) => fileExists(file)))).findIndex(Boolean);
324
+ return index === -1 ? void 0 : fileCandidates[index];
325
+ }
326
+ function isFoundryVTTUrl(p) {
327
+ const decodedBase = getDecodedBase();
328
+ return path.posix.normalize(p).startsWith(decodedBase);
329
+ }
330
+ async function foundryVTTUrlToLocal(p) {
331
+ const decodedBase = getDecodedBase();
332
+ let pathToTransform = path.posix.normalize("/" + p);
333
+ if (!pathToTransform.startsWith(decodedBase)) return void 0;
334
+ pathToTransform = path.relative(decodedBase, pathToTransform);
335
+ return findLocalFilePath(pathToTransform);
336
+ }
337
+ function localToFoundryVTTUrl(p) {
338
+ const decodedBase = getDecodedBase();
339
+ let pathToTransform = path.normalize(p);
340
+ for (const pth of [
341
+ getPublicDirectory(),
342
+ getSourceDirectory(),
343
+ getRoot()
344
+ ]) if (pathToTransform.startsWith(pth)) pathToTransform = pathToTransform.slice(pth.length);
345
+ return path.join(decodedBase, pathToTransform);
346
+ }
347
+ function getLanguageSourcePath(p, lang) {
348
+ const directory = path.parse(p).dir;
349
+ const finalSegments = path.basename(directory) === lang ? directory : path.join(directory, lang);
350
+ return path.join(getSourceDirectory(), finalSegments);
351
+ }
346
352
 
347
353
  //#endregion
348
354
  //#region src/language/loader.ts
349
- async function getLocalLanguageFiles(lang, outDir = false) {
355
+ async function getLocalLanguageFiles(lang, inOutDirectory = false) {
350
356
  const language = context.manifest.languages.find((l) => l.lang === lang);
351
- if (!language) logger_default.fail(`Cannot find language "${lang}"`);
357
+ if (!language) fail(`Cannot find language "${lang}"`);
352
358
  const langPath = language?.path ?? "";
353
- if (outDir) return [await path_utils_default.getOutDirFile(langPath)];
354
- const publicDirFile = await path_utils_default.getPublicDirFile(langPath);
355
- if (publicDirFile !== "") return [publicDirFile];
356
- const sourcePath = path_utils_default.getLanguageSourcePath(langPath, lang);
357
- if (await fs_utils_default.dirExists(sourcePath)) return await glob(path.join(sourcePath, "**/*.json"), { absolute: true });
358
- logger_default.warn(`No language folder found at: ${sourcePath}`);
359
+ if (inOutDirectory) return [await getOutDirectoryFile(langPath)];
360
+ const publicDirectoryFile = await getPublicDirectoryFile(langPath);
361
+ if (publicDirectoryFile !== "") return [publicDirectoryFile];
362
+ const sourcePath = getLanguageSourcePath(langPath, lang);
363
+ if (await directoryExists(sourcePath)) return await glob(path.join(sourcePath, "**/*.json"), { absolute: true });
364
+ warn(`No language folder found at: ${sourcePath}`);
359
365
  return [];
360
366
  }
361
- async function loadLanguage(lang, outDir = false) {
362
- const files = await getLocalLanguageFiles(lang, outDir);
367
+ async function loadLanguage(lang, inOutDirectory = false) {
368
+ const files = await getLocalLanguageFiles(lang, inOutDirectory);
363
369
  const result = /* @__PURE__ */ new Map();
364
370
  const reads = files.map(async (file) => {
365
371
  try {
366
- const json = await fs_utils_default.readJson(file);
372
+ const json = await readJson(file);
373
+ if (typeof json !== "object" || json === null) throw new Error(`Language file ${file} is not a valid JSON object`);
367
374
  languageTracker.addFile(lang, file);
368
375
  return [file, json];
369
- } catch (e) {
370
- logger_default.warn(e);
371
- return null;
376
+ } catch (error$1) {
377
+ warn(error$1);
378
+ return;
372
379
  }
373
380
  });
374
381
  const results = await Promise.all(reads);
@@ -378,17 +385,17 @@ async function loadLanguage(lang, outDir = false) {
378
385
 
379
386
  //#endregion
380
387
  //#region src/language/transformer.ts
381
- function flattenKeys(obj, prefix = "") {
388
+ function flattenKeys(object, prefix = "") {
382
389
  const result = {};
383
- for (const [key, val] of Object.entries(obj)) {
390
+ for (const [key, value] of Object.entries(object)) {
384
391
  const fullKey = prefix ? `${prefix}.${key}` : key;
385
- if (val && typeof val === "object" && !Array.isArray(val)) Object.assign(result, flattenKeys(val, fullKey));
386
- else result[fullKey] = val;
392
+ if (value && typeof value === "object" && !Array.isArray(value)) Object.assign(result, flattenKeys(value, fullKey));
393
+ else result[fullKey] = value;
387
394
  }
388
395
  return result;
389
396
  }
390
397
  function expandDotNotationKeys(target, source, depth = 0) {
391
- if (depth > 32) logger_default.fail("Max object expansion depth exceeded.");
398
+ if (depth > 32) fail("Max object expansion depth exceeded.");
392
399
  if (!source || typeof source !== "object" || Array.isArray(source)) return source;
393
400
  for (const [key, value] of Object.entries(source)) {
394
401
  let current = target;
@@ -411,28 +418,39 @@ function transform(dataMap) {
411
418
 
412
419
  //#endregion
413
420
  //#region src/language/validator.ts
421
+ function getFirstMapValueOrWarn(map, contextDescription) {
422
+ if (map.size === 0) {
423
+ warn(`${contextDescription} is empty.`);
424
+ return;
425
+ }
426
+ const first = map.values().next().value;
427
+ if (!first) {
428
+ warn(`${contextDescription} has no valid data.`);
429
+ return;
430
+ }
431
+ return first;
432
+ }
414
433
  async function validator() {
415
434
  const manifest = context.manifest;
416
435
  const baseLanguageData = await loadLanguage("en", true);
417
- if (baseLanguageData.size === 0) {
418
- logger_default.error("Base language \"en\" not found or could not be loaded.");
436
+ const base = getFirstMapValueOrWarn(baseLanguageData, "Base language \"en\"");
437
+ if (!base) {
438
+ error("Base language \"en\" not found or could not be loaded.");
419
439
  return;
420
440
  }
421
- const base = flattenKeys(baseLanguageData.values().next().value);
441
+ const baseFlattened = flattenKeys(base);
422
442
  for (const lang of manifest.languages) {
423
443
  if (lang.lang === "en") continue;
424
444
  const currentLanguageData = await loadLanguage(lang.lang, true);
425
- if (currentLanguageData.size === 0) {
426
- logger_default.warn(`Summary for language [${lang.lang}]: Could not be loaded.`);
427
- continue;
428
- }
429
- const current = flattenKeys(currentLanguageData.values().next().value);
430
- const missing = Object.keys(base).filter((key) => !(key in current));
431
- const extra = Object.keys(current).filter((key) => !(key in base));
432
- logger_default.info(`Summary for language [${lang.lang}]:`);
433
- if (missing.length) console.warn(`Missing keys: ${missing.length}`, missing.slice(0, 5));
434
- if (extra.length) console.warn(`Extra keys: ${extra.length}`, extra.slice(0, 5));
435
- if (!missing.length && !extra.length) console.log(" ✅ All keys match.");
445
+ const current = getFirstMapValueOrWarn(currentLanguageData, `Language "${lang.lang}"`);
446
+ if (!current) continue;
447
+ const currentFlattened = flattenKeys(current);
448
+ const missing = Object.keys(baseFlattened).filter((key) => !(key in currentFlattened));
449
+ const extra = Object.keys(currentFlattened).filter((key) => !(key in baseFlattened));
450
+ info(`Summary for language [${lang.lang}]:`);
451
+ if (missing.length > 0) console.warn(`Missing keys: ${missing.length}`, missing.slice(0, 5));
452
+ if (extra.length > 0) console.warn(`Extra keys: ${extra.length}`, extra.slice(0, 5));
453
+ if (missing.length === 0 && extra.length === 0) console.log(" ✅ All keys match.");
436
454
  }
437
455
  }
438
456
 
@@ -441,26 +459,26 @@ async function validator() {
441
459
  async function compileManifestPacks() {
442
460
  if (!context.manifest?.packs) return;
443
461
  for (const pack of context.manifest.packs) {
444
- const srcCandidates = [path.resolve(path_utils_default.getSourceDirectory(), pack.path), path.resolve(path_utils_default.getRoot(), pack.path)];
445
- const dest = path.resolve(path_utils_default.getOutDir(), pack.path);
446
- let chosenSrc;
447
- for (const candidate of srcCandidates) if (await fs_utils_default.dirExists(candidate)) {
448
- chosenSrc = candidate;
462
+ const sourceCandidates = [path.resolve(getSourceDirectory(), pack.path), path.resolve(getRoot(), pack.path)];
463
+ const destination = path.resolve(getOutDirectory(), pack.path);
464
+ let chosenSource;
465
+ for (const candidate of sourceCandidates) if (await directoryExists(candidate)) {
466
+ chosenSource = candidate;
449
467
  break;
450
468
  }
451
- if (!chosenSrc) {
452
- logger_default.warn(`Pack path not found for ${pack.path}, skipped.`);
469
+ if (!chosenSource) {
470
+ warn(`Pack path not found for ${pack.path}, skipped.`);
453
471
  continue;
454
472
  }
455
473
  const hasYaml = (await glob(["**/*.yaml", "**/*.yml"], {
456
- cwd: chosenSrc,
474
+ cwd: chosenSource,
457
475
  absolute: true
458
476
  })).length > 0;
459
- await compilePack(chosenSrc, dest, {
477
+ await compilePack(chosenSource, destination, {
460
478
  yaml: hasYaml,
461
479
  recursive: true
462
480
  });
463
- logger_default.info(`Compiled pack ${pack.path} (${hasYaml ? "YAML" : "JSON"}) from ${chosenSrc}`);
481
+ info(`Compiled pack ${pack.path} (${hasYaml ? "YAML" : "JSON"}) from ${chosenSource}`);
464
482
  }
465
483
  }
466
484
 
@@ -480,27 +498,27 @@ const handlebarsTracker = new HandlebarsTracker();
480
498
  //#endregion
481
499
  //#region src/server/http-middleware.ts
482
500
  function httpMiddlewareHook(server) {
483
- server.middlewares.use(async (req, res, next) => {
501
+ server.middlewares.use(async (request, response, next) => {
484
502
  const config = context.config;
485
- if (!path_utils_default.isFoundryVTTUrl(req.url ?? "")) {
503
+ if (!isFoundryVTTUrl(request.url ?? "")) {
486
504
  next();
487
505
  return;
488
506
  }
489
507
  const cssFileName = config.build.lib.cssFileName;
490
- const cssEntry = cssFileName ? path_utils_default.localToFoundryVTTUrl(`${cssFileName}.css`) : null;
491
- if (path.posix.normalize(req.url ?? "") === cssEntry) {
492
- logger_default.info(`Blocking CSS entry to ${req.url}`);
493
- res.setHeader("Content-Type", "text/css");
494
- res.end("/* The cake is in another castle. */");
508
+ const cssEntry = cssFileName ? localToFoundryVTTUrl(`${cssFileName}.css`) : void 0;
509
+ if (path.posix.normalize(request.url ?? "") === cssEntry) {
510
+ info(`Blocking CSS entry to ${request.url}`);
511
+ response.setHeader("Content-Type", "text/css");
512
+ response.end("/* The cake is in another castle. */");
495
513
  return;
496
514
  }
497
- const languages = context.manifest.languages.filter((lang) => path_utils_default.localToFoundryVTTUrl(lang.path) === path.posix.normalize(req.url ?? ""));
515
+ const languages = context.manifest.languages.filter((lang) => localToFoundryVTTUrl(lang.path) === path.posix.normalize(request.url ?? ""));
498
516
  if (languages.length === 1) {
499
517
  const lang = languages[0].lang;
500
518
  const language = await loadLanguage(lang);
501
519
  const jsonData = transform(language);
502
- res.setHeader("Content-Type", "application/json");
503
- res.end(JSON.stringify(jsonData, null, 2));
520
+ response.setHeader("Content-Type", "application/json");
521
+ response.end(JSON.stringify(jsonData, void 0, 2));
504
522
  return;
505
523
  }
506
524
  next();
@@ -510,34 +528,34 @@ function httpMiddlewareHook(server) {
510
528
  //#endregion
511
529
  //#region src/server/socket-proxy.ts
512
530
  function socketProxy(server) {
513
- const env = context.env;
531
+ const environment = context.env;
514
532
  new Server(server.httpServer, { path: "/socket.io" }).on("connection", (socket) => {
515
- const upstream = io(`http://${env.foundryUrl}:${env.foundryPort}`, {
533
+ const upstream = io(`http://${environment.foundryUrl}:${environment.foundryPort}`, {
516
534
  transports: ["websocket"],
517
535
  upgrade: false,
518
536
  query: socket.handshake.query
519
537
  });
520
- socket.onAny(async (event, ...args) => {
521
- const maybeAck = typeof args[args.length - 1] === "function" ? args.pop() : null;
538
+ socket.onAny(async (event, ...parameters) => {
539
+ const maybeAck = typeof parameters.at(-1) === "function" ? parameters.pop() : void 0;
522
540
  if (event === "template") {
523
- const localPath = await path_utils_default.foundryVTTUrlToLocal(args[0]);
541
+ const localPath = await foundryVTTUrlToLocal(parameters[0]);
524
542
  if (localPath) {
525
- const html = await fs_utils_default.readFile(localPath);
543
+ const html = await readFile(localPath);
526
544
  if (maybeAck) maybeAck({
527
545
  html,
528
546
  success: true
529
547
  });
530
- handlebarsTracker.addFile(args[0], localPath);
548
+ handlebarsTracker.addFile(parameters[0], localPath);
531
549
  return;
532
550
  }
533
551
  }
534
- upstream.emit(event, ...args, (response) => {
552
+ upstream.emit(event, ...parameters, (response) => {
535
553
  if (maybeAck) maybeAck(response);
536
554
  });
537
555
  });
538
- upstream.onAny((event, ...args) => {
539
- const maybeAck = typeof args[args.length - 1] === "function" ? args.pop() : null;
540
- socket.emit(event, ...args, (response) => {
556
+ upstream.onAny((event, ...parameters) => {
557
+ const maybeAck = typeof parameters.at(-1) === "function" ? parameters.pop() : void 0;
558
+ socket.emit(event, ...parameters, (response) => {
541
559
  if (maybeAck) maybeAck(response);
542
560
  });
543
561
  });
@@ -546,7 +564,7 @@ function socketProxy(server) {
546
564
 
547
565
  //#endregion
548
566
  //#region src/server/index.ts
549
- function setupDevServer(server) {
567
+ function setupDevelopmentServer(server) {
550
568
  handlebarsTracker.initialize(server);
551
569
  languageTracker.initialize(server);
552
570
  httpMiddlewareHook(server);
@@ -559,18 +577,12 @@ var hmr_client_default = `
559
577
  if (import.meta.hot) {
560
578
  const FVTT_PLUGIN = __FVTT_PLUGIN__
561
579
 
562
- function refreshApplications(path = null) {
580
+ function refreshApplications(renderData = {}) {
581
+ const options = { renderContext: 'hotReload', renderData }
563
582
  // AppV1 refresh
564
- Object.values(foundry.ui.windows).forEach(app => app.render(true))
583
+ for (const appV1 of Object.values(foundry.ui.windows)) appV1.render(false, { ...options })
565
584
  // AppV2 refresh
566
- if (path)
567
- foundry.applications.instances.forEach(appV2 => {
568
- Object.values(appV2.constructor.PARTS ?? {}).forEach(part => {
569
- const templates = Array.isArray(part.templates) ? part.templates : []
570
- if (part.template === path || templates.includes(path)) appV2.render(true)
571
- })
572
- })
573
- else foundry.applications.instances.forEach(appV2 => appV2.render(true))
585
+ for (const appV2 of foundry.applications.instances.values()) appV2.render({ ...options })
574
586
  }
575
587
 
576
588
  import.meta.hot.on('foundryvtt-template-update', ({ path }) => {
@@ -583,9 +595,19 @@ if (import.meta.hot) {
583
595
  console.error(error)
584
596
  return
585
597
  }
586
- Handlebars.registerPartial(path, template)
598
+ if (!Object.hasOwn(Handlebars, 'templateIds')) Handlebars.registerPartial(path, template)
599
+ else if (Handlebars.templateIds[path]?.size > 0) {
600
+ for (const id of Handlebars.templateIds[path])
601
+ if (id in Handlebars.partials) Handlebars.registerPartial(id, template)
602
+ } else foundry.applications.handlebars.getTemplate(path)
587
603
  console.log(\`Vite | Retrieved and compiled template \${path}\`)
588
- refreshApplications(path)
604
+ refreshApplications({
605
+ packageId: FVTT_PLUGIN.id,
606
+ packageType: FVTT_PLUGIN.isSystem ? 'system' : 'module',
607
+ content: response.html,
608
+ path,
609
+ extension: 'html',
610
+ })
589
611
  })
590
612
  })
591
613
 
@@ -615,7 +637,7 @@ if (import.meta.hot) {
615
637
  foundry.utils.mergeObject(targetObject, json)
616
638
  console.log(\`Vite | HMR: Reloaded language '\${lang}'\`)
617
639
  } catch (error) {
618
- console.error(\`Vite | HMR: Error reloading language '\${lang}' for \${FVTT_PLUGIN.id}\`, error);
640
+ console.error(\`Vite | HMR: Error reloading language '\${lang}' for \${FVTT_PLUGIN.id}\`, error)
619
641
  }
620
642
  }
621
643
 
@@ -627,15 +649,21 @@ if (import.meta.hot) {
627
649
  }
628
650
  promises.push(hmrLanguage(currentLang))
629
651
  await Promise.all(promises)
630
- refreshApplications()
652
+ refreshApplications({
653
+ packageId: FVTT_PLUGIN.id,
654
+ packageType: FVTT_PLUGIN.isSystem ? 'system' : 'module',
655
+ content: '',
656
+ path: '',
657
+ extension: 'json',
658
+ })
631
659
  })
632
660
  } else console.error('Vite | HMR is disabled')
633
661
  //`;
634
662
 
635
663
  //#endregion
636
664
  //#region src/index.ts
637
- async function foundryVTTPlugin(options = { buildPacks: true }) {
638
- context.env = await loadEnv();
665
+ async function foundryVTTPlugin({ buildPacks = true } = {}) {
666
+ context.env = await loadEnvironment();
639
667
  return {
640
668
  name: "vite-plugin-fvtt",
641
669
  async config(config) {
@@ -647,44 +675,47 @@ async function foundryVTTPlugin(options = { buildPacks: true }) {
647
675
  },
648
676
  async generateBundle() {
649
677
  for (const file of ["system.json", "module.json"]) {
650
- const src = path.resolve(file);
651
- if (!await path_utils_default.getPublicDirFile(file) && await fs_utils_default.fileExists(src)) {
652
- this.addWatchFile(src);
653
- const manifest = await fs_utils_default.readJson(src);
678
+ const source = path.resolve(file);
679
+ if (!await getPublicDirectoryFile(file) && await fileExists(source)) {
680
+ this.addWatchFile(source);
681
+ const manifest = await readJson(source);
654
682
  this.emitFile({
655
683
  type: "asset",
656
684
  fileName: file,
657
- source: JSON.stringify(manifest, null, 2)
685
+ source: JSON.stringify(manifest, void 0, 2)
658
686
  });
659
687
  }
660
688
  }
661
689
  const languages = context.manifest?.languages ?? [];
662
690
  if (languages.length > 0) for (const language of languages) {
663
- if (await path_utils_default.getPublicDirFile(language.path)) continue;
691
+ if (await getPublicDirectoryFile(language.path)) continue;
664
692
  getLocalLanguageFiles(language.lang).then((langFiles) => {
665
- langFiles.forEach((file) => this.addWatchFile(file));
693
+ for (const file of langFiles) this.addWatchFile(file);
666
694
  });
667
695
  const languageDataRaw = await loadLanguage(language.lang);
668
696
  const languageData = transform(languageDataRaw);
669
697
  this.emitFile({
670
698
  type: "asset",
671
699
  fileName: path.join(language.path),
672
- source: JSON.stringify(languageData, null, 2)
700
+ source: JSON.stringify(languageData, void 0, 2)
673
701
  });
674
702
  }
675
703
  },
676
704
  async writeBundle() {
677
- if (options.buildPacks) await compileManifestPacks();
705
+ if (buildPacks) await compileManifestPacks();
678
706
  },
679
707
  closeBundle() {
680
708
  if ((context.manifest?.languages ?? []).length > 0) validator();
681
709
  },
682
710
  load(id) {
683
711
  const config = context.config;
684
- const jsFileName = (config.build.rollupOptions?.output).entryFileNames;
712
+ const output = config.build.rollupOptions?.output;
713
+ let jsFileName;
714
+ if (Array.isArray(output)) jsFileName = String(output[0].entryFileNames);
715
+ else if (output) jsFileName = String(output.entryFileNames);
685
716
  if (id === jsFileName || id === `/${jsFileName}`) return `import '${`/@fs/${path.resolve(config.build.lib.entry)}`}';\n${hmr_client_default}`;
686
717
  },
687
- configureServer: setupDevServer
718
+ configureServer: setupDevelopmentServer
688
719
  };
689
720
  }
690
721
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-fvtt",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "A Vite plugin for module and system development for Foundry VTT",
5
5
  "keywords": [
6
6
  "vite",
@@ -27,14 +27,19 @@
27
27
  "README.md"
28
28
  ],
29
29
  "scripts": {
30
- "dev": "tsdown --watch",
31
30
  "build": "tsdown",
31
+ "dev": "tsdown --watch",
32
+ "eslint": "eslint .",
33
+ "prettier": "prettier --check .",
34
+ "typecheck": "tsc",
32
35
  "test": "vitest"
33
36
  },
34
37
  "devDependencies": {
35
38
  "@eslint/js": "^9.34.0",
36
39
  "@types/node": "^24.3.0",
37
40
  "eslint": "^9.34.0",
41
+ "eslint-plugin-sonarjs": "^3.0.5",
42
+ "eslint-plugin-unicorn": "^61.0.2",
38
43
  "prettier": "^3.6.2",
39
44
  "tsdown": "^0.15.1",
40
45
  "typescript": "^5.9.2",