@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.
- package/README.md +1 -1
- package/{config → build}/assetpack.mjs +103 -45
- package/build/defaults.mjs +157 -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 +402 -0
- package/build/internal/manifest.mjs +38 -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/{CaptionsPlugin-Dc8fIrjy.js → CaptionsPlugin-DKhbmJvH.js} +15 -15
- package/lib/{CaptionsPlugin-Dc8fIrjy.js.map → CaptionsPlugin-DKhbmJvH.js.map} +1 -1
- package/lib/{DataAdapter-BY2114sp.js → DataAdapter-BZFTkFQ4.js} +4 -4
- package/lib/{DataAdapter-BY2114sp.js.map → DataAdapter-BZFTkFQ4.js.map} +1 -1
- package/lib/{DebugRenderer-BJMLILqI.js → DebugRenderer-C7X-nlXE.js} +2 -2
- package/lib/{DebugRenderer-BJMLILqI.js.map → DebugRenderer-C7X-nlXE.js.map} +1 -1
- package/lib/{DevToolsPlugin-wQ5rOkn0.js → DevToolsPlugin-P-atJ-5U.js} +2 -2
- package/lib/{DevToolsPlugin-wQ5rOkn0.js.map → DevToolsPlugin-P-atJ-5U.js.map} +1 -1
- package/lib/{GSAPPlugin-DHhSZKv8.js → GSAPPlugin-CeH9275z.js} +2 -2
- package/lib/{GSAPPlugin-DHhSZKv8.js.map → GSAPPlugin-CeH9275z.js.map} +1 -1
- package/lib/{LayoutPlugin-B-DF9SYM.js → LayoutPlugin-iZ3k6RsK.js} +4 -4
- package/lib/{LayoutPlugin-B-DF9SYM.js.map → LayoutPlugin-iZ3k6RsK.js.map} +1 -1
- package/lib/{SpinePlugin-Dz_3dljz.js → SpinePlugin-BWiavCpO.js} +2 -2
- package/lib/{SpinePlugin-Dz_3dljz.js.map → SpinePlugin-BWiavCpO.js.map} +1 -1
- package/lib/{StatsPlugin-Cg82JMBD.js → StatsPlugin-CBpzDJL0.js} +4 -4
- package/lib/{StatsPlugin-Cg82JMBD.js.map → StatsPlugin-CBpzDJL0.js.map} +1 -1
- package/lib/{VoiceOverPlugin-BR3ImT8C.js → VoiceOverPlugin-YaiKJtJ6.js} +26 -26
- package/lib/{VoiceOverPlugin-BR3ImT8C.js.map → VoiceOverPlugin-YaiKJtJ6.js.map} +1 -1
- package/lib/caper.mjs +95 -95
- package/lib/caper.mjs.map +1 -1
- package/lib/core/Application.d.ts +1 -1
- package/lib/core/Application.d.ts.map +1 -1
- package/lib/core/interfaces/IApplicationOptions.d.ts +2 -1
- package/lib/core/interfaces/IApplicationOptions.d.ts.map +1 -1
- package/lib/core/interfaces/ICoreSignals.d.ts +5 -1
- package/lib/core/interfaces/ICoreSignals.d.ts.map +1 -1
- package/lib/display/Container.d.ts +1 -1
- package/lib/display/Container.d.ts.map +1 -1
- package/lib/display/ParticleContainer.d.ts +1 -1
- package/lib/display/ParticleContainer.d.ts.map +1 -1
- package/lib/plugins/Plugin.d.ts +1 -1
- package/lib/plugins/Plugin.d.ts.map +1 -1
- package/lib/plugins/defaults.d.ts.map +1 -1
- package/lib/plugins/gesture/GesturePlugin.d.ts +58 -0
- package/lib/plugins/gesture/GesturePlugin.d.ts.map +1 -0
- package/lib/plugins/gesture/gestureMath.d.ts +32 -0
- package/lib/plugins/gesture/gestureMath.d.ts.map +1 -0
- package/lib/plugins/gesture/gestureMath.test.d.ts +2 -0
- package/lib/plugins/gesture/gestureMath.test.d.ts.map +1 -0
- package/lib/plugins/gesture/index.d.ts +3 -0
- package/lib/plugins/gesture/index.d.ts.map +1 -0
- package/lib/plugins/gesture/types.d.ts +45 -0
- package/lib/plugins/gesture/types.d.ts.map +1 -0
- package/lib/plugins/index.d.ts +1 -0
- package/lib/plugins/index.d.ts.map +1 -1
- package/lib/plugins/input/Controls.d.ts +5 -2
- package/lib/plugins/input/Controls.d.ts.map +1 -1
- package/lib/plugins/input/InputPlugin.d.ts +9 -2
- package/lib/plugins/input/InputPlugin.d.ts.map +1 -1
- package/lib/plugins/input/interfaces.d.ts +5 -3
- package/lib/plugins/input/interfaces.d.ts.map +1 -1
- package/lib/plugins/input/touch/{TouchControls.d.ts → VirtualControls.d.ts} +5 -4
- package/lib/plugins/input/touch/VirtualControls.d.ts.map +1 -0
- package/lib/plugins/input/touch/index.d.ts +1 -1
- package/lib/plugins/input/touch/index.d.ts.map +1 -1
- package/lib/{registries-CYCaYKZI.js → registries-qQmLeN0b.js} +505 -354
- package/lib/registries-qQmLeN0b.js.map +1 -0
- package/lib/types/caper-app.d.ts +1 -0
- package/lib/utils/bind.test.d.ts +2 -0
- package/lib/utils/bind.test.d.ts.map +1 -0
- package/package.json +33 -33
- package/src/core/Application.ts +1 -1
- package/src/core/interfaces/IApplicationOptions.ts +2 -0
- package/src/core/interfaces/ICoreSignals.ts +9 -0
- package/src/display/Container.ts +1 -1
- package/src/display/ParticleContainer.ts +1 -1
- package/src/plugins/Plugin.ts +1 -1
- package/src/plugins/defaults.ts +6 -0
- package/src/plugins/gesture/GesturePlugin.ts +215 -0
- package/src/plugins/gesture/gestureMath.test.ts +75 -0
- package/src/plugins/gesture/gestureMath.ts +59 -0
- package/src/plugins/gesture/index.ts +2 -0
- package/src/plugins/gesture/types.ts +54 -0
- package/src/plugins/index.ts +1 -0
- package/src/plugins/input/Controls.ts +9 -3
- package/src/plugins/input/InputPlugin.ts +61 -14
- package/src/plugins/input/interfaces.ts +5 -3
- package/src/plugins/input/touch/{TouchControls.ts → VirtualControls.ts} +4 -1
- package/src/plugins/input/touch/index.ts +1 -1
- package/src/types/caper-app.d.ts +1 -0
- package/src/utils/bind.test.ts +99 -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/LICENSE +0 -22
- package/cli/vite.mjs +0 -55
- package/config/vite.mjs +0 -2464
- package/lib/plugins/input/touch/TouchControls.d.ts.map +0 -1
- package/lib/registries-CYCaYKZI.js.map +0 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything caper's build plugins learn by reading source rather than running
|
|
3
|
+
* it: an oxc parse wrapper, plus the extractors discovery uses to find a file's
|
|
4
|
+
* exported constants, its default-exported class, and the object passed to
|
|
5
|
+
* `defineConfig`.
|
|
6
|
+
*
|
|
7
|
+
* Source is parsed rather than imported because importing a project's modules
|
|
8
|
+
* pulls in @caperjs/core, whose @pixi/sound and GSAP dependencies run
|
|
9
|
+
* browser-only side effects at module top level and throw under plain Node.
|
|
10
|
+
*/
|
|
11
|
+
import { parseSync } from 'oxc-parser';
|
|
12
|
+
|
|
13
|
+
export const AST_NODE_TYPES = {
|
|
14
|
+
ArrayExpression: 'ArrayExpression',
|
|
15
|
+
CallExpression: 'CallExpression',
|
|
16
|
+
ClassDeclaration: 'ClassDeclaration',
|
|
17
|
+
ExportDefaultDeclaration: 'ExportDefaultDeclaration',
|
|
18
|
+
ExportNamedDeclaration: 'ExportNamedDeclaration',
|
|
19
|
+
Identifier: 'Identifier',
|
|
20
|
+
ImportDeclaration: 'ImportDeclaration',
|
|
21
|
+
Literal: 'Literal',
|
|
22
|
+
ObjectExpression: 'ObjectExpression',
|
|
23
|
+
Property: 'Property',
|
|
24
|
+
VariableDeclaration: 'VariableDeclaration',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Thin wrapper around oxc-parser that mirrors the shape we previously got
|
|
29
|
+
* from `@typescript-eslint/typescript-estree`: it returns the `Program`
|
|
30
|
+
* node directly, so `ast.body` / `for (const node of ast.body)` keeps working.
|
|
31
|
+
*
|
|
32
|
+
* Swapping `typescript-estree` (JS-written TS parser, ~500KB, slow) out for
|
|
33
|
+
* oxc-parser (Rust, bundled with Vite 8) was the biggest remaining DX win
|
|
34
|
+
* from the Phase 1b rolldown `PLUGIN_TIMINGS` report: the config/discovery
|
|
35
|
+
* AST parses are the hottest paths in the dev-server startup and HMR.
|
|
36
|
+
*/
|
|
37
|
+
export function parse(content, _options = {}) {
|
|
38
|
+
const result = parseSync('caper-discovery.ts', content, {
|
|
39
|
+
lang: 'ts',
|
|
40
|
+
sourceType: 'module',
|
|
41
|
+
});
|
|
42
|
+
if (result.errors && result.errors.length > 0) {
|
|
43
|
+
const first = result.errors[0];
|
|
44
|
+
const msg = first.message || JSON.stringify(first);
|
|
45
|
+
const err = new Error(`oxc-parser: ${msg}`);
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
return result.program;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function extractConfigReferences(configObject) {
|
|
52
|
+
const result = { defaultScene: undefined, pluginIds: [] };
|
|
53
|
+
if (!configObject || configObject.type !== AST_NODE_TYPES.ObjectExpression) return result;
|
|
54
|
+
|
|
55
|
+
for (const prop of configObject.properties) {
|
|
56
|
+
if (prop.type !== AST_NODE_TYPES.Property || prop.key?.type !== AST_NODE_TYPES.Identifier) continue;
|
|
57
|
+
if (prop.key.name === 'defaultScene' && prop.value?.type === AST_NODE_TYPES.Literal) {
|
|
58
|
+
result.defaultScene = prop.value.value;
|
|
59
|
+
}
|
|
60
|
+
if (prop.key.name === 'plugins' && prop.value?.type === AST_NODE_TYPES.ArrayExpression) {
|
|
61
|
+
for (const el of prop.value.elements) {
|
|
62
|
+
if (!el) continue;
|
|
63
|
+
if (el.type === AST_NODE_TYPES.Literal && typeof el.value === 'string') {
|
|
64
|
+
result.pluginIds.push(el.value);
|
|
65
|
+
} else if (el.type === AST_NODE_TYPES.ArrayExpression && el.elements[0]?.type === AST_NODE_TYPES.Literal) {
|
|
66
|
+
const first = el.elements[0];
|
|
67
|
+
if (typeof first.value === 'string') result.pluginIds.push(first.value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Locate the `defineConfig({...})` ObjectExpression in a parsed
|
|
77
|
+
* `caper.config.ts` AST. Handles both the default-export form
|
|
78
|
+
* (`export default defineConfig({...})`) and the named-const form
|
|
79
|
+
* (`export const config = defineConfig({...})`).
|
|
80
|
+
*/
|
|
81
|
+
export function findConfigObject(ast) {
|
|
82
|
+
let configObject;
|
|
83
|
+
for (const node of ast.body) {
|
|
84
|
+
if (
|
|
85
|
+
node.type === AST_NODE_TYPES.ExportDefaultDeclaration &&
|
|
86
|
+
node.declaration?.type === AST_NODE_TYPES.CallExpression &&
|
|
87
|
+
node.declaration.callee?.name === 'defineConfig'
|
|
88
|
+
) {
|
|
89
|
+
configObject = node.declaration.arguments[0];
|
|
90
|
+
} else if (
|
|
91
|
+
node.type === AST_NODE_TYPES.ExportNamedDeclaration &&
|
|
92
|
+
node.declaration?.type === AST_NODE_TYPES.VariableDeclaration
|
|
93
|
+
) {
|
|
94
|
+
const decl = node.declaration.declarations.find(
|
|
95
|
+
(d) => d.init?.type === AST_NODE_TYPES.CallExpression && d.init.callee?.name === 'defineConfig',
|
|
96
|
+
);
|
|
97
|
+
if (decl) configObject = decl.init.arguments[0];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return configObject;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Boolean build-time flags read straight out of `caper.config.ts`.
|
|
105
|
+
*
|
|
106
|
+
* Vite fixes a config's plugin list the moment the config object is created — no
|
|
107
|
+
* plugin hook can add one later, and `caper()` runs while the project's
|
|
108
|
+
* vite.config is still being evaluated, long before anything could execute
|
|
109
|
+
* caper.config.ts. So these are pulled with the same oxc AST parse discovery
|
|
110
|
+
* already uses rather than by importing the file: importing pulls in
|
|
111
|
+
* @caperjs/core, whose @pixi/sound + GSAP deps run
|
|
112
|
+
* browser-only top-level side effects that throw under Node (see
|
|
113
|
+
* `validateCaperConfig` for the gory details).
|
|
114
|
+
*
|
|
115
|
+
* Only boolean literals are honoured. A missing, empty, or unparseable
|
|
116
|
+
* config silently yields the defaults — this runs before the normal config
|
|
117
|
+
* error reporting, so it must never be the thing that fails the build.
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
export const DEFINE_HELPER_NAMES = new Set(['defineScene', 'definePlugin', 'definePopup', 'defineEntity', 'defineUI']);
|
|
121
|
+
|
|
122
|
+
export function findExportedConstants(ast) {
|
|
123
|
+
const exports = {};
|
|
124
|
+
|
|
125
|
+
function extractValue(node) {
|
|
126
|
+
switch (node.type) {
|
|
127
|
+
case AST_NODE_TYPES.Literal:
|
|
128
|
+
return node.value;
|
|
129
|
+
case AST_NODE_TYPES.ArrayExpression:
|
|
130
|
+
return node.elements.map((element) => element && extractValue(element)).filter((value) => value !== undefined);
|
|
131
|
+
case AST_NODE_TYPES.ObjectExpression: {
|
|
132
|
+
const obj = {};
|
|
133
|
+
for (const prop of node.properties) {
|
|
134
|
+
if (prop.type === AST_NODE_TYPES.Property && prop.key.type === AST_NODE_TYPES.Identifier) {
|
|
135
|
+
obj[prop.key.name] = extractValue(prop.value);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return obj;
|
|
139
|
+
}
|
|
140
|
+
case AST_NODE_TYPES.CallExpression: {
|
|
141
|
+
// Unwrap `defineScene({...})` / `definePlugin({...})` / etc.
|
|
142
|
+
// Other call expressions are opaque to discovery.
|
|
143
|
+
const calleeName = node.callee?.name;
|
|
144
|
+
if (calleeName && DEFINE_HELPER_NAMES.has(calleeName) && node.arguments[0]) {
|
|
145
|
+
return extractValue(node.arguments[0]);
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
default:
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Export names whose value came out of a `define*()` helper, in source order.
|
|
155
|
+
const wrapperKeys = [];
|
|
156
|
+
|
|
157
|
+
for (const node of ast.body) {
|
|
158
|
+
if (
|
|
159
|
+
node.type === AST_NODE_TYPES.ExportNamedDeclaration &&
|
|
160
|
+
node.declaration?.type === AST_NODE_TYPES.VariableDeclaration
|
|
161
|
+
) {
|
|
162
|
+
for (const declarator of node.declaration.declarations) {
|
|
163
|
+
if (declarator.id.type === AST_NODE_TYPES.Identifier && declarator.init) {
|
|
164
|
+
exports[declarator.id.name] = extractValue(declarator.init);
|
|
165
|
+
if (
|
|
166
|
+
declarator.init.type === AST_NODE_TYPES.CallExpression &&
|
|
167
|
+
DEFINE_HELPER_NAMES.has(declarator.init.callee?.name)
|
|
168
|
+
) {
|
|
169
|
+
wrapperKeys.push(declarator.id.name);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Flatten `export const scene = defineScene({...})` / `export const ui =
|
|
177
|
+
// defineUI({...})` wrappers onto the top level so discovery code can stay
|
|
178
|
+
// agnostic: `exports.id`, `exports.active`, `exports.assets` etc. work whether
|
|
179
|
+
// the file used individual exports or the helper form.
|
|
180
|
+
//
|
|
181
|
+
// Which exports get flattened is decided by *what they are* — a call to a
|
|
182
|
+
// define helper — rather than by matching a hardcoded list of export names.
|
|
183
|
+
// That list read ['scene', 'plugin', 'popup', 'entity'], so `defineUI` was
|
|
184
|
+
// silently ignored and every UI element registered under its class name
|
|
185
|
+
// instead of its declared id. Naming the export anything other than the kind
|
|
186
|
+
// (`export const ui_ = defineUI(...)`) failed the same way.
|
|
187
|
+
//
|
|
188
|
+
// Individual file-level exports still take precedence on conflict, and earlier
|
|
189
|
+
// wrappers win over later ones.
|
|
190
|
+
for (const wrapperKey of wrapperKeys) {
|
|
191
|
+
const wrapped = exports[wrapperKey];
|
|
192
|
+
if (wrapped && typeof wrapped === 'object' && !Array.isArray(wrapped)) {
|
|
193
|
+
for (const [k, v] of Object.entries(wrapped)) {
|
|
194
|
+
if (exports[k] === undefined) exports[k] = v;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return exports;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Finds the file's default-exported class. Handles both common forms:
|
|
204
|
+
*
|
|
205
|
+
* // inline
|
|
206
|
+
* export default class Foo extends Bar { ... }
|
|
207
|
+
*
|
|
208
|
+
* // named declaration + separate default export
|
|
209
|
+
* export class Foo extends Bar { ... }
|
|
210
|
+
* export default Foo;
|
|
211
|
+
*
|
|
212
|
+
* // or unexported named class
|
|
213
|
+
* class Foo extends Bar { ... }
|
|
214
|
+
* export default Foo;
|
|
215
|
+
*
|
|
216
|
+
* The separate-default form requires a second pass to resolve the
|
|
217
|
+
* identifier back to a matching `ClassDeclaration` in the same file.
|
|
218
|
+
*/
|
|
219
|
+
export function findDefaultExportedClass(ast) {
|
|
220
|
+
let identifierName = null;
|
|
221
|
+
|
|
222
|
+
for (const node of ast.body) {
|
|
223
|
+
if (node.type !== AST_NODE_TYPES.ExportDefaultDeclaration) continue;
|
|
224
|
+
// Form 1: `export default class Foo { ... }`
|
|
225
|
+
if (node.declaration.type === AST_NODE_TYPES.ClassDeclaration) {
|
|
226
|
+
return node.declaration;
|
|
227
|
+
}
|
|
228
|
+
// Form 2/3: `export default Foo;` — remember the name to resolve below.
|
|
229
|
+
if (node.declaration.type === AST_NODE_TYPES.Identifier) {
|
|
230
|
+
identifierName = node.declaration.name;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (!identifierName) return null;
|
|
235
|
+
|
|
236
|
+
// Resolve the identifier to a class declaration in the same file. Accept
|
|
237
|
+
// either a bare `class Foo {}` or an `export class Foo {}` form.
|
|
238
|
+
for (const node of ast.body) {
|
|
239
|
+
if (node.type === AST_NODE_TYPES.ClassDeclaration && node.id?.name === identifierName) {
|
|
240
|
+
return node;
|
|
241
|
+
}
|
|
242
|
+
if (
|
|
243
|
+
node.type === AST_NODE_TYPES.ExportNamedDeclaration &&
|
|
244
|
+
node.declaration?.type === AST_NODE_TYPES.ClassDeclaration &&
|
|
245
|
+
node.declaration.id?.name === identifierName
|
|
246
|
+
) {
|
|
247
|
+
return node.declaration;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Back-compat alias — scene code historically called `findDefaultExportedScene`.
|
|
255
|
+
export const findDefaultExportedScene = findDefaultExportedClass;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boolean build flags read out of `caper.config.ts` before anything can execute
|
|
3
|
+
* it. See the comment on `readCaperBuildFlags` for why this is an AST parse.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { AST_NODE_TYPES, findConfigObject, parse } from './ast.mjs';
|
|
8
|
+
import { cwd } from './util.mjs';
|
|
9
|
+
|
|
10
|
+
export function readCaperBuildFlags() {
|
|
11
|
+
const flags = { useWasm: false };
|
|
12
|
+
const configPath = path.resolve(cwd, 'caper.config.ts');
|
|
13
|
+
if (!fs.existsSync(configPath)) return flags;
|
|
14
|
+
|
|
15
|
+
let configObject;
|
|
16
|
+
try {
|
|
17
|
+
configObject = findConfigObject(parse(fs.readFileSync(configPath, 'utf-8')));
|
|
18
|
+
} catch {
|
|
19
|
+
return flags;
|
|
20
|
+
}
|
|
21
|
+
if (configObject?.type !== AST_NODE_TYPES.ObjectExpression) return flags;
|
|
22
|
+
|
|
23
|
+
for (const prop of configObject.properties) {
|
|
24
|
+
if (prop.type !== AST_NODE_TYPES.Property || prop.key?.type !== AST_NODE_TYPES.Identifier) continue;
|
|
25
|
+
if (!(prop.key.name in flags)) continue;
|
|
26
|
+
if (prop.value?.type === AST_NODE_TYPES.Literal && typeof prop.value.value === 'boolean') {
|
|
27
|
+
flags[prop.key.name] = prop.value.value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return flags;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Cross-reference validation over discovered scenes/plugins/popups/entities
|
|
35
|
+
* + the parsed `caper.config.ts` AST + the assetpack manifest. Emits
|
|
36
|
+
* warnings via the project logger; in dev mode also forwards warnings to
|
|
37
|
+
* the browser error overlay as info so they're impossible to miss.
|
|
38
|
+
*
|
|
39
|
+
* Checks:
|
|
40
|
+
* 1. Scene `assets.preload.bundles` / `assets.background.bundles` entries
|
|
41
|
+
* exist in the assetpack manifest.
|
|
42
|
+
* 2. Plugin IDs in `caper.config.ts` match a discovered plugin
|
|
43
|
+
* (npm `@caperjs/plugin-*` OR local `src/plugins/*`).
|
|
44
|
+
* 3. `defaultScene` in `caper.config.ts` matches a discovered scene ID.
|
|
45
|
+
* 4. No duplicate scene / plugin / popup / entity IDs across discovery.
|
|
46
|
+
*
|
|
47
|
+
* All issues are warnings, not errors — they shouldn't fail the build
|
|
48
|
+
* (typos are a dev-time problem; the runtime will still start, just with
|
|
49
|
+
* broken references the user needs to see).
|
|
50
|
+
*/
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem discovery: walks the conventional source directories and reads each
|
|
3
|
+
* file's AST to work out what it declares.
|
|
4
|
+
*
|
|
5
|
+
* Split from the list plugins so the plugins are only about emitting virtual
|
|
6
|
+
* modules, and the crawling can be reasoned about (and fixed) on its own.
|
|
7
|
+
*
|
|
8
|
+
* Every entry point takes an explicit `root` — vite's resolved `config.root`,
|
|
9
|
+
* passed down by the plugin that called it. That is the project root by
|
|
10
|
+
* definition; `process.cwd()` only happens to be the same one, and differs the
|
|
11
|
+
* moment anyone runs `vite --root elsewhere` or invokes vite from a parent
|
|
12
|
+
* directory in a monorepo. It is threaded as a parameter rather than kept in a
|
|
13
|
+
* module-level variable so nothing depends on load order or on who ran first.
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import {
|
|
18
|
+
AST_NODE_TYPES,
|
|
19
|
+
DEFINE_HELPER_NAMES,
|
|
20
|
+
findDefaultExportedClass,
|
|
21
|
+
findDefaultExportedScene,
|
|
22
|
+
findExportedConstants,
|
|
23
|
+
parse,
|
|
24
|
+
} from './ast.mjs';
|
|
25
|
+
import { logger } from './util.mjs';
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
export async function findTypeScriptFiles(dir) {
|
|
29
|
+
const files = [];
|
|
30
|
+
|
|
31
|
+
async function scan(directory) {
|
|
32
|
+
const entries = await fs.promises.readdir(directory, { withFileTypes: true });
|
|
33
|
+
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
const fullPath = path.join(directory, entry.name);
|
|
36
|
+
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
await scan(fullPath);
|
|
39
|
+
} else if (entry.isFile() && /\.ts?$/.test(entry.name)) {
|
|
40
|
+
files.push(fullPath);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await scan(dir);
|
|
46
|
+
return files;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Callees whose single argument is treated as the entire config object and
|
|
50
|
+
// unwrapped into the exported constant's value. Keep in sync with the
|
|
51
|
+
// identity helpers in src/utils/define.ts.
|
|
52
|
+
|
|
53
|
+
export async function discoverScenes(server, root) {
|
|
54
|
+
const scenesDir = path.resolve(root, 'src/scenes');
|
|
55
|
+
const scenes = [];
|
|
56
|
+
|
|
57
|
+
if (!fs.existsSync(scenesDir)) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const files = await findTypeScriptFiles(scenesDir);
|
|
62
|
+
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
try {
|
|
65
|
+
const content = await fs.promises.readFile(file, 'utf-8');
|
|
66
|
+
const ast = parse(content, {
|
|
67
|
+
jsx: false,
|
|
68
|
+
loc: true,
|
|
69
|
+
comment: false,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const sceneClass = findDefaultExportedScene(ast);
|
|
73
|
+
if (!sceneClass) continue;
|
|
74
|
+
|
|
75
|
+
const exports = findExportedConstants(ast);
|
|
76
|
+
|
|
77
|
+
const relativePath = file.replace(root, '').replace(/\\/g, '/').split('/src')[1];
|
|
78
|
+
// remove /src — the runtime import uses the raw alias (Vite resolves
|
|
79
|
+
// `.ts` automatically) while the typeof-import codegen needs the
|
|
80
|
+
// extension stripped so TypeScript's path-alias resolution finds it.
|
|
81
|
+
const importPath = `@${relativePath}`;
|
|
82
|
+
const importPathForTypes = importPath.replace(/\.ts$/, '');
|
|
83
|
+
const id = exports.id || sceneClass.id?.name || path.basename(file, '.ts');
|
|
84
|
+
|
|
85
|
+
scenes.push({
|
|
86
|
+
id,
|
|
87
|
+
importPath: importPathForTypes,
|
|
88
|
+
module:
|
|
89
|
+
exports.dynamic === false
|
|
90
|
+
? importPath
|
|
91
|
+
: {
|
|
92
|
+
toString: () => `() => import('${importPath}')`,
|
|
93
|
+
isFunction: true, // Add a flag to identify dynamic imports
|
|
94
|
+
},
|
|
95
|
+
active: exports.active === false ? false : true,
|
|
96
|
+
debugLabel: exports.debug?.label || id,
|
|
97
|
+
debugGroup: exports.debug?.group || undefined,
|
|
98
|
+
debugOrder: exports.debug?.order >= 0 ? exports.debug.order : Number.MAX_SAFE_INTEGER,
|
|
99
|
+
assets: exports.assets ?? undefined,
|
|
100
|
+
plugins: exports.plugins ?? undefined,
|
|
101
|
+
autoUnloadAssets: exports.assets?.autoUnload ?? false,
|
|
102
|
+
});
|
|
103
|
+
} catch (e) {
|
|
104
|
+
const errorMessage = `Error parsing scene file ${file}: ${e.message}`;
|
|
105
|
+
logger.error(errorMessage);
|
|
106
|
+
if (e.stack) {
|
|
107
|
+
logger.error(e.stack);
|
|
108
|
+
}
|
|
109
|
+
if (server) {
|
|
110
|
+
server.ws.send({
|
|
111
|
+
type: 'error',
|
|
112
|
+
err: {
|
|
113
|
+
message: e.message,
|
|
114
|
+
stack: e.stack,
|
|
115
|
+
id: file,
|
|
116
|
+
plugin: 'vite-plugin-scenes',
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return scenes;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function discoverLocalPlugins(server, root) {
|
|
126
|
+
const pluginsDir = path.resolve(root, 'src/plugins');
|
|
127
|
+
const plugins = [];
|
|
128
|
+
|
|
129
|
+
if (!fs.existsSync(pluginsDir)) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const files = await findTypeScriptFiles(pluginsDir);
|
|
134
|
+
|
|
135
|
+
for (const file of files) {
|
|
136
|
+
try {
|
|
137
|
+
const content = await fs.promises.readFile(file, 'utf-8');
|
|
138
|
+
const ast = parse(content, {
|
|
139
|
+
jsx: false,
|
|
140
|
+
loc: true,
|
|
141
|
+
comment: false,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const hasPluginWrapper = ast.body.some(
|
|
145
|
+
(n) =>
|
|
146
|
+
n.type === AST_NODE_TYPES.ExportNamedDeclaration &&
|
|
147
|
+
n.declaration?.type === AST_NODE_TYPES.VariableDeclaration &&
|
|
148
|
+
n.declaration.declarations.some(
|
|
149
|
+
(d) => d.init?.type === AST_NODE_TYPES.CallExpression && d.init.callee?.name === 'definePlugin',
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
if (!hasPluginWrapper) continue;
|
|
153
|
+
|
|
154
|
+
const pluginClass = findDefaultExportedClass(ast);
|
|
155
|
+
if (!pluginClass) continue;
|
|
156
|
+
|
|
157
|
+
const exports = findExportedConstants(ast);
|
|
158
|
+
|
|
159
|
+
const relativePath = file.replace(root, '').replace(/\\/g, '/').split('/src')[1];
|
|
160
|
+
const importPath = `@${relativePath.replace(/\.ts$/, '')}`;
|
|
161
|
+
const id = exports.id || pluginClass.id?.name || path.basename(file, '.ts');
|
|
162
|
+
const name = pluginClass.id?.name || id;
|
|
163
|
+
|
|
164
|
+
plugins.push({
|
|
165
|
+
id,
|
|
166
|
+
name,
|
|
167
|
+
isLocal: true,
|
|
168
|
+
importPath,
|
|
169
|
+
module:
|
|
170
|
+
exports.dynamic === false
|
|
171
|
+
? importPath
|
|
172
|
+
: {
|
|
173
|
+
toString: () => `() => import('${importPath}')`,
|
|
174
|
+
isFunction: true,
|
|
175
|
+
},
|
|
176
|
+
active: exports.active === false ? false : true,
|
|
177
|
+
requires: Array.isArray(exports.requires) ? exports.requires.filter((r) => typeof r === 'string') : [],
|
|
178
|
+
});
|
|
179
|
+
} catch (e) {
|
|
180
|
+
const errorMessage = `Error parsing plugin file ${file}: ${e.message}`;
|
|
181
|
+
logger.error(errorMessage);
|
|
182
|
+
if (e.stack) {
|
|
183
|
+
logger.error(e.stack);
|
|
184
|
+
}
|
|
185
|
+
if (server) {
|
|
186
|
+
server.ws.send({
|
|
187
|
+
type: 'error',
|
|
188
|
+
err: {
|
|
189
|
+
message: e.message,
|
|
190
|
+
stack: e.stack,
|
|
191
|
+
id: file,
|
|
192
|
+
plugin: 'vite-plugin-plugins',
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return plugins;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Discovers npm packages prefixed with `@caperjs/plugin-` in the host
|
|
204
|
+
* project's `package.json`. Always emitted as dynamic imports.
|
|
205
|
+
*/
|
|
206
|
+
|
|
207
|
+
export function discoverNpmPlugins(root) {
|
|
208
|
+
const packageJsonPath = path.resolve(root, 'package.json');
|
|
209
|
+
if (!fs.existsSync(packageJsonPath)) return [];
|
|
210
|
+
|
|
211
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
212
|
+
const allDependencies = {
|
|
213
|
+
...(packageJson.dependencies || {}),
|
|
214
|
+
...(packageJson.devDependencies || {}),
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
return Object.keys(allDependencies)
|
|
218
|
+
.filter((dep) => dep.startsWith('@caperjs/plugin-'))
|
|
219
|
+
.map((packageName) => {
|
|
220
|
+
const id = packageName.replace('@caperjs/plugin-', '');
|
|
221
|
+
return {
|
|
222
|
+
id,
|
|
223
|
+
name: packageName,
|
|
224
|
+
isLocal: false,
|
|
225
|
+
importPath: packageName,
|
|
226
|
+
module: {
|
|
227
|
+
toString: () => `() => import('${packageName}')`,
|
|
228
|
+
isFunction: true,
|
|
229
|
+
},
|
|
230
|
+
active: true,
|
|
231
|
+
// npm plugins can declare `requires` on the class itself
|
|
232
|
+
// (`public readonly requires = ['firebase']`); the runtime topo-sort
|
|
233
|
+
// reads from the live instance, so empty here is fine.
|
|
234
|
+
requires: [],
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function discoverPlugins(server, root) {
|
|
240
|
+
const [local, npm] = [await discoverLocalPlugins(server, root), discoverNpmPlugins(root)];
|
|
241
|
+
return [...npm, ...local];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Generic recursive-class-file discoverer. Walks a directory, AST-parses
|
|
246
|
+
* each .ts file, requires a default-exported class, and reads `id` /
|
|
247
|
+
* `active` / `dynamic` metadata (either from individual exports or a
|
|
248
|
+
* `defineX({...})` wrapper). Used for popups and entities — same shape
|
|
249
|
+
* as scenes/plugins minus the npm-package discovery path.
|
|
250
|
+
*
|
|
251
|
+
* `defaultDynamic` sets the emit mode when a file doesn't declare it
|
|
252
|
+
* explicitly. Entities default to `false` (static import) because the
|
|
253
|
+
* typed `this.add.entity(id, props)` factory needs sync access to the
|
|
254
|
+
* constructor; popups default to `true` because `show()` is async.
|
|
255
|
+
*/
|
|
256
|
+
|
|
257
|
+
export async function discoverLocalClassFiles({ dir, kind, server, root, defaultDynamic = true }) {
|
|
258
|
+
const rootDir = path.resolve(root, dir);
|
|
259
|
+
if (!fs.existsSync(rootDir)) return [];
|
|
260
|
+
|
|
261
|
+
const files = await findTypeScriptFiles(rootDir);
|
|
262
|
+
const results = [];
|
|
263
|
+
|
|
264
|
+
for (const file of files) {
|
|
265
|
+
try {
|
|
266
|
+
const content = await fs.promises.readFile(file, 'utf-8');
|
|
267
|
+
const ast = parse(content);
|
|
268
|
+
|
|
269
|
+
const cls = findDefaultExportedClass(ast);
|
|
270
|
+
if (!cls) continue;
|
|
271
|
+
|
|
272
|
+
const exports = findExportedConstants(ast);
|
|
273
|
+
|
|
274
|
+
const relativePath = file.replace(root, '').replace(/\\/g, '/').split('/src')[1];
|
|
275
|
+
const importPath = `@${relativePath.replace(/\.ts$/, '')}`;
|
|
276
|
+
const id = exports.id || cls.id?.name || path.basename(file, '.ts');
|
|
277
|
+
const name = cls.id?.name || id;
|
|
278
|
+
const isDynamic = exports.dynamic !== undefined ? exports.dynamic : defaultDynamic;
|
|
279
|
+
|
|
280
|
+
results.push({
|
|
281
|
+
id,
|
|
282
|
+
name,
|
|
283
|
+
importPath,
|
|
284
|
+
module: !isDynamic
|
|
285
|
+
? importPath
|
|
286
|
+
: {
|
|
287
|
+
toString: () => `() => import('${importPath}')`,
|
|
288
|
+
isFunction: true,
|
|
289
|
+
},
|
|
290
|
+
active: exports.active === false ? false : true,
|
|
291
|
+
});
|
|
292
|
+
} catch (e) {
|
|
293
|
+
const errorMessage = `Error parsing ${kind} file ${file}: ${e.message}`;
|
|
294
|
+
logger.error(errorMessage);
|
|
295
|
+
if (e.stack) logger.error(e.stack);
|
|
296
|
+
if (server) {
|
|
297
|
+
server.ws.send({
|
|
298
|
+
type: 'error',
|
|
299
|
+
err: {
|
|
300
|
+
message: e.message,
|
|
301
|
+
stack: e.stack,
|
|
302
|
+
id: file,
|
|
303
|
+
plugin: `vite-plugin-${kind}s`,
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return results;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export async function discoverPopups(server, root) {
|
|
314
|
+
return discoverLocalClassFiles({ dir: 'src/popups', kind: 'popup', server, root });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export async function discoverEntities(server, root) {
|
|
318
|
+
// Entities default to static imports so `this.add.entity(id, props)` can
|
|
319
|
+
// synchronously construct without awaiting a dynamic import. Opt into
|
|
320
|
+
// code-splitting per-entity with `defineEntity({ dynamic: true })`.
|
|
321
|
+
return discoverLocalClassFiles({ dir: 'src/entities', kind: 'entity', server, root, defaultDynamic: false });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export async function discoverUIs(server, root) {
|
|
325
|
+
// UI elements default to static imports so `this.add.ui(id, props)` can
|
|
326
|
+
// synchronously construct. Same rationale as entities.
|
|
327
|
+
return discoverLocalClassFiles({ dir: 'src/ui', kind: 'ui', server, root, defaultDynamic: false });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Factory for `virtual:caper-popups` / `virtual:caper-entities`.
|
|
332
|
+
* Thin wrapper around a discover function that generates the static-import
|
|
333
|
+
* + dynamic-import `() => import(...)` mix, same shape as the scenes/plugins
|
|
334
|
+
* virtual modules.
|
|
335
|
+
*/
|
|
336
|
+
|
|
337
|
+
export async function discoverLocaleKeys(server, root) {
|
|
338
|
+
const localesDir = path.resolve(root, 'src/locales');
|
|
339
|
+
if (!fs.existsSync(localesDir)) return [];
|
|
340
|
+
|
|
341
|
+
const files = (await fs.promises.readdir(localesDir)).filter((f) => /\.ts$/.test(f)).sort();
|
|
342
|
+
if (files.length === 0) return [];
|
|
343
|
+
|
|
344
|
+
// Pick the reference file: en.ts if present, else the first alphabetically.
|
|
345
|
+
const referenceFile = files.find((f) => f === 'en.ts') || files[0];
|
|
346
|
+
const filePath = path.join(localesDir, referenceFile);
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
350
|
+
if (!content.trim()) return [];
|
|
351
|
+
|
|
352
|
+
const ast = parse(content);
|
|
353
|
+
const defaultExport = ast.body.find((n) => n.type === AST_NODE_TYPES.ExportDefaultDeclaration);
|
|
354
|
+
if (!defaultExport) return [];
|
|
355
|
+
|
|
356
|
+
// Allow either `export default { ... }` or `export default satisfies ...`
|
|
357
|
+
let obj = defaultExport.declaration;
|
|
358
|
+
if (obj && obj.type === 'TSSatisfiesExpression') obj = obj.expression;
|
|
359
|
+
if (!obj || obj.type !== AST_NODE_TYPES.ObjectExpression) return [];
|
|
360
|
+
|
|
361
|
+
const keys = [];
|
|
362
|
+
const walk = (node, prefix) => {
|
|
363
|
+
if (node.type !== AST_NODE_TYPES.ObjectExpression) return;
|
|
364
|
+
for (const prop of node.properties) {
|
|
365
|
+
if (prop.type !== AST_NODE_TYPES.Property) continue;
|
|
366
|
+
let name;
|
|
367
|
+
if (prop.key.type === AST_NODE_TYPES.Identifier) name = prop.key.name;
|
|
368
|
+
else if (prop.key.type === AST_NODE_TYPES.Literal) name = String(prop.key.value);
|
|
369
|
+
else continue;
|
|
370
|
+
const dotPath = prefix ? `${prefix}.${name}` : name;
|
|
371
|
+
if (prop.value.type === AST_NODE_TYPES.ObjectExpression) {
|
|
372
|
+
walk(prop.value, dotPath);
|
|
373
|
+
} else if (prop.value.type === AST_NODE_TYPES.Literal) {
|
|
374
|
+
keys.push(dotPath);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
walk(obj, '');
|
|
379
|
+
return keys.sort();
|
|
380
|
+
} catch (e) {
|
|
381
|
+
const errorMessage = `Error parsing locale file ${filePath}: ${e.message}`;
|
|
382
|
+
logger.error(errorMessage);
|
|
383
|
+
if (server) {
|
|
384
|
+
server.ws.send({
|
|
385
|
+
type: 'error',
|
|
386
|
+
err: {
|
|
387
|
+
message: e.message,
|
|
388
|
+
stack: e.stack,
|
|
389
|
+
id: filePath,
|
|
390
|
+
plugin: 'vite-plugin-locales',
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Virtual module plugin for `virtual:caper-plugins`. Mirrors
|
|
400
|
+
* `sceneListPlugin`: supports static + dynamic imports per entry, preserves
|
|
401
|
+
* the full metadata shape so consumers can filter by `active` / read IDs.
|
|
402
|
+
*/
|