@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,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
|
+
*/
|