vite-plugin-fvtt 0.2.1 → 0.2.3

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,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.3] - 2025-09-20
4
+
5
+ ### Fixed
6
+
7
+ - Return absolute paths from globbing.
8
+ - Refactor path-utils import and usage across codebase, for better Windows support.
9
+ - Refactor logger to use static Logger class methods.
10
+ - Bump dependencies.
11
+
12
+ ### Added
13
+
14
+ - Add Vitest and fixture-based build test.
15
+
16
+ ## [0.2.2] - 2025-09-10
17
+
18
+ ### Fixed
19
+
20
+ - Improve windows path resolution.
21
+
3
22
  ## [0.2.1] - 2025-09-09
4
23
 
5
24
  ### Fixed
@@ -69,7 +88,9 @@
69
88
 
70
89
  - Initial Release
71
90
 
72
- [unreleased]: https://github.com/MatyeusM/vite-plugin-fvtt/compare/v0.2.1...HEAD
91
+ [unreleased]: https://github.com/MatyeusM/vite-plugin-fvtt/compare/v0.2.3...HEAD
92
+ [0.2.3]: https://github.com/MatyeusM/vite-plugin-fvtt/compare/v0.2.2...v0.2.3
93
+ [0.2.2]: https://github.com/MatyeusM/vite-plugin-fvtt/compare/v0.2.1...v0.2.2
73
94
  [0.2.1]: https://github.com/MatyeusM/vite-plugin-fvtt/compare/v0.2.0...v0.2.1
74
95
  [0.2.0]: https://github.com/MatyeusM/vite-plugin-fvtt/compare/v0.1.4...v0.2.0
75
96
  [0.1.4]: https://github.com/MatyeusM/vite-plugin-fvtt/compare/v0.1.3...v0.1.4
package/README.md CHANGED
@@ -104,7 +104,8 @@ Merging follows your manifest’s declared language paths, searching in root or
104
104
 
105
105
  ### **6. Packs**
106
106
 
107
- Packs are tried to be auto-discovered in the source directory. If the paths match, they are automatically compiled.
107
+ Packs are tried to be auto-discovered in the source directory. If the paths match, they are
108
+ automatically compiled.
108
109
 
109
110
  **Note:** Packs are currently not watched for changes.
110
111
 
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import fs from "fs-extra";
2
- import posix from "path/posix";
2
+ import path from "path";
3
3
  import dotenv from "dotenv";
4
4
  import { globSync } from "tinyglobby";
5
5
  import { compilePack } from "@foundryvtt/foundryvtt-cli";
@@ -12,7 +12,7 @@ const context = {};
12
12
  //#endregion
13
13
  //#region src/config/env.ts
14
14
  function loadEnv() {
15
- const envPath = posix.resolve(process.cwd(), ".env.foundryvtt.local");
15
+ const envPath = path.resolve(process.cwd(), ".env.foundryvtt.local");
16
16
  const { parsed } = dotenv.config({
17
17
  path: envPath,
18
18
  quiet: true
@@ -25,49 +25,52 @@ function loadEnv() {
25
25
 
26
26
  //#endregion
27
27
  //#region src/utils/logger.ts
28
- var Logger = class {
29
- namespace;
30
- colors = {
31
- info: "\x1B[32m",
28
+ var Logger = class Logger {
29
+ static namespace = "vite-plugin-fvtt";
30
+ static colors = {
31
+ info: "\x1B[36m",
32
32
  warn: "\x1B[33m",
33
33
  error: "\x1B[31m"
34
34
  };
35
- constructor({ namespace = "vite-plugin-fvtt" } = {}) {
36
- this.namespace = namespace;
35
+ static reset = "\x1B[0m";
36
+ initialize(namespace = "vite-plugin-fvtt") {
37
+ Logger.namespace = namespace;
37
38
  }
38
- format(level, message) {
39
- const color = this.colors[level] ?? "";
40
- const reset = "\x1B[0m";
41
- return `${color}[${this.namespace}] [${level.toUpperCase()}]${reset} ${message}`;
39
+ static format(level, message) {
40
+ return `${Logger.colors[level] ?? ""}[${Logger.namespace}] [${level.toUpperCase()}]${Logger.reset} ${message}`;
42
41
  }
43
- info(message) {
44
- console.log(this.format("info", message));
42
+ static info(message) {
43
+ console.log(Logger.format("info", message));
45
44
  }
46
- warn(message) {
47
- console.warn(this.format("warn", message));
45
+ static warn(message) {
46
+ console.warn(Logger.format("warn", message));
48
47
  }
49
- error(message) {
50
- console.error(this.format("error", message));
48
+ static error(message) {
49
+ console.error(Logger.format("error", message));
51
50
  }
52
- fail(message) {
53
- this.error(message);
54
- throw new Error(typeof message === "string" ? message : JSON.stringify(message, null, 2));
51
+ static fail(message) {
52
+ const formatted = Logger.format("error", Logger.stringify(message));
53
+ console.error(formatted);
54
+ throw new Error(formatted);
55
+ }
56
+ static stringify(message) {
57
+ if (message instanceof Error) return message.stack ?? message.message;
58
+ return typeof message === "string" ? message : JSON.stringify(message, null, 2);
55
59
  }
56
60
  };
57
- var logger_default = new Logger();
61
+ var logger_default = Logger;
58
62
 
59
63
  //#endregion
60
64
  //#region src/config/foundryvtt-manifest.ts
61
65
  function loadManifest(config) {
62
66
  if (context?.manifest) return context.manifest;
63
67
  const publicDir = config.publicDir || "public";
64
- const MANIFEST_LOCATIONS = [
68
+ const foundPath = [
65
69
  "system.json",
66
70
  "module.json",
67
71
  `${publicDir}/system.json`,
68
72
  `${publicDir}/module.json`
69
- ];
70
- const foundPath = MANIFEST_LOCATIONS.map((relPath) => posix.resolve(process.cwd(), relPath)).find((absPath) => fs.pathExistsSync(absPath));
73
+ ].map((relPath) => path.resolve(process.cwd(), relPath)).find((absPath) => fs.pathExistsSync(absPath));
71
74
  if (!foundPath) logger_default.fail(`Could not find a manifest file (system.json or module.json) in project root or ${publicDir}/.`);
72
75
  try {
73
76
  const data = fs.readJsonSync(foundPath);
@@ -75,7 +78,7 @@ function loadManifest(config) {
75
78
  const hasEsmodules = Array.isArray(data.esmodules) && data.esmodules.length > 0;
76
79
  const hasScripts = Array.isArray(data.scripts) && data.scripts.length > 0;
77
80
  if (hasEsmodules === hasScripts) logger_default.fail(`Manifest at ${foundPath} must define exactly one of "esmodules" or "scripts".`);
78
- const result = {
81
+ return {
79
82
  manifestType: foundPath.includes("module.json") ? "module" : "system",
80
83
  id: data.id,
81
84
  esmodules: Array.isArray(data.esmodules) ? data.esmodules : [],
@@ -84,7 +87,6 @@ function loadManifest(config) {
84
87
  languages: Array.isArray(data.languages) ? data.languages : [],
85
88
  packs: Array.isArray(data.packs) ? data.packs : []
86
89
  };
87
- return result;
88
90
  } catch (err) {
89
91
  logger_default.fail(`Failed to read manifest at ${foundPath}: ${err?.message || err}`);
90
92
  }
@@ -121,8 +123,7 @@ function createPartialViteConfig(config) {
121
123
  rollupOptions: { output: {
122
124
  entryFileNames: fileName,
123
125
  assetFileNames: (assetInfo) => {
124
- const names = assetInfo.names ?? [];
125
- if (names.some((n) => n.endsWith(".css"))) return cssFileName;
126
+ if ((assetInfo.names ?? []).some((n) => n.endsWith(".css"))) return cssFileName;
126
127
  return "[name][extname]";
127
128
  }
128
129
  } }
@@ -171,7 +172,7 @@ var AbstractFileTracker = class {
171
172
  });
172
173
  }
173
174
  addFile(value, filePath) {
174
- const absPath = posix.resolve(filePath);
175
+ const absPath = path.resolve(filePath);
175
176
  if (!this.tracked.has(absPath)) {
176
177
  this.tracked.set(absPath, value);
177
178
  this.watcher?.add(absPath);
@@ -212,75 +213,69 @@ var PathUtils = class PathUtils {
212
213
  static getDecodedBase() {
213
214
  if (!PathUtils._decodedBase) {
214
215
  const config = PathUtils.getConfig();
215
- PathUtils._decodedBase = PathUtils.normalize(decodeURI(config.base));
216
+ PathUtils._decodedBase = path.posix.normalize(decodeURI(config.base));
216
217
  }
217
218
  return PathUtils._decodedBase;
218
219
  }
219
220
  static getSourceDirectory() {
220
221
  if (!PathUtils._sourceDirectory) {
221
222
  const config = PathUtils.getConfig();
222
- const normalizedEntry = PathUtils.normalize(config.build.lib.entry.toString());
223
- const segments = normalizedEntry.split(posix.sep).filter(Boolean).filter((s) => s !== ".");
223
+ const segments = path.normalize(config.build.lib.entry.toString()).split(path.sep).filter(Boolean).filter((s) => s !== ".");
224
224
  const firstFolder = segments.length > 0 ? segments[0] : ".";
225
- PathUtils._sourceDirectory = posix.join(config.root, firstFolder);
225
+ PathUtils._sourceDirectory = path.join(config.root, firstFolder);
226
226
  }
227
227
  return PathUtils._sourceDirectory;
228
228
  }
229
229
  static getPublicDir() {
230
230
  if (!PathUtils._publicDir) {
231
231
  const config = PathUtils.getConfig();
232
- PathUtils._publicDir = PathUtils.normalize(posix.resolve(config.publicDir));
232
+ PathUtils._publicDir = path.resolve(config.publicDir);
233
233
  }
234
234
  return PathUtils._publicDir;
235
235
  }
236
236
  static getOutDir() {
237
237
  if (!PathUtils._outDir) {
238
238
  const config = PathUtils.getConfig();
239
- PathUtils._outDir = PathUtils.normalize(posix.resolve(config.build.outDir));
239
+ PathUtils._outDir = path.resolve(config.build.outDir);
240
240
  }
241
241
  return PathUtils._outDir;
242
242
  }
243
243
  static getRoot() {
244
244
  if (!PathUtils._root) {
245
245
  const config = PathUtils.getConfig();
246
- PathUtils._root = PathUtils.normalize(config.root);
246
+ PathUtils._root = path.resolve(config.root);
247
247
  }
248
248
  return PathUtils._root;
249
249
  }
250
250
  static getOutDirFile(p) {
251
- const file = posix.join(PathUtils.getOutDir(), p);
251
+ const file = path.join(PathUtils.getOutDir(), p);
252
252
  return fs.existsSync(file) ? file : "";
253
253
  }
254
254
  static getPublicDirFile(p) {
255
- const file = posix.join(PathUtils.getPublicDir(), p);
255
+ const file = path.join(PathUtils.getPublicDir(), p);
256
256
  return fs.existsSync(file) ? file : "";
257
257
  }
258
- static normalize(p) {
259
- return posix.normalize(p);
260
- }
261
258
  static findLocalFilePath(p) {
262
- const fileCandidates = [
259
+ return [
263
260
  PathUtils.getPublicDir(),
264
261
  PathUtils.getSourceDirectory(),
265
262
  PathUtils.getRoot()
266
- ].map((pth) => posix.join(pth, p));
267
- return fileCandidates.find((pth) => fs.existsSync(pth)) ?? null;
263
+ ].map((pth) => path.join(pth, p)).find((pth) => fs.existsSync(pth)) ?? null;
268
264
  }
269
265
  static isFoundryVTTUrl(p) {
270
266
  const decodedBase = PathUtils.getDecodedBase();
271
- const pathToCheck = PathUtils.normalize(p);
272
- return pathToCheck.startsWith(decodedBase);
267
+ return path.posix.normalize(p).startsWith(decodedBase);
273
268
  }
274
269
  static foundryVTTUrlToLocal(p) {
275
270
  const decodedBase = PathUtils.getDecodedBase();
276
- let pathToTransform = PathUtils.normalize("/" + p);
271
+ let pathToTransform = path.posix.normalize("/" + p);
277
272
  if (!pathToTransform.startsWith(decodedBase)) return null;
278
- pathToTransform = posix.relative(decodedBase, pathToTransform);
273
+ pathToTransform = path.relative(decodedBase, pathToTransform);
279
274
  return PathUtils.findLocalFilePath(pathToTransform);
280
275
  }
281
276
  static localToFoundryVTTUrl(p) {
282
277
  const decodedBase = PathUtils.getDecodedBase();
283
- let pathToTransform = PathUtils.normalize(p);
278
+ let pathToTransform = path.normalize(p);
284
279
  [
285
280
  PathUtils.getPublicDir(),
286
281
  PathUtils.getSourceDirectory(),
@@ -288,13 +283,12 @@ var PathUtils = class PathUtils {
288
283
  ].forEach((pth) => {
289
284
  if (pathToTransform.startsWith(pth)) pathToTransform = pathToTransform.slice(pth.length);
290
285
  });
291
- return posix.join(decodedBase, pathToTransform);
286
+ return path.join(decodedBase, pathToTransform);
292
287
  }
293
288
  static getLanguageSourcePath(p, lang) {
294
- const dir = posix.parse(p).dir;
295
- const lastDirName = posix.basename(dir);
296
- const finalSegments = lastDirName === lang ? dir : posix.join(dir, lang);
297
- return posix.join(PathUtils.getSourceDirectory(), finalSegments);
289
+ const dir = path.parse(p).dir;
290
+ const finalSegments = path.basename(dir) === lang ? dir : path.join(dir, lang);
291
+ return path.join(PathUtils.getSourceDirectory(), finalSegments);
298
292
  }
299
293
  };
300
294
  var path_utils_default = PathUtils;
@@ -302,14 +296,10 @@ var path_utils_default = PathUtils;
302
296
  //#endregion
303
297
  //#region src/language/loader.ts
304
298
  function getLocalLanguageFiles(lang, outDir = false) {
305
- const manifest = context.manifest;
306
- const language = manifest.languages.find((l) => l.lang === lang);
299
+ const language = context.manifest.languages.find((l) => l.lang === lang);
307
300
  if (!language) logger_default.fail(`Cannot find language "${lang}"`);
308
301
  const langPath = language?.path ?? "";
309
- if (outDir) {
310
- const languageFile = path_utils_default.getOutDirFile(langPath);
311
- return [languageFile];
312
- }
302
+ if (outDir) return [path_utils_default.getOutDirFile(langPath)];
313
303
  const publicDirFile = path_utils_default.getPublicDirFile(langPath);
314
304
  if (publicDirFile !== "") return [publicDirFile];
315
305
  const sourcePath = path_utils_default.getLanguageSourcePath(langPath, lang);
@@ -317,7 +307,7 @@ function getLocalLanguageFiles(lang, outDir = false) {
317
307
  logger_default.warn(`No language folder found at: ${sourcePath}`);
318
308
  return [];
319
309
  }
320
- return globSync(posix.join(sourcePath, "**/*.json"));
310
+ return globSync(path.join(sourcePath, "**/*.json"), { absolute: true });
321
311
  }
322
312
  function loadLanguage(lang, outDir = false) {
323
313
  const files = getLocalLanguageFiles(lang, outDir);
@@ -378,15 +368,15 @@ function validator() {
378
368
  if (lang.lang === "en") continue;
379
369
  const currentLanguageData = loadLanguage(lang.lang, true);
380
370
  if (currentLanguageData.size === 0) {
381
- console.warn(`Summary for language [${lang.lang}]: Could not be loaded.`);
371
+ logger_default.warn(`Summary for language [${lang.lang}]: Could not be loaded.`);
382
372
  continue;
383
373
  }
384
374
  const current = flattenKeys(currentLanguageData.values().next().value);
385
375
  const missing = Object.keys(base).filter((key) => !(key in current));
386
376
  const extra = Object.keys(current).filter((key) => !(key in base));
387
- console.log(`Summary for language [${lang.lang}]:`);
388
- if (missing.length) console.warn(`\tMissing keys: ${missing.length}`, missing.slice(0, 5));
389
- if (extra.length) console.warn(`\tExtra keys: ${extra.length}`, extra.slice(0, 5));
377
+ logger_default.info(`Summary for language [${lang.lang}]:`);
378
+ if (missing.length) console.warn(`Missing keys: ${missing.length}`, missing.slice(0, 5));
379
+ if (extra.length) console.warn(`Extra keys: ${extra.length}`, extra.slice(0, 5));
390
380
  if (!missing.length && !extra.length) console.log(" ✅ All keys match.");
391
381
  }
392
382
  }
@@ -396,18 +386,17 @@ function validator() {
396
386
  async function compileManifestPacks() {
397
387
  if (!context.manifest?.packs) return;
398
388
  for (const pack of context.manifest.packs) {
399
- const srcCandidates = [posix.resolve(path_utils_default.getSourceDirectory(), pack.path), posix.resolve(path_utils_default.getRoot(), pack.path)];
400
- const dest = posix.resolve(path_utils_default.getOutDir(), pack.path);
389
+ const srcCandidates = [path.resolve(path_utils_default.getSourceDirectory(), pack.path), path.resolve(path_utils_default.getRoot(), pack.path)];
390
+ const dest = path.resolve(path_utils_default.getOutDir(), pack.path);
401
391
  const chosenSrc = srcCandidates.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isDirectory());
402
392
  if (!chosenSrc) {
403
393
  logger_default.warn(`Pack path not found for ${pack.path}, skipped.`);
404
394
  continue;
405
395
  }
406
- const entries = fs.readdirSync(chosenSrc, {
396
+ const hasYaml = fs.readdirSync(chosenSrc, {
407
397
  recursive: true,
408
398
  encoding: "utf8"
409
- });
410
- const hasYaml = entries.some((entry) => entry.endsWith(".yaml") || entry.endsWith(".yml"));
399
+ }).some((entry) => entry.endsWith(".yaml") || entry.endsWith(".yml"));
411
400
  await compilePack(chosenSrc, dest, {
412
401
  yaml: hasYaml,
413
402
  recursive: true
@@ -440,13 +429,13 @@ function httpMiddlewareHook(server) {
440
429
  }
441
430
  const cssFileName = config.build.lib.cssFileName;
442
431
  const cssEntry = cssFileName ? path_utils_default.localToFoundryVTTUrl(`${cssFileName}.css`) : null;
443
- if (path_utils_default.normalize(req.url ?? "") === cssEntry) {
432
+ if (path.posix.normalize(req.url ?? "") === cssEntry) {
444
433
  logger_default.info(`Blocking CSS entry to ${req.url}`);
445
434
  res.setHeader("Content-Type", "text/css");
446
435
  res.end("/* The cake is in another castle. */");
447
436
  return;
448
437
  }
449
- const languages = context.manifest.languages.filter((lang) => path_utils_default.localToFoundryVTTUrl(lang.path) === path_utils_default.normalize(req.url ?? ""));
438
+ const languages = context.manifest.languages.filter((lang) => path_utils_default.localToFoundryVTTUrl(lang.path) === path.posix.normalize(req.url ?? ""));
450
439
  if (languages.length === 1) {
451
440
  const lang = languages[0].lang;
452
441
  const language = loadLanguage(lang);
@@ -463,8 +452,7 @@ function httpMiddlewareHook(server) {
463
452
  //#region src/server/socket-proxy.ts
464
453
  function socketProxy(server) {
465
454
  const env = context.env;
466
- const ioProxy = new Server(server.httpServer, { path: "/socket.io" });
467
- ioProxy.on("connection", (socket) => {
455
+ new Server(server.httpServer, { path: "/socket.io" }).on("connection", (socket) => {
468
456
  const upstream = io(`http://${env.foundryUrl}:${env.foundryPort}`, {
469
457
  transports: ["websocket"],
470
458
  upgrade: false,
@@ -488,8 +476,7 @@ function socketProxy(server) {
488
476
  });
489
477
  });
490
478
  upstream.onAny((event, ...args) => {
491
- const lastArg = args[args.length - 1];
492
- const maybeAck = typeof lastArg === "function" ? args.pop() : null;
479
+ const maybeAck = typeof args[args.length - 1] === "function" ? args.pop() : null;
493
480
  socket.emit(event, ...args, (response) => {
494
481
  if (maybeAck) maybeAck(response);
495
482
  });
@@ -601,7 +588,7 @@ function foundryVTTPlugin(options = { buildPacks: true }) {
601
588
  async generateBundle() {
602
589
  const manifestCandidates = ["system.json", "module.json"];
603
590
  for (const file of manifestCandidates) {
604
- const src = posix.resolve(file);
591
+ const src = path.resolve(file);
605
592
  if (!path_utils_default.getPublicDirFile(file) && fs.existsSync(src)) {
606
593
  this.addWatchFile(src);
607
594
  const manifest = fs.readJsonSync(src);
@@ -620,7 +607,7 @@ function foundryVTTPlugin(options = { buildPacks: true }) {
620
607
  const languageData = transform(languageDataRaw);
621
608
  this.emitFile({
622
609
  type: "asset",
623
- fileName: posix.join(language.path),
610
+ fileName: path.join(language.path),
624
611
  source: JSON.stringify(languageData, null, 2)
625
612
  });
626
613
  }
@@ -629,17 +616,12 @@ function foundryVTTPlugin(options = { buildPacks: true }) {
629
616
  if (options.buildPacks) await compileManifestPacks();
630
617
  },
631
618
  closeBundle() {
632
- const languages = context.manifest?.languages ?? [];
633
- if (languages.length > 0) validator();
619
+ if ((context.manifest?.languages ?? []).length > 0) validator();
634
620
  },
635
621
  load(id) {
636
622
  const config = context.config;
637
623
  const jsFileName = (config.build.rollupOptions?.output).entryFileNames;
638
- if (id === jsFileName || id === `/${jsFileName}`) {
639
- const entryPath = posix.resolve(config.build.lib.entry);
640
- const viteId = `/@fs/${entryPath}`;
641
- return `import '${viteId}';\n${hmr_client_default}`;
642
- }
624
+ if (id === jsFileName || id === `/${jsFileName}`) return `import '${`/@fs/${path.resolve(config.build.lib.entry)}`}';\n${hmr_client_default}`;
643
625
  },
644
626
  configureServer: setupDevServer
645
627
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-fvtt",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "A Vite plugin for module and system development for Foundry VTT",
5
5
  "keywords": [
6
6
  "vite",
@@ -28,7 +28,8 @@
28
28
  ],
29
29
  "scripts": {
30
30
  "dev": "tsdown --watch",
31
- "build": "tsdown"
31
+ "build": "tsdown",
32
+ "test": "vitest"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@eslint/js": "^9.34.0",
@@ -36,10 +37,11 @@
36
37
  "@types/node": "^24.3.0",
37
38
  "eslint": "^9.34.0",
38
39
  "prettier": "^3.6.2",
39
- "tsdown": "^0.14.2",
40
+ "tsdown": "^0.15.1",
40
41
  "typescript": "^5.9.2",
41
42
  "typescript-eslint": "^8.41.0",
42
- "vite": "*"
43
+ "vite": "*",
44
+ "vitest": "^3.2.4"
43
45
  },
44
46
  "dependencies": {
45
47
  "@foundryvtt/foundryvtt-cli": "^3.0.0",
@@ -47,7 +49,7 @@
47
49
  "fs-extra": "^11.3.1",
48
50
  "socket.io": "^4.8.1",
49
51
  "socket.io-client": "^4.8.1",
50
- "tinyglobby": "^0.2.14"
52
+ "tinyglobby": "^0.2.15"
51
53
  },
52
54
  "peerDependencies": {
53
55
  "vite": "^7.0.0"