@caperjs/core 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/{config → build}/assetpack.mjs +103 -45
- package/build/defaults.mjs +180 -0
- package/build/index.mjs +96 -0
- package/build/internal/ast.mjs +255 -0
- package/build/internal/buildFlags.mjs +50 -0
- package/build/internal/discovery.mjs +403 -0
- package/build/internal/manifest.mjs +38 -0
- package/build/internal/paths.mjs +26 -0
- package/build/internal/schema.mjs +65 -0
- package/build/internal/util.mjs +30 -0
- package/build/internal/validate.mjs +186 -0
- package/build/plugins/assetTypes.mjs +460 -0
- package/build/plugins/caperConfig.mjs +489 -0
- package/build/plugins/devHelper.mjs +31 -0
- package/build/plugins/lists.mjs +286 -0
- package/build/plugins/pruneFallbacks.mjs +150 -0
- package/build/plugins/pwa.mjs +240 -0
- package/build/plugins/runtime.mjs +129 -0
- package/cli.mjs +9 -6
- package/extras/llms.txt +23 -17
- package/lib/caper.mjs +1 -1
- package/lib/caper.mjs.map +1 -1
- package/lib/types/caper-app.d.ts +1 -0
- package/package.json +13 -14
- package/src/types/caper-app.d.ts +1 -0
- package/src/version.ts +1 -1
- package/templates/app/default/package.template.json +8 -4
- package/templates/app/default/vite.config.ts +15 -0
- package/cli/vite.mjs +0 -55
- package/config/vite.mjs +0 -2464
|
@@ -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
|
+
}
|