vite-plugin-fvtt 0.2.5 → 0.2.7
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 +36 -3
- package/README.md +12 -2
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +717 -0
- package/package.json +11 -5
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -692
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { glob } from "tinyglobby";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import { compilePack } from "@foundryvtt/foundryvtt-cli";
|
|
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/utils/fs-utilities.ts
|
|
13
|
+
async function checkType(p, check) {
|
|
14
|
+
try {
|
|
15
|
+
return check(await fs.stat(p));
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function fileExists(p) {
|
|
21
|
+
return checkType(p, (s) => s.isFile());
|
|
22
|
+
}
|
|
23
|
+
async function directoryExists(p) {
|
|
24
|
+
return checkType(p, (s) => s.isDirectory());
|
|
25
|
+
}
|
|
26
|
+
async function readFile(filePath, encoding = "utf8") {
|
|
27
|
+
return fs.readFile(filePath, { encoding });
|
|
28
|
+
}
|
|
29
|
+
async function readJson(filePath) {
|
|
30
|
+
try {
|
|
31
|
+
const content = await readFile(filePath);
|
|
32
|
+
return JSON.parse(content);
|
|
33
|
+
} catch {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/config/environment.ts
|
|
40
|
+
function parseEnvironment(content) {
|
|
41
|
+
const result = {};
|
|
42
|
+
for (const line of content.split(/\r?\n/)) {
|
|
43
|
+
const trimmed = line.trim();
|
|
44
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
45
|
+
const [key, ...rest] = trimmed.split("=");
|
|
46
|
+
result[key.trim()] = rest.join("=").trim();
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
async function loadEnvironment() {
|
|
51
|
+
const environmentPaths = await glob(".env.foundryvtt*", { absolute: true });
|
|
52
|
+
let merged = {
|
|
53
|
+
FOUNDRY_URL: "localhost",
|
|
54
|
+
FOUNDRY_PORT: "30000"
|
|
55
|
+
};
|
|
56
|
+
for (const file of environmentPaths) {
|
|
57
|
+
const content = await readFile(file);
|
|
58
|
+
merged = {
|
|
59
|
+
...merged,
|
|
60
|
+
...parseEnvironment(content)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
foundryUrl: merged.FOUNDRY_URL,
|
|
65
|
+
foundryPort: Number.parseInt(merged.FOUNDRY_PORT, 10)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/utils/logger.ts
|
|
71
|
+
let loggerNamespace = "vite-plugin-fvtt";
|
|
72
|
+
const colors = {
|
|
73
|
+
info: "\x1B[36m",
|
|
74
|
+
warn: "\x1B[33m",
|
|
75
|
+
error: "\x1B[31m"
|
|
76
|
+
};
|
|
77
|
+
const reset = "\x1B[0m";
|
|
78
|
+
function format(level, message) {
|
|
79
|
+
return `${colors[level] ?? ""}[${loggerNamespace}] [${level.toUpperCase()}]${reset} ${message}`;
|
|
80
|
+
}
|
|
81
|
+
function info(message) {
|
|
82
|
+
console.log(format("info", message));
|
|
83
|
+
}
|
|
84
|
+
function warn(message) {
|
|
85
|
+
console.warn(format("warn", message));
|
|
86
|
+
}
|
|
87
|
+
function error(message) {
|
|
88
|
+
console.error(format("error", message));
|
|
89
|
+
}
|
|
90
|
+
function fail(message) {
|
|
91
|
+
const formatted = format("error", stringify(message));
|
|
92
|
+
console.error(formatted);
|
|
93
|
+
throw new Error(formatted);
|
|
94
|
+
}
|
|
95
|
+
function stringify(message) {
|
|
96
|
+
if (message instanceof Error) return message.stack ?? message.message;
|
|
97
|
+
return typeof message === "string" ? message : JSON.stringify(message, void 0, 2);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/config/foundryvtt-manifest.ts
|
|
102
|
+
async function resolveManifestPath(publicDirectory) {
|
|
103
|
+
const paths = [
|
|
104
|
+
"system.json",
|
|
105
|
+
"module.json",
|
|
106
|
+
`${publicDirectory}/system.json`,
|
|
107
|
+
`${publicDirectory}/module.json`
|
|
108
|
+
].map((f) => path.resolve(process.cwd(), f));
|
|
109
|
+
const index = (await Promise.all(paths.map((p) => fileExists(p)))).findIndex(Boolean);
|
|
110
|
+
return index === -1 ? void 0 : paths[index];
|
|
111
|
+
}
|
|
112
|
+
function isLanguageEntry(object) {
|
|
113
|
+
return typeof object === "object" && object !== null && "lang" in object && "path" in object && typeof object.lang === "string" && typeof object.path === "string";
|
|
114
|
+
}
|
|
115
|
+
function isPackEntry(object) {
|
|
116
|
+
return typeof object === "object" && object !== null && "path" in object && typeof object.path === "string";
|
|
117
|
+
}
|
|
118
|
+
function validateManifest(rawData, foundPath) {
|
|
119
|
+
if (typeof rawData !== "object" || rawData === null) fail(`Manifest at ${foundPath} is not a valid JSON object.`);
|
|
120
|
+
const data = rawData;
|
|
121
|
+
if (typeof data.id !== "string") fail("Manifest is missing required \"id\" field or it is not a string");
|
|
122
|
+
const esmodules = Array.isArray(data.esmodules) ? data.esmodules.map(String) : [];
|
|
123
|
+
const scripts = Array.isArray(data.scripts) ? data.scripts.map(String) : [];
|
|
124
|
+
if (esmodules.length > 0 === scripts.length > 0) fail("Manifest must define exactly one of \"esmodules\" or \"scripts\"");
|
|
125
|
+
const styles = Array.isArray(data.styles) ? data.styles.map(String) : [];
|
|
126
|
+
const languages = Array.isArray(data.languages) && data.languages.every((entry) => isLanguageEntry(entry)) ? data.languages : [];
|
|
127
|
+
const packs = Array.isArray(data.packs) && data.packs.every((entry) => isPackEntry(entry)) ? data.packs : [];
|
|
128
|
+
return {
|
|
129
|
+
manifestType: foundPath.includes("module.json") ? "module" : "system",
|
|
130
|
+
id: data.id,
|
|
131
|
+
esmodules,
|
|
132
|
+
scripts,
|
|
133
|
+
styles,
|
|
134
|
+
languages,
|
|
135
|
+
packs
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function loadManifest(config) {
|
|
139
|
+
if (context?.manifest) return context.manifest;
|
|
140
|
+
const publicDirectory = config.publicDir || "public";
|
|
141
|
+
const foundPath = await resolveManifestPath(publicDirectory);
|
|
142
|
+
if (!foundPath) fail(`Could not find a manifest file (system.json or module.json) in project root or ${publicDirectory}/.`);
|
|
143
|
+
try {
|
|
144
|
+
return validateManifest(await readJson(foundPath), foundPath);
|
|
145
|
+
} catch (error$1) {
|
|
146
|
+
if (error$1 instanceof Error) fail(`Failed to read manifest at ${foundPath}: ${error$1.message}`);
|
|
147
|
+
fail(`Failed to read manifest at ${foundPath}: ${String(error$1)}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/config/vite-options.ts
|
|
153
|
+
function createPartialViteConfig(config) {
|
|
154
|
+
const base = config.base ?? `/${context.manifest?.manifestType}s/${context.manifest?.id}/`;
|
|
155
|
+
const useEsModules = context.manifest?.esmodules.length === 1;
|
|
156
|
+
const formats = useEsModules ? ["es"] : ["umd"];
|
|
157
|
+
const fileName = (useEsModules ? context.manifest?.esmodules[0] : context.manifest?.scripts?.[0]) ?? "scripts/bundle.js";
|
|
158
|
+
if (!(useEsModules || context.manifest?.scripts?.[0])) warn("No output file specified in manifest, using default \"bundle\" in the \"scripts/\" folder");
|
|
159
|
+
if (!context.manifest?.styles?.length) warn("No CSS file found in manifest");
|
|
160
|
+
const cssFileName = context.manifest?.styles[0] ?? "styles/bundle.css";
|
|
161
|
+
if (!context.manifest?.styles[0]) warn("No output css file specified in manifest, using default \"bundle\" in the \"styles/\" folder");
|
|
162
|
+
const foundryPort = context.env?.foundryPort ?? 3e4;
|
|
163
|
+
const foundryUrl = context.env?.foundryUrl ?? "localhost";
|
|
164
|
+
const library = config.build?.lib;
|
|
165
|
+
if (!library || typeof library !== "object") fail("This plugin needs a configured build.lib");
|
|
166
|
+
const entry = library.entry;
|
|
167
|
+
if (!entry) fail("Entry must be specified in lib");
|
|
168
|
+
if (typeof entry !== "string") fail("Only a singular string entry is supported for build.lib.entry");
|
|
169
|
+
const isWatch = process.argv.includes("--watch") || !!config.build?.watch;
|
|
170
|
+
return {
|
|
171
|
+
base,
|
|
172
|
+
build: {
|
|
173
|
+
emptyOutDir: config.build?.emptyOutDir ?? !isWatch,
|
|
174
|
+
lib: {
|
|
175
|
+
entry,
|
|
176
|
+
formats,
|
|
177
|
+
name: context.manifest?.id ?? "bundle",
|
|
178
|
+
cssFileName: "bundle"
|
|
179
|
+
},
|
|
180
|
+
minify: "esbuild",
|
|
181
|
+
rollupOptions: { output: {
|
|
182
|
+
entryFileNames: fileName,
|
|
183
|
+
assetFileNames: (assetInfo) => {
|
|
184
|
+
if ((assetInfo.names ?? []).some((n) => n.endsWith(".css"))) return cssFileName;
|
|
185
|
+
return "[name][extname]";
|
|
186
|
+
}
|
|
187
|
+
} }
|
|
188
|
+
},
|
|
189
|
+
define: { __FVTT_PLUGIN__: {
|
|
190
|
+
id: context.manifest?.id,
|
|
191
|
+
isSystem: context.manifest?.manifestType === "system"
|
|
192
|
+
} },
|
|
193
|
+
esbuild: config.esbuild ?? {
|
|
194
|
+
minifyIdentifiers: false,
|
|
195
|
+
minifySyntax: true,
|
|
196
|
+
minifyWhitespace: true,
|
|
197
|
+
keepNames: true
|
|
198
|
+
},
|
|
199
|
+
server: {
|
|
200
|
+
port: foundryPort + 1,
|
|
201
|
+
proxy: { [`^(?!${base})`]: `http://${foundryUrl}:${foundryPort}` }
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region src/server/trackers/abstract-file-tracker.ts
|
|
208
|
+
var AbstractFileTracker = class {
|
|
209
|
+
initialized = false;
|
|
210
|
+
tracked = /* @__PURE__ */ new Map();
|
|
211
|
+
watcher = void 0;
|
|
212
|
+
config;
|
|
213
|
+
constructor(config) {
|
|
214
|
+
this.config = config;
|
|
215
|
+
}
|
|
216
|
+
initialize(server) {
|
|
217
|
+
if (this.initialized) return;
|
|
218
|
+
this.initialized = true;
|
|
219
|
+
this.watcher = server.watcher;
|
|
220
|
+
this.watcher.on("change", (changedPath) => {
|
|
221
|
+
const value = this.tracked.get(changedPath);
|
|
222
|
+
if (!value) return;
|
|
223
|
+
info(`Attempting to hot reload ${changedPath}`);
|
|
224
|
+
const eventData = this.getEventData(changedPath, value);
|
|
225
|
+
server.ws.send({
|
|
226
|
+
type: "custom",
|
|
227
|
+
event: this.updateEvent,
|
|
228
|
+
data: eventData
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
addFile(value, filePath) {
|
|
233
|
+
const absPath = path.resolve(filePath);
|
|
234
|
+
if (!this.tracked.has(absPath)) {
|
|
235
|
+
this.tracked.set(absPath, value);
|
|
236
|
+
this.watcher?.add(absPath);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/server/trackers/language-tracker.ts
|
|
243
|
+
var LanguageTracker = class extends AbstractFileTracker {
|
|
244
|
+
updateEvent = "foundryvtt-language-update";
|
|
245
|
+
constructor() {
|
|
246
|
+
super({});
|
|
247
|
+
}
|
|
248
|
+
getEventData() {
|
|
249
|
+
return {};
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const languageTracker = new LanguageTracker();
|
|
253
|
+
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region src/utils/path-utilities.ts
|
|
256
|
+
let _config;
|
|
257
|
+
let _sourceDirectory;
|
|
258
|
+
let _decodedBase;
|
|
259
|
+
let _publicDirectory;
|
|
260
|
+
let _outDirectory;
|
|
261
|
+
let _root;
|
|
262
|
+
function getConfig() {
|
|
263
|
+
if (!_config) {
|
|
264
|
+
const config = context.config;
|
|
265
|
+
if (!config) fail("Path utils can only be called after vite has resolved the config");
|
|
266
|
+
_config = config;
|
|
267
|
+
}
|
|
268
|
+
return _config;
|
|
269
|
+
}
|
|
270
|
+
function getDecodedBase() {
|
|
271
|
+
if (!_decodedBase) {
|
|
272
|
+
const config = getConfig();
|
|
273
|
+
_decodedBase = path.posix.normalize(decodeURI(config.base));
|
|
274
|
+
}
|
|
275
|
+
return _decodedBase;
|
|
276
|
+
}
|
|
277
|
+
function getSourceDirectory() {
|
|
278
|
+
if (!_sourceDirectory) {
|
|
279
|
+
const config = getConfig();
|
|
280
|
+
const segments = path.normalize(config.build.lib.entry.toString()).split(path.sep).filter(Boolean).filter((s) => s !== ".");
|
|
281
|
+
const firstFolder = segments.length > 0 ? segments[0] : ".";
|
|
282
|
+
_sourceDirectory = path.join(config.root, firstFolder);
|
|
283
|
+
}
|
|
284
|
+
return _sourceDirectory;
|
|
285
|
+
}
|
|
286
|
+
function getPublicDirectory() {
|
|
287
|
+
if (!_publicDirectory) {
|
|
288
|
+
const config = getConfig();
|
|
289
|
+
_publicDirectory = path.resolve(config.publicDir);
|
|
290
|
+
}
|
|
291
|
+
return _publicDirectory;
|
|
292
|
+
}
|
|
293
|
+
function getOutDirectory() {
|
|
294
|
+
if (!_outDirectory) {
|
|
295
|
+
const config = getConfig();
|
|
296
|
+
_outDirectory = path.resolve(config.build.outDir);
|
|
297
|
+
}
|
|
298
|
+
return _outDirectory;
|
|
299
|
+
}
|
|
300
|
+
function getRoot() {
|
|
301
|
+
if (!_root) {
|
|
302
|
+
const config = getConfig();
|
|
303
|
+
_root = path.resolve(config.root);
|
|
304
|
+
}
|
|
305
|
+
return _root;
|
|
306
|
+
}
|
|
307
|
+
async function getOutDirectoryFile(p) {
|
|
308
|
+
const file = path.join(getOutDirectory(), p);
|
|
309
|
+
return await fileExists(file) ? file : "";
|
|
310
|
+
}
|
|
311
|
+
async function getPublicDirectoryFile(p) {
|
|
312
|
+
const file = path.join(getPublicDirectory(), p);
|
|
313
|
+
return await fileExists(file) ? file : "";
|
|
314
|
+
}
|
|
315
|
+
async function findLocalFilePath(p) {
|
|
316
|
+
const fileCandidates = [
|
|
317
|
+
getPublicDirectory(),
|
|
318
|
+
getSourceDirectory(),
|
|
319
|
+
getRoot()
|
|
320
|
+
].map((pth) => path.join(pth, p));
|
|
321
|
+
const index = (await Promise.all(fileCandidates.map((file) => fileExists(file)))).findIndex(Boolean);
|
|
322
|
+
return index === -1 ? void 0 : fileCandidates[index];
|
|
323
|
+
}
|
|
324
|
+
function isFoundryVTTUrl(p) {
|
|
325
|
+
const decodedBase = getDecodedBase();
|
|
326
|
+
return path.posix.normalize(p).startsWith(decodedBase);
|
|
327
|
+
}
|
|
328
|
+
async function foundryVTTUrlToLocal(p) {
|
|
329
|
+
const decodedBase = getDecodedBase();
|
|
330
|
+
let pathToTransform = path.posix.normalize("/" + p);
|
|
331
|
+
if (!pathToTransform.startsWith(decodedBase)) return void 0;
|
|
332
|
+
pathToTransform = path.relative(decodedBase, pathToTransform);
|
|
333
|
+
return findLocalFilePath(pathToTransform);
|
|
334
|
+
}
|
|
335
|
+
function localToFoundryVTTUrl(p) {
|
|
336
|
+
const decodedBase = getDecodedBase();
|
|
337
|
+
let pathToTransform = path.normalize(p);
|
|
338
|
+
for (const pth of [
|
|
339
|
+
getPublicDirectory(),
|
|
340
|
+
getSourceDirectory(),
|
|
341
|
+
getRoot()
|
|
342
|
+
]) if (pathToTransform.startsWith(pth)) pathToTransform = pathToTransform.slice(pth.length);
|
|
343
|
+
return path.join(decodedBase, pathToTransform);
|
|
344
|
+
}
|
|
345
|
+
function getLanguageSourcePath(p, lang) {
|
|
346
|
+
const directory = path.parse(p).dir;
|
|
347
|
+
const finalSegments = path.basename(directory) === lang ? directory : path.join(directory, lang);
|
|
348
|
+
return path.join(getSourceDirectory(), finalSegments);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/language/loader.ts
|
|
353
|
+
async function getLocalLanguageFiles(lang, inOutDirectory = false) {
|
|
354
|
+
const language = context.manifest.languages.find((l) => l.lang === lang);
|
|
355
|
+
if (!language) fail(`Cannot find language "${lang}"`);
|
|
356
|
+
const langPath = language?.path ?? "";
|
|
357
|
+
if (inOutDirectory) return [await getOutDirectoryFile(langPath)];
|
|
358
|
+
const publicDirectoryFile = await getPublicDirectoryFile(langPath);
|
|
359
|
+
if (publicDirectoryFile !== "") return [publicDirectoryFile];
|
|
360
|
+
const sourcePath = getLanguageSourcePath(langPath, lang);
|
|
361
|
+
if (await directoryExists(sourcePath)) return await glob(path.join(sourcePath, "**/*.json"), { absolute: true });
|
|
362
|
+
warn(`No language folder found at: ${sourcePath}`);
|
|
363
|
+
return [];
|
|
364
|
+
}
|
|
365
|
+
async function loadLanguage(lang, inOutDirectory = false) {
|
|
366
|
+
const files = await getLocalLanguageFiles(lang, inOutDirectory);
|
|
367
|
+
const result = /* @__PURE__ */ new Map();
|
|
368
|
+
const reads = files.map(async (file) => {
|
|
369
|
+
try {
|
|
370
|
+
const json = await readJson(file);
|
|
371
|
+
if (typeof json !== "object" || json === null) throw new Error(`Language file ${file} is not a valid JSON object`);
|
|
372
|
+
languageTracker.addFile(lang, file);
|
|
373
|
+
return [file, json];
|
|
374
|
+
} catch (error$1) {
|
|
375
|
+
warn(error$1);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
const results = await Promise.all(reads);
|
|
380
|
+
for (const entry of results) if (entry) result.set(entry[0], entry[1]);
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
//#endregion
|
|
385
|
+
//#region src/language/transformer.ts
|
|
386
|
+
function flattenKeys(object, prefix = "") {
|
|
387
|
+
const result = {};
|
|
388
|
+
for (const [key, value] of Object.entries(object)) {
|
|
389
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
390
|
+
if (value && typeof value === "object" && !Array.isArray(value)) Object.assign(result, flattenKeys(value, fullKey));
|
|
391
|
+
else result[fullKey] = value;
|
|
392
|
+
}
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
function expandDotNotationKeys(target, source, depth = 0) {
|
|
396
|
+
if (depth > 32) fail("Max object expansion depth exceeded.");
|
|
397
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) return source;
|
|
398
|
+
for (const [key, value] of Object.entries(source)) {
|
|
399
|
+
let current = target;
|
|
400
|
+
const parts = key.split(".");
|
|
401
|
+
const lastKey = parts.pop();
|
|
402
|
+
for (const part of parts) {
|
|
403
|
+
if (!(part in current)) current[part] = {};
|
|
404
|
+
current = current[part];
|
|
405
|
+
}
|
|
406
|
+
if (lastKey in current) console.warn(`Warning: Overwriting key "${lastKey}" during transformation.`);
|
|
407
|
+
current[lastKey] = expandDotNotationKeys({}, value, depth + 1);
|
|
408
|
+
}
|
|
409
|
+
return target;
|
|
410
|
+
}
|
|
411
|
+
function transform(dataMap) {
|
|
412
|
+
const mergedData = {};
|
|
413
|
+
for (const data of dataMap.values()) expandDotNotationKeys(mergedData, data);
|
|
414
|
+
return mergedData;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
//#endregion
|
|
418
|
+
//#region src/language/validator.ts
|
|
419
|
+
function getFirstMapValueOrWarn(map, contextDescription) {
|
|
420
|
+
if (map.size === 0) {
|
|
421
|
+
warn(`${contextDescription} is empty.`);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const first = map.values().next().value;
|
|
425
|
+
if (!first) {
|
|
426
|
+
warn(`${contextDescription} has no valid data.`);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
return first;
|
|
430
|
+
}
|
|
431
|
+
async function validator() {
|
|
432
|
+
const manifest = context.manifest;
|
|
433
|
+
const base = getFirstMapValueOrWarn(await loadLanguage("en", true), "Base language \"en\"");
|
|
434
|
+
if (!base) {
|
|
435
|
+
error("Base language \"en\" not found or could not be loaded.");
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const baseFlattened = flattenKeys(base);
|
|
439
|
+
for (const lang of manifest.languages) {
|
|
440
|
+
if (lang.lang === "en") continue;
|
|
441
|
+
const current = getFirstMapValueOrWarn(await loadLanguage(lang.lang, true), `Language "${lang.lang}"`);
|
|
442
|
+
if (!current) continue;
|
|
443
|
+
const currentFlattened = flattenKeys(current);
|
|
444
|
+
const missing = Object.keys(baseFlattened).filter((key) => !(key in currentFlattened));
|
|
445
|
+
const extra = Object.keys(currentFlattened).filter((key) => !(key in baseFlattened));
|
|
446
|
+
info(`Summary for language [${lang.lang}]:`);
|
|
447
|
+
if (missing.length > 0) console.warn(`Missing keys: ${missing.length}`, missing.slice(0, 5));
|
|
448
|
+
if (extra.length > 0) console.warn(`Extra keys: ${extra.length}`, extra.slice(0, 5));
|
|
449
|
+
if (missing.length === 0 && extra.length === 0) console.log(" ✅ All keys match.");
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region src/packs/compile-packs.ts
|
|
455
|
+
async function compileManifestPacks() {
|
|
456
|
+
if (!context.manifest?.packs) return;
|
|
457
|
+
for (const pack of context.manifest.packs) {
|
|
458
|
+
const sourceCandidates = [path.resolve(getSourceDirectory(), pack.path), path.resolve(getRoot(), pack.path)];
|
|
459
|
+
const destination = path.resolve(getOutDirectory(), pack.path);
|
|
460
|
+
let chosenSource;
|
|
461
|
+
for (const candidate of sourceCandidates) if (await directoryExists(candidate)) {
|
|
462
|
+
chosenSource = candidate;
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
if (!chosenSource) {
|
|
466
|
+
warn(`Pack path not found for ${pack.path}, skipped.`);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
const hasYaml = (await glob(["**/*.yaml", "**/*.yml"], {
|
|
470
|
+
cwd: chosenSource,
|
|
471
|
+
absolute: true
|
|
472
|
+
})).length > 0;
|
|
473
|
+
await compilePack(chosenSource, destination, {
|
|
474
|
+
yaml: hasYaml,
|
|
475
|
+
recursive: true
|
|
476
|
+
});
|
|
477
|
+
info(`Compiled pack ${pack.path} (${hasYaml ? "YAML" : "JSON"}) from ${chosenSource}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
//#endregion
|
|
482
|
+
//#region src/server/trackers/handlebars-tracker.ts
|
|
483
|
+
var HandlebarsTracker = class extends AbstractFileTracker {
|
|
484
|
+
updateEvent = "foundryvtt-template-update";
|
|
485
|
+
constructor() {
|
|
486
|
+
super(context.config);
|
|
487
|
+
}
|
|
488
|
+
getEventData(changedPath, value) {
|
|
489
|
+
return { path: value };
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
const handlebarsTracker = new HandlebarsTracker();
|
|
493
|
+
|
|
494
|
+
//#endregion
|
|
495
|
+
//#region src/server/http-middleware.ts
|
|
496
|
+
function httpMiddlewareHook(server) {
|
|
497
|
+
server.middlewares.use(async (request, response, next) => {
|
|
498
|
+
const config = context.config;
|
|
499
|
+
if (!isFoundryVTTUrl(request.url ?? "")) {
|
|
500
|
+
next();
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const cssFileName = config.build.lib.cssFileName;
|
|
504
|
+
const cssEntry = cssFileName ? localToFoundryVTTUrl(`${cssFileName}.css`) : void 0;
|
|
505
|
+
if (path.posix.normalize(request.url ?? "") === cssEntry) {
|
|
506
|
+
info(`Blocking CSS entry to ${request.url}`);
|
|
507
|
+
response.setHeader("Content-Type", "text/css");
|
|
508
|
+
response.end("/* The cake is in another castle. */");
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const languages = context.manifest.languages.filter((lang) => localToFoundryVTTUrl(lang.path) === path.posix.normalize(request.url ?? ""));
|
|
512
|
+
if (languages.length === 1) {
|
|
513
|
+
const lang = languages[0].lang;
|
|
514
|
+
const jsonData = transform(await loadLanguage(lang));
|
|
515
|
+
response.setHeader("Content-Type", "application/json");
|
|
516
|
+
response.end(JSON.stringify(jsonData, void 0, 2));
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
next();
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
//#endregion
|
|
524
|
+
//#region src/server/socket-proxy.ts
|
|
525
|
+
function socketProxy(server) {
|
|
526
|
+
const environment = context.env;
|
|
527
|
+
new Server(server.httpServer, { path: "/socket.io" }).on("connection", (socket) => {
|
|
528
|
+
const upstream = io(`http://${environment.foundryUrl}:${environment.foundryPort}`, {
|
|
529
|
+
transports: ["websocket"],
|
|
530
|
+
upgrade: false,
|
|
531
|
+
query: socket.handshake.query
|
|
532
|
+
});
|
|
533
|
+
socket.onAny(async (event, ...parameters) => {
|
|
534
|
+
const maybeAck = typeof parameters.at(-1) === "function" ? parameters.pop() : void 0;
|
|
535
|
+
if (event === "template") {
|
|
536
|
+
const localPath = await foundryVTTUrlToLocal(parameters[0]);
|
|
537
|
+
if (localPath) {
|
|
538
|
+
const html = await readFile(localPath);
|
|
539
|
+
if (maybeAck) maybeAck({
|
|
540
|
+
html,
|
|
541
|
+
success: true
|
|
542
|
+
});
|
|
543
|
+
handlebarsTracker.addFile(parameters[0], localPath);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
upstream.emit(event, ...parameters, (response) => {
|
|
548
|
+
if (maybeAck) maybeAck(response);
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
upstream.onAny((event, ...parameters) => {
|
|
552
|
+
const maybeAck = typeof parameters.at(-1) === "function" ? parameters.pop() : void 0;
|
|
553
|
+
socket.emit(event, ...parameters, (response) => {
|
|
554
|
+
if (maybeAck) maybeAck(response);
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region src/server/index.ts
|
|
562
|
+
function setupDevelopmentServer(server) {
|
|
563
|
+
handlebarsTracker.initialize(server);
|
|
564
|
+
languageTracker.initialize(server);
|
|
565
|
+
httpMiddlewareHook(server);
|
|
566
|
+
socketProxy(server);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
//#endregion
|
|
570
|
+
//#region src/server/hmr-client.ts
|
|
571
|
+
var hmr_client_default = `
|
|
572
|
+
if (import.meta.hot) {
|
|
573
|
+
const FVTT_PLUGIN = __FVTT_PLUGIN__
|
|
574
|
+
|
|
575
|
+
function refreshApplications(renderData = {}) {
|
|
576
|
+
const options = { renderContext: 'hotReload', renderData }
|
|
577
|
+
// AppV1 refresh
|
|
578
|
+
for (const appV1 of Object.values(foundry.ui.windows)) appV1.render(false, { ...options })
|
|
579
|
+
// AppV2 refresh
|
|
580
|
+
for (const appV2 of foundry.applications.instances.values()) appV2.render({ ...options })
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
import.meta.hot.on('foundryvtt-template-update', ({ path }) => {
|
|
584
|
+
game.socket.emit('template', path, response => {
|
|
585
|
+
if (response.error) new Error(response.error)
|
|
586
|
+
let template = undefined
|
|
587
|
+
try {
|
|
588
|
+
template = Handlebars.compile(response.html)
|
|
589
|
+
} catch (error) {
|
|
590
|
+
console.error(error)
|
|
591
|
+
return
|
|
592
|
+
}
|
|
593
|
+
if (!Object.hasOwn(Handlebars, 'templateIds')) Handlebars.registerPartial(path, template)
|
|
594
|
+
else if (Handlebars.templateIds[path]?.size > 0) {
|
|
595
|
+
for (const id of Handlebars.templateIds[path])
|
|
596
|
+
if (id in Handlebars.partials) Handlebars.registerPartial(id, template)
|
|
597
|
+
} else foundry.applications.handlebars.getTemplate(path)
|
|
598
|
+
console.log(\`Vite | Retrieved and compiled template \${path}\`)
|
|
599
|
+
refreshApplications({
|
|
600
|
+
packageId: FVTT_PLUGIN.id,
|
|
601
|
+
packageType: FVTT_PLUGIN.isSystem ? 'system' : 'module',
|
|
602
|
+
content: response.html,
|
|
603
|
+
path,
|
|
604
|
+
extension: 'html',
|
|
605
|
+
})
|
|
606
|
+
})
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
async function hmrLanguage(lang, targetObject = game.i18n.translations) {
|
|
610
|
+
try {
|
|
611
|
+
const languages = FVTT_PLUGIN.isSystem
|
|
612
|
+
? game.system.languages
|
|
613
|
+
: game.modules.get(FVTT_PLUGIN.id)?.languages
|
|
614
|
+
if (!languages) {
|
|
615
|
+
console.warn(
|
|
616
|
+
'Vite | Got a HMR request to reload languages, however no languages were found.',
|
|
617
|
+
)
|
|
618
|
+
return
|
|
619
|
+
}
|
|
620
|
+
const langEntry = languages.find(l => l.lang === lang)
|
|
621
|
+
if (!langEntry) {
|
|
622
|
+
console.warn('Vite | Got an HMR request for an undefined language')
|
|
623
|
+
return
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const url = langEntry.path
|
|
627
|
+
const resp = await fetch(url)
|
|
628
|
+
if (!resp.ok) throw new Error('Failed to fetch language file!')
|
|
629
|
+
|
|
630
|
+
const json = await resp.json()
|
|
631
|
+
|
|
632
|
+
foundry.utils.mergeObject(targetObject, json)
|
|
633
|
+
console.log(\`Vite | HMR: Reloaded language '\${lang}'\`)
|
|
634
|
+
} catch (error) {
|
|
635
|
+
console.error(\`Vite | HMR: Error reloading language '\${lang}' for \${FVTT_PLUGIN.id}\`, error)
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
import.meta.hot.on('foundryvtt-language-update', async () => {
|
|
640
|
+
const currentLang = game.i18n.lang
|
|
641
|
+
const promises = []
|
|
642
|
+
if (currentLang !== 'en') {
|
|
643
|
+
promises.push(hmrLanguage('en', game.i18n._fallback))
|
|
644
|
+
}
|
|
645
|
+
promises.push(hmrLanguage(currentLang))
|
|
646
|
+
await Promise.all(promises)
|
|
647
|
+
refreshApplications({
|
|
648
|
+
packageId: FVTT_PLUGIN.id,
|
|
649
|
+
packageType: FVTT_PLUGIN.isSystem ? 'system' : 'module',
|
|
650
|
+
content: '',
|
|
651
|
+
path: '',
|
|
652
|
+
extension: 'json',
|
|
653
|
+
})
|
|
654
|
+
})
|
|
655
|
+
} else console.error('Vite | HMR is disabled')
|
|
656
|
+
//`;
|
|
657
|
+
|
|
658
|
+
//#endregion
|
|
659
|
+
//#region src/index.ts
|
|
660
|
+
async function foundryVTTPlugin({ buildPacks = true } = {}) {
|
|
661
|
+
context.env = await loadEnvironment();
|
|
662
|
+
return {
|
|
663
|
+
name: "vite-plugin-fvtt",
|
|
664
|
+
async config(config) {
|
|
665
|
+
context.manifest = await loadManifest(config) ?? void 0;
|
|
666
|
+
return createPartialViteConfig(config);
|
|
667
|
+
},
|
|
668
|
+
configResolved(config) {
|
|
669
|
+
context.config = config;
|
|
670
|
+
},
|
|
671
|
+
async generateBundle() {
|
|
672
|
+
for (const file of ["system.json", "module.json"]) {
|
|
673
|
+
const source = path.resolve(file);
|
|
674
|
+
if (!await getPublicDirectoryFile(file) && await fileExists(source)) {
|
|
675
|
+
this.addWatchFile(source);
|
|
676
|
+
const manifest = await readJson(source);
|
|
677
|
+
this.emitFile({
|
|
678
|
+
type: "asset",
|
|
679
|
+
fileName: file,
|
|
680
|
+
source: JSON.stringify(manifest, void 0, 2)
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
const languages = context.manifest?.languages ?? [];
|
|
685
|
+
if (languages.length > 0) for (const language of languages) {
|
|
686
|
+
if (await getPublicDirectoryFile(language.path)) continue;
|
|
687
|
+
getLocalLanguageFiles(language.lang).then((langFiles) => {
|
|
688
|
+
for (const file of langFiles) this.addWatchFile(file);
|
|
689
|
+
});
|
|
690
|
+
const languageData = transform(await loadLanguage(language.lang));
|
|
691
|
+
this.emitFile({
|
|
692
|
+
type: "asset",
|
|
693
|
+
fileName: path.join(language.path),
|
|
694
|
+
source: JSON.stringify(languageData, void 0, 2)
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
async writeBundle() {
|
|
699
|
+
if (buildPacks) await compileManifestPacks();
|
|
700
|
+
},
|
|
701
|
+
closeBundle() {
|
|
702
|
+
if ((context.manifest?.languages ?? []).length > 0) validator();
|
|
703
|
+
},
|
|
704
|
+
load(id) {
|
|
705
|
+
const config = context.config;
|
|
706
|
+
const output = config.build.rollupOptions?.output;
|
|
707
|
+
let jsFileName;
|
|
708
|
+
if (Array.isArray(output)) jsFileName = String(output[0].entryFileNames);
|
|
709
|
+
else if (output) jsFileName = String(output.entryFileNames);
|
|
710
|
+
if (id === jsFileName || id === `/${jsFileName}`) return `import '${`/@fs/${path.resolve(config.build.lib.entry)}`}';\n${hmr_client_default}`;
|
|
711
|
+
},
|
|
712
|
+
configureServer: setupDevelopmentServer
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
//#endregion
|
|
717
|
+
export { foundryVTTPlugin as default };
|