@joystyk/pwa 0.0.0 → 0.2.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +215 -2
  3. package/dist/asset-manifest.d.ts +76 -0
  4. package/dist/asset-manifest.js +80 -0
  5. package/dist/asset-manifest.js.map +1 -0
  6. package/dist/build/documents.d.ts +79 -0
  7. package/dist/build/documents.js +135 -0
  8. package/dist/build/documents.js.map +1 -0
  9. package/dist/build/graph.d.ts +39 -0
  10. package/dist/build/graph.js +70 -0
  11. package/dist/build/graph.js.map +1 -0
  12. package/dist/build/icons.d.ts +30 -0
  13. package/dist/build/icons.js +192 -0
  14. package/dist/build/icons.js.map +1 -0
  15. package/dist/build/index.d.ts +4 -0
  16. package/dist/build/index.js +5 -0
  17. package/dist/build/plugin.d.ts +32 -0
  18. package/dist/build/plugin.js +172 -0
  19. package/dist/build/plugin.js.map +1 -0
  20. package/dist/client/availability.d.ts +12 -0
  21. package/dist/client/availability.js +94 -0
  22. package/dist/client/availability.js.map +1 -0
  23. package/dist/client/control.d.ts +3 -0
  24. package/dist/client/control.js +19 -0
  25. package/dist/client/control.js.map +1 -0
  26. package/dist/client/elements.d.ts +45 -0
  27. package/dist/client/elements.js +319 -0
  28. package/dist/client/elements.js.map +1 -0
  29. package/dist/client/forced.d.ts +33 -0
  30. package/dist/client/forced.js +57 -0
  31. package/dist/client/forced.js.map +1 -0
  32. package/dist/client/index.d.ts +7 -0
  33. package/dist/client/index.js +8 -0
  34. package/dist/client/pinned.d.ts +13 -0
  35. package/dist/client/pinned.js +29 -0
  36. package/dist/client/pinned.js.map +1 -0
  37. package/dist/client/pins.d.ts +43 -0
  38. package/dist/client/pins.js +140 -0
  39. package/dist/client/pins.js.map +1 -0
  40. package/dist/client/register.d.ts +64 -0
  41. package/dist/client/register.js +255 -0
  42. package/dist/client/register.js.map +1 -0
  43. package/dist/control.d.ts +46 -0
  44. package/dist/control.js +88 -0
  45. package/dist/control.js.map +1 -0
  46. package/dist/index.d.ts +7 -0
  47. package/dist/index.js +8 -0
  48. package/dist/names.d.ts +47 -0
  49. package/dist/names.js +64 -0
  50. package/dist/names.js.map +1 -0
  51. package/dist/offline/offline.css +94 -0
  52. package/dist/offline/offline.d.ts +1 -0
  53. package/dist/offline/offline.js +26 -0
  54. package/dist/offline/offline.js.map +1 -0
  55. package/dist/offline-mark.d.ts +15 -0
  56. package/dist/offline-mark.js +51 -0
  57. package/dist/offline-mark.js.map +1 -0
  58. package/dist/pin-record.d.ts +14 -0
  59. package/dist/pin-record.js +36 -0
  60. package/dist/pin-record.js.map +1 -0
  61. package/dist/preloads.d.ts +2 -0
  62. package/dist/preloads.js +26 -0
  63. package/dist/preloads.js.map +1 -0
  64. package/dist/routing.d.ts +57 -0
  65. package/dist/routing.js +81 -0
  66. package/dist/routing.js.map +1 -0
  67. package/dist/sw/remove-sw.d.ts +1 -0
  68. package/dist/sw/remove-sw.js +16 -0
  69. package/dist/sw/remove-sw.js.map +1 -0
  70. package/dist/sw/sw.d.ts +1 -0
  71. package/dist/sw/sw.js +165 -0
  72. package/dist/sw/sw.js.map +1 -0
  73. package/dist/validate.d.ts +8 -0
  74. package/dist/validate.js +24 -0
  75. package/dist/validate.js.map +1 -0
  76. package/package.json +65 -11
@@ -0,0 +1 @@
1
+ {"version":3,"file":"documents.js","names":[],"sources":["../../src/build/documents.ts"],"sourcesContent":["// The build's own documents, as pure functions of the hashed file names: the web app\n// manifest, the offline page, a game's asset manifest and the precache list.\nimport { createHash } from 'node:crypto';\nimport { ASSET_MANIFEST_VERSION, type AssetFile, type GameAssetManifest } from '../asset-manifest.ts';\nimport { fileKind } from '../routing.ts';\nimport { BRAND_BACKGROUND } from './icons.ts';\n\n/** A short content hash for a file name or a precache revision (base64url, like Vite's). */\nexport const contentHash = (content: string | Uint8Array, length = 8): string =>\n createHash('sha256').update(content).digest('base64url').replace(/[-_]/g, 'x').slice(0, length);\n\nexport type IconFiles = {\n readonly svg: string;\n readonly png192: string;\n readonly png512: string;\n readonly maskable512: string;\n};\n\n/** manifest.webmanifest: what an installed Joystyk looks like. */\nexport const webManifest = (icons: IconFiles) => ({\n id: '/',\n name: 'Joystyk',\n short_name: 'Joystyk',\n description:\n 'Free games for your browser, for every age. Solo, against the computer, or pass the device to a friend.',\n lang: 'en',\n dir: 'ltr',\n start_url: '/',\n scope: '/',\n display: 'standalone',\n background_color: BRAND_BACKGROUND,\n theme_color: BRAND_BACKGROUND,\n categories: ['games', 'entertainment'],\n icons: [\n { src: icons.svg, sizes: 'any', type: 'image/svg+xml', purpose: 'any' },\n { src: icons.png192, sizes: '192x192', type: 'image/png', purpose: 'any' },\n { src: icons.png512, sizes: '512x512', type: 'image/png', purpose: 'any' },\n { src: icons.maskable512, sizes: '512x512', type: 'image/png', purpose: 'maskable' },\n ],\n});\n\nconst escapeAttr = (value: string): string =>\n value.replace(/&/g, '&amp;').replace(/\"/g, '&quot;').replace(/</g, '&lt;');\n\n/** The offline page. Static: its script fills in the list of games this device can play. */\nexport const offlinePage = (files: {\n readonly css: string;\n readonly js: string;\n readonly icon: string;\n}): string => `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>You're offline | Joystyk</title>\n<meta name=\"robots\" content=\"noindex\">\n<meta name=\"theme-color\" content=\"${BRAND_BACKGROUND}\">\n<link rel=\"icon\" href=\"${escapeAttr(files.icon)}\" type=\"image/svg+xml\">\n<link rel=\"stylesheet\" href=\"${escapeAttr(files.css)}\">\n<script defer src=\"${escapeAttr(files.js)}\"></script>\n</head>\n<body>\n<main>\n<p class=\"kicker\">Joystyk</p>\n<h1>You're offline</h1>\n<p id=\"where\" class=\"lede\">This page needs an internet connection.</p>\n<section aria-labelledby=\"ready-heading\">\n<h2 id=\"ready-heading\">Games you can play offline</h2>\n<ul id=\"games\" hidden></ul>\n<p id=\"none\">No games are saved on this device yet. Next time you're online, open a game's page and choose “Make available offline”.</p>\n</section>\n<p class=\"actions\"><button type=\"button\" id=\"retry\">Try again</button> <a href=\"/\">See all games</a></p>\n</main>\n</body>\n</html>\n`;\n\n/** A game as the site describes it to the build. */\nexport type BuildGame = {\n readonly id: string;\n readonly name: string;\n /** `manifest.versions.build`. */\n readonly version: string;\n readonly rulesVersion: number;\n readonly saveVersion: number;\n /** The folder holding the game's code (its entry's folder). */\n readonly root: string;\n /** The game manifest's `assets`: where this asset manifest is served. */\n readonly assets: string;\n /** The game's page, e.g. /games/nim. */\n readonly page: string;\n /** Catalog data the page loads on client-side navigation, e.g. /api/games/nim. */\n readonly data: readonly string[];\n};\n\n/** `games/<id>/manifest.json`: the game's files plus the shell's, sizes from `sizeOf`. */\nexport const gameAssetManifest = (\n game: BuildGame,\n release: string,\n shell: ReadonlySet<string>,\n own: ReadonlySet<string>,\n sizeOf: (file: string) => number,\n): GameAssetManifest => {\n const describe = (file: string, scope: 'shell' | 'game'): AssetFile => {\n const url = `/${file}`;\n const kind = fileKind(url) ?? 'code';\n return { url, size: sizeOf(file), kind, scope };\n };\n const files = [\n ...[...shell].sort().map((file) => describe(file, 'shell')),\n ...[...own].sort().map((file) => describe(file, 'game')),\n ];\n return {\n schemaVersion: ASSET_MANIFEST_VERSION,\n id: game.id,\n name: game.name,\n version: game.version,\n rulesVersion: game.rulesVersion,\n saveVersion: game.saveVersion,\n release,\n page: game.page,\n documents: [game.page, ...game.data],\n files,\n totalBytes: files.reduce((sum, file) => sum + file.size, 0),\n };\n};\n\n/** One Workbox precache entry. `revision: null` for a URL whose name already holds a hash. */\nexport type PrecacheEntry = { readonly url: string; readonly revision: string | null };\n\n/** Where a precache entry's bytes come from at build time, so each can be checked. */\nexport type PrecacheSource =\n | { readonly url: string; readonly hashed: true; readonly file: string }\n | { readonly url: string; readonly hashed: false; readonly body: Uint8Array | string };\n\n/**\n * The precache list. A hashed file must exist on disk (`exists`); an unhashed entry gets a\n * revision from its rendered bytes. Throws listing every missing entry, because one bad entry\n * fails the worker's install for every visitor.\n */\nexport const precacheEntries = (\n sources: readonly PrecacheSource[],\n exists: (file: string) => boolean,\n): PrecacheEntry[] => {\n const missing = sources.filter((source) => (source.hashed ? !exists(source.file) : source.body.length === 0));\n if (missing.length > 0) {\n throw new Error(\n `[pwa] Precache entries missing from the build: ${missing.map((s) => s.url).join(', ')}. ` +\n 'Each one would fail the service worker install for every visitor; fix the build before deploying.',\n );\n }\n const seen = new Set<string>();\n const entries: PrecacheEntry[] = [];\n for (const source of sources) {\n if (seen.has(source.url)) continue;\n seen.add(source.url);\n entries.push({ url: source.url, revision: source.hashed ? null : contentHash(source.body, 16) });\n }\n return entries.sort((a, b) => a.url.localeCompare(b.url));\n};\n\nexport const INJECTION_POINT = 'self.__WB_MANIFEST';\n\n/** Puts the precache list into the bundled worker, or throws when the injection point is gone. */\nexport const injectManifest = (code: string, entries: readonly PrecacheEntry[]): string => {\n const at = code.indexOf(INJECTION_POINT);\n if (at === -1 || code.indexOf(INJECTION_POINT, at + 1) !== -1) {\n throw new Error(\n `[pwa] Expected \"${INJECTION_POINT}\" exactly once in the bundled service worker; sw.ts must call precache(${INJECTION_POINT}).`,\n );\n }\n return code.replace(INJECTION_POINT, JSON.stringify(entries));\n};\n"],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /** One chunk or asset in Vite's build manifest. */
2
+ export type ViteManifestChunk = {
3
+ readonly file: string;
4
+ readonly name?: string;
5
+ readonly src?: string;
6
+ readonly isEntry?: boolean;
7
+ readonly isDynamicEntry?: boolean;
8
+ readonly imports?: readonly string[];
9
+ readonly dynamicImports?: readonly string[];
10
+ readonly css?: readonly string[];
11
+ readonly assets?: readonly string[];
12
+ };
13
+ export type ViteManifest = Readonly<Record<string, ViteManifestChunk>>;
14
+ /** A game as the build sees it. */
15
+ export type GameSource = {
16
+ readonly id: string;
17
+ /** The folder holding the game's code; a dynamic entry inside it belongs to the game. */
18
+ readonly root: string;
19
+ };
20
+ /** The site's entry key: the one chunk marked `isEntry`. */
21
+ export declare const entryKey: (manifest: ViteManifest) => string;
22
+ /** Original module IDs by emitted file; merged dynamic entries may have no manifest `src`. */
23
+ export type ChunkSources = ReadonlyMap<string, readonly string[]>;
24
+ /** Each game's dynamic entries (manifest keys), by game id. `viteRoot` resolves `src`. */
25
+ export declare const gameEntries: (manifest: ViteManifest, games: readonly GameSource[], viteRoot: string, sources?: ChunkSources) => Map<string, string[]>;
26
+ /** Every file (chunks, their CSS and assets) reachable from `start`, as paths relative to outDir. */
27
+ export declare const reachable: (manifest: ViteManifest, start: readonly string[], blocked: ReadonlySet<string>) => Set<string>;
28
+ export type FileSets = {
29
+ /** What every page needs: the entry, hydration, the launcher's lazy pieces. */
30
+ readonly shell: ReadonlySet<string>;
31
+ /** Per game: the files only that game's code reaches (the shell's are listed there). */
32
+ readonly games: ReadonlyMap<string, ReadonlySet<string>>;
33
+ };
34
+ /**
35
+ * Splits the build into the shell and each game. `unlisted` games' code is kept out of the
36
+ * shell but gets no set. Throws when a listed game has no dynamic entry in the build: a game
37
+ * whose code can't be found can't be pinned completely.
38
+ */
39
+ export declare const fileSets: (manifest: ViteManifest, games: readonly GameSource[], viteRoot: string, unlisted?: readonly GameSource[], sources?: ChunkSources) => FileSets;
@@ -0,0 +1,70 @@
1
+ import { isAbsolute, relative, resolve } from "node:path";
2
+ //#region src/build/graph.ts
3
+ var inside = (root, file) => {
4
+ const rel = relative(root, file);
5
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
6
+ };
7
+ /** The site's entry key: the one chunk marked `isEntry`. */
8
+ var entryKey = (manifest) => {
9
+ const entries = Object.entries(manifest).filter(([, chunk]) => chunk.isEntry === true);
10
+ if (entries.length !== 1) throw new Error(`[pwa] Expected one entry in the Vite manifest, found ${String(entries.length)}: ${entries.map(([k]) => k).join(", ")}.`);
11
+ return entries[0]?.[0] ?? "";
12
+ };
13
+ /** Each game's dynamic entries (manifest keys), by game id. `viteRoot` resolves `src`. */
14
+ var gameEntries = (manifest, games, viteRoot, sources = /* @__PURE__ */ new Map()) => {
15
+ const owned = new Map(games.map((game) => [game.id, []]));
16
+ const dynamicTargets = new Set(Object.values(manifest).flatMap((chunk) => chunk.dynamicImports ?? []));
17
+ for (const [key, chunk] of Object.entries(manifest)) {
18
+ if (chunk.isDynamicEntry !== true && !dynamicTargets.has(key)) continue;
19
+ const files = [...chunk.src === void 0 ? [] : [resolve(viteRoot, chunk.src)], ...sources.get(chunk.file) ?? []];
20
+ for (const game of games) if (files.some((file) => inside(resolve(game.root), file))) owned.get(game.id)?.push(key);
21
+ }
22
+ return owned;
23
+ };
24
+ /** Every file (chunks, their CSS and assets) reachable from `start`, as paths relative to outDir. */
25
+ var reachable = (manifest, start, blocked) => {
26
+ const files = /* @__PURE__ */ new Set();
27
+ const seen = /* @__PURE__ */ new Set();
28
+ const queue = [...start];
29
+ while (queue.length > 0) {
30
+ const key = queue.shift() ?? "";
31
+ if (seen.has(key)) continue;
32
+ seen.add(key);
33
+ const chunk = manifest[key];
34
+ if (chunk === void 0) throw new Error(`[pwa] The Vite manifest refers to "${key}" but has no entry for it.`);
35
+ files.add(chunk.file);
36
+ for (const file of [...chunk.css ?? [], ...chunk.assets ?? []]) files.add(file);
37
+ queue.push(...chunk.imports ?? []);
38
+ queue.push(...(chunk.dynamicImports ?? []).filter((next) => !blocked.has(next) || start.includes(next)));
39
+ }
40
+ return files;
41
+ };
42
+ /**
43
+ * Splits the build into the shell and each game. `unlisted` games' code is kept out of the
44
+ * shell but gets no set. Throws when a listed game has no dynamic entry in the build: a game
45
+ * whose code can't be found can't be pinned completely.
46
+ */
47
+ var fileSets = (manifest, games, viteRoot, unlisted = [], sources = /* @__PURE__ */ new Map()) => {
48
+ const owned = gameEntries(manifest, games, viteRoot, sources);
49
+ const hidden = gameEntries(manifest, unlisted, viteRoot, sources);
50
+ const allGameEntries = new Set([...owned.values(), ...hidden.values()].flat());
51
+ const shell = reachable(manifest, [entryKey(manifest)], allGameEntries);
52
+ const chunkFiles = new Set(Object.values(manifest).map((chunk) => chunk.file));
53
+ const workers = [...shell].filter((file) => /\.m?js$/.test(file) && !chunkFiles.has(file));
54
+ if (workers.length > 0) throw new Error(`[pwa] The shell reaches worker scripts: ${workers.join(", ")}. Every visitor would precache them and every pin would copy them. Name a game's worker from code the game loads lazily (a dynamic import() under the game's folder), as apps/site/src/games.ts does for job workers.`);
55
+ const perGame = /* @__PURE__ */ new Map();
56
+ for (const game of games) {
57
+ const starts = owned.get(game.id) ?? [];
58
+ if (starts.length === 0) throw new Error(`[pwa] Game "${game.id}" has no lazily loaded code under ${game.root}. Its entry should load its module with a dynamic import().`);
59
+ const own = reachable(manifest, starts, allGameEntries);
60
+ perGame.set(game.id, new Set([...own].filter((file) => !shell.has(file))));
61
+ }
62
+ return {
63
+ shell,
64
+ games: perGame
65
+ };
66
+ };
67
+ //#endregion
68
+ export { entryKey, fileSets, gameEntries, reachable };
69
+
70
+ //# sourceMappingURL=graph.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph.js","names":[],"sources":["../../src/build/graph.ts"],"sourcesContent":["// Which built files the shell needs and which each game needs, read from Vite's build\n// manifest (`.vite/manifest.json`). Pure: the plugin passes the manifest in, and tests pass\n// small hand-written ones.\n//\n// The shell is everything the site's entry reaches, except games: a game's code sits behind a\n// dynamic import whose source lies in the game's folder. A game needs the shell plus\n// everything its own dynamic entries reach. Static imports are always followed, even into\n// another game's chunk, because the bundler may have put shared code there; dynamic imports\n// of another game are not.\n//\n// A file the bundler emits for a module, such as a worker (`new Worker(new URL(…))`), is an\n// asset of the chunk holding that module, so it goes wherever that chunk goes. A game's worker\n// factory imported statically by the site would make the worker part of the shell: precached\n// for every visitor and copied into every game's pin. The build refuses that.\nimport { isAbsolute, relative, resolve } from 'node:path';\n\n/** One chunk or asset in Vite's build manifest. */\nexport type ViteManifestChunk = {\n readonly file: string;\n readonly name?: string;\n readonly src?: string;\n readonly isEntry?: boolean;\n readonly isDynamicEntry?: boolean;\n readonly imports?: readonly string[];\n readonly dynamicImports?: readonly string[];\n readonly css?: readonly string[];\n readonly assets?: readonly string[];\n};\n\nexport type ViteManifest = Readonly<Record<string, ViteManifestChunk>>;\n\n/** A game as the build sees it. */\nexport type GameSource = {\n readonly id: string;\n /** The folder holding the game's code; a dynamic entry inside it belongs to the game. */\n readonly root: string;\n};\n\nconst inside = (root: string, file: string): boolean => {\n const rel = relative(root, file);\n return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);\n};\n\n/** The site's entry key: the one chunk marked `isEntry`. */\nexport const entryKey = (manifest: ViteManifest): string => {\n const entries = Object.entries(manifest).filter(([, chunk]) => chunk.isEntry === true);\n if (entries.length !== 1)\n throw new Error(\n `[pwa] Expected one entry in the Vite manifest, found ${String(entries.length)}: ${entries.map(([k]) => k).join(', ')}.`,\n );\n return entries[0]?.[0] ?? '';\n};\n\n/** Original module IDs by emitted file; merged dynamic entries may have no manifest `src`. */\nexport type ChunkSources = ReadonlyMap<string, readonly string[]>;\n\n/** Each game's dynamic entries (manifest keys), by game id. `viteRoot` resolves `src`. */\nexport const gameEntries = (\n manifest: ViteManifest,\n games: readonly GameSource[],\n viteRoot: string,\n sources: ChunkSources = new Map(),\n): Map<string, string[]> => {\n const owned = new Map<string, string[]>(games.map((game) => [game.id, []]));\n const dynamicTargets = new Set(Object.values(manifest).flatMap((chunk) => chunk.dynamicImports ?? []));\n for (const [key, chunk] of Object.entries(manifest)) {\n if (chunk.isDynamicEntry !== true && !dynamicTargets.has(key)) continue;\n const files = [\n ...(chunk.src === undefined ? [] : [resolve(viteRoot, chunk.src)]),\n ...(sources.get(chunk.file) ?? []),\n ];\n for (const game of games) if (files.some((file) => inside(resolve(game.root), file))) owned.get(game.id)?.push(key);\n }\n return owned;\n};\n\n/** Every file (chunks, their CSS and assets) reachable from `start`, as paths relative to outDir. */\nexport const reachable = (\n manifest: ViteManifest,\n start: readonly string[],\n blocked: ReadonlySet<string>,\n): Set<string> => {\n const files = new Set<string>();\n const seen = new Set<string>();\n const queue = [...start];\n while (queue.length > 0) {\n const key = queue.shift() ?? '';\n if (seen.has(key)) continue;\n seen.add(key);\n const chunk = manifest[key];\n if (chunk === undefined) throw new Error(`[pwa] The Vite manifest refers to \"${key}\" but has no entry for it.`);\n files.add(chunk.file);\n for (const file of [...(chunk.css ?? []), ...(chunk.assets ?? [])]) files.add(file);\n queue.push(...(chunk.imports ?? []));\n queue.push(...(chunk.dynamicImports ?? []).filter((next) => !blocked.has(next) || start.includes(next)));\n }\n return files;\n};\n\nexport type FileSets = {\n /** What every page needs: the entry, hydration, the launcher's lazy pieces. */\n readonly shell: ReadonlySet<string>;\n /** Per game: the files only that game's code reaches (the shell's are listed there). */\n readonly games: ReadonlyMap<string, ReadonlySet<string>>;\n};\n\n/**\n * Splits the build into the shell and each game. `unlisted` games' code is kept out of the\n * shell but gets no set. Throws when a listed game has no dynamic entry in the build: a game\n * whose code can't be found can't be pinned completely.\n */\nexport const fileSets = (\n manifest: ViteManifest,\n games: readonly GameSource[],\n viteRoot: string,\n unlisted: readonly GameSource[] = [],\n sources: ChunkSources = new Map(),\n): FileSets => {\n const owned = gameEntries(manifest, games, viteRoot, sources);\n const hidden = gameEntries(manifest, unlisted, viteRoot, sources);\n const allGameEntries = new Set([...owned.values(), ...hidden.values()].flat());\n const shell = reachable(manifest, [entryKey(manifest)], allGameEntries);\n const chunkFiles = new Set(Object.values(manifest).map((chunk) => chunk.file));\n const workers = [...shell].filter((file) => /\\.m?js$/.test(file) && !chunkFiles.has(file));\n if (workers.length > 0) {\n throw new Error(\n `[pwa] The shell reaches worker scripts: ${workers.join(', ')}. Every visitor would precache them and every pin would copy them. ` +\n \"Name a game's worker from code the game loads lazily (a dynamic import() under the game's folder), as apps/site/src/games.ts does for job workers.\",\n );\n }\n const perGame = new Map<string, Set<string>>();\n for (const game of games) {\n const starts = owned.get(game.id) ?? [];\n if (starts.length === 0) {\n throw new Error(\n `[pwa] Game \"${game.id}\" has no lazily loaded code under ${game.root}. Its entry should load its module with a dynamic import().`,\n );\n }\n const own = reachable(manifest, starts, allGameEntries);\n perGame.set(game.id, new Set([...own].filter((file) => !shell.has(file))));\n }\n return { shell, games: perGame };\n};\n"],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ type Shape = {
2
+ readonly kind: 'rect';
3
+ readonly x: number;
4
+ readonly y: number;
5
+ readonly w: number;
6
+ readonly h: number;
7
+ readonly r: number;
8
+ readonly fill: string;
9
+ } | {
10
+ readonly kind: 'circle';
11
+ readonly cx: number;
12
+ readonly cy: number;
13
+ readonly r: number;
14
+ readonly fill: string;
15
+ };
16
+ /** The brand tile color, also the web manifest's theme and background color. */
17
+ export declare const BRAND_BACKGROUND = "#1b1530";
18
+ /**
19
+ * The icon's shapes. `maskable` fills the whole square and shrinks the joystick into the
20
+ * central safe zone (a circle of 40% of the size), as maskable icons require.
21
+ */
22
+ export declare const iconShapes: (maskable: boolean) => Shape[];
23
+ export declare const iconSvg: (maskable?: boolean) => string;
24
+ /** Rasterizes the shapes to straight (non-premultiplied) RGBA, `size` pixels square. */
25
+ export declare const rasterize: (shapes: readonly Shape[], size: number, samples?: number) => Uint8Array;
26
+ export declare const crc32: (bytes: Uint8Array) => number;
27
+ /** Encodes straight RGBA pixels as an 8-bit RGBA PNG. */
28
+ export declare const encodePng: (rgba: Uint8Array, width: number, height: number) => Buffer;
29
+ export declare const iconPng: (size: number, maskable?: boolean) => Buffer;
30
+ export {};
@@ -0,0 +1,192 @@
1
+ import { deflateSync } from "node:zlib";
2
+ //#region src/build/icons.ts
3
+ /** The brand tile color, also the web manifest's theme and background color. */
4
+ var BRAND_BACKGROUND = "#1b1530";
5
+ /** The joystick, in a 32-unit square. */
6
+ var JOYSTICK = [
7
+ {
8
+ kind: "rect",
9
+ x: 5,
10
+ y: 21,
11
+ w: 22,
12
+ h: 6,
13
+ r: 1.5,
14
+ fill: "#e9e4ff"
15
+ },
16
+ {
17
+ kind: "rect",
18
+ x: 14.5,
19
+ y: 9,
20
+ w: 3,
21
+ h: 13,
22
+ r: 0,
23
+ fill: "#b3a9d9"
24
+ },
25
+ {
26
+ kind: "circle",
27
+ cx: 16,
28
+ cy: 9,
29
+ r: 5,
30
+ fill: "#ff4fa8"
31
+ },
32
+ {
33
+ kind: "circle",
34
+ cx: 14.3,
35
+ cy: 7.3,
36
+ r: 1.5,
37
+ fill: "#ffc2e0"
38
+ },
39
+ {
40
+ kind: "circle",
41
+ cx: 23,
42
+ cy: 24,
43
+ r: 1.6,
44
+ fill: "#4fd8ff"
45
+ }
46
+ ];
47
+ /**
48
+ * The icon's shapes. `maskable` fills the whole square and shrinks the joystick into the
49
+ * central safe zone (a circle of 40% of the size), as maskable icons require.
50
+ */
51
+ var iconShapes = (maskable) => {
52
+ const tile = {
53
+ kind: "rect",
54
+ x: 0,
55
+ y: 0,
56
+ w: 32,
57
+ h: 32,
58
+ r: maskable ? 0 : 7,
59
+ fill: BRAND_BACKGROUND
60
+ };
61
+ if (!maskable) return [tile, ...JOYSTICK];
62
+ const k = .72;
63
+ const at = (v) => 16 + (v - 16) * k;
64
+ return [tile, ...JOYSTICK.map((s) => s.kind === "rect" ? {
65
+ ...s,
66
+ x: at(s.x),
67
+ y: at(s.y),
68
+ w: s.w * k,
69
+ h: s.h * k,
70
+ r: s.r * k
71
+ } : {
72
+ ...s,
73
+ cx: at(s.cx),
74
+ cy: at(s.cy),
75
+ r: s.r * k
76
+ })];
77
+ };
78
+ var num = (n) => String(Math.round(n * 1e3) / 1e3);
79
+ var iconSvg = (maskable = false) => {
80
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">${iconShapes(maskable).map((s) => s.kind === "rect" ? `<rect x="${num(s.x)}" y="${num(s.y)}" width="${num(s.w)}" height="${num(s.h)}"${s.r > 0 ? ` rx="${num(s.r)}"` : ""} fill="${s.fill}"/>` : `<circle cx="${num(s.cx)}" cy="${num(s.cy)}" r="${num(s.r)}" fill="${s.fill}"/>`).join("")}</svg>\n`;
81
+ };
82
+ var parseHex = (hex) => {
83
+ const v = Number.parseInt(hex.slice(1), 16);
84
+ return [
85
+ v >> 16 & 255,
86
+ v >> 8 & 255,
87
+ v & 255,
88
+ 255
89
+ ];
90
+ };
91
+ var covers = (s, x, y) => {
92
+ if (s.kind === "circle") return (x - s.cx) ** 2 + (y - s.cy) ** 2 <= s.r * s.r;
93
+ if (x < s.x || x > s.x + s.w || y < s.y || y > s.y + s.h) return false;
94
+ if (s.r === 0) return true;
95
+ const dx = Math.max(s.x + s.r - x, 0, x - (s.x + s.w - s.r));
96
+ const dy = Math.max(s.y + s.r - y, 0, y - (s.y + s.h - s.r));
97
+ return dx * dx + dy * dy <= s.r * s.r;
98
+ };
99
+ /** Rasterizes the shapes to straight (non-premultiplied) RGBA, `size` pixels square. */
100
+ var rasterize = (shapes, size, samples = 4) => {
101
+ const colors = shapes.map((s) => parseHex(s.fill));
102
+ const out = new Uint8Array(size * size * 4);
103
+ const scale = 32 / size;
104
+ const n = samples * samples;
105
+ for (let py = 0; py < size; py += 1) for (let px = 0; px < size; px += 1) {
106
+ let r = 0;
107
+ let g = 0;
108
+ let b = 0;
109
+ let a = 0;
110
+ for (let sy = 0; sy < samples; sy += 1) for (let sx = 0; sx < samples; sx += 1) {
111
+ const x = (px + (sx + .5) / samples) * scale;
112
+ const y = (py + (sy + .5) / samples) * scale;
113
+ for (let i = shapes.length - 1; i >= 0; i -= 1) {
114
+ const shape = shapes[i];
115
+ const color = colors[i];
116
+ if (shape !== void 0 && color !== void 0 && covers(shape, x, y)) {
117
+ r += color[0];
118
+ g += color[1];
119
+ b += color[2];
120
+ a += 255;
121
+ break;
122
+ }
123
+ }
124
+ }
125
+ const o = (py * size + px) * 4;
126
+ const covered = a / 255;
127
+ out[o] = covered === 0 ? 0 : Math.round(r / covered);
128
+ out[o + 1] = covered === 0 ? 0 : Math.round(g / covered);
129
+ out[o + 2] = covered === 0 ? 0 : Math.round(b / covered);
130
+ out[o + 3] = Math.round(a / n);
131
+ }
132
+ return out;
133
+ };
134
+ var CRC_TABLE = (() => {
135
+ const table = /* @__PURE__ */ new Uint32Array(256);
136
+ for (let n = 0; n < 256; n += 1) {
137
+ let c = n;
138
+ for (let k = 0; k < 8; k += 1) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
139
+ table[n] = c >>> 0;
140
+ }
141
+ return table;
142
+ })();
143
+ var crc32 = (bytes) => {
144
+ let c = 4294967295;
145
+ for (const byte of bytes) c = (CRC_TABLE[(c ^ byte) & 255] ?? 0) ^ c >>> 8;
146
+ return (c ^ 4294967295) >>> 0;
147
+ };
148
+ var chunk = (type, data) => {
149
+ const head = Buffer.alloc(8);
150
+ head.writeUInt32BE(data.length, 0);
151
+ head.write(type, 4, "ascii");
152
+ const crc = Buffer.alloc(4);
153
+ crc.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), data])), 0);
154
+ return Buffer.concat([
155
+ head,
156
+ data,
157
+ crc
158
+ ]);
159
+ };
160
+ /** Encodes straight RGBA pixels as an 8-bit RGBA PNG. */
161
+ var encodePng = (rgba, width, height) => {
162
+ const header = Buffer.alloc(13);
163
+ header.writeUInt32BE(width, 0);
164
+ header.writeUInt32BE(height, 4);
165
+ header[8] = 8;
166
+ header[9] = 6;
167
+ const rows = Buffer.alloc((width * 4 + 1) * height);
168
+ for (let y = 0; y < height; y += 1) {
169
+ rows[y * (width * 4 + 1)] = 0;
170
+ rows.set(rgba.subarray(y * width * 4, (y + 1) * width * 4), y * (width * 4 + 1) + 1);
171
+ }
172
+ return Buffer.concat([
173
+ Buffer.from([
174
+ 137,
175
+ 80,
176
+ 78,
177
+ 71,
178
+ 13,
179
+ 10,
180
+ 26,
181
+ 10
182
+ ]),
183
+ chunk("IHDR", header),
184
+ chunk("IDAT", deflateSync(rows, { level: 9 })),
185
+ chunk("IEND", /* @__PURE__ */ new Uint8Array(0))
186
+ ]);
187
+ };
188
+ var iconPng = (size, maskable = false) => encodePng(rasterize(iconShapes(maskable), size), size, size);
189
+ //#endregion
190
+ export { BRAND_BACKGROUND, crc32, encodePng, iconPng, iconShapes, iconSvg, rasterize };
191
+
192
+ //# sourceMappingURL=icons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icons.js","names":[],"sources":["../../src/build/icons.ts"],"sourcesContent":["// Joystyk's app icons, drawn at build time from one shape list: an arcade joystick (base,\n// stick, ball top and a fire button) on the brand's night-purple tile, the same art as the\n// masthead logo and the favicon. The SVG is the shape list written out; the PNGs are the same\n// shapes rasterized here with 4×4 supersampling and written with a minimal PNG encoder, so the\n// build needs no image library.\nimport { deflateSync } from 'node:zlib';\n\ntype Rgba = readonly [number, number, number, number];\n\ntype Shape =\n | {\n readonly kind: 'rect';\n readonly x: number;\n readonly y: number;\n readonly w: number;\n readonly h: number;\n readonly r: number;\n readonly fill: string;\n }\n | { readonly kind: 'circle'; readonly cx: number; readonly cy: number; readonly r: number; readonly fill: string };\n\n/** The brand tile color, also the web manifest's theme and background color. */\nexport const BRAND_BACKGROUND = '#1b1530';\n\n/** The joystick, in a 32-unit square. */\nconst JOYSTICK: readonly Shape[] = [\n { kind: 'rect', x: 5, y: 21, w: 22, h: 6, r: 1.5, fill: '#e9e4ff' },\n { kind: 'rect', x: 14.5, y: 9, w: 3, h: 13, r: 0, fill: '#b3a9d9' },\n { kind: 'circle', cx: 16, cy: 9, r: 5, fill: '#ff4fa8' },\n { kind: 'circle', cx: 14.3, cy: 7.3, r: 1.5, fill: '#ffc2e0' },\n { kind: 'circle', cx: 23, cy: 24, r: 1.6, fill: '#4fd8ff' },\n];\n\n/**\n * The icon's shapes. `maskable` fills the whole square and shrinks the joystick into the\n * central safe zone (a circle of 40% of the size), as maskable icons require.\n */\nexport const iconShapes = (maskable: boolean): Shape[] => {\n const tile: Shape = { kind: 'rect', x: 0, y: 0, w: 32, h: 32, r: maskable ? 0 : 7, fill: BRAND_BACKGROUND };\n if (!maskable) return [tile, ...JOYSTICK];\n const k = 0.72;\n const at = (v: number): number => 16 + (v - 16) * k;\n return [\n tile,\n ...JOYSTICK.map((s): Shape =>\n s.kind === 'rect'\n ? { ...s, x: at(s.x), y: at(s.y), w: s.w * k, h: s.h * k, r: s.r * k }\n : { ...s, cx: at(s.cx), cy: at(s.cy), r: s.r * k },\n ),\n ];\n};\n\nconst num = (n: number): string => String(Math.round(n * 1000) / 1000);\n\nexport const iconSvg = (maskable = false): string => {\n const body = iconShapes(maskable)\n .map((s) =>\n s.kind === 'rect'\n ? `<rect x=\"${num(s.x)}\" y=\"${num(s.y)}\" width=\"${num(s.w)}\" height=\"${num(s.h)}\"${s.r > 0 ? ` rx=\"${num(s.r)}\"` : ''} fill=\"${s.fill}\"/>`\n : `<circle cx=\"${num(s.cx)}\" cy=\"${num(s.cy)}\" r=\"${num(s.r)}\" fill=\"${s.fill}\"/>`,\n )\n .join('');\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\">${body}</svg>\\n`;\n};\n\nconst parseHex = (hex: string): Rgba => {\n const v = Number.parseInt(hex.slice(1), 16);\n return [(v >> 16) & 255, (v >> 8) & 255, v & 255, 255];\n};\n\nconst covers = (s: Shape, x: number, y: number): boolean => {\n if (s.kind === 'circle') return (x - s.cx) ** 2 + (y - s.cy) ** 2 <= s.r * s.r;\n if (x < s.x || x > s.x + s.w || y < s.y || y > s.y + s.h) return false;\n if (s.r === 0) return true;\n const dx = Math.max(s.x + s.r - x, 0, x - (s.x + s.w - s.r));\n const dy = Math.max(s.y + s.r - y, 0, y - (s.y + s.h - s.r));\n return dx * dx + dy * dy <= s.r * s.r;\n};\n\n/** Rasterizes the shapes to straight (non-premultiplied) RGBA, `size` pixels square. */\nexport const rasterize = (shapes: readonly Shape[], size: number, samples = 4): Uint8Array => {\n const colors = shapes.map((s) => parseHex(s.fill));\n const out = new Uint8Array(size * size * 4);\n const scale = 32 / size;\n const n = samples * samples;\n for (let py = 0; py < size; py += 1) {\n for (let px = 0; px < size; px += 1) {\n let r = 0;\n let g = 0;\n let b = 0;\n let a = 0;\n for (let sy = 0; sy < samples; sy += 1) {\n for (let sx = 0; sx < samples; sx += 1) {\n const x = (px + (sx + 0.5) / samples) * scale;\n const y = (py + (sy + 0.5) / samples) * scale;\n for (let i = shapes.length - 1; i >= 0; i -= 1) {\n const shape = shapes[i];\n const color = colors[i];\n if (shape !== undefined && color !== undefined && covers(shape, x, y)) {\n r += color[0];\n g += color[1];\n b += color[2];\n a += 255;\n break;\n }\n }\n }\n }\n const o = (py * size + px) * 4;\n const covered = a / 255;\n out[o] = covered === 0 ? 0 : Math.round(r / covered);\n out[o + 1] = covered === 0 ? 0 : Math.round(g / covered);\n out[o + 2] = covered === 0 ? 0 : Math.round(b / covered);\n out[o + 3] = Math.round(a / n);\n }\n }\n return out;\n};\n\nconst CRC_TABLE = (() => {\n const table = new Uint32Array(256);\n for (let n = 0; n < 256; n += 1) {\n let c = n;\n for (let k = 0; k < 8; k += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;\n table[n] = c >>> 0;\n }\n return table;\n})();\n\nexport const crc32 = (bytes: Uint8Array): number => {\n let c = 0xffffffff;\n for (const byte of bytes) c = (CRC_TABLE[(c ^ byte) & 255] ?? 0) ^ (c >>> 8);\n return (c ^ 0xffffffff) >>> 0;\n};\n\nconst chunk = (type: string, data: Uint8Array): Buffer => {\n const head = Buffer.alloc(8);\n head.writeUInt32BE(data.length, 0);\n head.write(type, 4, 'ascii');\n const crc = Buffer.alloc(4);\n crc.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), data])), 0);\n return Buffer.concat([head, data, crc]);\n};\n\n/** Encodes straight RGBA pixels as an 8-bit RGBA PNG. */\nexport const encodePng = (rgba: Uint8Array, width: number, height: number): Buffer => {\n const header = Buffer.alloc(13);\n header.writeUInt32BE(width, 0);\n header.writeUInt32BE(height, 4);\n header[8] = 8; // bit depth\n header[9] = 6; // RGBA\n const rows = Buffer.alloc((width * 4 + 1) * height);\n for (let y = 0; y < height; y += 1) {\n rows[y * (width * 4 + 1)] = 0; // filter: none\n rows.set(rgba.subarray(y * width * 4, (y + 1) * width * 4), y * (width * 4 + 1) + 1);\n }\n return Buffer.concat([\n Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),\n chunk('IHDR', header),\n chunk('IDAT', deflateSync(rows, { level: 9 })),\n chunk('IEND', new Uint8Array(0)),\n ]);\n};\n\nexport const iconPng = (size: number, maskable = false): Buffer =>\n encodePng(rasterize(iconShapes(maskable), size), size, size);\n"],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export { contentHash, gameAssetManifest, injectManifest, INJECTION_POINT, offlinePage, precacheEntries, webManifest, type BuildGame, type IconFiles, type PrecacheEntry, type PrecacheSource, } from './documents.js';
2
+ export { entryKey, fileSets, gameEntries, reachable, type FileSets, type GameSource, type ViteManifest, type ViteManifestChunk, } from './graph.js';
3
+ export { BRAND_BACKGROUND, crc32, encodePng, iconPng, iconShapes, iconSvg, rasterize } from './icons.js';
4
+ export { joystykPwa, type PwaPluginOptions, type SiteBuildInput } from './plugin.js';
@@ -0,0 +1,5 @@
1
+ import { BRAND_BACKGROUND, crc32, encodePng, iconPng, iconShapes, iconSvg, rasterize } from "./icons.js";
2
+ import { INJECTION_POINT, contentHash, gameAssetManifest, injectManifest, offlinePage, precacheEntries, webManifest } from "./documents.js";
3
+ import { entryKey, fileSets, gameEntries, reachable } from "./graph.js";
4
+ import { joystykPwa } from "./plugin.js";
5
+ export { BRAND_BACKGROUND, INJECTION_POINT, contentHash, crc32, encodePng, entryKey, fileSets, gameAssetManifest, gameEntries, iconPng, iconShapes, iconSvg, injectManifest, joystykPwa, offlinePage, precacheEntries, rasterize, reachable, webManifest };
@@ -0,0 +1,32 @@
1
+ import { type Plugin } from 'vite';
2
+ import { type BuildGame } from './documents.js';
3
+ import { type GameSource } from './graph.js';
4
+ /** What the site supplies at build time, once the client build is on disk. */
5
+ export type SiteBuildInput = {
6
+ /** The games this build lists: each gets an asset manifest. */
7
+ readonly games: readonly BuildGame[];
8
+ /**
9
+ * Games whose code is in the bundle but which this build doesn't list, such as test games
10
+ * outside test builds. Their code is kept out of the shell and gets no asset manifest.
11
+ */
12
+ readonly unlisted?: readonly GameSource[];
13
+ /** Answers a request exactly as the production server will; used to hash the shell pages. */
14
+ readonly render: (path: string) => Promise<Response>;
15
+ /** Server-rendered paths to precache, such as '/' and '/favicon.svg'. */
16
+ readonly shellPages: readonly string[];
17
+ };
18
+ export type PwaPluginOptions = {
19
+ /** The release ID baked into the worker (git SHA + date). */
20
+ readonly release: string;
21
+ /**
22
+ * When the release's code was committed, as an RFC 3339 date-time. Releases are ordered by
23
+ * it: the flag file's minimum release is one (control.ts).
24
+ */
25
+ readonly releaseTime: string;
26
+ /** Loads the site's half, given the output directory. Called after the client is written. */
27
+ readonly site: (context: {
28
+ readonly outDir: string;
29
+ }) => Promise<SiteBuildInput>;
30
+ };
31
+ /** The plugin. Add it to the site's client build only. */
32
+ export declare const joystykPwa: (options: PwaPluginOptions) => Plugin;
@@ -0,0 +1,172 @@
1
+ import { OFFLINE_URL, WEB_MANIFEST_URL } from "../names.js";
2
+ import { iconPng, iconSvg } from "./icons.js";
3
+ import { contentHash, gameAssetManifest, injectManifest, offlinePage, precacheEntries, webManifest } from "./documents.js";
4
+ import { fileSets } from "./graph.js";
5
+ import { DEFAULT_CONTROL, RELEASE_RECORD_FILE, canonicalReleaseTime } from "../control.js";
6
+ import * as z from "zod/mini";
7
+ import { dirname, join, resolve } from "node:path";
8
+ import { existsSync, statSync } from "node:fs";
9
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
10
+ import { fileURLToPath } from "node:url";
11
+ import { build } from "vite";
12
+ //#region src/build/plugin.ts
13
+ var SOURCE = fileURLToPath(new URL("..", import.meta.url));
14
+ var script = (folder, name) => {
15
+ const source = join(SOURCE, folder, `${name}.ts`);
16
+ return existsSync(source) ? source : join(SOURCE, folder, `${name}.js`);
17
+ };
18
+ /** Bundles one browser or worker script into a single minified classic script. */
19
+ var bundleScript = async (entry, define = {}) => {
20
+ const result = await build({
21
+ configFile: false,
22
+ root: SOURCE,
23
+ logLevel: "warn",
24
+ publicDir: false,
25
+ define: {
26
+ "process.env.NODE_ENV": JSON.stringify("production"),
27
+ ...define
28
+ },
29
+ build: {
30
+ write: false,
31
+ emptyOutDir: false,
32
+ copyPublicDir: false,
33
+ minify: true,
34
+ target: "es2022",
35
+ sourcemap: false,
36
+ reportCompressedSize: false,
37
+ lib: {
38
+ entry,
39
+ formats: ["iife"],
40
+ name: "joystykPwa",
41
+ fileName: () => "bundle.js"
42
+ }
43
+ }
44
+ });
45
+ for (const output of Array.isArray(result) ? result : [result]) {
46
+ if (!("output" in output)) continue;
47
+ for (const item of output.output) if (item.type === "chunk" && item.isEntry) return item.code;
48
+ }
49
+ throw new Error(`[pwa] Bundling ${entry} produced no script.`);
50
+ };
51
+ var write = async (file, content) => {
52
+ await mkdir(dirname(file), { recursive: true });
53
+ await writeFile(file, content);
54
+ };
55
+ var manifestSchema = z.record(z.string(), z.object({
56
+ file: z.string(),
57
+ name: z.optional(z.string()),
58
+ src: z.optional(z.string()),
59
+ isEntry: z.optional(z.boolean()),
60
+ isDynamicEntry: z.optional(z.boolean()),
61
+ imports: z.optional(z.array(z.string())),
62
+ dynamicImports: z.optional(z.array(z.string())),
63
+ css: z.optional(z.array(z.string())),
64
+ assets: z.optional(z.array(z.string()))
65
+ }));
66
+ /** The plugin. Add it to the site's client build only. */
67
+ var joystykPwa = (options) => {
68
+ const releaseTime = canonicalReleaseTime(options.releaseTime);
69
+ if (releaseTime === null) throw new Error(`[pwa] releaseTime "${options.releaseTime}" isn't a date-time such as 2026-10-09T14:03:12Z.`);
70
+ let config;
71
+ let logger;
72
+ return {
73
+ name: "joystyk-pwa",
74
+ apply: "build",
75
+ enforce: "post",
76
+ configResolved(resolved) {
77
+ config = resolved;
78
+ logger = resolved.logger;
79
+ },
80
+ async writeBundle(output, bundle) {
81
+ if (config === void 0 || config.build.ssr !== false) return;
82
+ const outDir = resolve(config.root, output.dir ?? config.build.outDir);
83
+ const manifestPath = join(outDir, ".vite", "manifest.json");
84
+ if (!existsSync(manifestPath)) throw new Error("[pwa] The client build needs `build.manifest: true`.");
85
+ const manifest = manifestSchema.parse(JSON.parse(await readFile(manifestPath, "utf8")));
86
+ const sources = new Map(Object.values(bundle).filter((item) => item.type === "chunk").map((chunk) => [chunk.fileName, chunk.moduleIds]));
87
+ const site = await options.site({ outDir });
88
+ const hashed = async (name, ext, content) => {
89
+ const file = `assets/${name}-${contentHash(content)}.${ext}`;
90
+ await write(join(outDir, file), content);
91
+ return file;
92
+ };
93
+ const iconFiles = {
94
+ svg: await hashed("icon", "svg", iconSvg()),
95
+ png192: await hashed("icon-192", "png", iconPng(192)),
96
+ png512: await hashed("icon-512", "png", iconPng(512)),
97
+ maskable512: await hashed("icon-maskable-512", "png", iconPng(512, true))
98
+ };
99
+ const offlineCss = await hashed("offline", "css", await readFile(join(SOURCE, "offline", "offline.css")));
100
+ const offlineJs = await hashed("offline", "js", await bundleScript(script("offline", "offline")));
101
+ const url = (file) => `/${file}`;
102
+ const manifestJson = `${JSON.stringify(webManifest({
103
+ svg: url(iconFiles.svg),
104
+ png192: url(iconFiles.png192),
105
+ png512: url(iconFiles.png512),
106
+ maskable512: url(iconFiles.maskable512)
107
+ }), null, 2)}\n`;
108
+ await write(join(outDir, "manifest.webmanifest"), manifestJson);
109
+ const offlineHtml = offlinePage({
110
+ css: url(offlineCss),
111
+ js: url(offlineJs),
112
+ icon: url(iconFiles.svg)
113
+ });
114
+ await write(join(outDir, "offline.html"), offlineHtml);
115
+ const sets = fileSets(manifest, site.games, config.root, site.unlisted ?? [], sources);
116
+ const sizeOf = (file) => statSync(join(outDir, file)).size;
117
+ for (const game of site.games) {
118
+ const assets = gameAssetManifest(game, options.release, sets.shell, sets.games.get(game.id) ?? /* @__PURE__ */ new Set(), sizeOf);
119
+ if (!game.assets.startsWith("/") || game.assets.includes("..")) throw new Error(`[pwa] ${game.id}: assets "${game.assets}" must be a site path.`);
120
+ await write(join(outDir, game.assets), `${JSON.stringify(assets, null, 2)}\n`);
121
+ }
122
+ const rendered = [];
123
+ for (const path of site.shellPages) {
124
+ const response = await site.render(path);
125
+ if (response.status !== 200) throw new Error(`[pwa] Precaching ${path}: the server answers ${String(response.status)}, not 200.`);
126
+ rendered.push({
127
+ url: path,
128
+ hashed: false,
129
+ body: new Uint8Array(await response.arrayBuffer())
130
+ });
131
+ }
132
+ const files = [
133
+ ...sets.shell,
134
+ ...Object.values(iconFiles),
135
+ offlineCss,
136
+ offlineJs
137
+ ];
138
+ const entries = precacheEntries([
139
+ ...rendered,
140
+ {
141
+ url: OFFLINE_URL,
142
+ hashed: false,
143
+ body: offlineHtml
144
+ },
145
+ {
146
+ url: WEB_MANIFEST_URL,
147
+ hashed: false,
148
+ body: manifestJson
149
+ },
150
+ ...files.map((file) => ({
151
+ url: url(file),
152
+ hashed: true,
153
+ file
154
+ }))
155
+ ], (file) => existsSync(join(outDir, file)));
156
+ const worker = injectManifest(await bundleScript(script("sw", "sw"), { __JOYSTYK_RELEASE__: JSON.stringify(options.release) }), entries);
157
+ await write(join(outDir, "sw.js"), worker);
158
+ await write(join(outDir, "sw-remove.js"), await bundleScript(script("sw", "remove-sw")));
159
+ await write(join(outDir, "pwa-control.json"), `${JSON.stringify(DEFAULT_CONTROL, null, 2)}\n`);
160
+ const record = {
161
+ release: options.release,
162
+ releaseTime
163
+ };
164
+ await write(join(outDir, RELEASE_RECORD_FILE), `${JSON.stringify(record, null, 2)}\n`);
165
+ logger?.info(`[pwa] sw.js ${(worker.length / 1024).toFixed(1)} kB for release ${options.release} (${releaseTime}):${String(entries.length)} precache entries; asset manifests for ${site.games.map((g) => g.id).join(", ") || "no games"}.`);
166
+ }
167
+ };
168
+ };
169
+ //#endregion
170
+ export { joystykPwa };
171
+
172
+ //# sourceMappingURL=plugin.js.map