@studiokloek/vite-config 0.13.1 → 0.14.0

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.
@@ -0,0 +1,862 @@
1
+ import { build, createServer, isCSSRequest, normalizePath } from "vite";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { z } from "zod";
5
+ import { svelte, vitePreprocess } from "@sveltejs/vite-plugin-svelte";
6
+ import { mkdir, opendir, readFile, rm, writeFile } from "node:fs/promises";
7
+ import { fileURLToPath } from "node:url";
8
+ import { kloekAssets, resolveConfig } from "@studiokloek/vite-assets-plugin";
9
+ import os from "node:os";
10
+ import Handlebars from "handlebars";
11
+ import legacyPlugin from "@vitejs/plugin-legacy";
12
+ import { visualizer } from "rollup-plugin-visualizer";
13
+ import { minify } from "html-minifier-next";
14
+ import sharp from "sharp";
15
+ import checker from "vite-plugin-checker";
16
+ import fullReload from "vite-plugin-full-reload";
17
+ import mkcert from "vite-plugin-mkcert";
18
+ import filenamify from "filenamify";
19
+ import { zip } from "zip-a-folder";
20
+ //#region src/core/game.ts
21
+ function isRecord(value) {
22
+ return typeof value === "object" && value !== null && !Array.isArray(value);
23
+ }
24
+ /** Legt `game` per sectie over `generic` heen; secties die maar aan één kant bestaan blijven behouden. */
25
+ function mergeGameSettings(generic, game) {
26
+ const merged = { ...generic };
27
+ for (const [key, value] of Object.entries(game)) {
28
+ const base = merged[key];
29
+ merged[key] = isRecord(base) && isRecord(value) ? {
30
+ ...base,
31
+ ...value
32
+ } : value;
33
+ }
34
+ return merged;
35
+ }
36
+ function parseGamesSettings(settings, version) {
37
+ const games = {};
38
+ for (const game of settings.games) {
39
+ const merged = mergeGameSettings(settings.generic, game);
40
+ const pageId = merged.info.page ?? merged.info.id ?? "unknown";
41
+ merged.info = {
42
+ ...merged.info,
43
+ page: pageId,
44
+ version: merged.info.version ?? version
45
+ };
46
+ games[pageId] = merged;
47
+ }
48
+ return {
49
+ development: settings.generic,
50
+ games
51
+ };
52
+ }
53
+ /** De pagina die de dev-server opent: de enige game, of anders het overzicht. */
54
+ function getPageToServe(basePath, serveParams, games) {
55
+ const pageIds = Object.keys(games);
56
+ const page = pageIds.length === 1 ? pageIds[0] : "development";
57
+ return path.posix.join(basePath, `${page}.html${serveParams}`);
58
+ }
59
+ /** Het pagina-id uit een url (`/sub/verdwaald.html?debug` → `verdwaald`), of `''` voor iets anders dan html. */
60
+ function getPageIdFromUrl(url) {
61
+ if (!url) return "";
62
+ let pathname;
63
+ try {
64
+ pathname = new URL(url, "https://studiokloek.nl").pathname;
65
+ } catch {
66
+ return "";
67
+ }
68
+ if (!pathname.endsWith(".html")) return "";
69
+ return path.posix.basename(pathname, ".html");
70
+ }
71
+ //#endregion
72
+ //#region src/config/schema.ts
73
+ /**
74
+ * De game-secties zijn vrije template-data voor handlebars en de manifest; alleen de velden die
75
+ * vite-config zelf gebruikt, krijgen een type. Andere velden blijven gewoon behouden.
76
+ */
77
+ const gameSettingsSchema = z.looseObject({
78
+ info: z.looseObject({
79
+ id: z.string().optional(),
80
+ page: z.string().optional(),
81
+ version: z.string().optional(),
82
+ client: z.string().optional(),
83
+ year: z.union([z.string(), z.number()]).optional()
84
+ }).prefault({}),
85
+ meta: z.looseObject({
86
+ title: z.string().optional(),
87
+ description: z.string().optional(),
88
+ apptitle: z.string().optional()
89
+ }).prefault({}),
90
+ options: z.looseObject({
91
+ backgroundColor: z.string().optional(),
92
+ orientation: z.string().optional(),
93
+ fullscreen: z.string().optional()
94
+ }).prefault({})
95
+ });
96
+ const settingsSchema = z.strictObject({
97
+ /** Instellingen die voor iedere game gelden. */
98
+ generic: gameSettingsSchema.prefault({}),
99
+ /** Per game; wordt per sectie over `generic` heen gelegd. */
100
+ games: z.array(gameSettingsSchema).default([])
101
+ });
102
+ const devSchema = z.strictObject({
103
+ /** Query-string achter de pagina die bij het starten geopend wordt. */
104
+ serveParams: z.string().default(""),
105
+ /** Ook bij wijzigingen in `.svelte`-bestanden de hele pagina herladen. */
106
+ fullReloadSvelte: z.boolean().default(true),
107
+ cors: z.boolean().default(true),
108
+ /** Lokaal certificaat via mkcert. */
109
+ https: z.boolean().default(true)
110
+ });
111
+ const imageQualitySchema = z.strictObject({
112
+ jpeg: z.number().min(0).max(100).default(80),
113
+ /** Minimale en maximale kwaliteit (0–1); de maximale wordt gebruikt. */
114
+ png: z.tuple([z.number().min(0).max(1), z.number().min(0).max(1)]).default([.5, .7])
115
+ });
116
+ const buildSchema = z.strictObject({
117
+ basePath: z.string().default("/"),
118
+ /** Extra bundels voor oudere browsers via `@vitejs/plugin-legacy`. */
119
+ legacy: z.boolean().default(false),
120
+ imageQuality: imageQualitySchema.prefault({}),
121
+ /** Mappen in de build-output waarvan de afbeeldingen niet opnieuw gecomprimeerd worden. */
122
+ imageExclude: z.array(z.string()).default([
123
+ "sprites",
124
+ "sounds",
125
+ "fonts"
126
+ ]),
127
+ /** Een webmanifest per game. */
128
+ manifest: z.boolean().default(false),
129
+ analyzeBundle: z.boolean().default(false),
130
+ sourceMaps: z.union([z.boolean(), z.enum(["inline", "hidden"])]).default(false),
131
+ createZip: z.boolean().default(false),
132
+ /** Extra patronen voor bestanden die uit de build-output verwijderd worden. */
133
+ cleanPatterns: z.array(z.string()).default([])
134
+ });
135
+ /** `package.json#vite`. Onbekende keys zijn een fout, zodat een typefout niet stil genegeerd wordt. */
136
+ const viteSchema = z.strictObject({
137
+ dev: devSchema.prefault({}),
138
+ build: buildSchema.prefault({}),
139
+ /** Extra packages die maar één keer in de bundel mogen voorkomen. */
140
+ dedupe: z.array(z.string()).default([])
141
+ });
142
+ /** De delen van `package.json` die vite-config leest; de rest van het bestand blijft buiten beschouwing. */
143
+ const packageSchema = z.looseObject({
144
+ name: z.string().default("project"),
145
+ version: z.string().default("0.0.0"),
146
+ description: z.string().default(""),
147
+ browserslist: z.union([z.string(), z.array(z.string())]).optional(),
148
+ vite: viteSchema.prefault({}),
149
+ settings: settingsSchema.prefault({}),
150
+ kloekAssets: z.record(z.string(), z.unknown()).optional()
151
+ });
152
+ function parsePackageConfig(input, file = "package.json") {
153
+ const result = packageSchema.safeParse(input ?? {});
154
+ if (!result.success) {
155
+ const issues = result.error.issues.map((issue) => ` ${issue.path.join(".") || "(root)"}: ${issue.message}`).join("\n");
156
+ throw new TypeError(`Invalid config in ${file}:\n${issues}`);
157
+ }
158
+ return result.data;
159
+ }
160
+ //#endregion
161
+ //#region src/config/load.ts
162
+ /** Leest en valideert `package.json` van het project in `cwd`. */
163
+ function loadProject(cwd = process.cwd()) {
164
+ const root = normalizePath(path.resolve(cwd));
165
+ const file = path.join(root, "package.json");
166
+ let data;
167
+ try {
168
+ data = JSON.parse(readFileSync(file, "utf8"));
169
+ } catch (error) {
170
+ throw new Error(`vite-config: kan ${file} niet lezen`, { cause: error });
171
+ }
172
+ const config = parsePackageConfig(data, file);
173
+ return {
174
+ cwd: root,
175
+ sourceDirectory: path.join(root, "source"),
176
+ outputDirectory: path.join(root, "public"),
177
+ package: config,
178
+ ...parseGamesSettings(config.settings, config.version)
179
+ };
180
+ }
181
+ //#endregion
182
+ //#region src/core/fs.ts
183
+ /** Loopt een map recursief door zonder symlinks te volgen, en levert alleen bestanden op. */
184
+ async function* walkFiles(directory) {
185
+ let entries;
186
+ try {
187
+ entries = await opendir(directory);
188
+ } catch {
189
+ return;
190
+ }
191
+ for await (const entry of entries) {
192
+ const fullPath = path.join(directory, entry.name);
193
+ if (entry.isDirectory()) yield* walkFiles(fullPath);
194
+ else if (entry.isFile()) yield fullPath;
195
+ }
196
+ }
197
+ /** `filePath` relatief t.o.v. `directory`, altijd met `/` als scheidingsteken. */
198
+ function relativePosix(directory, filePath) {
199
+ return path.relative(directory, filePath).split(path.sep).join("/");
200
+ }
201
+ let packageRoot;
202
+ /**
203
+ * De root van dit package, gevonden vanaf deze module. Werkt zowel vanuit `dist/` als vanuit
204
+ * `src/` (tests), zodat meegeleverde bestanden als `partials/` zonder kopieerstap te vinden zijn.
205
+ */
206
+ function packageDirectory(...segments) {
207
+ if (packageRoot === void 0) {
208
+ let current = path.dirname(fileURLToPath(import.meta.url));
209
+ while (!existsSync(path.join(current, "package.json"))) {
210
+ const parent = path.dirname(current);
211
+ if (parent === current) throw new Error("vite-config: kan de package-root niet vinden");
212
+ current = parent;
213
+ }
214
+ packageRoot = current;
215
+ }
216
+ return path.join(packageRoot, ...segments);
217
+ }
218
+ //#endregion
219
+ //#region src/plugins/assets.ts
220
+ /**
221
+ * De config voor de assets-plugin met absolute paden. Vite's root is `source/`, maar de paden in
222
+ * `package.json#kloekAssets` gelden (net als bij de `kloek-assets` CLI) vanaf de projectmap.
223
+ */
224
+ async function resolveAssetsConfig(project) {
225
+ const config = project.package.kloekAssets;
226
+ if (config === void 0) return;
227
+ const resolved = await resolveConfig(config, { root: project.cwd });
228
+ return {
229
+ ...config,
230
+ entry: resolved.entryDirectory,
231
+ output: {
232
+ ...config.output,
233
+ directory: resolved.outputDirectory
234
+ },
235
+ scriptDirectory: resolved.scriptDirectory,
236
+ cacheDirectory: resolved.cacheDirectory
237
+ };
238
+ }
239
+ /** De assets-plugin, alleen als het project een `kloekAssets`-config in package.json heeft. */
240
+ async function assetsPlugins(project) {
241
+ const config = await resolveAssetsConfig(project);
242
+ return config === void 0 ? [] : [kloekAssets(config)];
243
+ }
244
+ //#endregion
245
+ //#region src/plugins/lib/handlebars/partials.ts
246
+ const VALID_EXTENSIONS = /* @__PURE__ */ new Set([".html", ".hbs"]);
247
+ /**
248
+ * De partial-naam voor een bestand: het pad binnen de partials-map zonder extensie
249
+ * (`header/production.hbs` → `header/production`), of `undefined` als het geen partial is.
250
+ */
251
+ function partialName(directories, file) {
252
+ const extension = path.extname(file);
253
+ if (!VALID_EXTENSIONS.has(extension)) return;
254
+ for (const directory of directories) {
255
+ const relative = relativePosix(directory, file);
256
+ if (!relative.startsWith("../") && !path.isAbsolute(relative)) return relative.slice(0, -extension.length);
257
+ }
258
+ }
259
+ /** Registreert alle partials; bij dubbele namen wint de laatste map (die van het project). */
260
+ async function registerPartials(handlebars, directories) {
261
+ for (const directory of directories) for await (const file of walkFiles(directory)) {
262
+ const name = partialName([directory], file);
263
+ if (name !== void 0) handlebars.registerPartial(name, await readFile(file, "utf8"));
264
+ }
265
+ }
266
+ const helpers = {
267
+ json: (object, indent) => JSON.stringify(object, void 0, typeof indent === "number" ? indent : 0),
268
+ concat: (...values) => values.slice(0, -1).join(""),
269
+ eq: (a, b) => a === b,
270
+ neq: (a, b) => a !== b,
271
+ isdefined: (value) => value !== void 0
272
+ };
273
+ //#endregion
274
+ //#region src/plugins/lib/handlebars/handlebars-plugin.ts
275
+ /** Rendert iedere html-pagina als handlebars-template, vóórdat Vite hem verder verwerkt. */
276
+ function handlebarsPlugin(options) {
277
+ const handlebars = Handlebars.create();
278
+ let partialsLoaded;
279
+ handlebars.registerHelper({
280
+ ...helpers,
281
+ "resolve-root"(file) {
282
+ const resolvedPath = normalizePath(path.resolve(options.root, file));
283
+ return os.platform() === "win32" ? `/${resolvedPath}` : resolvedPath;
284
+ }
285
+ });
286
+ return {
287
+ name: "vite-plugin-kloek-handlebars",
288
+ async hotUpdate({ file, type }) {
289
+ const name = partialName(options.partialDirectories, file);
290
+ if (name === void 0) return;
291
+ if (type === "delete") handlebars.unregisterPartial(name);
292
+ else handlebars.registerPartial(name, await readFile(file, "utf8"));
293
+ if (this.environment.name === "client") this.environment.hot.send({ type: "full-reload" });
294
+ return [];
295
+ },
296
+ transformIndexHtml: {
297
+ order: "pre",
298
+ async handler(html, context) {
299
+ partialsLoaded ??= registerPartials(handlebars, options.partialDirectories);
300
+ await partialsLoaded;
301
+ const pageId = getPageIdFromUrl(normalizePath(context.path));
302
+ return handlebars.compile(html)(pageId ? options.context(pageId) : {});
303
+ }
304
+ }
305
+ };
306
+ }
307
+ //#endregion
308
+ //#region src/plugins/lib/html-plugin.ts
309
+ const PAGE_TEMPLATE = "{{> game data=data}}";
310
+ const DEVELOPMENT_TEMPLATE = "{{> development data=data}}";
311
+ /**
312
+ * Levert per game een html-pagina die door handlebars (`game.hbs`) ingevuld wordt. Bij een build zijn
313
+ * de pagina's virtuele modules, dus er komen geen tijdelijke bestanden in `source/`.
314
+ */
315
+ function htmlPlugin(project) {
316
+ const pages = new Set(Object.keys(project.games).map((pageId) => normalizePath(path.join(project.sourceDirectory, `${pageId}.html`))));
317
+ return {
318
+ name: "vite-plugin-kloek-html",
319
+ enforce: "pre",
320
+ resolveId(source) {
321
+ const id = normalizePath(source);
322
+ return pages.has(id) ? id : void 0;
323
+ },
324
+ load(id) {
325
+ return pages.has(id) ? PAGE_TEMPLATE : void 0;
326
+ },
327
+ configureServer(server) {
328
+ return () => {
329
+ server.middlewares.use(async (request, response, next) => {
330
+ const url = request.originalUrl ?? "";
331
+ const pageId = getPageIdFromUrl(url);
332
+ let template;
333
+ if (pageId === "development") template = DEVELOPMENT_TEMPLATE;
334
+ else if (pageId in project.games) template = PAGE_TEMPLATE;
335
+ if (template === void 0) {
336
+ next();
337
+ return;
338
+ }
339
+ try {
340
+ const html = await server.transformIndexHtml(url, template, url);
341
+ response.setHeader("Content-Type", "text/html");
342
+ response.end(html);
343
+ } catch (error) {
344
+ next(error);
345
+ }
346
+ });
347
+ };
348
+ }
349
+ };
350
+ }
351
+ //#endregion
352
+ //#region src/plugins/base.ts
353
+ async function basePlugins(project, environment) {
354
+ const env = {
355
+ command: environment.command,
356
+ mode: environment.mode
357
+ };
358
+ return [
359
+ htmlPlugin(project),
360
+ svelte({
361
+ preprocess: vitePreprocess(),
362
+ onwarn(warning, handler) {
363
+ if (warning.code.startsWith("a11y")) return;
364
+ handler(warning);
365
+ }
366
+ }),
367
+ handlebarsPlugin({
368
+ partialDirectories: [packageDirectory("partials"), path.join(project.sourceDirectory, "partials")],
369
+ root: project.sourceDirectory,
370
+ context(pageId) {
371
+ if (pageId === "development") return {
372
+ env,
373
+ data: project.development,
374
+ games: project.games
375
+ };
376
+ return {
377
+ env,
378
+ data: project.games[pageId] ?? {}
379
+ };
380
+ }
381
+ }),
382
+ ...await assetsPlugins(project)
383
+ ];
384
+ }
385
+ //#endregion
386
+ //#region src/plugins/lib/banner-plugin.ts
387
+ /** De tekst als commentaarblok, zoals bovenaan iedere js- en css-file. */
388
+ function renderBanner(content) {
389
+ return `/**\n${content.split("\n").map((line) => line ? ` * ${line}` : " *").join("\n")}\n */\n`;
390
+ }
391
+ /** Zet een banner bovenaan de gegenereerde js (na minificatie, met kloppende sourcemaps) en css. */
392
+ function bannerPlugin(content) {
393
+ const banner = renderBanner(content);
394
+ return {
395
+ name: "vite-plugin-kloek-banner",
396
+ apply: "build",
397
+ config() {
398
+ return { build: { rolldownOptions: { output: { postBanner: banner } } } };
399
+ },
400
+ generateBundle(_, bundle) {
401
+ for (const file of Object.values(bundle)) if (file.type === "asset" && file.fileName.endsWith(".css")) file.source = `${banner}${typeof file.source === "string" ? file.source : new TextDecoder().decode(file.source)}`;
402
+ }
403
+ };
404
+ }
405
+ //#endregion
406
+ //#region src/plugins/lib/clean-plugin.ts
407
+ const DEFAULT_PATTERNS = [
408
+ "**/.DS_Store",
409
+ "**/._*",
410
+ "**/Thumbs.db",
411
+ "**/desktop.ini",
412
+ "**/.gitignore",
413
+ "**/.gitkeep"
414
+ ];
415
+ function patternToRegExp(pattern) {
416
+ let source = pattern;
417
+ let matchInSubdirectories = false;
418
+ if (source.startsWith("**/")) {
419
+ matchInSubdirectories = true;
420
+ source = source.slice(3);
421
+ }
422
+ const body = source.replaceAll(/[.*+?^${}()|[\]\\]/g, (character) => character === "*" ? "[^/]*" : `\\${character}`);
423
+ return new RegExp(matchInSubdirectories ? `(?:^|/)${body}$` : `^${body}$`);
424
+ }
425
+ function cleanPlugin(patterns = []) {
426
+ const expressions = [...DEFAULT_PATTERNS, ...patterns].map((pattern) => patternToRegExp(pattern));
427
+ let outDir = "";
428
+ return {
429
+ name: "vite-plugin-kloek-clean",
430
+ enforce: "post",
431
+ apply: "build",
432
+ configResolved(config) {
433
+ outDir = path.resolve(config.root, config.build.outDir);
434
+ },
435
+ async closeBundle() {
436
+ const actions = [];
437
+ for await (const filePath of walkFiles(outDir)) {
438
+ const relativePath = relativePosix(outDir, filePath);
439
+ if (expressions.some((expression) => expression.test(relativePath))) actions.push(rm(filePath).catch(() => void 0));
440
+ }
441
+ await Promise.all(actions);
442
+ }
443
+ };
444
+ }
445
+ //#endregion
446
+ //#region src/plugins/lib/html-minify-plugin.ts
447
+ const OPTIONS = {
448
+ collapseWhitespace: true,
449
+ keepClosingSlash: true,
450
+ removeComments: true,
451
+ removeRedundantAttributes: true,
452
+ removeDefaultTypeAttributes: true,
453
+ useShortDoctype: true,
454
+ minifyCSS: true
455
+ };
456
+ /** Minificeert de html-pagina's in de build-output. */
457
+ function htmlMinifyPlugin() {
458
+ return {
459
+ name: "vite-plugin-kloek-html-minify",
460
+ enforce: "post",
461
+ apply: "build",
462
+ async generateBundle(_, bundle) {
463
+ for (const file of Object.values(bundle)) if (file.type === "asset" && file.fileName.endsWith(".html") && typeof file.source === "string") file.source = await minify(file.source, OPTIONS);
464
+ }
465
+ };
466
+ }
467
+ //#endregion
468
+ //#region src/plugins/lib/image-plugin.ts
469
+ const EXTENSIONS = /* @__PURE__ */ new Set([
470
+ ".png",
471
+ ".jpg",
472
+ ".jpeg"
473
+ ]);
474
+ /** Comprimeert één afbeelding; geeft `undefined` als het resultaat niet kleiner is. */
475
+ async function optimizeImage(input, extension, quality) {
476
+ const image = sharp(input);
477
+ const output = extension === ".png" ? await image.png({
478
+ palette: true,
479
+ quality: Math.round(Math.max(...quality.png) * 100),
480
+ effort: 8
481
+ }).toBuffer() : await image.jpeg({
482
+ quality: quality.jpeg,
483
+ mozjpeg: true
484
+ }).toBuffer();
485
+ return output.length < input.length ? output : void 0;
486
+ }
487
+ function formatBytes(bytes) {
488
+ return bytes >= 1048576 ? `${(bytes / 1024 / 1024).toFixed(1)} MB` : `${Math.round(bytes / 1024)} kB`;
489
+ }
490
+ /**
491
+ * Comprimeert na een productie-build de losse png/jpg's in de output. Sprites, geluid en fonts komen
492
+ * al gecomprimeerd uit de assets-plugin en worden overgeslagen.
493
+ */
494
+ function imagePlugin(options) {
495
+ const exclude = new Set(options.exclude);
496
+ let outDir = "";
497
+ let enabled = false;
498
+ let logger = console;
499
+ return {
500
+ name: "vite-plugin-kloek-image",
501
+ enforce: "post",
502
+ apply: "build",
503
+ configResolved(config) {
504
+ outDir = path.resolve(config.root, config.build.outDir);
505
+ enabled = config.mode === "production";
506
+ logger = config.logger;
507
+ },
508
+ async closeBundle() {
509
+ if (!enabled) return;
510
+ const files = [];
511
+ for await (const file of walkFiles(outDir)) {
512
+ const [firstSegment] = relativePosix(outDir, file).split("/");
513
+ if (EXTENSIONS.has(path.extname(file).toLowerCase()) && !exclude.has(firstSegment ?? "")) files.push(file);
514
+ }
515
+ let before = 0;
516
+ let saved = 0;
517
+ const queue = [...files];
518
+ const workers = Array.from({ length: Math.min(os.availableParallelism(), 4) }, async () => {
519
+ for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
520
+ const input = await readFile(file);
521
+ const output = await optimizeImage(input, path.extname(file).toLowerCase(), options.quality);
522
+ before += input.length;
523
+ if (output !== void 0) {
524
+ saved += input.length - output.length;
525
+ await writeFile(file, output);
526
+ }
527
+ }
528
+ });
529
+ await Promise.all(workers);
530
+ if (files.length > 0) logger.info(`kloek-image: ${files.length} afbeeldingen, ${formatBytes(saved)} kleiner (${formatBytes(before)} → ${formatBytes(before - saved)})`);
531
+ }
532
+ };
533
+ }
534
+ //#endregion
535
+ //#region src/plugins/lib/manifest-plugin.ts
536
+ function renderGameManifest(pageId, settings, base) {
537
+ const manifest = {
538
+ short_name: `${settings.meta.apptitle ?? ""}`,
539
+ name: `${settings.meta.title ?? ""}`,
540
+ description: `${settings.meta.description ?? ""}`,
541
+ scope: `${base}${pageId}.html`,
542
+ start_url: `${base}${pageId}.html`,
543
+ display: "fullscreen",
544
+ orientation: `${settings.options.orientation ?? "landscape"}`,
545
+ icons: [{
546
+ src: `${base}meta/${settings.info.id}/icons/chrome-touch-icon-192x192.png`,
547
+ type: "image/png",
548
+ sizes: "192x192"
549
+ }, {
550
+ src: `${base}meta/${settings.info.id}/icons/icon.png`,
551
+ type: "image/png",
552
+ sizes: "1024x1024"
553
+ }],
554
+ background_color: `${settings.options.backgroundColor ?? ""}`,
555
+ theme_color: `${settings.options.backgroundColor ?? ""}`
556
+ };
557
+ return `${JSON.stringify(manifest, null, 2)}\n`;
558
+ }
559
+ /** Schrijft voor iedere game een `<page>.webmanifest`. */
560
+ function manifestPlugin(games) {
561
+ let base = "/";
562
+ return {
563
+ name: "vite-plugin-kloek-manifest",
564
+ enforce: "post",
565
+ apply: "build",
566
+ configResolved(config) {
567
+ base = config.base;
568
+ },
569
+ generateBundle() {
570
+ for (const [pageId, settings] of Object.entries(games)) this.emitFile({
571
+ type: "asset",
572
+ fileName: `${pageId}.webmanifest`,
573
+ source: renderGameManifest(pageId, settings, base)
574
+ });
575
+ }
576
+ };
577
+ }
578
+ //#endregion
579
+ //#region src/plugins/build.ts
580
+ const BANNER = `. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
581
+
582
+ __ , _ _
583
+ / ) | o /| / | | | |
584
+ \\__ _|_ __| __ |__/ | | __ _ | |
585
+ \\ | | | / | | / \\_ | \\ |/ / \\_|/ |/_)
586
+ (___/ |_/ \\_/|_/\\_/|_/|_/\\__/ | \\_/|__/\\__/ |__/| \\_/
587
+
588
+ Concept, ontwerp & ontwikkeling
589
+ https://studiokloek.nl
590
+
591
+ Studio Kloek ❤ PixiJS, GSAP & howler.js
592
+
593
+ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .`;
594
+ function buildPlugins(project) {
595
+ const build = project.package.vite.build;
596
+ const plugins = [];
597
+ if (build.legacy) plugins.push(legacyPlugin({ targets: project.package.browserslist ?? "defaults" }));
598
+ plugins.push(imagePlugin({
599
+ quality: build.imageQuality,
600
+ exclude: build.imageExclude
601
+ }), htmlMinifyPlugin(), bannerPlugin(BANNER));
602
+ if (build.manifest) plugins.push(manifestPlugin(project.games));
603
+ plugins.push(cleanPlugin(build.cleanPatterns));
604
+ if (build.analyzeBundle) plugins.push(visualizer({
605
+ template: "treemap",
606
+ open: true,
607
+ gzipSize: true,
608
+ filename: "bundle-analyse.html"
609
+ }));
610
+ return plugins;
611
+ }
612
+ //#endregion
613
+ //#region src/plugins/serve.ts
614
+ function servePlugins(project) {
615
+ const { dev } = project.package.vite;
616
+ const fullReloadPaths = ["script/**/*.ts"];
617
+ if (dev.fullReloadSvelte) fullReloadPaths.push("svelte/**/*.svelte");
618
+ const plugins = [fullReload(fullReloadPaths, {
619
+ root: project.sourceDirectory,
620
+ always: true
621
+ })];
622
+ if (dev.https) plugins.push(mkcert());
623
+ plugins.push(checker({
624
+ root: project.cwd,
625
+ typescript: { root: project.cwd },
626
+ biome: true
627
+ }));
628
+ return plugins;
629
+ }
630
+ //#endregion
631
+ //#region src/core/chunks.ts
632
+ const VENDOR_CHUNKS = [
633
+ {
634
+ match: "lodash-decorators",
635
+ chunk: "lodash-decorators"
636
+ },
637
+ {
638
+ match: "lodash",
639
+ chunk: "lodash"
640
+ },
641
+ {
642
+ match: "date-fns-tz",
643
+ chunk: "date-fns"
644
+ },
645
+ {
646
+ match: "date-fns",
647
+ chunk: "date-fns"
648
+ },
649
+ {
650
+ match: "svelte",
651
+ chunk: "svelte"
652
+ },
653
+ {
654
+ match: "pixi-spine",
655
+ chunk: "pixi"
656
+ },
657
+ {
658
+ match: "@pixi-spine",
659
+ chunk: "pixi"
660
+ },
661
+ {
662
+ match: "@pixi",
663
+ chunk: "pixi"
664
+ },
665
+ {
666
+ match: "pixi",
667
+ chunk: "pixi"
668
+ },
669
+ {
670
+ match: "@firebase",
671
+ chunk: "firebase"
672
+ },
673
+ {
674
+ match: "@studiokloek",
675
+ chunk: "studiokloek"
676
+ },
677
+ {
678
+ match: "capacitor",
679
+ chunk: "capacitor"
680
+ },
681
+ {
682
+ match: "@capacitor",
683
+ chunk: "capacitor"
684
+ },
685
+ {
686
+ match: "gsap",
687
+ chunk: "gsap"
688
+ },
689
+ {
690
+ match: "howler",
691
+ chunk: "howler"
692
+ },
693
+ {
694
+ match: "firebase",
695
+ chunk: "firebase"
696
+ },
697
+ {
698
+ match: "overmind",
699
+ chunk: "overmind"
700
+ }
701
+ ];
702
+ /** De package-naam uit een module-id, via het laatste `node_modules/`-segment (werkt ook met pnpm). */
703
+ function packageNameFromId(id) {
704
+ const normalized = id.replaceAll("\\", "/");
705
+ const index = normalized.lastIndexOf("/node_modules/");
706
+ if (index === -1) return;
707
+ const [first, second] = normalized.slice(index + 14).split("/");
708
+ if (!first) return;
709
+ return first.startsWith("@") && second ? `${first}/${second}` : first;
710
+ }
711
+ function matchesPackage(name, match) {
712
+ if (match.startsWith("@")) return name.startsWith(`${match}/`);
713
+ if (name === match) return true;
714
+ const charAfter = name[match.length];
715
+ return name.startsWith(match) && (charAfter === "-" || charAfter === ".");
716
+ }
717
+ /** De vendor-chunk voor een module, of `undefined` voor eigen code en css. */
718
+ function vendorChunkName(id) {
719
+ if (isCSSRequest(id)) return;
720
+ const name = packageNameFromId(id);
721
+ if (name === void 0) return;
722
+ return VENDOR_CHUNKS.find((entry) => matchesPackage(name, entry.match))?.chunk ?? "vendor";
723
+ }
724
+ //#endregion
725
+ //#region src/config/build.ts
726
+ function defineBuildConfig(project) {
727
+ const input = {};
728
+ for (const pageId of Object.keys(project.games)) input[pageId] = path.join(project.sourceDirectory, `${pageId}.html`);
729
+ return {
730
+ reportCompressedSize: true,
731
+ chunkSizeWarningLimit: 2e3,
732
+ outDir: project.outputDirectory,
733
+ emptyOutDir: true,
734
+ assetsDir: "generated",
735
+ sourcemap: project.package.vite.build.sourceMaps,
736
+ rolldownOptions: {
737
+ input,
738
+ output: {
739
+ codeSplitting: { groups: [{
740
+ debugName: "vendor-chunks",
741
+ name: (id) => vendorChunkName(id) ?? null
742
+ }] },
743
+ comments: { legal: false }
744
+ }
745
+ }
746
+ };
747
+ }
748
+ //#endregion
749
+ //#region src/config/vite.ts
750
+ const DEDUPE = [
751
+ "@studiokloek/ts-core-lib",
752
+ "@capacitor/app",
753
+ "@capacitor/core",
754
+ "@capacitor/device",
755
+ "@capacitor/network",
756
+ "@capacitor/preferences",
757
+ "@capacitor/splash-screen",
758
+ "@capacitor/status-bar",
759
+ "bowser",
760
+ "date-fns-tz",
761
+ "date-fns",
762
+ "error-stack-parser-es",
763
+ "firebase",
764
+ "fontfaceobserver",
765
+ "gsap",
766
+ "howler",
767
+ "lodash-decorators",
768
+ "lodash",
769
+ "overmind",
770
+ "pixi-spine",
771
+ "pixi",
772
+ "pubsub-js",
773
+ "random-js",
774
+ "ress",
775
+ "slugify",
776
+ "superagent",
777
+ "ts-events",
778
+ "ts-mixer",
779
+ "tslib"
780
+ ];
781
+ /** De volledige Vite-config voor een kloek-project; standaard dat in de huidige map. */
782
+ async function defineKloekViteConfig(environment, project = loadProject()) {
783
+ const { vite } = project.package;
784
+ const config = {
785
+ mode: environment.mode,
786
+ logLevel: "info",
787
+ base: vite.build.basePath,
788
+ root: project.sourceDirectory,
789
+ envDir: path.join(project.cwd, "env"),
790
+ envPrefix: ["KLOEK_", "VITE_"],
791
+ publicDir: "static",
792
+ resolve: {
793
+ alias: {
794
+ "@meta": path.join(project.sourceDirectory, "static", "meta"),
795
+ "@fonts": path.join(project.sourceDirectory, "static", "fonts")
796
+ },
797
+ dedupe: [.../* @__PURE__ */ new Set([...DEDUPE, ...vite.dedupe])],
798
+ tsconfigPaths: true
799
+ },
800
+ css: { preprocessorOptions: { scss: { charset: false } } },
801
+ plugins: await basePlugins(project, environment)
802
+ };
803
+ if (environment.command === "serve") {
804
+ config.server = {
805
+ host: true,
806
+ cors: vite.dev.cors,
807
+ open: getPageToServe(vite.build.basePath, vite.dev.serveParams, project.games)
808
+ };
809
+ config.plugins = [...config.plugins ?? [], ...servePlugins(project)];
810
+ } else {
811
+ config.build = defineBuildConfig(project);
812
+ config.plugins = [...config.plugins ?? [], ...buildPlugins(project)];
813
+ }
814
+ return config;
815
+ }
816
+ //#endregion
817
+ //#region src/core/zip.ts
818
+ /** Pakt de build-output in als `zips/<naam>-v<versie>.zip`; geeft het pad van de zip terug. */
819
+ async function addBuildFolderToZip(project) {
820
+ const destinationDirectory = path.join(project.cwd, "zips");
821
+ const destinationFile = path.join(destinationDirectory, filenamify(`${project.package.name}-v${project.package.version}.zip`, { replacement: "-" }));
822
+ await mkdir(destinationDirectory, { recursive: true });
823
+ await rm(destinationFile, { force: true });
824
+ await zip(project.outputDirectory, destinationFile);
825
+ return destinationFile;
826
+ }
827
+ //#endregion
828
+ //#region src/commands.ts
829
+ async function kloekDevelopment(options = {}) {
830
+ const project = loadProject(options.cwd);
831
+ const config = await defineKloekViteConfig({
832
+ command: "serve",
833
+ mode: options.mode ?? "development"
834
+ }, project);
835
+ const server = await createServer({
836
+ configFile: false,
837
+ ...config
838
+ });
839
+ await server.listen();
840
+ server.printUrls();
841
+ server.bindCLIShortcuts({ print: true });
842
+ }
843
+ async function kloekBuild(options = {}) {
844
+ const project = loadProject(options.cwd);
845
+ const config = await defineKloekViteConfig({
846
+ command: "build",
847
+ mode: options.mode ?? "production"
848
+ }, project);
849
+ const buildConfig = { ...config.build };
850
+ if (options.emptyOutDir !== void 0) buildConfig.emptyOutDir = options.emptyOutDir;
851
+ if (options.sourcemaps !== void 0 || options.inlineSourcemaps) buildConfig.sourcemap = options.sourcemaps ?? true ? options.inlineSourcemaps ? "inline" : true : false;
852
+ if (options.base !== void 0) config.base = options.base;
853
+ if (options.logLevel !== void 0) config.logLevel = options.logLevel;
854
+ await build({
855
+ configFile: false,
856
+ ...config,
857
+ build: buildConfig
858
+ });
859
+ if (options.createZip ?? project.package.vite.build.createZip) await addBuildFolderToZip(project);
860
+ }
861
+ //#endregion
862
+ export { packageDirectory as a, defineBuildConfig as i, kloekDevelopment as n, loadProject as o, defineKloekViteConfig as r, parsePackageConfig as s, kloekBuild as t };