@caperjs/core 0.1.1 → 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 (109) hide show
  1. package/README.md +1 -1
  2. package/{config → build}/assetpack.mjs +103 -45
  3. package/build/defaults.mjs +157 -0
  4. package/build/index.mjs +96 -0
  5. package/build/internal/ast.mjs +255 -0
  6. package/build/internal/buildFlags.mjs +50 -0
  7. package/build/internal/discovery.mjs +402 -0
  8. package/build/internal/manifest.mjs +38 -0
  9. package/build/internal/schema.mjs +65 -0
  10. package/build/internal/util.mjs +30 -0
  11. package/build/internal/validate.mjs +186 -0
  12. package/build/plugins/assetTypes.mjs +460 -0
  13. package/build/plugins/caperConfig.mjs +489 -0
  14. package/build/plugins/devHelper.mjs +31 -0
  15. package/build/plugins/lists.mjs +286 -0
  16. package/build/plugins/pruneFallbacks.mjs +150 -0
  17. package/build/plugins/pwa.mjs +240 -0
  18. package/build/plugins/runtime.mjs +129 -0
  19. package/cli.mjs +9 -6
  20. package/extras/llms.txt +23 -17
  21. package/lib/{CaptionsPlugin-Dc8fIrjy.js → CaptionsPlugin-DKhbmJvH.js} +15 -15
  22. package/lib/{CaptionsPlugin-Dc8fIrjy.js.map → CaptionsPlugin-DKhbmJvH.js.map} +1 -1
  23. package/lib/{DataAdapter-BY2114sp.js → DataAdapter-BZFTkFQ4.js} +4 -4
  24. package/lib/{DataAdapter-BY2114sp.js.map → DataAdapter-BZFTkFQ4.js.map} +1 -1
  25. package/lib/{DebugRenderer-BJMLILqI.js → DebugRenderer-C7X-nlXE.js} +2 -2
  26. package/lib/{DebugRenderer-BJMLILqI.js.map → DebugRenderer-C7X-nlXE.js.map} +1 -1
  27. package/lib/{DevToolsPlugin-wQ5rOkn0.js → DevToolsPlugin-P-atJ-5U.js} +2 -2
  28. package/lib/{DevToolsPlugin-wQ5rOkn0.js.map → DevToolsPlugin-P-atJ-5U.js.map} +1 -1
  29. package/lib/{GSAPPlugin-DHhSZKv8.js → GSAPPlugin-CeH9275z.js} +2 -2
  30. package/lib/{GSAPPlugin-DHhSZKv8.js.map → GSAPPlugin-CeH9275z.js.map} +1 -1
  31. package/lib/{LayoutPlugin-B-DF9SYM.js → LayoutPlugin-iZ3k6RsK.js} +4 -4
  32. package/lib/{LayoutPlugin-B-DF9SYM.js.map → LayoutPlugin-iZ3k6RsK.js.map} +1 -1
  33. package/lib/{SpinePlugin-Dz_3dljz.js → SpinePlugin-BWiavCpO.js} +2 -2
  34. package/lib/{SpinePlugin-Dz_3dljz.js.map → SpinePlugin-BWiavCpO.js.map} +1 -1
  35. package/lib/{StatsPlugin-Cg82JMBD.js → StatsPlugin-CBpzDJL0.js} +4 -4
  36. package/lib/{StatsPlugin-Cg82JMBD.js.map → StatsPlugin-CBpzDJL0.js.map} +1 -1
  37. package/lib/{VoiceOverPlugin-BR3ImT8C.js → VoiceOverPlugin-YaiKJtJ6.js} +26 -26
  38. package/lib/{VoiceOverPlugin-BR3ImT8C.js.map → VoiceOverPlugin-YaiKJtJ6.js.map} +1 -1
  39. package/lib/caper.mjs +95 -95
  40. package/lib/caper.mjs.map +1 -1
  41. package/lib/core/Application.d.ts +1 -1
  42. package/lib/core/Application.d.ts.map +1 -1
  43. package/lib/core/interfaces/IApplicationOptions.d.ts +2 -1
  44. package/lib/core/interfaces/IApplicationOptions.d.ts.map +1 -1
  45. package/lib/core/interfaces/ICoreSignals.d.ts +5 -1
  46. package/lib/core/interfaces/ICoreSignals.d.ts.map +1 -1
  47. package/lib/display/Container.d.ts +1 -1
  48. package/lib/display/Container.d.ts.map +1 -1
  49. package/lib/display/ParticleContainer.d.ts +1 -1
  50. package/lib/display/ParticleContainer.d.ts.map +1 -1
  51. package/lib/plugins/Plugin.d.ts +1 -1
  52. package/lib/plugins/Plugin.d.ts.map +1 -1
  53. package/lib/plugins/defaults.d.ts.map +1 -1
  54. package/lib/plugins/gesture/GesturePlugin.d.ts +58 -0
  55. package/lib/plugins/gesture/GesturePlugin.d.ts.map +1 -0
  56. package/lib/plugins/gesture/gestureMath.d.ts +32 -0
  57. package/lib/plugins/gesture/gestureMath.d.ts.map +1 -0
  58. package/lib/plugins/gesture/gestureMath.test.d.ts +2 -0
  59. package/lib/plugins/gesture/gestureMath.test.d.ts.map +1 -0
  60. package/lib/plugins/gesture/index.d.ts +3 -0
  61. package/lib/plugins/gesture/index.d.ts.map +1 -0
  62. package/lib/plugins/gesture/types.d.ts +45 -0
  63. package/lib/plugins/gesture/types.d.ts.map +1 -0
  64. package/lib/plugins/index.d.ts +1 -0
  65. package/lib/plugins/index.d.ts.map +1 -1
  66. package/lib/plugins/input/Controls.d.ts +5 -2
  67. package/lib/plugins/input/Controls.d.ts.map +1 -1
  68. package/lib/plugins/input/InputPlugin.d.ts +9 -2
  69. package/lib/plugins/input/InputPlugin.d.ts.map +1 -1
  70. package/lib/plugins/input/interfaces.d.ts +5 -3
  71. package/lib/plugins/input/interfaces.d.ts.map +1 -1
  72. package/lib/plugins/input/touch/{TouchControls.d.ts → VirtualControls.d.ts} +5 -4
  73. package/lib/plugins/input/touch/VirtualControls.d.ts.map +1 -0
  74. package/lib/plugins/input/touch/index.d.ts +1 -1
  75. package/lib/plugins/input/touch/index.d.ts.map +1 -1
  76. package/lib/{registries-CYCaYKZI.js → registries-qQmLeN0b.js} +505 -354
  77. package/lib/registries-qQmLeN0b.js.map +1 -0
  78. package/lib/types/caper-app.d.ts +1 -0
  79. package/lib/utils/bind.test.d.ts +2 -0
  80. package/lib/utils/bind.test.d.ts.map +1 -0
  81. package/package.json +33 -33
  82. package/src/core/Application.ts +1 -1
  83. package/src/core/interfaces/IApplicationOptions.ts +2 -0
  84. package/src/core/interfaces/ICoreSignals.ts +9 -0
  85. package/src/display/Container.ts +1 -1
  86. package/src/display/ParticleContainer.ts +1 -1
  87. package/src/plugins/Plugin.ts +1 -1
  88. package/src/plugins/defaults.ts +6 -0
  89. package/src/plugins/gesture/GesturePlugin.ts +215 -0
  90. package/src/plugins/gesture/gestureMath.test.ts +75 -0
  91. package/src/plugins/gesture/gestureMath.ts +59 -0
  92. package/src/plugins/gesture/index.ts +2 -0
  93. package/src/plugins/gesture/types.ts +54 -0
  94. package/src/plugins/index.ts +1 -0
  95. package/src/plugins/input/Controls.ts +9 -3
  96. package/src/plugins/input/InputPlugin.ts +61 -14
  97. package/src/plugins/input/interfaces.ts +5 -3
  98. package/src/plugins/input/touch/{TouchControls.ts → VirtualControls.ts} +4 -1
  99. package/src/plugins/input/touch/index.ts +1 -1
  100. package/src/types/caper-app.d.ts +1 -0
  101. package/src/utils/bind.test.ts +99 -0
  102. package/src/version.ts +1 -1
  103. package/templates/app/default/package.template.json +8 -4
  104. package/templates/app/default/vite.config.ts +15 -0
  105. package/LICENSE +0 -22
  106. package/cli/vite.mjs +0 -55
  107. package/config/vite.mjs +0 -2464
  108. package/lib/plugins/input/touch/TouchControls.d.ts.map +0 -1
  109. package/lib/registries-CYCaYKZI.js.map +0 -1
package/config/vite.mjs DELETED
@@ -1,2464 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import process from 'node:process';
4
- import { parseSync } from 'oxc-parser';
5
- import { createLogger, mergeConfig } from 'vite';
6
- import { VitePWA } from 'vite-plugin-pwa';
7
- import { viteStaticCopy } from 'vite-plugin-static-copy';
8
- import wasm from 'vite-plugin-wasm';
9
- import { z } from 'zod';
10
-
11
- // oxc-parser emits an ESTree-compatible AST. We keep the `AST_NODE_TYPES`
12
- // shape so existing call sites don't have to change — every value below is
13
- // just the ESTree node-type string. If you add a new check, reference the
14
- // ESTree spec at https://github.com/estree/estree.
15
- const AST_NODE_TYPES = {
16
- ArrayExpression: 'ArrayExpression',
17
- CallExpression: 'CallExpression',
18
- ClassDeclaration: 'ClassDeclaration',
19
- ExportDefaultDeclaration: 'ExportDefaultDeclaration',
20
- ExportNamedDeclaration: 'ExportNamedDeclaration',
21
- Identifier: 'Identifier',
22
- ImportDeclaration: 'ImportDeclaration',
23
- Literal: 'Literal',
24
- ObjectExpression: 'ObjectExpression',
25
- Property: 'Property',
26
- VariableDeclaration: 'VariableDeclaration',
27
- };
28
-
29
- /**
30
- * Thin wrapper around oxc-parser that mirrors the shape we previously got
31
- * from `@typescript-eslint/typescript-estree`: it returns the `Program`
32
- * node directly, so `ast.body` / `for (const node of ast.body)` keeps working.
33
- *
34
- * Swapping `typescript-estree` (JS-written TS parser, ~500KB, slow) out for
35
- * oxc-parser (Rust, bundled with Vite 8) was the biggest remaining DX win
36
- * from the Phase 1b rolldown `PLUGIN_TIMINGS` report: the config/discovery
37
- * AST parses are the hottest paths in the dev-server startup and HMR.
38
- */
39
- function parse(content, _options = {}) {
40
- const result = parseSync('caper-discovery.ts', content, {
41
- lang: 'ts',
42
- sourceType: 'module',
43
- });
44
- if (result.errors && result.errors.length > 0) {
45
- const first = result.errors[0];
46
- const msg = first.message || JSON.stringify(first);
47
- const err = new Error(`oxc-parser: ${msg}`);
48
- throw err;
49
- }
50
- return result.program;
51
- }
52
-
53
- const env = process.env.NODE_ENV;
54
- const cwd = process.cwd();
55
-
56
- const logger = createLogger('caper-config');
57
-
58
- import { assetpackPlugin } from './assetpack.mjs';
59
-
60
- const DTS_FILE_NAME = 'caper-app.d.ts';
61
- const ASSET_DTS_FILE_NAME = 'caper-assets.d.ts';
62
-
63
- /**
64
- * Strict-fields Zod schema for `caper.config.ts`.
65
- *
66
- * Only validates the fields that produce cryptic runtime errors today when
67
- * they're malformed (wrong type, wrong shape). Everything else is passed
68
- * through via `.loose()` so the schema doesn't need to stay 1:1 with
69
- * `IApplicationOptions` — that would be a maintenance trap. Cross-reference
70
- * validation (`defaultScene` must exist in discovered scenes, `plugins[]`
71
- * IDs must exist) is deferred to Phase 5 #9 (build-time validation).
72
- *
73
- * Validation runs inside the Vite plugin in dev mode only, so Zod never
74
- * ships to the client bundle.
75
- */
76
- const pluginConfigSchema = z.union([
77
- z.string(),
78
- z.tuple([
79
- z.string(),
80
- z
81
- .object({
82
- autoLoad: z.boolean().optional(),
83
- options: z.any().optional(),
84
- })
85
- .loose(),
86
- ]),
87
- ]);
88
-
89
- const caperConfigSchema = z
90
- .object({
91
- id: z.string().min(1).optional(),
92
- application: z.any().optional(),
93
- defaultScene: z.string().min(1).optional(),
94
- defaultSceneLoadMethod: z.string().optional(),
95
- plugins: z.array(pluginConfigSchema).optional(),
96
- scenes: z.any().optional(),
97
- assets: z
98
- .object({
99
- manifest: z.any().optional(),
100
- preload: z
101
- .object({
102
- bundles: z.array(z.string()).optional(),
103
- })
104
- .loose()
105
- .optional(),
106
- background: z
107
- .object({
108
- bundles: z.array(z.string()).optional(),
109
- })
110
- .loose()
111
- .optional(),
112
- })
113
- .loose()
114
- .optional(),
115
- useStore: z.boolean().optional(),
116
- useSpine: z.boolean().optional(),
117
- useLayout: z.boolean().optional(),
118
- useVoiceover: z.boolean().optional(),
119
- useHash: z.boolean().optional(),
120
- // Build-time only — read by readCaperBuildFlags(), no runtime effect.
121
- useWasm: z.boolean().optional(),
122
- showStats: z.boolean().optional(),
123
- showSceneDebugMenu: z.boolean().optional(),
124
- resizeToContainer: z.boolean().optional(),
125
- logger: z.string().optional(),
126
- sceneGroupOrder: z.array(z.string()).optional(),
127
- })
128
- .loose();
129
-
130
- /**
131
- * Evaluate the user's `caper.config.ts` through Vite's own module
132
- * graph and validate the default export. Dev-only — requires a live
133
- * `server` (i.e. `vite dev`). Returns `true` on success.
134
- */
135
- async function validateCaperConfig(server) {
136
- if (!server || typeof server.ssrLoadModule !== 'function') return true;
137
- const configPath = path.resolve(cwd, 'caper.config.ts');
138
- if (!fs.existsSync(configPath)) return true;
139
-
140
- // caper.config.ts pulls in @caperjs/core, which statically bundles
141
- // @pixi/sound and GSAP. Both run browser-only top-level side effects
142
- // during module init, which throw one after another under Vite SSR
143
- // (Node, no DOM) and abort the whole ssrLoadModule — so config
144
- // validation silently never runs. Install minimal stubs just for the
145
- // duration of the load, and only for whichever globals aren't already
146
- // defined (jsdom, a future Vite DOM environment, etc.).
147
- //
148
- // - document.createElement('audio').canPlayType: @pixi/sound's
149
- // utils/supported.mjs probes playable formats at module top level.
150
- // - createElement(...).style: GSAP's CSSPlugin auto-registers at
151
- // import time and does `'transform' in tempDiv.style` on a div it
152
- // creates via document.createElement — needs a `style` object (any
153
- // object satisfies the `in` check; contents are never read here).
154
- // - globalThis.window: @pixi/sound's WebAudioContext/SoundLibrary
155
- // singleton construction reads `window` at module top level.
156
- const hadDocument = 'document' in globalThis;
157
- const hadWindow = 'window' in globalThis;
158
- if (!hadDocument) {
159
- globalThis.document = {
160
- createElement: () => ({ canPlayType: () => '', style: {} }),
161
- addEventListener: () => {},
162
- removeEventListener: () => {},
163
- };
164
- }
165
- if (!hadWindow) {
166
- globalThis.window = globalThis;
167
- }
168
-
169
- let mod;
170
- try {
171
- mod = await server.ssrLoadModule(configPath);
172
- } catch {
173
- // Config failed to load — the normal type-regen path already surfaces
174
- // this error through the websocket overlay, so don't double-report.
175
- return false;
176
- } finally {
177
- if (!hadDocument) delete globalThis.document;
178
- if (!hadWindow) delete globalThis.window;
179
- }
180
- const cfg = mod.default;
181
- if (!cfg) return true;
182
-
183
- const result = caperConfigSchema.safeParse(cfg);
184
- if (result.success) return true;
185
-
186
- const issues = result.error.issues
187
- .map((i) => ` - ${i.path.length ? i.path.join('.') : '(root)'}: ${i.message}`)
188
- .join('\n');
189
- const message = `Invalid caper.config.ts:\n${issues}`;
190
- logger.error(`[caper] ${message}`);
191
-
192
- server.ws.send({
193
- type: 'error',
194
- err: {
195
- message,
196
- id: configPath,
197
- plugin: 'vite-plugin-caper-config',
198
- },
199
- });
200
- return false;
201
- }
202
-
203
- // write a debounce function
204
- const debounce = (func, wait) => {
205
- let timeout;
206
- return (...args) => {
207
- clearTimeout(timeout);
208
- timeout = setTimeout(() => func.apply(this, args), wait);
209
- };
210
- };
211
-
212
- const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
213
-
214
- // Function to generate TypeScript types from the manifest
215
- async function generateAssetTypes(manifest) {
216
- // Flat totals per category (back-compat: AssetTextures, AssetAudio, etc.)
217
- const assetsByType = {
218
- textures: new Set(),
219
- spritesheets: new Set(),
220
- tpsFrames: new Set(),
221
- fonts: new Set(),
222
- bitmapFonts: new Set(),
223
- fontFamilies: new Set(),
224
- bitmapFontFamilies: new Set(),
225
- audio: new Set(),
226
- json: new Set(),
227
- spine: new Set(),
228
- rive: new Set(),
229
- };
230
-
231
- // Per-bundle breakdown so we can narrow e.g. AssetTexturesIn<'menu'> → only
232
- // textures that actually ship in the 'menu' bundle. Shape:
233
- // byBundle.textures.menu = Set(['menu/logo'])
234
- const byBundle = {
235
- textures: {},
236
- spritesheets: {},
237
- tpsFrames: {},
238
- fonts: {},
239
- bitmapFonts: {},
240
- audio: {},
241
- json: {},
242
- spine: {},
243
- rive: {},
244
- };
245
- const addToBundle = (category, bundleName, alias) => {
246
- if (!byBundle[category][bundleName]) byBundle[category][bundleName] = new Set();
247
- byBundle[category][bundleName].add(alias);
248
- };
249
-
250
- const bundles = manifest.bundles || [];
251
- const bundleNames = new Set();
252
-
253
- // Process each bundle
254
- for (const bundle of bundles) {
255
- bundleNames.add(bundle.name);
256
-
257
- // Process each asset in the bundle
258
- for (const asset of bundle.assets) {
259
- const aliases = asset.alias || [];
260
- const srcs = Array.isArray(asset.src) ? asset.src : [asset.src];
261
- const firstSrc = srcs[0];
262
- const ext = path.extname(firstSrc).toLowerCase();
263
-
264
- // Add to appropriate category based on extension and data tags
265
- if (asset.data?.tags?.tps || (ext === '.json' && firstSrc.includes('sheet'))) {
266
- aliases.forEach((alias) => {
267
- assetsByType.spritesheets.add(alias);
268
- addToBundle('spritesheets', bundle.name, alias);
269
- });
270
-
271
- // Extract frame names from TPS JSON files
272
- if (asset.data?.tags?.tps) {
273
- try {
274
- // Find the first .json file in the src array
275
- const jsonSrc = srcs.find((src) => src.endsWith('.json'));
276
- if (jsonSrc) {
277
- // Construct the full path to the JSON file
278
- const jsonPath = path.join(process.cwd(), 'public', 'assets', jsonSrc);
279
-
280
- // Read and parse the JSON file
281
- const jsonContent = await fs.promises.readFile(jsonPath, 'utf8');
282
- const tpsData = JSON.parse(jsonContent);
283
-
284
- // Extract frame names from the "frames" object
285
- if (tpsData.frames) {
286
- Object.keys(tpsData.frames).forEach((frameName) => {
287
- assetsByType.tpsFrames.add(frameName);
288
- addToBundle('tpsFrames', bundle.name, frameName);
289
- });
290
- }
291
- }
292
- } catch (error) {
293
- logger.warn(`Failed to load TPS frames from ${firstSrc}:`, error.message);
294
- }
295
- }
296
- } else if (ext === '.json' && !firstSrc.includes('atlas')) {
297
- aliases.forEach((alias) => {
298
- assetsByType.json.add(alias);
299
- addToBundle('json', bundle.name, alias);
300
- });
301
- } else if (['.png', '.webp', '.jpg', '.jpeg', '.svg'].includes(ext)) {
302
- aliases.forEach((alias) => {
303
- assetsByType.textures.add(alias);
304
- addToBundle('textures', bundle.name, alias);
305
- });
306
- } else if (['.mp3', '.ogg', '.wav'].includes(ext)) {
307
- aliases.forEach((alias) => {
308
- assetsByType.audio.add(alias);
309
- addToBundle('audio', bundle.name, alias);
310
- });
311
- } else if (['.ttf', '.woff', '.woff2'].includes(ext) || asset.data?.tags?.wf) {
312
- aliases.forEach((alias) => {
313
- assetsByType.fonts.add(alias);
314
- addToBundle('fonts', bundle.name, alias);
315
- });
316
- if (asset.data?.family) {
317
- assetsByType.fontFamilies.add(asset.data.family);
318
- }
319
- } else if (['.fnt'].includes(ext)) {
320
- aliases.forEach((alias) => {
321
- assetsByType.bitmapFonts.add(alias);
322
- assetsByType.bitmapFontFamilies.add(alias);
323
- addToBundle('bitmapFonts', bundle.name, alias);
324
- });
325
- } else if (['.atlas', '.skel', '.json'].some((e) => firstSrc.includes(e)) && firstSrc.includes('spine')) {
326
- aliases.forEach((alias) => {
327
- assetsByType.spine.add(alias);
328
- addToBundle('spine', bundle.name, alias);
329
- });
330
- } else if (ext === '.riv') {
331
- aliases.forEach((alias) => {
332
- assetsByType.rive.add(alias);
333
- addToBundle('rive', bundle.name, alias);
334
- });
335
- }
336
- }
337
- }
338
-
339
- // Convert Sets to sorted arrays for consistent output
340
- const types = Object.fromEntries(Object.entries(assetsByType).map(([key, value]) => [key, [...value].sort()]));
341
-
342
- // Helper to emit a per-bundle mapped type body like:
343
- // '\n menu: \'menu/logo\' | \'menu/bg\';\n game: \'game/hero\';\n '
344
- // Returns `never` if no bundle contains anything in the category.
345
- const emitByBundleMap = (category) => {
346
- const entries = Object.entries(byBundle[category])
347
- .map(([bundleName, set]) => [bundleName, [...set].sort()])
348
- .filter(([, arr]) => arr.length > 0);
349
- if (entries.length === 0) return '{}';
350
- const lines = entries.map(([name, arr]) => ` ${name}: '${arr.join("' | '")}';`);
351
- return `{\n${lines.join('\n')}\n }`;
352
- };
353
-
354
- return `// This file is auto-generated. Do not edit.
355
- import type { ResolvedAsset, Texture, Spritesheet } from 'pixi.js';
356
-
357
- /**
358
- * Available bundle names in the asset manifest
359
- * @example
360
- * const bundle: AssetBundles = 'game';
361
- */
362
- export type AssetBundles = ${[...bundleNames].length ? `\n | '${[...bundleNames].sort().join("'\n | '")}'` : 'never'};
363
-
364
- /**
365
- * Available texture names in the asset manifest
366
- * @example
367
- * const texture: AssetTextures = 'game/wordmark';
368
- */
369
- export type AssetTextures = ${types.textures.length ? `\n | '${types.textures.join("'\n | '")}'` : 'never'};
370
-
371
- /**
372
- * Per-bundle texture map. Use \`AssetTexturesIn<'menu'>\` to get the exact
373
- * set of textures shipped in a single bundle, so a scene that only loads
374
- * the 'menu' bundle can't accidentally reference a 'game/*' texture.
375
- */
376
- export type AssetTexturesByBundle = ${emitByBundleMap('textures')};
377
- export type AssetTexturesIn<B extends AssetBundles> = B extends keyof AssetTexturesByBundle ? AssetTexturesByBundle[B] : never;
378
-
379
- /**
380
- * Available spritesheet names in the asset manifest
381
- * @example
382
- * const spritesheet: AssetSpritesheets = 'game/sheet';
383
- */
384
- export type AssetSpritesheets = ${types.spritesheets.length ? `\n | '${types.spritesheets.join("'\n | '")}'` : 'never'};
385
-
386
- export type AssetSpritesheetsByBundle = ${emitByBundleMap('spritesheets')};
387
- export type AssetSpritesheetsIn<B extends AssetBundles> = B extends keyof AssetSpritesheetsByBundle ? AssetSpritesheetsByBundle[B] : never;
388
-
389
- /**
390
- * Available TPS frame names from spritesheets
391
- * @example
392
- * const frame: AssetTPSFrames = 'btn/blue';
393
- */
394
- export type AssetTPSFrames = ${types.tpsFrames.length ? `\n | '${types.tpsFrames.join("'\n | '")}'` : 'never'};
395
-
396
- export type AssetTPSFramesByBundle = ${emitByBundleMap('tpsFrames')};
397
- export type AssetTPSFramesIn<B extends AssetBundles> = B extends keyof AssetTPSFramesByBundle ? AssetTPSFramesByBundle[B] : never;
398
-
399
- /**
400
- * Available font names in the asset manifest
401
- * @example
402
- * const font: AssetFonts = 'SpaceGrotesk-Regular';
403
- */
404
- export type AssetFonts = ${types.fonts.length ? `\n | '${types.fonts.join("'\n | '")}'` : 'never'};
405
-
406
- /**
407
- * Available font names in the asset manifest
408
- * @example
409
- * const font: AssetFonts = 'SpaceGrotesk-Regular';
410
- */
411
- export type AssetBitmapFonts = ${types.bitmapFonts.length ? `\n | '${types.bitmapFonts.join("'\n | '")}'` : 'never'};
412
-
413
- /**
414
- * Available audio names in the asset manifest
415
- * @example
416
- * const audio: AssetAudio = 'click';
417
- */
418
- export type AssetAudio = ${types.audio.length ? `\n | '${types.audio.join("'\n | '")}'` : 'never'};
419
-
420
- export type AssetAudioByBundle = ${emitByBundleMap('audio')};
421
- export type AssetAudioIn<B extends AssetBundles> = B extends keyof AssetAudioByBundle ? AssetAudioByBundle[B] : never;
422
-
423
- /**
424
- * Available JSON file names in the asset manifest
425
- * @example
426
- * const json: AssetJson = 'locales/en';
427
- */
428
- export type AssetJson = ${types.json.length ? `\n | '${types.json.join("'\n | '")}'` : 'never'};
429
-
430
- /**
431
- * Available Spine animation names in the asset manifest
432
- * @example
433
- * const spine: AssetSpine = 'spine/hero';
434
- */
435
- export type AssetSpine = ${types.spine.length ? `\n | '${types.spine.join("'\n | '")}'` : 'never'};
436
-
437
- /**
438
- * Available Rive animation names in the asset manifest
439
- * @example
440
- * const rive: AssetRive = 'static/marty';
441
- */
442
- export type AssetRive = ${types.rive.length ? `\n | '${types.rive.join("'\n | '")}'` : 'never'};
443
-
444
- /**
445
- * Available font family names
446
- * @example
447
- * const fontFamily: AssetFontFamilies = 'Space Grotesk';
448
- */
449
- export type AssetFontFamilies = ${types.fontFamilies.length ? `\n | '${types.fontFamilies.join("'\n | '")}'` : 'never'};
450
-
451
- /**
452
- * Available font family names
453
- * @example
454
- * const fontFamily: AssetFontFamilies = 'Space Grotesk';
455
- */
456
- export type AssetBitmapFontFamilies = ${types.bitmapFontFamilies.length ? `\n | '${types.bitmapFontFamilies.join("'\n | '")}'` : 'never'};
457
-
458
- /**
459
- * Union type of all asset names
460
- * @example
461
- * const asset: AssetAlias = 'game/wordmark';
462
- */
463
- export type AssetAlias =
464
- | AssetTextures
465
- | AssetSpritesheets
466
- | AssetTPSFrames
467
- | AssetFonts
468
- | AssetBitmapFonts
469
- | AssetAudio
470
- | AssetJson
471
- | AssetSpine
472
- | AssetRive;
473
-
474
- /**
475
- * Type-safe manifest structure
476
- */
477
- export interface AssetManifest {
478
- bundles: {
479
- [K in AssetBundles]: {
480
- name: K;
481
- assets: ResolvedAsset[];
482
- }
483
- };
484
- }
485
-
486
- /**
487
- * Type-safe asset types after loading
488
- */
489
- export interface AssetTypes {
490
- textures: Record<AssetTextures, Texture>;
491
- spritesheets: Record<AssetSpritesheets, Spritesheet>;
492
- tpsFrames: Record<AssetTPSFrames, Texture>;
493
- fonts: Record<AssetFonts, any>;
494
- audio: Record<AssetAudio, HTMLAudioElement>;
495
- json: Record<AssetJson, any>;
496
- spine: Record<AssetSpine, any>;
497
- rive: Record<AssetRive, any>;
498
- fontFamilies: Record<AssetFontFamilies, any>;
499
- bitmapFonts: Record<AssetBitmapFonts, any>;
500
- bitmapFontFamilies: Record<AssetBitmapFontFamilies, any>;
501
- }
502
-
503
- /**
504
- * Helper type to get the asset type for a given alias
505
- * @example
506
- * type MyTextureType = AssetTypeOf<'game/wordmark'>; // Texture
507
- */
508
- export type AssetTypeOf<T extends AssetAlias> =
509
- T extends AssetTextures ? Texture :
510
- T extends AssetSpritesheets ? Spritesheet :
511
- T extends AssetTPSFrames ? Texture :
512
- T extends AssetFonts ? any :
513
- T extends AssetBitmapFonts ? any :
514
- T extends AssetAudio ? HTMLAudioElement :
515
- T extends AssetJson ? any :
516
- T extends AssetSpine ? any :
517
- T extends AssetRive ? any :
518
- T extends AssetFontFamilies ? any :
519
- T extends AssetBitmapFontFamilies ? any :
520
- never;
521
-
522
-
523
- /**
524
- * Get the bundle name for a given asset
525
- * @example
526
- * type MyBundle = AssetBundleOf<'game/wordmark'>; // 'game'
527
- */
528
- export type AssetBundleOf<T extends AssetAlias> = Extract<AssetBundles, T extends \`\${infer B}/\${string}\` ? B : never>;
529
-
530
- /**
531
- * Add type overrides to the framework
532
- */
533
- declare module '@caperjs/core' {
534
- interface AssetTypeOverrides {
535
- Texture: AssetTextures;
536
- TPSFrames: AssetTPSFrames;
537
- SpriteSheet: AssetSpritesheets;
538
- SpineData: AssetSpine;
539
- Audio: AssetAudio;
540
- FontFamily: AssetFontFamilies;
541
- BitmapFontFamily: AssetBitmapFontFamilies;
542
- Bundles: AssetBundles;
543
- }
544
- }
545
- `;
546
- }
547
-
548
- // Function to write types file
549
- async function writeAssetTypes(manifest, outputDir) {
550
- const types = await generateAssetTypes(manifest);
551
- // Change output path to ./src/types/
552
- const srcTypesDir = path.join(process.cwd(), 'src', 'types');
553
-
554
- try {
555
- // Ensure the directory exists
556
- await fs.promises.mkdir(srcTypesDir, { recursive: true });
557
- const typesPath = path.join(srcTypesDir, ASSET_DTS_FILE_NAME);
558
- await fs.promises.writeFile(typesPath, types, 'utf8');
559
- logger.info(`Caper asset types plugin:: Generated types at ${typesPath}`);
560
- } catch (error) {
561
- logger.error('Caper asset types plugin:: Error writing types file:', error);
562
- // Fallback to original location if src folder doesn't exist
563
- const typesPath = path.join(outputDir, ASSET_DTS_FILE_NAME);
564
- await fs.promises.writeFile(typesPath, types, 'utf8');
565
- logger.info(`Caper asset types plugin:: Generated types at fallback location ${typesPath}`);
566
- }
567
- }
568
-
569
- // Asset types generation plugin
570
- export function assetTypesPlugin(manifestUrl = 'assets.json') {
571
- let viteServer;
572
- let manifestWatcher;
573
- let ispPwaEnabled = false;
574
-
575
- async function generate(manifestUrl) {
576
- try {
577
- const manifestPath = path.join(process.cwd(), 'public', 'assets', manifestUrl);
578
- if (!fs.existsSync(manifestPath)) {
579
- logger.warn(`Caper asset types plugin:: manifest not found at ${manifestPath}, skipping type generation`);
580
- return;
581
- }
582
- const manifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8'));
583
- await writeAssetTypes(manifest, path.dirname(manifestPath));
584
- logger.info('Caper asset types plugin:: manifest changed, reloading browser...');
585
- viteServer?.ws?.send({ type: 'full-reload' });
586
- } catch (error) {
587
- logger.error('Caper asset types plugin:: Error handling manifest change:', error);
588
- }
589
- }
590
-
591
- const debouncedHandleManifestChange = debounce(generate, 300);
592
-
593
- return {
594
- name: 'vite-plugin-asset-types',
595
- config(config) {
596
- ispPwaEnabled = config.plugins.some((p) => p.name === 'vite-plugin-pwa');
597
- },
598
- async buildStart() {
599
- // a short delay to allow assetpack to generate the manifest
600
- await delay(500);
601
- await generate(manifestUrl);
602
- await delay(500);
603
- },
604
- configureServer(server) {
605
- viteServer = server;
606
-
607
- // Watch for manifest changes in development
608
- const manifestPath = path.join(process.cwd(), 'public', 'assets', manifestUrl);
609
- server.watcher.add(manifestPath);
610
- logger.info(`Caper asset types plugin:: watching manifest at ${manifestPath}`);
611
-
612
- const handleChange = async (file) => {
613
- if (file === manifestPath) {
614
- await debouncedHandleManifestChange(manifestUrl);
615
- }
616
- };
617
-
618
- server.watcher.on('add', handleChange);
619
- server.watcher.on('change', handleChange);
620
- },
621
- async buildEnd() {
622
- manifestWatcher?.close();
623
-
624
- // Generate types in build mode as well
625
- try {
626
- const manifestPath = path.join(process.cwd(), 'public', 'assets', manifestUrl);
627
- if (fs.existsSync(manifestPath)) {
628
- const manifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8'));
629
- await writeAssetTypes(manifest, path.dirname(manifestPath));
630
- }
631
- } catch (error) {
632
- logger.error('Caper asset types plugin:: Error generating types during build:', error);
633
- }
634
- await delay(500);
635
- },
636
- async closeBundle() {
637
- if (ispPwaEnabled && env !== 'development') {
638
- logger.info('Caper asset types plugin:: PWA enabled, generating types one last time after bundle');
639
- await generate(manifestUrl);
640
- }
641
- },
642
- };
643
- }
644
-
645
- /**
646
- * Read the assetpack manifest from disk and return the set of bundle names.
647
- * Returns `null` if the manifest doesn't exist yet — in that case the caller
648
- * should skip bundle-name validation rather than spam false warnings.
649
- */
650
- function loadManifestBundleNames(manifestUrl = 'assets.json') {
651
- const manifestPath = path.join(process.cwd(), 'public', 'assets', manifestUrl);
652
- if (!fs.existsSync(manifestPath)) return null;
653
- try {
654
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
655
- const names = new Set();
656
- for (const bundle of manifest.bundles || []) {
657
- if (bundle?.name) names.add(bundle.name);
658
- }
659
- return names;
660
- } catch (e) {
661
- logger.warn(`[caper] Could not parse manifest for build-time validation: ${e.message}`);
662
- return null;
663
- }
664
- }
665
-
666
- /**
667
- * Extract `defaultScene` string and the `plugins: [...]` id list from an
668
- * already-parsed `defineConfig({...})` ObjectExpression AST node. Returns
669
- * `{ defaultScene, pluginIds }` where either may be undefined if absent.
670
- *
671
- * Plugin entries can be either a string literal or a tuple literal whose
672
- * first element is a string literal — anything else (dynamic expressions,
673
- * spreads, non-literal identifiers) is skipped silently, since we can't
674
- * statically know the ID.
675
- */
676
- function extractConfigReferences(configObject) {
677
- const result = { defaultScene: undefined, pluginIds: [] };
678
- if (!configObject || configObject.type !== AST_NODE_TYPES.ObjectExpression) return result;
679
-
680
- for (const prop of configObject.properties) {
681
- if (prop.type !== AST_NODE_TYPES.Property || prop.key?.type !== AST_NODE_TYPES.Identifier) continue;
682
- if (prop.key.name === 'defaultScene' && prop.value?.type === AST_NODE_TYPES.Literal) {
683
- result.defaultScene = prop.value.value;
684
- }
685
- if (prop.key.name === 'plugins' && prop.value?.type === AST_NODE_TYPES.ArrayExpression) {
686
- for (const el of prop.value.elements) {
687
- if (!el) continue;
688
- if (el.type === AST_NODE_TYPES.Literal && typeof el.value === 'string') {
689
- result.pluginIds.push(el.value);
690
- } else if (el.type === AST_NODE_TYPES.ArrayExpression && el.elements[0]?.type === AST_NODE_TYPES.Literal) {
691
- const first = el.elements[0];
692
- if (typeof first.value === 'string') result.pluginIds.push(first.value);
693
- }
694
- }
695
- }
696
- }
697
- return result;
698
- }
699
-
700
- /**
701
- * Locate the `defineConfig({...})` ObjectExpression in a parsed
702
- * `caper.config.ts` AST. Handles both the default-export form
703
- * (`export default defineConfig({...})`) and the named-const form
704
- * (`export const config = defineConfig({...})`).
705
- */
706
- function findConfigObject(ast) {
707
- let configObject;
708
- for (const node of ast.body) {
709
- if (
710
- node.type === AST_NODE_TYPES.ExportDefaultDeclaration &&
711
- node.declaration?.type === AST_NODE_TYPES.CallExpression &&
712
- node.declaration.callee?.name === 'defineConfig'
713
- ) {
714
- configObject = node.declaration.arguments[0];
715
- } else if (
716
- node.type === AST_NODE_TYPES.ExportNamedDeclaration &&
717
- node.declaration?.type === AST_NODE_TYPES.VariableDeclaration
718
- ) {
719
- const decl = node.declaration.declarations.find(
720
- (d) => d.init?.type === AST_NODE_TYPES.CallExpression && d.init.callee?.name === 'defineConfig',
721
- );
722
- if (decl) configObject = decl.init.arguments[0];
723
- }
724
- }
725
- return configObject;
726
- }
727
-
728
- /**
729
- * Boolean build-time flags read straight out of `caper.config.ts`.
730
- *
731
- * Vite fixes a config's plugin list the moment the config object is created —
732
- * no plugin hook can add one later, and the CLI merges `defaultConfig` before
733
- * anything has evaluated the user's config module. So these are pulled with
734
- * the same oxc AST parse discovery already uses rather than by importing the
735
- * file: importing pulls in @caperjs/core, whose @pixi/sound + GSAP deps run
736
- * browser-only top-level side effects that throw under Node (see
737
- * `validateCaperConfig` for the gory details).
738
- *
739
- * Only boolean literals are honoured. A missing, empty, or unparseable
740
- * config silently yields the defaults — this runs before the normal config
741
- * error reporting, so it must never be the thing that fails the build.
742
- */
743
- function readCaperBuildFlags() {
744
- const flags = { useWasm: false };
745
- const configPath = path.resolve(cwd, 'caper.config.ts');
746
- if (!fs.existsSync(configPath)) return flags;
747
-
748
- let configObject;
749
- try {
750
- configObject = findConfigObject(parse(fs.readFileSync(configPath, 'utf-8')));
751
- } catch {
752
- return flags;
753
- }
754
- if (configObject?.type !== AST_NODE_TYPES.ObjectExpression) return flags;
755
-
756
- for (const prop of configObject.properties) {
757
- if (prop.type !== AST_NODE_TYPES.Property || prop.key?.type !== AST_NODE_TYPES.Identifier) continue;
758
- if (!(prop.key.name in flags)) continue;
759
- if (prop.value?.type === AST_NODE_TYPES.Literal && typeof prop.value.value === 'boolean') {
760
- flags[prop.key.name] = prop.value.value;
761
- }
762
- }
763
- return flags;
764
- }
765
-
766
- /**
767
- * Cross-reference validation over discovered scenes/plugins/popups/entities
768
- * + the parsed `caper.config.ts` AST + the assetpack manifest. Emits
769
- * warnings via the project logger; in dev mode also forwards warnings to
770
- * the browser error overlay as info so they're impossible to miss.
771
- *
772
- * Checks:
773
- * 1. Scene `assets.preload.bundles` / `assets.background.bundles` entries
774
- * exist in the assetpack manifest.
775
- * 2. Plugin IDs in `caper.config.ts` match a discovered plugin
776
- * (npm `@caperjs/plugin-*` OR local `src/plugins/*`).
777
- * 3. `defaultScene` in `caper.config.ts` matches a discovered scene ID.
778
- * 4. No duplicate scene / plugin / popup / entity IDs across discovery.
779
- *
780
- * All issues are warnings, not errors — they shouldn't fail the build
781
- * (typos are a dev-time problem; the runtime will still start, just with
782
- * broken references the user needs to see).
783
- */
784
- function runBuildTimeValidation({
785
- server,
786
- configPath,
787
- configObject,
788
- scenes,
789
- plugins,
790
- popups,
791
- entities,
792
- breakpointsName,
793
- }) {
794
- const warnings = [];
795
- const bundleNames = loadManifestBundleNames();
796
-
797
- // 1. Scene bundle references
798
- if (bundleNames && bundleNames.size > 0) {
799
- for (const scene of scenes) {
800
- for (const kind of ['preload', 'background']) {
801
- const bundlesField = scene.assets?.[kind]?.bundles;
802
- if (!bundlesField) continue;
803
- const list = Array.isArray(bundlesField) ? bundlesField : [bundlesField];
804
- for (const bundle of list) {
805
- if (typeof bundle !== 'string') continue;
806
- if (!bundleNames.has(bundle)) {
807
- warnings.push(
808
- `Scene '${scene.id}' references ${kind} bundle '${bundle}' which is not in the assetpack manifest. ` +
809
- `Known bundles: ${[...bundleNames].join(', ') || '(none)'}.`,
810
- );
811
- }
812
- }
813
- }
814
- }
815
- }
816
-
817
- // 2 + 3. caper.config.ts cross-references
818
- const { defaultScene, pluginIds: configPluginIds } = extractConfigReferences(configObject);
819
- const discoveredPluginIds = new Set(plugins.map((p) => p.id));
820
- const discoveredSceneIds = new Set(scenes.map((s) => s.id));
821
-
822
- for (const id of configPluginIds) {
823
- if (!discoveredPluginIds.has(id)) {
824
- warnings.push(
825
- `caper.config.ts references plugin '${id}' which was not discovered. ` +
826
- `Known plugin IDs: ${[...discoveredPluginIds].join(', ') || '(none)'}.`,
827
- );
828
- }
829
- }
830
- if (defaultScene && !discoveredSceneIds.has(defaultScene)) {
831
- warnings.push(
832
- `caper.config.ts defaultScene is '${defaultScene}' but no scene with that id was discovered. ` +
833
- `Known scene IDs: ${[...discoveredSceneIds].join(', ') || '(none)'}.`,
834
- );
835
- }
836
-
837
- // 4. Plugin `requires` cross-references (local plugins only — npm
838
- // plugins can declare requires on the class itself, which the runtime
839
- // topo-sort handles, but the AST can't see).
840
- for (const p of plugins) {
841
- if (!p.isLocal || !Array.isArray(p.requires) || p.requires.length === 0) continue;
842
- for (const req of p.requires) {
843
- if (!discoveredPluginIds.has(req)) {
844
- warnings.push(
845
- `Plugin '${p.id}' requires '${req}' which is not a discovered plugin. ` +
846
- `Known plugin IDs: ${[...discoveredPluginIds].join(', ') || '(none)'}.`,
847
- );
848
- }
849
- }
850
- }
851
-
852
- // 4b. Plugin `requires` cycle detection (build-time, so the dev sees the
853
- // cycle in the terminal/overlay before bootstrap even tries to run).
854
- const cycleEdges = new Map();
855
- for (const p of plugins) {
856
- cycleEdges.set(
857
- p.id,
858
- (p.requires ?? []).filter((r) => discoveredPluginIds.has(r)),
859
- );
860
- }
861
- const cycle = detectCycle(cycleEdges);
862
- if (cycle) {
863
- warnings.push(`Plugin dependency cycle: ${cycle.join(' → ')}`);
864
- }
865
-
866
- // 5. Duplicate-id detection
867
- const checkDuplicates = (label, list) => {
868
- const seen = new Map();
869
- for (const item of list) {
870
- if (seen.has(item.id)) {
871
- warnings.push(`Duplicate ${label} id '${item.id}' — multiple files export the same id.`);
872
- } else {
873
- seen.set(item.id, true);
874
- }
875
- }
876
- };
877
- checkDuplicates('scene', scenes);
878
- checkDuplicates('plugin', plugins);
879
- checkDuplicates('popup', popups);
880
- checkDuplicates('entity', entities);
881
-
882
- // 6. Breakpoints declared in config but not via defineBreakpoints() — the
883
- // names will work at runtime but get no intellisense.
884
- if (!breakpointsName && configObject?.type === AST_NODE_TYPES.ObjectExpression) {
885
- const hasKey = configObject.properties.some(
886
- (p) => p.type === AST_NODE_TYPES.Property && p.key?.name === 'breakpoints',
887
- );
888
- if (hasKey) {
889
- warnings.push(
890
- `caper.config.ts sets \`breakpoints\` but no \`defineBreakpoints()\` export was found — breakpoint names will not be type-checked. Wrap the object: \`export const breakpoints = defineBreakpoints({ ... })\`.`,
891
- );
892
- }
893
- }
894
-
895
- if (warnings.length === 0) return;
896
-
897
- for (const msg of warnings) {
898
- // Vite's createLogger is suppressed inside the dts plugin chain during
899
- // builds, so go straight to console.warn — yellow ANSI so it stands out
900
- // amid Vite's own output.
901
- console.warn(`\x1b[33m[caper] ${msg}\x1b[0m`);
902
- }
903
-
904
- if (server?.ws) {
905
- // Surface the first warning in the browser overlay so a dev in the
906
- // tab notices. Subsequent ones are in the terminal — don't spam the
907
- // overlay with a stack of them.
908
- server.ws.send({
909
- type: 'error',
910
- err: {
911
- message:
912
- `caper build-time validation (${warnings.length} warning${warnings.length === 1 ? '' : 's'}):\n` +
913
- warnings.map((w) => ' • ' + w).join('\n'),
914
- id: configPath,
915
- plugin: 'vite-plugin-caper-config',
916
- },
917
- });
918
- }
919
- }
920
-
921
- /**
922
- * DFS-based cycle detection over a `Map<id, dependencyIds[]>` graph.
923
- * Returns the cycle path (e.g. `['A','B','A']`) on the first cycle found,
924
- * or `null` if the graph is acyclic. Used by build-time validation so a
925
- * plugin requires-cycle surfaces in the terminal before bootstrap runs.
926
- */
927
- function detectCycle(edges) {
928
- const WHITE = 0;
929
- const GRAY = 1;
930
- const BLACK = 2;
931
- const color = new Map();
932
- for (const id of edges.keys()) color.set(id, WHITE);
933
- const stack = [];
934
-
935
- function visit(id) {
936
- if (color.get(id) === GRAY) {
937
- const startIdx = stack.indexOf(id);
938
- return [...stack.slice(startIdx), id];
939
- }
940
- if (color.get(id) === BLACK) return null;
941
- color.set(id, GRAY);
942
- stack.push(id);
943
- for (const dep of edges.get(id) ?? []) {
944
- const found = visit(dep);
945
- if (found) return found;
946
- }
947
- stack.pop();
948
- color.set(id, BLACK);
949
- return null;
950
- }
951
-
952
- for (const id of edges.keys()) {
953
- const found = visit(id);
954
- if (found) return found;
955
- }
956
- return null;
957
- }
958
-
959
- function caperConfigPlugin(isProject = true) {
960
- const virtualModuleId = 'virtual:caper-config';
961
- const resolvedVirtualModuleId = '\0' + virtualModuleId;
962
-
963
- async function generateTypes(server) {
964
- if (!isProject) return { types: 'export {}' };
965
-
966
- const configPath = path.resolve(cwd, 'caper.config.ts');
967
-
968
- let ast;
969
- try {
970
- const content = await fs.promises.readFile(configPath, 'utf-8');
971
- if (!content.trim()) {
972
- // This can happen during file saves, where the file is temporarily empty.
973
- return { types: '/* caper.config.ts is empty, skipping augmentation. */' };
974
- }
975
- ast = parse(content, { jsx: false });
976
- } catch (e) {
977
- if (e.code === 'ENOENT') {
978
- // This can happen during file saves that do a delete/recreate.
979
- return { types: '/* caper.config.ts not found, skipping augmentation. */' };
980
- }
981
- logger.error(`[caper] Error parsing caper.config.ts: ${e.message}`);
982
- if (e.stack) {
983
- logger.error(e.stack);
984
- }
985
- return {
986
- types: '/* Error parsing caper.config.ts, skipping augmentation. See console for details. */',
987
- error: e,
988
- };
989
- }
990
-
991
- let appClassName = 'Application';
992
- let appImportPath = '@caperjs/core';
993
- let dataTypeName = 'Record<string, any>';
994
- let dataSchemaName = '';
995
- let hasActions = false;
996
- let hasContexts = false;
997
- let breakpointsName = '';
998
-
999
- let configObject;
1000
-
1001
- for (const node of ast.body) {
1002
- if (
1003
- node.type === AST_NODE_TYPES.ExportNamedDeclaration &&
1004
- node.declaration?.type === AST_NODE_TYPES.VariableDeclaration
1005
- ) {
1006
- // Find data schema
1007
- const dataDecl = node.declaration.declarations.find(
1008
- (d) => d.init?.type === AST_NODE_TYPES.CallExpression && d.init.callee.name === 'defineData',
1009
- );
1010
- if (dataDecl && dataDecl.id.type === AST_NODE_TYPES.Identifier) {
1011
- dataSchemaName = dataDecl.id.name;
1012
- dataTypeName = `typeof ${dataSchemaName}`;
1013
- }
1014
-
1015
- // Find breakpoints (detect by callee, like defineData — more robust
1016
- // than matching on the variable name).
1017
- const bpDecl = node.declaration.declarations.find(
1018
- (d) => d.init?.type === AST_NODE_TYPES.CallExpression && d.init.callee.name === 'defineBreakpoints',
1019
- );
1020
- if (bpDecl && bpDecl.id.type === AST_NODE_TYPES.Identifier) {
1021
- breakpointsName = bpDecl.id.name;
1022
- }
1023
-
1024
- // Find actions
1025
- if (node.declaration.declarations.some((d) => d.id.name === 'actions')) {
1026
- hasActions = true;
1027
- }
1028
-
1029
- // Find contexts
1030
- if (node.declaration.declarations.some((d) => d.id.name === 'contexts')) {
1031
- hasContexts = true;
1032
- }
1033
-
1034
- // Find defineConfig to get application class
1035
- const configDecl = node.declaration.declarations.find(
1036
- (d) => d.init?.type === AST_NODE_TYPES.CallExpression && d.init.callee.name === 'defineConfig',
1037
- );
1038
- if (configDecl?.init.type === AST_NODE_TYPES.CallExpression) {
1039
- configObject = configDecl.init.arguments[0];
1040
- }
1041
- } else if (
1042
- node.type === AST_NODE_TYPES.ExportDefaultDeclaration &&
1043
- node.declaration.type === AST_NODE_TYPES.CallExpression &&
1044
- node.declaration.callee.name === 'defineConfig'
1045
- ) {
1046
- configObject = node.declaration.arguments[0];
1047
- }
1048
- }
1049
-
1050
- if (configObject?.type === AST_NODE_TYPES.ObjectExpression) {
1051
- const appProperty = configObject.properties.find(
1052
- (p) =>
1053
- p.type === AST_NODE_TYPES.Property &&
1054
- p.key.name === 'application' &&
1055
- p.value.type === AST_NODE_TYPES.Identifier,
1056
- );
1057
-
1058
- if (appProperty) {
1059
- const importedAppName = appProperty.value.name;
1060
- const importDecl = ast.body.find(
1061
- (n) =>
1062
- n.type === AST_NODE_TYPES.ImportDeclaration && n.specifiers.some((s) => s.local.name === importedAppName),
1063
- );
1064
- if (importDecl) {
1065
- appClassName = importedAppName;
1066
- appImportPath = importDecl.source.value;
1067
- }
1068
- }
1069
- }
1070
-
1071
- // Discover scenes, plugins, popups, entities, and locale keys.
1072
- const scenes = await discoverScenes(server);
1073
- const plugins = await discoverPlugins(server);
1074
- const popups = await discoverPopups(server);
1075
- const entities = await discoverEntities(server);
1076
- const uis = await discoverUIs(server);
1077
- const localeKeys = await discoverLocaleKeys(server);
1078
-
1079
- const sceneIds = scenes.filter((s) => s.active !== false).map((s) => s.id);
1080
- const pluginIds = plugins.filter((p) => p.active !== false).map((p) => p.id);
1081
- const popupIds = popups.filter((p) => p.active !== false).map((p) => p.id);
1082
- const entityIds = entities.filter((e) => e.active !== false).map((e) => e.id);
1083
- const uiIds = uis.filter((u) => u.active !== false).map((u) => u.id);
1084
-
1085
- // Build-time cross-reference validation. Warnings for typos that would
1086
- // silently fail at runtime (missing bundles, unknown plugin IDs,
1087
- // defaultScene pointing at a non-existent scene, duplicate IDs).
1088
- runBuildTimeValidation({
1089
- server,
1090
- configPath,
1091
- configObject,
1092
- scenes,
1093
- plugins,
1094
- popups,
1095
- entities,
1096
- breakpointsName,
1097
- });
1098
-
1099
- const sceneIdType = sceneIds.length > 0 ? `\n | '${sceneIds.join("'\n | '")}'` : 'string';
1100
- const pluginIdType = pluginIds.length > 0 ? `\n | '${pluginIds.join("'\n | '")}'` : 'string';
1101
- const popupIdType = popupIds.length > 0 ? `\n | '${popupIds.join("'\n | '")}'` : 'string';
1102
- const entityIdType = entityIds.length > 0 ? `\n | '${entityIds.join("'\n | '")}'` : 'string';
1103
- const uiIdType = uiIds.length > 0 ? `\n | '${uiIds.join("'\n | '")}'` : 'string';
1104
- const localeKeyType = localeKeys.length > 0 ? `\n | '${localeKeys.join("'\n | '")}'` : 'string';
1105
-
1106
- // Emit a keyed `{ id: typeof import('...').default }` map for each of
1107
- // scenes/popups/entities so the framework can derive typed constructor
1108
- // props via `ConstructorParameters<...>[0]` / `InstanceType<...>` at call
1109
- // sites (`this.add.entity(id, props)` etc). Uses the `@/` alias path the
1110
- // discovery already emits — the generated .d.ts lives inside the user's
1111
- // project so tsconfig `paths` applies.
1112
- const buildClassMap = (items) => {
1113
- const active = items.filter((i) => i.active !== false && i.importPath);
1114
- if (active.length === 0) return ' [id: string]: never;';
1115
- return active
1116
- .map((item) => ` ${JSON.stringify(item.id)}: typeof import('${item.importPath}').default;`)
1117
- .join('\n');
1118
- };
1119
-
1120
- const sceneClassMap = buildClassMap(scenes);
1121
- const popupClassMap = buildClassMap(popups);
1122
- const entityClassMap = buildClassMap(entities);
1123
- const uiClassMap = buildClassMap(uis);
1124
-
1125
- const configDir = path.dirname(configPath);
1126
- const dtsDir = path.resolve(configDir, 'src/types');
1127
-
1128
- let relativeAppPath = appImportPath;
1129
- if (appImportPath.startsWith('.')) {
1130
- relativeAppPath = path.relative(dtsDir, path.resolve(configDir, appImportPath)).replace(/\\/g, '/');
1131
- if (relativeAppPath.endsWith('.ts')) {
1132
- relativeAppPath = relativeAppPath.slice(0, -3);
1133
- }
1134
- }
1135
-
1136
- const relativeConfigPath = path.relative(dtsDir, configPath).replace(/\\/g, '/').replace(/\.ts$/, '');
1137
-
1138
- const imports = [];
1139
- if (appClassName === 'Application') {
1140
- imports.push(`import type { Application } from '@caperjs/core';`);
1141
- }
1142
- if (fs.existsSync(configPath)) {
1143
- const configParts = [];
1144
- if (dataSchemaName) {
1145
- configParts.push(dataSchemaName);
1146
- }
1147
- if (hasActions) {
1148
- configParts.push('actions');
1149
- }
1150
- if (hasContexts) {
1151
- configParts.push('contexts');
1152
- }
1153
- if (breakpointsName) {
1154
- configParts.push(breakpointsName);
1155
- }
1156
- if (configParts.length > 0) {
1157
- imports.push(`import type { ${configParts.join(', ')} } from '${relativeConfigPath}';`);
1158
- }
1159
- }
1160
- if (appClassName !== 'Application') {
1161
- imports.push(`import type { ${appClassName} } from '${relativeAppPath}';`);
1162
- }
1163
-
1164
- return {
1165
- types: `
1166
- // This file is auto-generated by caper. Do not edit.
1167
- ${imports.join('\n')}
1168
- // Data
1169
- type AppData = ${dataTypeName};
1170
-
1171
- // Action Contexts
1172
- ${hasContexts ? 'type AppContexts = (typeof contexts)[number];' : 'type AppContexts = string;'}
1173
-
1174
- // Actions
1175
- ${hasActions ? 'type AppActionMap = typeof actions;' : 'type AppActionMap = Record<string, any>;'}
1176
- ${hasActions ? 'type AppActions = keyof AppActionMap;' : 'type AppActions = string;'}
1177
-
1178
- // Scenes
1179
- type AppScenes = ${sceneIdType};
1180
-
1181
- // Plugins
1182
- type AppPlugins = ${pluginIdType};
1183
-
1184
- // Popups
1185
- type AppPopups = ${popupIdType};
1186
-
1187
- // Entities
1188
- type AppEntities = ${entityIdType};
1189
-
1190
- // UI Elements
1191
- type AppUIs = ${uiIdType};
1192
-
1193
- // Class maps — typeof-import pointers to the real discovered classes. The
1194
- // framework uses these with ConstructorParameters<> + InstanceType<> + infer
1195
- // to derive typed props and return types for this.add.entity / popups.show /
1196
- // scenes.load without any AST type extraction.
1197
- type AppSceneClasses = {
1198
- ${sceneClassMap}
1199
- };
1200
-
1201
- type AppPopupClasses = {
1202
- ${popupClassMap}
1203
- };
1204
-
1205
- type AppEntityClasses = {
1206
- ${entityClassMap}
1207
- };
1208
-
1209
- type AppUIClasses = {
1210
- ${uiClassMap}
1211
- };
1212
-
1213
- // Locale keys (flattened dot-paths from src/locales/<reference>.ts)
1214
- type AppLocaleKeys = ${localeKeyType};${breakpointsName ? `\n\n// Breakpoints\ntype AppBreakpoints = keyof (typeof ${breakpointsName})['tiers'] & string;\ntype AppBreakpointModes = keyof (typeof ${breakpointsName})['modes'] & string;` : ''}
1215
-
1216
- /**
1217
- * Add type overrides to the framework
1218
- */
1219
- declare module '@caperjs/core' {
1220
- interface AppTypeOverrides {
1221
- App: ${appClassName};
1222
- Data: AppData;
1223
- Contexts: AppContexts;
1224
- Actions: AppActions;
1225
- ActionMap: AppActionMap;
1226
- Scenes: AppScenes;
1227
- Plugins: AppPlugins;
1228
- Popups: AppPopups;
1229
- Entities: AppEntities;
1230
- SceneClasses: AppSceneClasses;
1231
- PopupClasses: AppPopupClasses;
1232
- EntityClasses: AppEntityClasses;
1233
- UIs: AppUIs;
1234
- UIClasses: AppUIClasses;
1235
- LocaleKeys: AppLocaleKeys;${breakpointsName ? `\n Breakpoints: AppBreakpoints;\n BreakpointModes: AppBreakpointModes;` : ''}
1236
- Eases: Eases;
1237
- }
1238
- }
1239
- `,
1240
- };
1241
- }
1242
-
1243
- async function build(msg = `Building ${DTS_FILE_NAME}`, server) {
1244
- logger.info(msg);
1245
- const { types, error } = await generateTypes(server);
1246
-
1247
- if (error) {
1248
- if (server) {
1249
- server.ws.send({
1250
- type: 'error',
1251
- err: {
1252
- message: error.message,
1253
- stack: error.stack,
1254
- id: path.resolve(cwd, 'caper.config.ts'),
1255
- plugin: 'vite-plugin-caper-config',
1256
- },
1257
- });
1258
- }
1259
- return;
1260
- }
1261
-
1262
- const typesDir = path.resolve(cwd, 'src/types');
1263
- await fs.promises.mkdir(typesDir, { recursive: true });
1264
- await fs.promises.writeFile(path.join(typesDir, DTS_FILE_NAME), types, 'utf-8');
1265
-
1266
- // Dev-only: evaluate + validate the user's config through Vite's SSR
1267
- // module graph. Failures surface in the overlay with file:line detail.
1268
- // Prod builds skip this (no server).
1269
- if (server) {
1270
- await validateCaperConfig(server);
1271
- server.ws.send({ type: 'full-reload' });
1272
- }
1273
- }
1274
-
1275
- return {
1276
- name: 'vite-plugin-caper-config',
1277
- enforce: 'pre',
1278
- resolveId(id) {
1279
- if (id === virtualModuleId) {
1280
- return resolvedVirtualModuleId;
1281
- }
1282
- },
1283
- async load(id) {
1284
- if (id === resolvedVirtualModuleId) {
1285
- return `
1286
- const configModule = await import('/caper.config.ts');
1287
- export default configModule.default;
1288
- `;
1289
- }
1290
- },
1291
- async buildStart() {
1292
- if (!isProject) return;
1293
- await build('Generating types from caper.config.ts');
1294
- },
1295
- configureServer(server) {
1296
- if (!isProject) return;
1297
-
1298
- const configPath = path.resolve(cwd, 'caper.config.ts');
1299
- const scenesDir = path.resolve(cwd, 'src/scenes');
1300
- const pluginsDir = path.resolve(cwd, 'src/plugins');
1301
- const popupsDir = path.resolve(cwd, 'src/popups');
1302
- const entitiesDir = path.resolve(cwd, 'src/entities');
1303
- const localesDir = path.resolve(cwd, 'src/locales');
1304
-
1305
- const handleFileChange = async (file) => {
1306
- const isScene = file.startsWith(scenesDir);
1307
- const isPlugin = file.startsWith(pluginsDir);
1308
- const isPopup = file.startsWith(popupsDir);
1309
- const isEntity = file.startsWith(entitiesDir);
1310
- const isLocale = file.startsWith(localesDir);
1311
- const isConfig = file === configPath;
1312
-
1313
- if (!isScene && !isPlugin && !isPopup && !isEntity && !isLocale && !isConfig) return;
1314
-
1315
- const msg = isScene
1316
- ? 'Scene file changed'
1317
- : isPlugin
1318
- ? 'Plugin file changed'
1319
- : isPopup
1320
- ? 'Popup file changed'
1321
- : isEntity
1322
- ? 'Entity file changed'
1323
- : isLocale
1324
- ? 'Locale file changed'
1325
- : 'Config file changed';
1326
- await build(`${msg}, regenerating types...`, server);
1327
- };
1328
-
1329
- server.watcher.add(configPath);
1330
- server.watcher.add(scenesDir);
1331
- server.watcher.add(pluginsDir);
1332
- server.watcher.add(popupsDir);
1333
- server.watcher.add(entitiesDir);
1334
- server.watcher.add(localesDir);
1335
-
1336
- server.watcher.on('change', handleFileChange);
1337
- server.watcher.on('add', handleFileChange);
1338
- server.watcher.on('unlink', handleFileChange);
1339
- },
1340
- };
1341
- }
1342
-
1343
- /** PLUGINS */
1344
- /**
1345
- * Walks `src/plugins/` recursively, AST-parses each TypeScript file, and
1346
- * returns a list of local plugin metadata objects.
1347
- *
1348
- * - requires `export const <name> = definePlugin({...})` — files without
1349
- * this marker are skipped (so sibling helper modules that incidentally
1350
- * default-export a class don't get phantom-registered as plugins)
1351
- * - requires a default-exported class in the file
1352
- * - honours `export const id`, `export const active`, `export const dynamic`
1353
- * (also flattened from inside `definePlugin({...})` by findExportedConstants)
1354
- * - default is dynamic import (code-split), opt out with `dynamic = false`
1355
- * - plugin ID defaults to exported `id` → class name → filename
1356
- */
1357
- async function discoverLocalPlugins(server) {
1358
- const pluginsDir = path.resolve(process.cwd(), 'src/plugins');
1359
- const plugins = [];
1360
-
1361
- if (!fs.existsSync(pluginsDir)) {
1362
- return [];
1363
- }
1364
-
1365
- const files = await findTypeScriptFiles(pluginsDir);
1366
-
1367
- for (const file of files) {
1368
- try {
1369
- const content = await fs.promises.readFile(file, 'utf-8');
1370
- const ast = parse(content, {
1371
- jsx: false,
1372
- loc: true,
1373
- comment: false,
1374
- });
1375
-
1376
- const hasPluginWrapper = ast.body.some(
1377
- (n) =>
1378
- n.type === AST_NODE_TYPES.ExportNamedDeclaration &&
1379
- n.declaration?.type === AST_NODE_TYPES.VariableDeclaration &&
1380
- n.declaration.declarations.some(
1381
- (d) => d.init?.type === AST_NODE_TYPES.CallExpression && d.init.callee?.name === 'definePlugin',
1382
- ),
1383
- );
1384
- if (!hasPluginWrapper) continue;
1385
-
1386
- const pluginClass = findDefaultExportedClass(ast);
1387
- if (!pluginClass) continue;
1388
-
1389
- const exports = findExportedConstants(ast);
1390
-
1391
- const relativePath = file.replace(process.cwd(), '').replace(/\\/g, '/').split('/src')[1];
1392
- const importPath = `@${relativePath.replace(/\.ts$/, '')}`;
1393
- const id = exports.id || pluginClass.id?.name || path.basename(file, '.ts');
1394
- const name = pluginClass.id?.name || id;
1395
-
1396
- plugins.push({
1397
- id,
1398
- name,
1399
- isLocal: true,
1400
- importPath,
1401
- module:
1402
- exports.dynamic === false
1403
- ? importPath
1404
- : {
1405
- toString: () => `() => import('${importPath}')`,
1406
- isFunction: true,
1407
- },
1408
- active: exports.active === false ? false : true,
1409
- requires: Array.isArray(exports.requires) ? exports.requires.filter((r) => typeof r === 'string') : [],
1410
- });
1411
- } catch (e) {
1412
- const errorMessage = `Error parsing plugin file ${file}: ${e.message}`;
1413
- logger.error(errorMessage);
1414
- if (e.stack) {
1415
- logger.error(e.stack);
1416
- }
1417
- if (server) {
1418
- server.ws.send({
1419
- type: 'error',
1420
- err: {
1421
- message: e.message,
1422
- stack: e.stack,
1423
- id: file,
1424
- plugin: 'vite-plugin-plugins',
1425
- },
1426
- });
1427
- }
1428
- }
1429
- }
1430
-
1431
- return plugins;
1432
- }
1433
-
1434
- /**
1435
- * Discovers npm packages prefixed with `@caperjs/plugin-` in the host
1436
- * project's `package.json`. Always emitted as dynamic imports.
1437
- */
1438
- function discoverNpmPlugins() {
1439
- const packageJsonPath = path.resolve(process.cwd(), 'package.json');
1440
- if (!fs.existsSync(packageJsonPath)) return [];
1441
-
1442
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
1443
- const allDependencies = {
1444
- ...(packageJson.dependencies || {}),
1445
- ...(packageJson.devDependencies || {}),
1446
- };
1447
-
1448
- return Object.keys(allDependencies)
1449
- .filter((dep) => dep.startsWith('@caperjs/plugin-'))
1450
- .map((packageName) => {
1451
- const id = packageName.replace('@caperjs/plugin-', '');
1452
- return {
1453
- id,
1454
- name: packageName,
1455
- isLocal: false,
1456
- importPath: packageName,
1457
- module: {
1458
- toString: () => `() => import('${packageName}')`,
1459
- isFunction: true,
1460
- },
1461
- active: true,
1462
- // npm plugins can declare `requires` on the class itself
1463
- // (`public readonly requires = ['firebase']`); the runtime topo-sort
1464
- // reads from the live instance, so empty here is fine.
1465
- requires: [],
1466
- };
1467
- });
1468
- }
1469
-
1470
- async function discoverPlugins(server) {
1471
- const [local, npm] = [await discoverLocalPlugins(server), discoverNpmPlugins()];
1472
- return [...npm, ...local];
1473
- }
1474
-
1475
- /**
1476
- * Generic recursive-class-file discoverer. Walks a directory, AST-parses
1477
- * each .ts file, requires a default-exported class, and reads `id` /
1478
- * `active` / `dynamic` metadata (either from individual exports or a
1479
- * `defineX({...})` wrapper). Used for popups and entities — same shape
1480
- * as scenes/plugins minus the npm-package discovery path.
1481
- *
1482
- * `defaultDynamic` sets the emit mode when a file doesn't declare it
1483
- * explicitly. Entities default to `false` (static import) because the
1484
- * typed `this.add.entity(id, props)` factory needs sync access to the
1485
- * constructor; popups default to `true` because `show()` is async.
1486
- */
1487
- async function discoverLocalClassFiles({ dir, kind, server, defaultDynamic = true }) {
1488
- const rootDir = path.resolve(process.cwd(), dir);
1489
- if (!fs.existsSync(rootDir)) return [];
1490
-
1491
- const files = await findTypeScriptFiles(rootDir);
1492
- const results = [];
1493
-
1494
- for (const file of files) {
1495
- try {
1496
- const content = await fs.promises.readFile(file, 'utf-8');
1497
- const ast = parse(content);
1498
-
1499
- const cls = findDefaultExportedClass(ast);
1500
- if (!cls) continue;
1501
-
1502
- const exports = findExportedConstants(ast);
1503
-
1504
- const relativePath = file.replace(process.cwd(), '').replace(/\\/g, '/').split('/src')[1];
1505
- const importPath = `@${relativePath.replace(/\.ts$/, '')}`;
1506
- const id = exports.id || cls.id?.name || path.basename(file, '.ts');
1507
- const name = cls.id?.name || id;
1508
- const isDynamic = exports.dynamic !== undefined ? exports.dynamic : defaultDynamic;
1509
-
1510
- results.push({
1511
- id,
1512
- name,
1513
- importPath,
1514
- module: !isDynamic
1515
- ? importPath
1516
- : {
1517
- toString: () => `() => import('${importPath}')`,
1518
- isFunction: true,
1519
- },
1520
- active: exports.active === false ? false : true,
1521
- });
1522
- } catch (e) {
1523
- const errorMessage = `Error parsing ${kind} file ${file}: ${e.message}`;
1524
- logger.error(errorMessage);
1525
- if (e.stack) logger.error(e.stack);
1526
- if (server) {
1527
- server.ws.send({
1528
- type: 'error',
1529
- err: {
1530
- message: e.message,
1531
- stack: e.stack,
1532
- id: file,
1533
- plugin: `vite-plugin-${kind}s`,
1534
- },
1535
- });
1536
- }
1537
- }
1538
- }
1539
-
1540
- return results;
1541
- }
1542
-
1543
- async function discoverPopups(server) {
1544
- return discoverLocalClassFiles({ dir: 'src/popups', kind: 'popup', server });
1545
- }
1546
-
1547
- async function discoverEntities(server) {
1548
- // Entities default to static imports so `this.add.entity(id, props)` can
1549
- // synchronously construct without awaiting a dynamic import. Opt into
1550
- // code-splitting per-entity with `defineEntity({ dynamic: true })`.
1551
- return discoverLocalClassFiles({ dir: 'src/entities', kind: 'entity', server, defaultDynamic: false });
1552
- }
1553
-
1554
- async function discoverUIs(server) {
1555
- // UI elements default to static imports so `this.add.ui(id, props)` can
1556
- // synchronously construct. Same rationale as entities.
1557
- return discoverLocalClassFiles({ dir: 'src/ui', kind: 'ui', server, defaultDynamic: false });
1558
- }
1559
-
1560
- /**
1561
- * Factory for `virtual:caper-popups` / `virtual:caper-entities`.
1562
- * Thin wrapper around a discover function that generates the static-import
1563
- * + dynamic-import `() => import(...)` mix, same shape as the scenes/plugins
1564
- * virtual modules.
1565
- */
1566
- function createClassListPlugin({ virtualModuleId, discoverFn, exportName, pluginName }) {
1567
- const resolvedVirtualModuleId = '\0' + virtualModuleId;
1568
- let server;
1569
-
1570
- const extractClassName = (item) => {
1571
- const basename = path.basename(item.module);
1572
- return basename.replace(/\.ts$/, '');
1573
- };
1574
-
1575
- const generate = (items) => {
1576
- const staticItems = items.filter((i) => i.module && !i.module.isFunction);
1577
- const imports = staticItems.map((i) => `import ${extractClassName(i)} from '${i.module}';`);
1578
-
1579
- return `
1580
- ${imports.join('\n')}
1581
- export const ${exportName} = [
1582
- ${items
1583
- .map((item) => {
1584
- const moduleExpr =
1585
- item.module && !item.module.isFunction
1586
- ? extractClassName(item)
1587
- : (item.module?.toString?.() ?? `() => import('${item.importPath}')`);
1588
- return `{
1589
- id: '${item.id}',
1590
- name: '${item.name}',
1591
- active: ${item.active},
1592
- module: ${moduleExpr}
1593
- }`;
1594
- })
1595
- .join(',\n')}
1596
- ];
1597
- `;
1598
- };
1599
-
1600
- return {
1601
- name: pluginName,
1602
- configureServer(s) {
1603
- server = s;
1604
- },
1605
- resolveId(id) {
1606
- if (id === virtualModuleId) return resolvedVirtualModuleId;
1607
- },
1608
- async load(id) {
1609
- if (id === resolvedVirtualModuleId) {
1610
- const items = await discoverFn(server);
1611
- return generate(items);
1612
- }
1613
- },
1614
- };
1615
- }
1616
-
1617
- export function popupListPlugin(isProject = true) {
1618
- if (!isProject) {
1619
- const virtualModuleId = 'virtual:caper-popups';
1620
- const resolvedVirtualModuleId = '\0' + virtualModuleId;
1621
- return {
1622
- name: 'vite-plugin-popups',
1623
- resolveId: (id) => (id === virtualModuleId ? resolvedVirtualModuleId : undefined),
1624
- load: (id) => (id === resolvedVirtualModuleId ? 'export const popupList = [];' : undefined),
1625
- };
1626
- }
1627
- return createClassListPlugin({
1628
- virtualModuleId: 'virtual:caper-popups',
1629
- discoverFn: discoverPopups,
1630
- exportName: 'popupList',
1631
- pluginName: 'vite-plugin-popups',
1632
- });
1633
- }
1634
-
1635
- export function entityListPlugin(isProject = true) {
1636
- if (!isProject) {
1637
- const virtualModuleId = 'virtual:caper-entities';
1638
- const resolvedVirtualModuleId = '\0' + virtualModuleId;
1639
- return {
1640
- name: 'vite-plugin-entities',
1641
- resolveId: (id) => (id === virtualModuleId ? resolvedVirtualModuleId : undefined),
1642
- load: (id) => (id === resolvedVirtualModuleId ? 'export const entityList = [];' : undefined),
1643
- };
1644
- }
1645
- return createClassListPlugin({
1646
- virtualModuleId: 'virtual:caper-entities',
1647
- discoverFn: discoverEntities,
1648
- exportName: 'entityList',
1649
- pluginName: 'vite-plugin-entities',
1650
- });
1651
- }
1652
-
1653
- export function uiListPlugin(isProject = true) {
1654
- if (!isProject) {
1655
- const virtualModuleId = 'virtual:caper-uis';
1656
- const resolvedVirtualModuleId = '\0' + virtualModuleId;
1657
- return {
1658
- name: 'vite-plugin-uis',
1659
- resolveId: (id) => (id === virtualModuleId ? resolvedVirtualModuleId : undefined),
1660
- load: (id) => (id === resolvedVirtualModuleId ? 'export const uiList = [];' : undefined),
1661
- };
1662
- }
1663
- return createClassListPlugin({
1664
- virtualModuleId: 'virtual:caper-uis',
1665
- discoverFn: discoverUIs,
1666
- exportName: 'uiList',
1667
- pluginName: 'vite-plugin-uis',
1668
- });
1669
- }
1670
-
1671
- /**
1672
- * Walks `src/locales/`, picks a reference locale file (prefers `en.ts`,
1673
- * falls back alphabetically), AST-parses its default-exported object, and
1674
- * flattens every leaf path into a dot-notation key.
1675
- *
1676
- * Given:
1677
- * export default { foo: 'bar', obj: { nested: 'baz' }, replace: { x: '...' } };
1678
- * Returns:
1679
- * ['foo', 'obj.nested', 'replace.x']
1680
- *
1681
- * Only string literal leaves are emitted. Arrays, function calls, and other
1682
- * non-object/non-literal expressions are ignored (they can't be typed
1683
- * meaningfully at this layer).
1684
- */
1685
- async function discoverLocaleKeys(server) {
1686
- const localesDir = path.resolve(process.cwd(), 'src/locales');
1687
- if (!fs.existsSync(localesDir)) return [];
1688
-
1689
- const files = (await fs.promises.readdir(localesDir)).filter((f) => /\.ts$/.test(f)).sort();
1690
- if (files.length === 0) return [];
1691
-
1692
- // Pick the reference file: en.ts if present, else the first alphabetically.
1693
- const referenceFile = files.find((f) => f === 'en.ts') || files[0];
1694
- const filePath = path.join(localesDir, referenceFile);
1695
-
1696
- try {
1697
- const content = await fs.promises.readFile(filePath, 'utf-8');
1698
- if (!content.trim()) return [];
1699
-
1700
- const ast = parse(content);
1701
- const defaultExport = ast.body.find((n) => n.type === AST_NODE_TYPES.ExportDefaultDeclaration);
1702
- if (!defaultExport) return [];
1703
-
1704
- // Allow either `export default { ... }` or `export default satisfies ...`
1705
- let obj = defaultExport.declaration;
1706
- if (obj && obj.type === 'TSSatisfiesExpression') obj = obj.expression;
1707
- if (!obj || obj.type !== AST_NODE_TYPES.ObjectExpression) return [];
1708
-
1709
- const keys = [];
1710
- const walk = (node, prefix) => {
1711
- if (node.type !== AST_NODE_TYPES.ObjectExpression) return;
1712
- for (const prop of node.properties) {
1713
- if (prop.type !== AST_NODE_TYPES.Property) continue;
1714
- let name;
1715
- if (prop.key.type === AST_NODE_TYPES.Identifier) name = prop.key.name;
1716
- else if (prop.key.type === AST_NODE_TYPES.Literal) name = String(prop.key.value);
1717
- else continue;
1718
- const dotPath = prefix ? `${prefix}.${name}` : name;
1719
- if (prop.value.type === AST_NODE_TYPES.ObjectExpression) {
1720
- walk(prop.value, dotPath);
1721
- } else if (prop.value.type === AST_NODE_TYPES.Literal) {
1722
- keys.push(dotPath);
1723
- }
1724
- }
1725
- };
1726
- walk(obj, '');
1727
- return keys.sort();
1728
- } catch (e) {
1729
- const errorMessage = `Error parsing locale file ${filePath}: ${e.message}`;
1730
- logger.error(errorMessage);
1731
- if (server) {
1732
- server.ws.send({
1733
- type: 'error',
1734
- err: {
1735
- message: e.message,
1736
- stack: e.stack,
1737
- id: filePath,
1738
- plugin: 'vite-plugin-locales',
1739
- },
1740
- });
1741
- }
1742
- return [];
1743
- }
1744
- }
1745
-
1746
- /**
1747
- * Virtual module plugin for `virtual:caper-plugins`. Mirrors
1748
- * `sceneListPlugin`: supports static + dynamic imports per entry, preserves
1749
- * the full metadata shape so consumers can filter by `active` / read IDs.
1750
- */
1751
- export function pluginListPlugin(isProject = true) {
1752
- function extractClassName(plugin) {
1753
- // For static (non-function) imports, the module value is the raw import path.
1754
- const basename = path.basename(plugin.module);
1755
- return basename.replace(/\.ts$/, '');
1756
- }
1757
-
1758
- function generatePluginListModule(plugins) {
1759
- const staticPlugins = plugins.filter((p) => p.module && !p.module.isFunction && p.isLocal);
1760
-
1761
- // npm static imports would collide on class-name derivation from the pkg
1762
- // path, so only local plugins support `dynamic = false`. npm packages are
1763
- // always dynamic.
1764
- const imports = staticPlugins.map((p) => `import ${extractClassName(p)} from '${p.module}';`);
1765
-
1766
- return `
1767
- ${imports.join('\n')}
1768
- export const pluginsList = [
1769
- ${plugins
1770
- .map((plugin) => {
1771
- const moduleExpr =
1772
- plugin.module && !plugin.module.isFunction && plugin.isLocal
1773
- ? extractClassName(plugin)
1774
- : (plugin.module?.toString?.() ?? `() => import('${plugin.importPath}')`);
1775
- return `{
1776
- name: '${plugin.name}',
1777
- id: '${plugin.id}',
1778
- isLocal: ${plugin.isLocal},
1779
- active: ${plugin.active},
1780
- requires: ${JSON.stringify(plugin.requires || [])},
1781
- module: ${moduleExpr}
1782
- }`;
1783
- })
1784
- .join(',\n')}
1785
- ];
1786
- `;
1787
- }
1788
-
1789
- const virtualModuleId = 'virtual:caper-plugins';
1790
- const resolvedVirtualModuleId = '\0' + virtualModuleId;
1791
-
1792
- let server;
1793
-
1794
- return {
1795
- name: 'vite-plugin-plugins',
1796
- configureServer(s) {
1797
- server = s;
1798
- },
1799
- resolveId(id) {
1800
- if (id === virtualModuleId) {
1801
- return resolvedVirtualModuleId;
1802
- }
1803
- },
1804
- async load(id) {
1805
- if (id === resolvedVirtualModuleId) {
1806
- const plugins = isProject ? await discoverPlugins(server) : [];
1807
- return generatePluginListModule(plugins);
1808
- }
1809
- },
1810
- };
1811
- }
1812
-
1813
- // scene list plugin
1814
- async function findTypeScriptFiles(dir) {
1815
- const files = [];
1816
-
1817
- async function scan(directory) {
1818
- const entries = await fs.promises.readdir(directory, { withFileTypes: true });
1819
-
1820
- for (const entry of entries) {
1821
- const fullPath = path.join(directory, entry.name);
1822
-
1823
- if (entry.isDirectory()) {
1824
- await scan(fullPath);
1825
- } else if (entry.isFile() && /\.ts?$/.test(entry.name)) {
1826
- files.push(fullPath);
1827
- }
1828
- }
1829
- }
1830
-
1831
- await scan(dir);
1832
- return files;
1833
- }
1834
-
1835
- // Callees whose single argument is treated as the entire config object and
1836
- // unwrapped into the exported constant's value. Keep in sync with the
1837
- // identity helpers in src/utils/define.ts.
1838
- const DEFINE_HELPER_NAMES = new Set(['defineScene', 'definePlugin', 'definePopup', 'defineEntity', 'defineUI']);
1839
-
1840
- function findExportedConstants(ast) {
1841
- const exports = {};
1842
-
1843
- function extractValue(node) {
1844
- switch (node.type) {
1845
- case AST_NODE_TYPES.Literal:
1846
- return node.value;
1847
- case AST_NODE_TYPES.ArrayExpression:
1848
- return node.elements.map((element) => element && extractValue(element)).filter((value) => value !== undefined);
1849
- case AST_NODE_TYPES.ObjectExpression: {
1850
- const obj = {};
1851
- for (const prop of node.properties) {
1852
- if (prop.type === AST_NODE_TYPES.Property && prop.key.type === AST_NODE_TYPES.Identifier) {
1853
- obj[prop.key.name] = extractValue(prop.value);
1854
- }
1855
- }
1856
- return obj;
1857
- }
1858
- case AST_NODE_TYPES.CallExpression: {
1859
- // Unwrap `defineScene({...})` / `definePlugin({...})` / etc.
1860
- // Other call expressions are opaque to discovery.
1861
- const calleeName = node.callee?.name;
1862
- if (calleeName && DEFINE_HELPER_NAMES.has(calleeName) && node.arguments[0]) {
1863
- return extractValue(node.arguments[0]);
1864
- }
1865
- return undefined;
1866
- }
1867
- default:
1868
- return undefined;
1869
- }
1870
- }
1871
-
1872
- for (const node of ast.body) {
1873
- if (
1874
- node.type === AST_NODE_TYPES.ExportNamedDeclaration &&
1875
- node.declaration?.type === AST_NODE_TYPES.VariableDeclaration
1876
- ) {
1877
- for (const declarator of node.declaration.declarations) {
1878
- if (declarator.id.type === AST_NODE_TYPES.Identifier && declarator.init) {
1879
- exports[declarator.id.name] = extractValue(declarator.init);
1880
- }
1881
- }
1882
- }
1883
- }
1884
-
1885
- // Flatten `export const scene = defineScene({...})` / `export const plugin
1886
- // = definePlugin({...})` wrappers onto the top level so discovery code can
1887
- // stay agnostic: `exports.id`, `exports.active`, `exports.assets`, etc.
1888
- // work whether the user wrote individual exports or the helper form.
1889
- // Individual file-level exports take precedence on conflict.
1890
- for (const wrapperKey of ['scene', 'plugin', 'popup', 'entity']) {
1891
- const wrapped = exports[wrapperKey];
1892
- if (wrapped && typeof wrapped === 'object' && !Array.isArray(wrapped)) {
1893
- for (const [k, v] of Object.entries(wrapped)) {
1894
- if (exports[k] === undefined) exports[k] = v;
1895
- }
1896
- }
1897
- }
1898
-
1899
- return exports;
1900
- }
1901
-
1902
- /**
1903
- * Finds the file's default-exported class. Handles both common forms:
1904
- *
1905
- * // inline
1906
- * export default class Foo extends Bar { ... }
1907
- *
1908
- * // named declaration + separate default export
1909
- * export class Foo extends Bar { ... }
1910
- * export default Foo;
1911
- *
1912
- * // or unexported named class
1913
- * class Foo extends Bar { ... }
1914
- * export default Foo;
1915
- *
1916
- * The separate-default form requires a second pass to resolve the
1917
- * identifier back to a matching `ClassDeclaration` in the same file.
1918
- */
1919
- function findDefaultExportedClass(ast) {
1920
- let identifierName = null;
1921
-
1922
- for (const node of ast.body) {
1923
- if (node.type !== AST_NODE_TYPES.ExportDefaultDeclaration) continue;
1924
- // Form 1: `export default class Foo { ... }`
1925
- if (node.declaration.type === AST_NODE_TYPES.ClassDeclaration) {
1926
- return node.declaration;
1927
- }
1928
- // Form 2/3: `export default Foo;` — remember the name to resolve below.
1929
- if (node.declaration.type === AST_NODE_TYPES.Identifier) {
1930
- identifierName = node.declaration.name;
1931
- break;
1932
- }
1933
- }
1934
- if (!identifierName) return null;
1935
-
1936
- // Resolve the identifier to a class declaration in the same file. Accept
1937
- // either a bare `class Foo {}` or an `export class Foo {}` form.
1938
- for (const node of ast.body) {
1939
- if (node.type === AST_NODE_TYPES.ClassDeclaration && node.id?.name === identifierName) {
1940
- return node;
1941
- }
1942
- if (
1943
- node.type === AST_NODE_TYPES.ExportNamedDeclaration &&
1944
- node.declaration?.type === AST_NODE_TYPES.ClassDeclaration &&
1945
- node.declaration.id?.name === identifierName
1946
- ) {
1947
- return node.declaration;
1948
- }
1949
- }
1950
-
1951
- return null;
1952
- }
1953
-
1954
- // Back-compat alias — scene code historically called `findDefaultExportedScene`.
1955
- const findDefaultExportedScene = findDefaultExportedClass;
1956
-
1957
- async function discoverScenes(server) {
1958
- const scenesDir = path.resolve(process.cwd(), 'src/scenes');
1959
- const scenes = [];
1960
-
1961
- if (!fs.existsSync(scenesDir)) {
1962
- return [];
1963
- }
1964
-
1965
- const files = await findTypeScriptFiles(scenesDir);
1966
-
1967
- for (const file of files) {
1968
- try {
1969
- const content = await fs.promises.readFile(file, 'utf-8');
1970
- const ast = parse(content, {
1971
- jsx: false,
1972
- loc: true,
1973
- comment: false,
1974
- });
1975
-
1976
- const sceneClass = findDefaultExportedScene(ast);
1977
- if (!sceneClass) continue;
1978
-
1979
- const exports = findExportedConstants(ast);
1980
-
1981
- const relativePath = file.replace(process.cwd(), '').replace(/\\/g, '/').split('/src')[1];
1982
- // remove /src — the runtime import uses the raw alias (Vite resolves
1983
- // `.ts` automatically) while the typeof-import codegen needs the
1984
- // extension stripped so TypeScript's path-alias resolution finds it.
1985
- const importPath = `@${relativePath}`;
1986
- const importPathForTypes = importPath.replace(/\.ts$/, '');
1987
- const id = exports.id || sceneClass.id?.name || path.basename(file, '.ts');
1988
-
1989
- scenes.push({
1990
- id,
1991
- importPath: importPathForTypes,
1992
- module:
1993
- exports.dynamic === false
1994
- ? importPath
1995
- : {
1996
- toString: () => `() => import('${importPath}')`,
1997
- isFunction: true, // Add a flag to identify dynamic imports
1998
- },
1999
- active: exports.active === false ? false : true,
2000
- debugLabel: exports.debug?.label || id,
2001
- debugGroup: exports.debug?.group || undefined,
2002
- debugOrder: exports.debug?.order >= 0 ? exports.debug.order : Number.MAX_SAFE_INTEGER,
2003
- assets: exports.assets ?? undefined,
2004
- plugins: exports.plugins ?? undefined,
2005
- autoUnloadAssets: exports.assets?.autoUnload ?? false,
2006
- });
2007
- } catch (e) {
2008
- const errorMessage = `Error parsing scene file ${file}: ${e.message}`;
2009
- logger.error(errorMessage);
2010
- if (e.stack) {
2011
- logger.error(e.stack);
2012
- }
2013
- if (server) {
2014
- server.ws.send({
2015
- type: 'error',
2016
- err: {
2017
- message: e.message,
2018
- stack: e.stack,
2019
- id: file,
2020
- plugin: 'vite-plugin-scenes',
2021
- },
2022
- });
2023
- }
2024
- }
2025
- }
2026
- return scenes;
2027
- }
2028
-
2029
- export function sceneListPlugin(isProject = true) {
2030
- function extractClassName(scene) {
2031
- const basename = path.basename(scene.module);
2032
- // remove .ts
2033
- return basename.replace('.ts', '');
2034
- }
2035
-
2036
- function generateSceneListModule(scenes) {
2037
- // extract non function scenes from the list
2038
- const nonFunctionScenes = scenes.filter((scene) => !scene.module.isFunction);
2039
-
2040
- const imports = nonFunctionScenes.map((scene) => `import ${extractClassName(scene)} from '${scene.module}';`);
2041
-
2042
- const result = `
2043
- ${imports.join('\n')}
2044
- export const sceneList = [
2045
- ${scenes
2046
- .map(
2047
- (scene) => `{
2048
- id: '${scene.id}',
2049
- active: ${scene.active},
2050
- module: ${scene.module.isFunction ? scene.module.toString() : extractClassName(scene)},
2051
- debugLabel: ${JSON.stringify(scene.debugLabel)},
2052
- debugGroup: ${JSON.stringify(scene.debugGroup)},
2053
- debugOrder: ${scene.debugOrder},
2054
- assets: ${JSON.stringify(scene.assets)},
2055
- plugins: ${JSON.stringify(scene.plugins)},
2056
- autoUnloadAssets: ${scene.autoUnloadAssets}
2057
- }`,
2058
- )
2059
- .join(',\n')}
2060
- ];
2061
- `;
2062
- return result;
2063
- }
2064
-
2065
- const virtualModuleId = 'virtual:caper-scenes';
2066
- const resolvedVirtualModuleId = '\0' + virtualModuleId;
2067
-
2068
- let server;
2069
-
2070
- return {
2071
- name: 'vite-plugin-scenes',
2072
- configureServer(s) {
2073
- server = s;
2074
- },
2075
- resolveId(id) {
2076
- if (id === virtualModuleId) {
2077
- return resolvedVirtualModuleId;
2078
- }
2079
- },
2080
- async load(id) {
2081
- if (id === resolvedVirtualModuleId) {
2082
- let scenes = [];
2083
- if (isProject) {
2084
- scenes = await discoverScenes(server);
2085
- }
2086
- return generateSceneListModule(scenes);
2087
- }
2088
- },
2089
- };
2090
- }
2091
-
2092
- function createCaperRuntimePlugin() {
2093
- const virtualModuleId = 'caper-runtime';
2094
- const resolvedVirtualModuleId = '\0' + virtualModuleId;
2095
-
2096
- return {
2097
- name: 'vite-plugin-caper-runtime',
2098
- enforce: 'pre',
2099
- // Auto-inject the runtime entry so apps don't need a hand-written
2100
- // `src/index.ts` that just does `import('caper-runtime')`. Legacy HTML that
2101
- // already references the runtime (or a src/index.(ts|js) entry) is left
2102
- // untouched so existing apps keep working.
2103
- transformIndexHtml: {
2104
- order: 'pre',
2105
- handler(html) {
2106
- const referencesRuntime = html.includes('caper-runtime');
2107
- const referencesLegacyEntry = /<script[^>]*\bsrc=["'][^"']*src\/index\.(ts|js)["']/.test(html);
2108
- if (referencesRuntime || referencesLegacyEntry) {
2109
- return html;
2110
- }
2111
- return {
2112
- html,
2113
- tags: [
2114
- {
2115
- tag: 'script',
2116
- attrs: { type: 'module' },
2117
- children: 'import("caper-runtime");',
2118
- injectTo: 'body',
2119
- },
2120
- ],
2121
- };
2122
- },
2123
- },
2124
- resolveId(id) {
2125
- if (id === virtualModuleId) {
2126
- return resolvedVirtualModuleId;
2127
- }
2128
- },
2129
- load(id) {
2130
- if (id === resolvedVirtualModuleId) {
2131
- return `
2132
- import { pluginsList } from 'virtual:caper-plugins';
2133
- import { sceneList } from 'virtual:caper-scenes';
2134
- import { popupList } from 'virtual:caper-popups';
2135
- import { entityList } from 'virtual:caper-entities';
2136
- import { uiList } from 'virtual:caper-uis';
2137
- import { create, signalCaperReady, installCaperGlobal } from '@caperjs/core';
2138
-
2139
- (globalThis).Caper = (globalThis).Caper || {};
2140
-
2141
- // Install Caper.apps / Caper.ready() / Caper.automation immediately
2142
- // so automation drivers can await Caper.ready() before boot finishes.
2143
- installCaperGlobal();
2144
-
2145
- try {
2146
- (globalThis).Caper.APP_NAME = __CAPER_APP_NAME;
2147
- (globalThis).Caper.APP_VERSION = __CAPER_APP_VERSION;
2148
- } catch (e) {
2149
- console.error('Failed to set app name and version', e);
2150
- }
2151
-
2152
- (globalThis).Caper.sceneList = sceneList;
2153
- (globalThis).Caper.pluginsList = pluginsList;
2154
- (globalThis).Caper.popupList = popupList;
2155
- (globalThis).Caper.entityList = entityList;
2156
- (globalThis).Caper.uiList = uiList;
2157
-
2158
- (globalThis).Caper.sceneIds = sceneList.map((scene) => scene.id);
2159
- (globalThis).Caper.pluginIds = pluginsList.map((plugin) => plugin.id);
2160
- (globalThis).Caper.popupIds = popupList.map((popup) => popup.id);
2161
- (globalThis).Caper.entityIds = entityList.map((entity) => entity.id);
2162
- (globalThis).Caper.uiIds = uiList.map((ui) => ui.id);
2163
-
2164
- (globalThis).Caper.get = function (key) {
2165
- (globalThis).Caper = (globalThis).Caper || {};
2166
- return key ? (globalThis).Caper[key] : (globalThis).Caper;
2167
- };
2168
-
2169
- async function bootstrap() {
2170
- const configModule = await import('virtual:caper-config');
2171
- const config = configModule.default;
2172
- (globalThis).Caper.config = config;
2173
- const app = await create(config);
2174
- const mains = import.meta.glob('/src/main.ts', { eager: true });
2175
- const mainPath = Object.keys(mains)[0];
2176
-
2177
- if (mainPath) {
2178
- const mainModule = mains[mainPath];
2179
- if (mainModule && typeof mainModule.default === 'function') {
2180
- await mainModule.default(app);
2181
- }
2182
- }
2183
-
2184
- signalCaperReady(app);
2185
- }
2186
-
2187
- // Pass dev-ness through to the framework. This virtual module is
2188
- // transformed in the CONSUMER app's vite context, so import.meta.env
2189
- // is real here — inside the pre-built framework lib it has already
2190
- // been compiled away, so globals.ts reads Caper.__dev instead.
2191
- (globalThis).Caper.__dev = !!import.meta.env.DEV;
2192
-
2193
- // Mark this app as managed by the vite runtime so create() does not
2194
- // double-signal readiness, then guard against a double bootstrap.
2195
- // (ES module caching already prevents re-evaluating this id; this is
2196
- // belt-and-braces.)
2197
- (globalThis).Caper.__runtimeManaged = true;
2198
- if (!(globalThis).__CAPER_BOOTSTRAPPED__) {
2199
- (globalThis).__CAPER_BOOTSTRAPPED__ = true;
2200
- bootstrap();
2201
- }
2202
- `;
2203
- }
2204
- },
2205
- };
2206
- }
2207
-
2208
- function createCaperPWAPlugin() {
2209
- const entryId = 'caper-pwa';
2210
- const resolvedVirtualModuleId = '\0' + entryId;
2211
-
2212
- return {
2213
- name: 'vite-virtual-entry',
2214
- enforce: 'pre',
2215
- resolveId(id) {
2216
- if (id === entryId) {
2217
- return resolvedVirtualModuleId;
2218
- }
2219
- },
2220
- load(id) {
2221
- if (id === resolvedVirtualModuleId) {
2222
- return `
2223
- import {pwaInfo} from 'virtual:pwa-info';
2224
- import {registerSW} from 'virtual:pwa-register';
2225
-
2226
- window.Caper = window.Caper || {};
2227
- window.Caper.pwa = window.Caper.pwa || {};
2228
-
2229
- window.Caper.pwa.info = pwaInfo;
2230
-
2231
- window.Caper.pwa.onRegisteredSW = function(swScriptUrl){
2232
- console.log('Caper PWA: SW registered: ', swScriptUrl)
2233
- }
2234
-
2235
- window.Caper.pwa.offlineReady = function(){
2236
- console.log('Caper PWA: ready to work offline')
2237
- }
2238
-
2239
- window.Caper.pwa.register = function(){
2240
- registerSW({
2241
- immediate: true,
2242
- onRegisteredSW(swScriptUrl) {
2243
- window.Caper.pwa.onRegisteredSW(swScriptUrl)
2244
- },
2245
- onOfflineReady() {
2246
- window.Caper.pwa.offlineReady()
2247
- },
2248
- });
2249
- }
2250
- `;
2251
- }
2252
- },
2253
- config(config) {
2254
- const input = config.build?.rolldownOptions?.input || config.build?.rollupOptions?.input || 'index.html';
2255
- const inputs = Array.isArray(input) ? input : [input];
2256
-
2257
- return {
2258
- build: {
2259
- rolldownOptions: {
2260
- input: [entryId, ...inputs],
2261
- output: {
2262
- entryFileNames: (chunkInfo) => {
2263
- if (chunkInfo.facadeModuleId?.includes('caper-pqa')) {
2264
- return 'assets/caper-[hash].js';
2265
- }
2266
- return 'assets/[name]-[hash].js';
2267
- },
2268
- },
2269
- },
2270
- },
2271
- };
2272
- },
2273
- };
2274
- }
2275
-
2276
- function caperDevHelperPlugin() {
2277
- return {
2278
- name: 'vite-plugin-caper-dev-helper',
2279
- configureServer(server) {
2280
- server.ws.on('caper:show-error', (data) => {
2281
- const { error } = data;
2282
- // Send the 'error' event back to the client
2283
- server.ws.send({
2284
- type: 'error',
2285
- err: {
2286
- message: error.message || 'An unknown error occurred.',
2287
- stack: error.stack || new Error(error.message).stack,
2288
- id: error.id,
2289
- loc: {
2290
- file: error.id,
2291
- line: error.line,
2292
- column: error.column,
2293
- },
2294
- plugin: 'caper-dev-helper',
2295
- },
2296
- });
2297
- });
2298
- },
2299
- };
2300
- }
2301
-
2302
- /** END PLUGINS */
2303
-
2304
- /** CONFIG */
2305
- const buildFlags = readCaperBuildFlags();
2306
-
2307
- /**
2308
- * @type {Partial<import('vite').UserConfig>}
2309
- */
2310
- const defaultConfig = {
2311
- cacheDir: '.cache',
2312
- logLevel: 'info',
2313
- publicDir: './public',
2314
- base: env === 'development' ? '/' : './',
2315
- server: {
2316
- port: 3000,
2317
- host: true,
2318
- open: true,
2319
- },
2320
- preview: {
2321
- host: true,
2322
- port: 8080,
2323
- },
2324
- build: {
2325
- sourcemap: env === 'development',
2326
- rolldownOptions: {
2327
- external: ['caper-globals'],
2328
- output: {
2329
- // Rolldown requires manualChunks as a function (Rollup allowed an object).
2330
- manualChunks(id) {
2331
- if (id.includes('node_modules/gsap/')) return 'gsap';
2332
- },
2333
- chunkFileNames: 'assets/[name]-[hash].js',
2334
- entryFileNames: 'assets/[name]-[hash].js',
2335
- assetFileNames: 'assets/[name]-[hash][extname]',
2336
- },
2337
- },
2338
- },
2339
- plugins: [
2340
- // wasm() only matters for ESM-integration `.wasm` imports (a bare
2341
- // `import init from './foo.wasm'`), and it costs an enforce:'pre'
2342
- // resolveId+load hook on every module in the graph. Nothing in caper
2343
- // needs it: Rive fetches its wasm at runtime via `locateFile`, pixi's
2344
- // KTX/basis transcoders fetch theirs from a CDN, spine-core is pure JS,
2345
- // and Vite 8 handles `?init` / `?url` wasm natively. Off by default —
2346
- // set `useWasm: true` in caper.config.ts if your project imports a
2347
- // .wasm module directly.
2348
- ...(buildFlags.useWasm ? [wasm()] : []),
2349
- // vite-plugin-top-level-await dropped in Vite 8 upgrade: Chrome 111 /
2350
- // Safari 16.4 default targets support TLA natively.
2351
- // vite-plugin-html dropped: was called with no args, Vite handles
2352
- // HTML transforms natively.
2353
- createCaperRuntimePlugin(),
2354
- viteStaticCopy({
2355
- targets: [
2356
- {
2357
- src: './node_modules/@caperjs/core/src/plugins/captions/font/*.*',
2358
- dest: './assets/caper/font',
2359
- },
2360
- ],
2361
- }),
2362
- pluginListPlugin(),
2363
- sceneListPlugin(),
2364
- popupListPlugin(),
2365
- entityListPlugin(),
2366
- uiListPlugin(),
2367
- assetpackPlugin(),
2368
- assetTypesPlugin(),
2369
- caperConfigPlugin(),
2370
- caperDevHelperPlugin(),
2371
- ],
2372
- resolve: {
2373
- alias: {
2374
- '@': path.resolve(cwd, './src'),
2375
- },
2376
- // Force a single instance of each singleton lib. pixi keeps global
2377
- // registries (extensions, TextureSource cache, Ticker.shared) and relies on
2378
- // instanceof; two copies (via linked/transitive installs) split its state.
2379
- dedupe: ['pixi.js', 'gsap', '@pixi/sound'],
2380
- },
2381
- // Don't prebundle @pixi/ui: esbuild inlines its own copy of pixi.js into the
2382
- // dep chunk, giving two pixi instances — every cross-boundary
2383
- // `instanceof Texture/Sprite` then fails. Served as source, its
2384
- // `import "pixi.js"` resolves to the same optimized pixi as the app. Its
2385
- // nested typed-signals dep (CJS) still needs prebundling for interop.
2386
- optimizeDeps: {
2387
- exclude: ['@pixi/ui'],
2388
- include: ['@pixi/ui > typed-signals'],
2389
- },
2390
- define: {
2391
- __CAPER_APP_NAME: JSON.stringify(process.env.npm_package_name),
2392
- __CAPER_APP_VERSION: JSON.stringify(process.env.npm_package_version),
2393
- },
2394
- };
2395
-
2396
- // config without assetpack plugin
2397
- const noAssetpackConfig = { ...defaultConfig };
2398
- // remove assetpack plugin and asset types plugin
2399
- noAssetpackConfig.plugins = noAssetpackConfig.plugins.filter(
2400
- (plugin) => plugin.name !== 'vite-plugin-assetpack' && plugin.name !== 'vite-plugin-asset-types',
2401
- );
2402
-
2403
- // withPWA
2404
- const injectRegister = process.env.SW_INLINE ?? 'auto';
2405
- const selfDestroying = process.env.SW_DESTROY === 'true';
2406
- /**
2407
- * @type {Partial<import('vite-plugin-pwa').VitePWAOptions>}
2408
- */
2409
- const defaultPWAConfig = {
2410
- base: '/',
2411
- buildBase: './',
2412
- registerType: 'autoUpdate',
2413
- injectRegister,
2414
- selfDestroying,
2415
- devOptions: {
2416
- enabled: process.env.SW_DEV === 'true',
2417
- navigateFallback: 'index.html',
2418
- suppressWarnings: true,
2419
- },
2420
- manifest: {
2421
- name: process.env.npm_package_name,
2422
- short_name: process.env.npm_package_name,
2423
- description: process.env.npm_package_description,
2424
- theme_color: '#ffffff',
2425
- background_color: '#000000',
2426
- display: 'fullscreen',
2427
- orientation: 'portrait',
2428
- categories: ['game', 'application'],
2429
- },
2430
- };
2431
-
2432
- /**
2433
- * @param {Partial<import('vite-plugin-pwa').VitePWAOptions>} userPWAConfig
2434
- * @param {Partial<import('vite').UserConfig>} userConfig
2435
- * @returns {import('vite').UserConfig}
2436
- */
2437
- function withPWA(userPWAConfig = {}, userConfig = {}) {
2438
- const pwaConfig = {
2439
- ...defaultPWAConfig,
2440
- ...userPWAConfig,
2441
- devOptions: { ...defaultPWAConfig.devOptions, ...userPWAConfig.devOptions },
2442
- manifest: {
2443
- ...defaultPWAConfig.manifest,
2444
- ...(userPWAConfig?.manifest ?? {}),
2445
- icons: [...(userPWAConfig?.manifest?.icons ?? [])],
2446
- },
2447
- };
2448
- const config = mergeConfig(defaultConfig, userConfig);
2449
- config.plugins.push(VitePWA(pwaConfig));
2450
- config.plugins.push(createCaperPWAPlugin());
2451
- return config;
2452
- }
2453
-
2454
- /**
2455
- * @param {Partial<import('vite').UserConfig>} userConfig
2456
- * @returns {import('vite').UserConfig}
2457
- */
2458
- function extendConfig(userConfig = {}) {
2459
- return mergeConfig(defaultConfig, userConfig);
2460
- }
2461
-
2462
- export { defaultConfig, extendConfig, noAssetpackConfig, withPWA };
2463
-
2464
- export default defaultConfig;