@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
@@ -0,0 +1,489 @@
1
+ /**
2
+ * The `virtual:caper-config` module plus everything that guards it: schema
3
+ * validation of `caper.config.ts`, the build-time id/bundle/cycle checks, and the
4
+ * generated `caper-app.d.ts` that types scene ids, action names and data keys.
5
+ *
6
+ * The config is loaded through vite's own `ssrLoadModule` rather than imported,
7
+ * so the project's TypeScript and path aliases resolve the same way they do for
8
+ * the app itself.
9
+ */
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { AST_NODE_TYPES, extractConfigReferences, findConfigObject, parse } from '../internal/ast.mjs';
13
+ import {
14
+ discoverEntities,
15
+ discoverLocaleKeys,
16
+ discoverPlugins,
17
+ discoverPopups,
18
+ discoverScenes,
19
+ discoverUIs,
20
+ } from '../internal/discovery.mjs';
21
+ import { caperConfigSchema } from '../internal/schema.mjs';
22
+ import { cwd, DTS_FILE_NAME, logger } from '../internal/util.mjs';
23
+ import { runBuildTimeValidation } from '../internal/validate.mjs';
24
+ import { loadManifestBundleNames } from '../internal/manifest.mjs';
25
+
26
+ async function validateCaperConfig(server, root) {
27
+ if (!server || typeof server.ssrLoadModule !== 'function') return true;
28
+ const configPath = path.resolve(root, 'caper.config.ts');
29
+ if (!fs.existsSync(configPath)) return true;
30
+
31
+ // caper.config.ts pulls in @caperjs/core, which statically bundles
32
+ // @pixi/sound and GSAP. Both run browser-only top-level side effects
33
+ // during module init, which throw one after another under Vite SSR
34
+ // (Node, no DOM) and abort the whole ssrLoadModule — so config
35
+ // validation silently never runs. Install minimal stubs just for the
36
+ // duration of the load, and only for whichever globals aren't already
37
+ // defined (jsdom, a future Vite DOM environment, etc.).
38
+ //
39
+ // - document.createElement('audio').canPlayType: @pixi/sound's
40
+ // utils/supported.mjs probes playable formats at module top level.
41
+ // - createElement(...).style: GSAP's CSSPlugin auto-registers at
42
+ // import time and does `'transform' in tempDiv.style` on a div it
43
+ // creates via document.createElement — needs a `style` object (any
44
+ // object satisfies the `in` check; contents are never read here).
45
+ // - globalThis.window: @pixi/sound's WebAudioContext/SoundLibrary
46
+ // singleton construction reads `window` at module top level.
47
+ const hadDocument = 'document' in globalThis;
48
+ const hadWindow = 'window' in globalThis;
49
+ if (!hadDocument) {
50
+ globalThis.document = {
51
+ createElement: () => ({ canPlayType: () => '', style: {} }),
52
+ addEventListener: () => {},
53
+ removeEventListener: () => {},
54
+ };
55
+ }
56
+ if (!hadWindow) {
57
+ globalThis.window = globalThis;
58
+ }
59
+
60
+ let mod;
61
+ try {
62
+ mod = await server.ssrLoadModule(configPath);
63
+ } catch {
64
+ // Config failed to load — the normal type-regen path already surfaces
65
+ // this error through the websocket overlay, so don't double-report.
66
+ return false;
67
+ } finally {
68
+ if (!hadDocument) delete globalThis.document;
69
+ if (!hadWindow) delete globalThis.window;
70
+ }
71
+ const cfg = mod.default;
72
+ if (!cfg) return true;
73
+
74
+ const result = caperConfigSchema.safeParse(cfg);
75
+ if (result.success) return true;
76
+
77
+ const issues = result.error.issues
78
+ .map((i) => ` - ${i.path.length ? i.path.join('.') : '(root)'}: ${i.message}`)
79
+ .join('\n');
80
+ const message = `Invalid caper.config.ts:\n${issues}`;
81
+ logger.error(`[caper] ${message}`);
82
+
83
+ server.ws.send({
84
+ type: 'error',
85
+ err: {
86
+ message,
87
+ id: configPath,
88
+ plugin: 'vite-plugin-caper-config',
89
+ },
90
+ });
91
+ return false;
92
+ }
93
+
94
+ // write a debounce function
95
+
96
+ export function caperConfigPlugin(isProject = true) {
97
+ // vite's resolved project root. Everything below resolves against this rather
98
+ // than process.cwd(), so `vite --root elsewhere` and monorepo invocations from
99
+ // a parent directory find the right caper.config.ts and src/ tree.
100
+ let root = cwd;
101
+ const virtualModuleId = 'virtual:caper-config';
102
+ const resolvedVirtualModuleId = '\0' + virtualModuleId;
103
+
104
+ async function generateTypes(server, root) {
105
+ if (!isProject) return { types: 'export {}' };
106
+
107
+ const configPath = path.resolve(root, 'caper.config.ts');
108
+
109
+ let ast;
110
+ try {
111
+ const content = await fs.promises.readFile(configPath, 'utf-8');
112
+ if (!content.trim()) {
113
+ // This can happen during file saves, where the file is temporarily empty.
114
+ return { types: '/* caper.config.ts is empty, skipping augmentation. */' };
115
+ }
116
+ ast = parse(content, { jsx: false });
117
+ } catch (e) {
118
+ if (e.code === 'ENOENT') {
119
+ // This can happen during file saves that do a delete/recreate.
120
+ return { types: '/* caper.config.ts not found, skipping augmentation. */' };
121
+ }
122
+ logger.error(`[caper] Error parsing caper.config.ts: ${e.message}`);
123
+ if (e.stack) {
124
+ logger.error(e.stack);
125
+ }
126
+ return {
127
+ types: '/* Error parsing caper.config.ts, skipping augmentation. See console for details. */',
128
+ error: e,
129
+ };
130
+ }
131
+
132
+ let appClassName = 'Application';
133
+ let appImportPath = '@caperjs/core';
134
+ let dataTypeName = 'Record<string, any>';
135
+ let dataSchemaName = '';
136
+ let hasActions = false;
137
+ let hasContexts = false;
138
+ let breakpointsName = '';
139
+
140
+ let configObject;
141
+
142
+ for (const node of ast.body) {
143
+ if (
144
+ node.type === AST_NODE_TYPES.ExportNamedDeclaration &&
145
+ node.declaration?.type === AST_NODE_TYPES.VariableDeclaration
146
+ ) {
147
+ // Find data schema
148
+ const dataDecl = node.declaration.declarations.find(
149
+ (d) => d.init?.type === AST_NODE_TYPES.CallExpression && d.init.callee.name === 'defineData',
150
+ );
151
+ if (dataDecl && dataDecl.id.type === AST_NODE_TYPES.Identifier) {
152
+ dataSchemaName = dataDecl.id.name;
153
+ dataTypeName = `typeof ${dataSchemaName}`;
154
+ }
155
+
156
+ // Find breakpoints (detect by callee, like defineData — more robust
157
+ // than matching on the variable name).
158
+ const bpDecl = node.declaration.declarations.find(
159
+ (d) => d.init?.type === AST_NODE_TYPES.CallExpression && d.init.callee.name === 'defineBreakpoints',
160
+ );
161
+ if (bpDecl && bpDecl.id.type === AST_NODE_TYPES.Identifier) {
162
+ breakpointsName = bpDecl.id.name;
163
+ }
164
+
165
+ // Find actions
166
+ if (node.declaration.declarations.some((d) => d.id.name === 'actions')) {
167
+ hasActions = true;
168
+ }
169
+
170
+ // Find contexts
171
+ if (node.declaration.declarations.some((d) => d.id.name === 'contexts')) {
172
+ hasContexts = true;
173
+ }
174
+
175
+ // Find defineConfig to get application class
176
+ const configDecl = node.declaration.declarations.find(
177
+ (d) => d.init?.type === AST_NODE_TYPES.CallExpression && d.init.callee.name === 'defineConfig',
178
+ );
179
+ if (configDecl?.init.type === AST_NODE_TYPES.CallExpression) {
180
+ configObject = configDecl.init.arguments[0];
181
+ }
182
+ } else if (
183
+ node.type === AST_NODE_TYPES.ExportDefaultDeclaration &&
184
+ node.declaration.type === AST_NODE_TYPES.CallExpression &&
185
+ node.declaration.callee.name === 'defineConfig'
186
+ ) {
187
+ configObject = node.declaration.arguments[0];
188
+ }
189
+ }
190
+
191
+ if (configObject?.type === AST_NODE_TYPES.ObjectExpression) {
192
+ const appProperty = configObject.properties.find(
193
+ (p) =>
194
+ p.type === AST_NODE_TYPES.Property &&
195
+ p.key.name === 'application' &&
196
+ p.value.type === AST_NODE_TYPES.Identifier,
197
+ );
198
+
199
+ if (appProperty) {
200
+ const importedAppName = appProperty.value.name;
201
+ const importDecl = ast.body.find(
202
+ (n) =>
203
+ n.type === AST_NODE_TYPES.ImportDeclaration && n.specifiers.some((s) => s.local.name === importedAppName),
204
+ );
205
+ if (importDecl) {
206
+ appClassName = importedAppName;
207
+ appImportPath = importDecl.source.value;
208
+ }
209
+ }
210
+ }
211
+
212
+ // Discover scenes, plugins, popups, entities, and locale keys.
213
+ const scenes = await discoverScenes(server, root);
214
+ const plugins = await discoverPlugins(server, root);
215
+ const popups = await discoverPopups(server, root);
216
+ const entities = await discoverEntities(server, root);
217
+ const uis = await discoverUIs(server, root);
218
+ const localeKeys = await discoverLocaleKeys(server, root);
219
+
220
+ const sceneIds = scenes.filter((s) => s.active !== false).map((s) => s.id);
221
+ const pluginIds = plugins.filter((p) => p.active !== false).map((p) => p.id);
222
+ const popupIds = popups.filter((p) => p.active !== false).map((p) => p.id);
223
+ const entityIds = entities.filter((e) => e.active !== false).map((e) => e.id);
224
+ const uiIds = uis.filter((u) => u.active !== false).map((u) => u.id);
225
+
226
+ // Build-time cross-reference validation. Warnings for typos that would
227
+ // silently fail at runtime (missing bundles, unknown plugin IDs,
228
+ // defaultScene pointing at a non-existent scene, duplicate IDs).
229
+ runBuildTimeValidation({
230
+ server,
231
+ configPath,
232
+ configObject,
233
+ scenes,
234
+ plugins,
235
+ popups,
236
+ entities,
237
+ breakpointsName,
238
+ });
239
+
240
+ const sceneIdType = sceneIds.length > 0 ? `\n | '${sceneIds.join("'\n | '")}'` : 'string';
241
+ const pluginIdType = pluginIds.length > 0 ? `\n | '${pluginIds.join("'\n | '")}'` : 'string';
242
+ const popupIdType = popupIds.length > 0 ? `\n | '${popupIds.join("'\n | '")}'` : 'string';
243
+ const entityIdType = entityIds.length > 0 ? `\n | '${entityIds.join("'\n | '")}'` : 'string';
244
+ const uiIdType = uiIds.length > 0 ? `\n | '${uiIds.join("'\n | '")}'` : 'string';
245
+ const localeKeyType = localeKeys.length > 0 ? `\n | '${localeKeys.join("'\n | '")}'` : 'string';
246
+
247
+ // Emit a keyed `{ id: typeof import('...').default }` map for each of
248
+ // scenes/popups/entities so the framework can derive typed constructor
249
+ // props via `ConstructorParameters<...>[0]` / `InstanceType<...>` at call
250
+ // sites (`this.add.entity(id, props)` etc). Uses the `@/` alias path the
251
+ // discovery already emits — the generated .d.ts lives inside the user's
252
+ // project so tsconfig `paths` applies.
253
+ const buildClassMap = (items) => {
254
+ const active = items.filter((i) => i.active !== false && i.importPath);
255
+ if (active.length === 0) return ' [id: string]: never;';
256
+ return active
257
+ .map((item) => ` ${JSON.stringify(item.id)}: typeof import('${item.importPath}').default;`)
258
+ .join('\n');
259
+ };
260
+
261
+ const sceneClassMap = buildClassMap(scenes);
262
+ const popupClassMap = buildClassMap(popups);
263
+ const entityClassMap = buildClassMap(entities);
264
+ const uiClassMap = buildClassMap(uis);
265
+
266
+ const configDir = path.dirname(configPath);
267
+ const dtsDir = path.resolve(configDir, 'src/types');
268
+
269
+ let relativeAppPath = appImportPath;
270
+ if (appImportPath.startsWith('.')) {
271
+ relativeAppPath = path.relative(dtsDir, path.resolve(configDir, appImportPath)).replace(/\\/g, '/');
272
+ if (relativeAppPath.endsWith('.ts')) {
273
+ relativeAppPath = relativeAppPath.slice(0, -3);
274
+ }
275
+ }
276
+
277
+ const relativeConfigPath = path.relative(dtsDir, configPath).replace(/\\/g, '/').replace(/\.ts$/, '');
278
+
279
+ const imports = [];
280
+ if (appClassName === 'Application') {
281
+ imports.push(`import type { Application } from '@caperjs/core';`);
282
+ }
283
+ if (fs.existsSync(configPath)) {
284
+ const configParts = [];
285
+ if (dataSchemaName) {
286
+ configParts.push(dataSchemaName);
287
+ }
288
+ if (hasActions) {
289
+ configParts.push('actions');
290
+ }
291
+ if (hasContexts) {
292
+ configParts.push('contexts');
293
+ }
294
+ if (breakpointsName) {
295
+ configParts.push(breakpointsName);
296
+ }
297
+ if (configParts.length > 0) {
298
+ imports.push(`import type { ${configParts.join(', ')} } from '${relativeConfigPath}';`);
299
+ }
300
+ }
301
+ if (appClassName !== 'Application') {
302
+ imports.push(`import type { ${appClassName} } from '${relativeAppPath}';`);
303
+ }
304
+
305
+ return {
306
+ types: `
307
+ // This file is auto-generated by caper. Do not edit.
308
+ ${imports.join('\n')}
309
+ // Data
310
+ type AppData = ${dataTypeName};
311
+
312
+ // Action Contexts
313
+ ${hasContexts ? 'type AppContexts = (typeof contexts)[number];' : 'type AppContexts = string;'}
314
+
315
+ // Actions
316
+ ${hasActions ? 'type AppActionMap = typeof actions;' : 'type AppActionMap = Record<string, any>;'}
317
+ ${hasActions ? 'type AppActions = keyof AppActionMap;' : 'type AppActions = string;'}
318
+
319
+ // Scenes
320
+ type AppScenes = ${sceneIdType};
321
+
322
+ // Plugins
323
+ type AppPlugins = ${pluginIdType};
324
+
325
+ // Popups
326
+ type AppPopups = ${popupIdType};
327
+
328
+ // Entities
329
+ type AppEntities = ${entityIdType};
330
+
331
+ // UI Elements
332
+ type AppUIs = ${uiIdType};
333
+
334
+ // Class maps — typeof-import pointers to the real discovered classes. The
335
+ // framework uses these with ConstructorParameters<> + InstanceType<> + infer
336
+ // to derive typed props and return types for this.add.entity / popups.show /
337
+ // scenes.load without any AST type extraction.
338
+ type AppSceneClasses = {
339
+ ${sceneClassMap}
340
+ };
341
+
342
+ type AppPopupClasses = {
343
+ ${popupClassMap}
344
+ };
345
+
346
+ type AppEntityClasses = {
347
+ ${entityClassMap}
348
+ };
349
+
350
+ type AppUIClasses = {
351
+ ${uiClassMap}
352
+ };
353
+
354
+ // Locale keys (flattened dot-paths from src/locales/<reference>.ts)
355
+ type AppLocaleKeys = ${localeKeyType};${breakpointsName ? `\n\n// Breakpoints\ntype AppBreakpoints = keyof (typeof ${breakpointsName})['tiers'] & string;\ntype AppBreakpointModes = keyof (typeof ${breakpointsName})['modes'] & string;` : ''}
356
+
357
+ /**
358
+ * Add type overrides to the framework
359
+ */
360
+ declare module '@caperjs/core' {
361
+ interface AppTypeOverrides {
362
+ App: ${appClassName};
363
+ Data: AppData;
364
+ Contexts: AppContexts;
365
+ Actions: AppActions;
366
+ ActionMap: AppActionMap;
367
+ Scenes: AppScenes;
368
+ Plugins: AppPlugins;
369
+ Popups: AppPopups;
370
+ Entities: AppEntities;
371
+ SceneClasses: AppSceneClasses;
372
+ PopupClasses: AppPopupClasses;
373
+ EntityClasses: AppEntityClasses;
374
+ UIs: AppUIs;
375
+ UIClasses: AppUIClasses;
376
+ LocaleKeys: AppLocaleKeys;${breakpointsName ? `\n Breakpoints: AppBreakpoints;\n BreakpointModes: AppBreakpointModes;` : ''}
377
+ Eases: Eases;
378
+ }
379
+ }
380
+ `,
381
+ };
382
+ }
383
+
384
+ async function build(msg = `Building ${DTS_FILE_NAME}`, server) {
385
+ logger.info(msg);
386
+ const { types, error } = await generateTypes(server, root);
387
+
388
+ if (error) {
389
+ if (server) {
390
+ server.ws.send({
391
+ type: 'error',
392
+ err: {
393
+ message: error.message,
394
+ stack: error.stack,
395
+ id: path.resolve(root, 'caper.config.ts'),
396
+ plugin: 'vite-plugin-caper-config',
397
+ },
398
+ });
399
+ }
400
+ return;
401
+ }
402
+
403
+ const typesDir = path.resolve(root, 'src/types');
404
+ await fs.promises.mkdir(typesDir, { recursive: true });
405
+ await fs.promises.writeFile(path.join(typesDir, DTS_FILE_NAME), types, 'utf-8');
406
+
407
+ // Dev-only: evaluate + validate the user's config through Vite's SSR
408
+ // module graph. Failures surface in the overlay with file:line detail.
409
+ // Prod builds skip this (no server).
410
+ if (server) {
411
+ await validateCaperConfig(server, root);
412
+ server.ws.send({ type: 'full-reload' });
413
+ }
414
+ }
415
+
416
+ return {
417
+ name: 'vite-plugin-caper-config',
418
+ configResolved(config) {
419
+ root = config.root;
420
+ },
421
+ enforce: 'pre',
422
+ resolveId(id) {
423
+ if (id === virtualModuleId) {
424
+ return resolvedVirtualModuleId;
425
+ }
426
+ },
427
+ async load(id) {
428
+ if (id === resolvedVirtualModuleId) {
429
+ return `
430
+ const configModule = await import('/caper.config.ts');
431
+ export default configModule.default;
432
+ `;
433
+ }
434
+ },
435
+ async buildStart() {
436
+ if (!isProject) return;
437
+ await build('Generating types from caper.config.ts');
438
+ },
439
+ configureServer(server) {
440
+ if (!isProject) return;
441
+
442
+ const configPath = path.resolve(root, 'caper.config.ts');
443
+ const scenesDir = path.resolve(root, 'src/scenes');
444
+ const pluginsDir = path.resolve(root, 'src/plugins');
445
+ const popupsDir = path.resolve(root, 'src/popups');
446
+ const entitiesDir = path.resolve(root, 'src/entities');
447
+ const localesDir = path.resolve(root, 'src/locales');
448
+
449
+ const handleFileChange = async (file) => {
450
+ const isScene = file.startsWith(scenesDir);
451
+ const isPlugin = file.startsWith(pluginsDir);
452
+ const isPopup = file.startsWith(popupsDir);
453
+ const isEntity = file.startsWith(entitiesDir);
454
+ const isLocale = file.startsWith(localesDir);
455
+ const isConfig = file === configPath;
456
+
457
+ if (!isScene && !isPlugin && !isPopup && !isEntity && !isLocale && !isConfig) return;
458
+
459
+ const msg = isScene
460
+ ? 'Scene file changed'
461
+ : isPlugin
462
+ ? 'Plugin file changed'
463
+ : isPopup
464
+ ? 'Popup file changed'
465
+ : isEntity
466
+ ? 'Entity file changed'
467
+ : isLocale
468
+ ? 'Locale file changed'
469
+ : 'Config file changed';
470
+ await build(`${msg}, regenerating types...`, server);
471
+ };
472
+
473
+ server.watcher.add(configPath);
474
+ server.watcher.add(scenesDir);
475
+ server.watcher.add(pluginsDir);
476
+ server.watcher.add(popupsDir);
477
+ server.watcher.add(entitiesDir);
478
+ server.watcher.add(localesDir);
479
+
480
+ server.watcher.on('change', handleFileChange);
481
+ server.watcher.on('add', handleFileChange);
482
+ server.watcher.on('unlink', handleFileChange);
483
+ },
484
+ };
485
+ }
486
+
487
+ /** END PLUGINS */
488
+
489
+ /** CONFIG */
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Bridges a runtime error from the app back to vite's dev overlay.
3
+ *
4
+ * The app sends `caper:show-error` over vite's websocket; this echoes it back as
5
+ * vite's own `error` event so the overlay renders it like a build failure.
6
+ */
7
+ export function caperDevHelperPlugin() {
8
+ return {
9
+ name: 'vite-plugin-caper-dev-helper',
10
+ configureServer(server) {
11
+ server.ws.on('caper:show-error', (data) => {
12
+ const { error } = data;
13
+ // Send the 'error' event back to the client
14
+ server.ws.send({
15
+ type: 'error',
16
+ err: {
17
+ message: error.message || 'An unknown error occurred.',
18
+ stack: error.stack || new Error(error.message).stack,
19
+ id: error.id,
20
+ loc: {
21
+ file: error.id,
22
+ line: error.line,
23
+ column: error.column,
24
+ },
25
+ plugin: 'caper-dev-helper',
26
+ },
27
+ });
28
+ });
29
+ },
30
+ };
31
+ }