@caperjs/core 0.6.1 → 0.7.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/build/index.d.mts +6 -0
- package/build/index.mjs +7 -0
- package/build/plugins/solid.mjs +30 -0
- package/cli/doctor.mjs +128 -0
- package/cli/doctor.test.mjs +154 -0
- package/extras/llms.txt +192 -4
- package/extras/skills/caper/SKILL.md +51 -3
- package/lib/{CaptionsPlugin-BtVJiQs_.js → CaptionsPlugin-Boj-gm3A.js} +5 -5
- package/lib/{CaptionsPlugin-BtVJiQs_.js.map → CaptionsPlugin-Boj-gm3A.js.map} +1 -1
- package/lib/{DataAdapter-BtNz4yi2.js → DataAdapter-Cra9dJ6W.js} +9 -9
- package/lib/{DataAdapter-BtNz4yi2.js.map → DataAdapter-Cra9dJ6W.js.map} +1 -1
- package/lib/{DebugRenderer-DK8FAED1.js → DebugRenderer-19S58S4h.js} +2 -2
- package/lib/{DebugRenderer-DK8FAED1.js.map → DebugRenderer-19S58S4h.js.map} +1 -1
- package/lib/{DevToolsPlugin-BS9idGRi.js → DevToolsPlugin-BTSkRsdS.js} +2 -2
- package/lib/{DevToolsPlugin-BS9idGRi.js.map → DevToolsPlugin-BTSkRsdS.js.map} +1 -1
- package/lib/{GSAPPlugin-DVDbDVP8.js → GSAPPlugin-BcL7vjyw.js} +5 -5
- package/lib/{GSAPPlugin-DVDbDVP8.js.map → GSAPPlugin-BcL7vjyw.js.map} +1 -1
- package/lib/{LayoutPlugin-qQJ5qJud.js → LayoutPlugin-BDhSngz_.js} +4 -4
- package/lib/{LayoutPlugin-qQJ5qJud.js.map → LayoutPlugin-BDhSngz_.js.map} +1 -1
- package/lib/ScreenDebugPlugin-C6pxs_3O.js +103 -0
- package/lib/ScreenDebugPlugin-C6pxs_3O.js.map +1 -0
- package/lib/{SpinePlugin-CPNRM2J1.js → SpinePlugin-CrQhaK6r.js} +2 -2
- package/lib/{SpinePlugin-CPNRM2J1.js.map → SpinePlugin-CrQhaK6r.js.map} +1 -1
- package/lib/{StatsPlugin-BHihicBl.js → StatsPlugin-Cu625dmk.js} +2 -2
- package/lib/{StatsPlugin-BHihicBl.js.map → StatsPlugin-Cu625dmk.js.map} +1 -1
- package/lib/{VoiceOverPlugin-BdTHxgku.js → VoiceOverPlugin-D4rfHFyJ.js} +5 -5
- package/lib/{VoiceOverPlugin-BdTHxgku.js.map → VoiceOverPlugin-D4rfHFyJ.js.map} +1 -1
- package/lib/caper.mjs +98 -97
- package/lib/caper.mjs.map +1 -1
- package/lib/{const-BQItSP39.js → const-BQ2iMCtc.js} +180 -180
- package/lib/{const-BQItSP39.js.map → const-BQ2iMCtc.js.map} +1 -1
- package/lib/core/Application.d.ts.map +1 -1
- package/lib/plugins/ScreenDebugPlugin.d.ts +22 -0
- package/lib/plugins/ScreenDebugPlugin.d.ts.map +1 -0
- package/lib/plugins/ScreenDebugPlugin.test.d.ts +2 -0
- package/lib/plugins/ScreenDebugPlugin.test.d.ts.map +1 -0
- package/lib/plugins/index.d.ts +2 -0
- package/lib/plugins/index.d.ts.map +1 -1
- package/lib/plugins/screenDebugFlag.d.ts +2 -0
- package/lib/plugins/screenDebugFlag.d.ts.map +1 -0
- package/lib/plugins/screenDebugFlag.test.d.ts +2 -0
- package/lib/plugins/screenDebugFlag.test.d.ts.map +1 -0
- package/lib/{registries-DlqHRO90.js → registries-BmDw_GV1.js} +237 -221
- package/lib/registries-BmDw_GV1.js.map +1 -0
- package/package.json +1 -1
- package/src/core/Application.ts +8 -0
- package/src/plugins/ScreenDebugPlugin.test.ts +61 -0
- package/src/plugins/ScreenDebugPlugin.ts +174 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/screenDebugFlag.test.ts +45 -0
- package/src/plugins/screenDebugFlag.ts +26 -0
- package/src/version.ts +1 -1
- package/lib/registries-DlqHRO90.js.map +0 -1
package/build/index.d.mts
CHANGED
|
@@ -19,6 +19,12 @@ export interface CaperOptions {
|
|
|
19
19
|
autoRegister?: boolean;
|
|
20
20
|
update?: 'prompt' | 'auto' | 'manual';
|
|
21
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* Compile `.tsx` with `@caperjs/solid`'s Solid JSX plugin, resolved from the
|
|
24
|
+
* app's own `node_modules`. Absent or `false` imports nothing. An object
|
|
25
|
+
* forwards `include` (default `['**\/*.tsx']`).
|
|
26
|
+
*/
|
|
27
|
+
solid?: boolean | { include?: string[] };
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
/**
|
package/build/index.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { entityListPlugin, pluginListPlugin, popupListPlugin, sceneListPlugin, u
|
|
|
25
25
|
import { pngFallbackPrunePlugin } from './plugins/pruneFallbacks.mjs';
|
|
26
26
|
import { caperPwaPlugins } from './plugins/pwa.mjs';
|
|
27
27
|
import { createCaperRuntimePlugin } from './plugins/runtime.mjs';
|
|
28
|
+
import { caperSolidPlugin } from './plugins/solid.mjs';
|
|
28
29
|
import { createCaperViewportPlugin } from './plugins/viewport.mjs';
|
|
29
30
|
|
|
30
31
|
const buildFlags = readCaperBuildFlags();
|
|
@@ -96,6 +97,9 @@ function caperPluginList({ assets = {}, pwa } = {}) {
|
|
|
96
97
|
* DOM update banner; 'auto' reloads the page as soon as a new build lands;
|
|
97
98
|
* 'manual' installs no UI at all and leaves it to the game, which listens to
|
|
98
99
|
* `app.onPwaUpdateAvailable`).
|
|
100
|
+
* @property {boolean|{include?: string[]}} [solid] Compile `.tsx` with
|
|
101
|
+
* `@caperjs/solid`'s Solid JSX plugin. Absent or `false` imports nothing.
|
|
102
|
+
* An object forwards `include` (default `['**\/*.tsx']`).
|
|
99
103
|
*/
|
|
100
104
|
|
|
101
105
|
/**
|
|
@@ -108,6 +112,9 @@ export function caper(options = {}) {
|
|
|
108
112
|
name: 'caper:defaults',
|
|
109
113
|
config: (userConfig, env) => caperDefaults(userConfig, env),
|
|
110
114
|
},
|
|
115
|
+
// Vite awaits promises in the plugins array, so the lazy import stays off
|
|
116
|
+
// `caper()`'s own signature — projects keep writing `plugins: [caper()]`.
|
|
117
|
+
...(options.solid ? [caperSolidPlugin(options.solid)] : []),
|
|
111
118
|
...caperPluginList(options),
|
|
112
119
|
];
|
|
113
120
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `caper({ solid: true })` — pull `@caperjs/solid`'s Vite plugin into the preset.
|
|
7
|
+
*
|
|
8
|
+
* The dependency arrow only ever points from the app: core knows the specifier,
|
|
9
|
+
* never the package. Nothing is imported unless the app opts in.
|
|
10
|
+
*
|
|
11
|
+
* Resolution starts at the app root, not at this file. Under pnpm (and any other
|
|
12
|
+
* non-hoisted layout) `@caperjs/solid` is not a sibling of `@caperjs/core`, so a
|
|
13
|
+
* bare `import('@caperjs/solid/vite')` from here would fail even when the app has
|
|
14
|
+
* the package installed.
|
|
15
|
+
*
|
|
16
|
+
* @param {true|{ include?: string[] }} solid The preset's `solid` option.
|
|
17
|
+
* @param {string} [from] Directory to resolve from; the app root by default.
|
|
18
|
+
* @returns {Promise<import('vite').PluginOption>}
|
|
19
|
+
*/
|
|
20
|
+
export async function caperSolidPlugin(solid, from = process.cwd()) {
|
|
21
|
+
const req = createRequire(path.join(from, 'package.json'));
|
|
22
|
+
let entry;
|
|
23
|
+
try {
|
|
24
|
+
entry = req.resolve('@caperjs/solid/vite');
|
|
25
|
+
} catch {
|
|
26
|
+
throw new Error('[caper] solid: true requires @caperjs/solid to be installed (pnpm add @caperjs/solid solid-js)');
|
|
27
|
+
}
|
|
28
|
+
const { caperSolid } = await import(pathToFileURL(entry).href);
|
|
29
|
+
return caperSolid(typeof solid === 'object' ? solid : undefined);
|
|
30
|
+
}
|
package/cli/doctor.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { dim, green, red, yellow } from 'kleur/colors';
|
|
|
2
2
|
|
|
3
3
|
import { execFile } from 'node:child_process';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
8
|
|
|
@@ -61,10 +62,110 @@ const npmLatestVersion = (pkg, timeoutMs = 5000) =>
|
|
|
61
62
|
});
|
|
62
63
|
});
|
|
63
64
|
|
|
65
|
+
const readJson = (file) => {
|
|
66
|
+
if (!fs.existsSync(file)) return null;
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** tsconfig.json is JSON with comments and trailing commas often enough to matter. */
|
|
75
|
+
const readJsonc = (file) => {
|
|
76
|
+
if (!fs.existsSync(file)) return null;
|
|
77
|
+
try {
|
|
78
|
+
const raw = fs
|
|
79
|
+
.readFileSync(file, 'utf-8')
|
|
80
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
81
|
+
.replace(/(^|[^:"'\\])\/\/.*$/gm, '$1')
|
|
82
|
+
.replace(/,(\s*[}\]])/g, '$1');
|
|
83
|
+
return JSON.parse(raw);
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
64
89
|
const push = (checks, id, status, label, hint) => {
|
|
65
90
|
checks.push({ id, status, label, ...(hint ? { hint } : {}) });
|
|
66
91
|
};
|
|
67
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Resolve a tsconfig `extends` entry to an absolute config file path,
|
|
95
|
+
* TypeScript-style: relative specs resolve against the extending config's
|
|
96
|
+
* directory, everything else resolves node-module-style (bare package name,
|
|
97
|
+
* `+ .json`, `+ /tsconfig.json`) rooted at that same config file so an app's
|
|
98
|
+
* own node_modules is used. Returns null (never throws) when nothing resolves.
|
|
99
|
+
*/
|
|
100
|
+
const resolveExtendsSpec = (spec, fromConfigFile) => {
|
|
101
|
+
const fromDir = path.dirname(fromConfigFile);
|
|
102
|
+
const isRelative = spec.startsWith('./') || spec.startsWith('../') || path.isAbsolute(spec);
|
|
103
|
+
|
|
104
|
+
if (isRelative) {
|
|
105
|
+
const base = path.isAbsolute(spec) ? spec : path.resolve(fromDir, spec);
|
|
106
|
+
if (fs.existsSync(base) && fs.statSync(base).isFile()) return base;
|
|
107
|
+
const withJson = base.endsWith('.json') ? base : `${base}.json`;
|
|
108
|
+
return fs.existsSync(withJson) ? withJson : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const require = createRequire(fromConfigFile);
|
|
112
|
+
const candidates = [spec, `${spec}.json`, `${spec}/tsconfig.json`];
|
|
113
|
+
for (const candidate of candidates) {
|
|
114
|
+
try {
|
|
115
|
+
return require.resolve(candidate);
|
|
116
|
+
} catch {
|
|
117
|
+
// try the next candidate
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Some bare specs are effectively relative paths, or node-module resolution
|
|
121
|
+
// fails for other plausible reasons — fall back to filesystem paths relative
|
|
122
|
+
// to the extending config's directory before giving up.
|
|
123
|
+
for (const candidate of candidates) {
|
|
124
|
+
const asPath = path.resolve(fromDir, candidate);
|
|
125
|
+
if (fs.existsSync(asPath) && fs.statSync(asPath).isFile()) return asPath;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Effective `compilerOptions` for a tsconfig, walking `extends` (string or
|
|
132
|
+
* array, TS 5 style) like `tsc` does: the own config's fields override
|
|
133
|
+
* wholesale, later array entries beat earlier ones, and each base can itself
|
|
134
|
+
* extend further (recursion). Cycle-guarded per ancestor chain and depth
|
|
135
|
+
* capped; unreadable/unresolvable bases are silently skipped so doctor never
|
|
136
|
+
* crashes on a weird config.
|
|
137
|
+
*/
|
|
138
|
+
const resolveEffectiveCompilerOptions = (configFile, ancestors = new Set(), depth = 0) => {
|
|
139
|
+
if (depth > 10) return {};
|
|
140
|
+
|
|
141
|
+
let real;
|
|
142
|
+
try {
|
|
143
|
+
real = fs.realpathSync(configFile);
|
|
144
|
+
} catch {
|
|
145
|
+
real = configFile;
|
|
146
|
+
}
|
|
147
|
+
if (ancestors.has(real)) return {};
|
|
148
|
+
|
|
149
|
+
const config = readJsonc(configFile);
|
|
150
|
+
if (!config) return {};
|
|
151
|
+
|
|
152
|
+
const nextAncestors = new Set(ancestors);
|
|
153
|
+
nextAncestors.add(real);
|
|
154
|
+
|
|
155
|
+
let inherited = {};
|
|
156
|
+
if (config.extends) {
|
|
157
|
+
const specs = Array.isArray(config.extends) ? config.extends : [config.extends];
|
|
158
|
+
for (const spec of specs) {
|
|
159
|
+
const resolved = resolveExtendsSpec(spec, configFile);
|
|
160
|
+
if (!resolved) continue;
|
|
161
|
+
const baseOptions = resolveEffectiveCompilerOptions(resolved, nextAncestors, depth + 1);
|
|
162
|
+
inherited = { ...inherited, ...baseOptions };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { ...inherited, ...(config.compilerOptions ?? {}) };
|
|
167
|
+
};
|
|
168
|
+
|
|
68
169
|
export async function runChecks(cwd, { online = true } = {}) {
|
|
69
170
|
const checks = [];
|
|
70
171
|
const installedVersion = readInstalledVersion();
|
|
@@ -169,6 +270,33 @@ export async function runChecks(cwd, { online = true } = {}) {
|
|
|
169
270
|
});
|
|
170
271
|
push(checks, 'peers', peerStatus, `peer deps: ${peerResults.join(', ')}`, peerStatus !== 'ok' ? 'pnpm install' : undefined);
|
|
171
272
|
|
|
273
|
+
// Solid JSX only typechecks with three tsconfig settings, and none of them can
|
|
274
|
+
// be defaulted from caper's base config: `jsxFactory` is a type-lookup root
|
|
275
|
+
// that exists because a transitive @types/react would otherwise shadow the
|
|
276
|
+
// global JSX namespace. Apps without @caperjs/solid never see this check.
|
|
277
|
+
const appPkg = readJson(path.join(cwd, 'package.json'));
|
|
278
|
+
const appDeps = { ...appPkg?.dependencies, ...appPkg?.devDependencies };
|
|
279
|
+
if (appDeps['@caperjs/solid']) {
|
|
280
|
+
const tsconfigPath = path.join(cwd, 'tsconfig.json');
|
|
281
|
+
const tsconfig = readJsonc(tsconfigPath);
|
|
282
|
+
if (!tsconfig) {
|
|
283
|
+
push(checks, 'solid-tsconfig', 'fail', 'solid tsconfig unreadable', 'tsconfig.json is missing or not parseable');
|
|
284
|
+
} else {
|
|
285
|
+
const compilerOptions = resolveEffectiveCompilerOptions(tsconfigPath);
|
|
286
|
+
const missing = [];
|
|
287
|
+
if (compilerOptions.jsx !== 'preserve') missing.push('"jsx": "preserve"');
|
|
288
|
+
if (compilerOptions.jsxFactory !== 'CaperJSX.h') missing.push('"jsxFactory": "CaperJSX.h"');
|
|
289
|
+
if (!compilerOptions.types?.includes('@caperjs/solid/jsx')) missing.push('"@caperjs/solid/jsx" in types');
|
|
290
|
+
push(
|
|
291
|
+
checks,
|
|
292
|
+
'solid-tsconfig',
|
|
293
|
+
missing.length ? 'fail' : 'ok',
|
|
294
|
+
missing.length ? 'solid tsconfig incomplete' : 'solid tsconfig',
|
|
295
|
+
missing.length ? `add ${missing.join(', ')} to tsconfig.json compilerOptions` : undefined,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
172
300
|
const caches = ['.assetpack', '.cache', 'dist'].filter((name) => fs.existsSync(path.join(cwd, name)));
|
|
173
301
|
push(checks, 'caches', 'ok', `caches${caches.length ? `: ${caches.join(', ')}` : ' clean'}`, caches.length ? 'rm -rf them on weird asset/name mismatches' : undefined);
|
|
174
302
|
|
package/cli/doctor.test.mjs
CHANGED
|
@@ -99,6 +99,160 @@ ${END_MARKER}`;
|
|
|
99
99
|
expect(find(checks, 'agent').status).toBe('ok');
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
+
it('skips the solid tsconfig check when the app does not use @caperjs/solid', async () => {
|
|
103
|
+
const cwd = makeTempDir();
|
|
104
|
+
fs.writeFileSync(path.join(cwd, 'package.json'), JSON.stringify({ dependencies: { '@caperjs/core': '^1' } }), 'utf-8');
|
|
105
|
+
|
|
106
|
+
const checks = await runChecks(cwd, { online: false });
|
|
107
|
+
|
|
108
|
+
expect(find(checks, 'solid-tsconfig')).toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('names every tsconfig setting a solid app is missing', async () => {
|
|
112
|
+
const cwd = makeTempDir();
|
|
113
|
+
fs.writeFileSync(path.join(cwd, 'package.json'), JSON.stringify({ dependencies: { '@caperjs/solid': '^1' } }), 'utf-8');
|
|
114
|
+
fs.writeFileSync(path.join(cwd, 'tsconfig.json'), '{\n // solid needs more than this\n "compilerOptions": { "jsx": "preserve" }\n}\n', 'utf-8');
|
|
115
|
+
|
|
116
|
+
const checks = await runChecks(cwd, { online: false });
|
|
117
|
+
const solid = find(checks, 'solid-tsconfig');
|
|
118
|
+
|
|
119
|
+
expect(solid.status).toBe('fail');
|
|
120
|
+
expect(solid.hint).toContain('"jsxFactory": "CaperJSX.h"');
|
|
121
|
+
expect(solid.hint).toContain('"@caperjs/solid/jsx" in types');
|
|
122
|
+
expect(solid.hint).not.toContain('"jsx": "preserve"');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('passes a solid app whose tsconfig has all three settings', async () => {
|
|
126
|
+
const cwd = makeTempDir();
|
|
127
|
+
fs.writeFileSync(path.join(cwd, 'package.json'), JSON.stringify({ devDependencies: { '@caperjs/solid': '^1' } }), 'utf-8');
|
|
128
|
+
fs.writeFileSync(
|
|
129
|
+
path.join(cwd, 'tsconfig.json'),
|
|
130
|
+
JSON.stringify({
|
|
131
|
+
compilerOptions: { jsx: 'preserve', jsxFactory: 'CaperJSX.h', types: ['@caperjs/core/client', '@caperjs/solid/jsx'] },
|
|
132
|
+
}),
|
|
133
|
+
'utf-8',
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const checks = await runChecks(cwd, { online: false });
|
|
137
|
+
|
|
138
|
+
expect(find(checks, 'solid-tsconfig').status).toBe('ok');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('resolves solid tsconfig settings inherited through a relative extends', async () => {
|
|
142
|
+
const cwd = makeTempDir();
|
|
143
|
+
fs.writeFileSync(path.join(cwd, 'package.json'), JSON.stringify({ dependencies: { '@caperjs/solid': '^1' } }), 'utf-8');
|
|
144
|
+
fs.writeFileSync(
|
|
145
|
+
path.join(cwd, 'base.tsconfig.json'),
|
|
146
|
+
JSON.stringify({
|
|
147
|
+
compilerOptions: { jsx: 'preserve', jsxFactory: 'CaperJSX.h', types: ['@caperjs/core/client', '@caperjs/solid/jsx'] },
|
|
148
|
+
}),
|
|
149
|
+
'utf-8',
|
|
150
|
+
);
|
|
151
|
+
fs.writeFileSync(path.join(cwd, 'tsconfig.json'), JSON.stringify({ extends: './base.tsconfig.json' }), 'utf-8');
|
|
152
|
+
|
|
153
|
+
const checks = await runChecks(cwd, { online: false });
|
|
154
|
+
|
|
155
|
+
expect(find(checks, 'solid-tsconfig').status).toBe('ok');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("lets the app's own tsconfig override a bad base setting", async () => {
|
|
159
|
+
const cwd = makeTempDir();
|
|
160
|
+
fs.writeFileSync(path.join(cwd, 'package.json'), JSON.stringify({ dependencies: { '@caperjs/solid': '^1' } }), 'utf-8');
|
|
161
|
+
fs.writeFileSync(
|
|
162
|
+
path.join(cwd, 'base.tsconfig.json'),
|
|
163
|
+
JSON.stringify({
|
|
164
|
+
compilerOptions: { jsx: 'preserve', jsxFactory: 'Wrong.h', types: ['@caperjs/core/client', '@caperjs/solid/jsx'] },
|
|
165
|
+
}),
|
|
166
|
+
'utf-8',
|
|
167
|
+
);
|
|
168
|
+
fs.writeFileSync(
|
|
169
|
+
path.join(cwd, 'tsconfig.json'),
|
|
170
|
+
JSON.stringify({ extends: './base.tsconfig.json', compilerOptions: { jsxFactory: 'CaperJSX.h' } }),
|
|
171
|
+
'utf-8',
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const checks = await runChecks(cwd, { online: false });
|
|
175
|
+
|
|
176
|
+
expect(find(checks, 'solid-tsconfig').status).toBe('ok');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("fails when the app's own tsconfig overrides a good base setting with a bad one", async () => {
|
|
180
|
+
const cwd = makeTempDir();
|
|
181
|
+
fs.writeFileSync(path.join(cwd, 'package.json'), JSON.stringify({ dependencies: { '@caperjs/solid': '^1' } }), 'utf-8');
|
|
182
|
+
fs.writeFileSync(
|
|
183
|
+
path.join(cwd, 'base.tsconfig.json'),
|
|
184
|
+
JSON.stringify({
|
|
185
|
+
compilerOptions: { jsx: 'preserve', jsxFactory: 'CaperJSX.h', types: ['@caperjs/core/client', '@caperjs/solid/jsx'] },
|
|
186
|
+
}),
|
|
187
|
+
'utf-8',
|
|
188
|
+
);
|
|
189
|
+
fs.writeFileSync(
|
|
190
|
+
path.join(cwd, 'tsconfig.json'),
|
|
191
|
+
JSON.stringify({ extends: './base.tsconfig.json', compilerOptions: { jsxFactory: 'Wrong.h' } }),
|
|
192
|
+
'utf-8',
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const checks = await runChecks(cwd, { online: false });
|
|
196
|
+
const solid = find(checks, 'solid-tsconfig');
|
|
197
|
+
|
|
198
|
+
expect(solid.status).toBe('fail');
|
|
199
|
+
expect(solid.hint).toContain('"jsxFactory": "CaperJSX.h"');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('resolves solid tsconfig settings from a later entry in an extends array', async () => {
|
|
203
|
+
const cwd = makeTempDir();
|
|
204
|
+
fs.writeFileSync(path.join(cwd, 'package.json'), JSON.stringify({ dependencies: { '@caperjs/solid': '^1' } }), 'utf-8');
|
|
205
|
+
fs.writeFileSync(path.join(cwd, 'first.tsconfig.json'), JSON.stringify({ compilerOptions: { strict: true } }), 'utf-8');
|
|
206
|
+
fs.writeFileSync(
|
|
207
|
+
path.join(cwd, 'second.tsconfig.json'),
|
|
208
|
+
JSON.stringify({
|
|
209
|
+
compilerOptions: { jsx: 'preserve', jsxFactory: 'CaperJSX.h', types: ['@caperjs/core/client', '@caperjs/solid/jsx'] },
|
|
210
|
+
}),
|
|
211
|
+
'utf-8',
|
|
212
|
+
);
|
|
213
|
+
fs.writeFileSync(path.join(cwd, 'tsconfig.json'), JSON.stringify({ extends: ['./first.tsconfig.json', './second.tsconfig.json'] }), 'utf-8');
|
|
214
|
+
|
|
215
|
+
const checks = await runChecks(cwd, { online: false });
|
|
216
|
+
|
|
217
|
+
expect(find(checks, 'solid-tsconfig').status).toBe('ok');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('resolves solid tsconfig settings through a chained extends', async () => {
|
|
221
|
+
const cwd = makeTempDir();
|
|
222
|
+
fs.writeFileSync(path.join(cwd, 'package.json'), JSON.stringify({ dependencies: { '@caperjs/solid': '^1' } }), 'utf-8');
|
|
223
|
+
fs.writeFileSync(
|
|
224
|
+
path.join(cwd, 'grandparent.tsconfig.json'),
|
|
225
|
+
JSON.stringify({
|
|
226
|
+
compilerOptions: { jsx: 'preserve', jsxFactory: 'CaperJSX.h', types: ['@caperjs/core/client', '@caperjs/solid/jsx'] },
|
|
227
|
+
}),
|
|
228
|
+
'utf-8',
|
|
229
|
+
);
|
|
230
|
+
fs.writeFileSync(path.join(cwd, 'parent.tsconfig.json'), JSON.stringify({ extends: './grandparent.tsconfig.json' }), 'utf-8');
|
|
231
|
+
fs.writeFileSync(path.join(cwd, 'tsconfig.json'), JSON.stringify({ extends: './parent.tsconfig.json' }), 'utf-8');
|
|
232
|
+
|
|
233
|
+
const checks = await runChecks(cwd, { online: false });
|
|
234
|
+
|
|
235
|
+
expect(find(checks, 'solid-tsconfig').status).toBe('ok');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('does not throw on a broken extends path and falls back to the own tsconfig', async () => {
|
|
239
|
+
const cwd = makeTempDir();
|
|
240
|
+
fs.writeFileSync(path.join(cwd, 'package.json'), JSON.stringify({ dependencies: { '@caperjs/solid': '^1' } }), 'utf-8');
|
|
241
|
+
fs.writeFileSync(
|
|
242
|
+
path.join(cwd, 'tsconfig.json'),
|
|
243
|
+
JSON.stringify({ extends: './does-not-exist.json', compilerOptions: { jsx: 'preserve' } }),
|
|
244
|
+
'utf-8',
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const checks = await runChecks(cwd, { online: false });
|
|
248
|
+
const solid = find(checks, 'solid-tsconfig');
|
|
249
|
+
|
|
250
|
+
expect(solid.status).toBe('fail');
|
|
251
|
+
expect(solid.hint).toContain('"jsxFactory": "CaperJSX.h"');
|
|
252
|
+
expect(solid.hint).toContain('"@caperjs/solid/jsx" in types');
|
|
253
|
+
expect(solid.hint).not.toContain('"jsx": "preserve"');
|
|
254
|
+
});
|
|
255
|
+
|
|
102
256
|
it('reports a linked package missing its build', async () => {
|
|
103
257
|
const cwd = makeTempDir();
|
|
104
258
|
const checkout = path.join(cwd, 'checkout');
|
package/extras/llms.txt
CHANGED
|
@@ -40,7 +40,8 @@ produce working code.
|
|
|
40
40
|
15. [Common recipes](#15-common-recipes)
|
|
41
41
|
16. [Gotchas & verification loop](#16-gotchas--verification-loop)
|
|
42
42
|
17. [Automation bridge — drive the running app](#17-automation-bridge--drive-the-running-app)
|
|
43
|
-
18. [
|
|
43
|
+
18. [Declarative layer — `@caperjs/solid`](#18-declarative-layer--caperjssolid)
|
|
44
|
+
19. [API reference index](#19-api-reference-index)
|
|
44
45
|
|
|
45
46
|
---
|
|
46
47
|
|
|
@@ -464,6 +465,12 @@ Every allowed key in `caper.config.ts`. Defer shape details to
|
|
|
464
465
|
|
|
465
466
|
Plus all standard PixiJS `ApplicationOptions` (`antialias`, `width`, `height`, etc.).
|
|
466
467
|
|
|
468
|
+
Screen viewport debugging is available through the built-in `screenDebug` overlay.
|
|
469
|
+
Open the game URL in a normal browser tab with `?caper-screen-debug=1` to enable it.
|
|
470
|
+
Then relaunch the installed PWA; the same-origin setting persists without an address bar.
|
|
471
|
+
Use `?caper-screen-debug=0` in a browser tab to disable it.
|
|
472
|
+
A screenshot containing the overlay and marker lines is the diagnostic artifact.
|
|
473
|
+
|
|
467
474
|
**Scene/plugin/popup/entity registrations are NOT in this table** — they're
|
|
468
475
|
discovered from disk. Plugins list above is the one exception.
|
|
469
476
|
|
|
@@ -1213,6 +1220,9 @@ Each is a separate npm package. Install → list in `plugins: [...]` → use via
|
|
|
1213
1220
|
Install via pnpm/npm, then register in `caper.config.ts plugins: [...]`.
|
|
1214
1221
|
Deep API lives in each plugin's own README on npm.
|
|
1215
1222
|
|
|
1223
|
+
`@caperjs/solid` is first-party too but is **not** a plugin — it is the optional
|
|
1224
|
+
declarative view layer and never appears in `plugins: [...]`. See §18.
|
|
1225
|
+
|
|
1216
1226
|
Canonical registration shape:
|
|
1217
1227
|
|
|
1218
1228
|
```ts
|
|
@@ -1638,7 +1648,180 @@ Rules of thumb:
|
|
|
1638
1648
|
|
|
1639
1649
|
---
|
|
1640
1650
|
|
|
1641
|
-
## 18.
|
|
1651
|
+
## 18. Declarative layer — `@caperjs/solid`
|
|
1652
|
+
|
|
1653
|
+
**Optional and purely additive.** `@caperjs/solid` is a separate package (npm
|
|
1654
|
+
`@caperjs/solid`, source `packages/plugin-solid/`) that lets a display object
|
|
1655
|
+
declare its members with SolidJS JSX instead of building them by hand. It is
|
|
1656
|
+
**not** an `IPlugin` — nothing goes in `plugins: [...]`, there is no `app.solid`.
|
|
1657
|
+
Core stays Solid-free; imperative Caper (§6) is unchanged and always available.
|
|
1658
|
+
|
|
1659
|
+
Reach for it when a screen is **UI-shaped**: state→view plumbing, lists that
|
|
1660
|
+
insert/remove, panels that show and hide. Keep gameplay (`update()` loops,
|
|
1661
|
+
physics, entity simulation) imperative — this layer does nothing for it.
|
|
1662
|
+
|
|
1663
|
+
### 18.1 The `compose()` contract
|
|
1664
|
+
|
|
1665
|
+
A `Container` (or `Scene`) subclass extends `ComposableContainer` /
|
|
1666
|
+
`ComposableScene` and returns JSX from `compose()`:
|
|
1667
|
+
|
|
1668
|
+
- **Mount once.** The tree mounts the first time the object hits the stage,
|
|
1669
|
+
driven by Pixi's native `'added'` event — so a subclass that overrides
|
|
1670
|
+
`added()` without calling `super.added()` cannot break it.
|
|
1671
|
+
- **Dispose on `destroy()`.** The Solid root is torn down there; re-adding the
|
|
1672
|
+
object does **not** remount.
|
|
1673
|
+
- **Signals do every update after that.** Fine-grained, no VDOM, no re-render,
|
|
1674
|
+
no diff.
|
|
1675
|
+
|
|
1676
|
+
> `compose()` declares what exists, signals say when state changed, `update()`
|
|
1677
|
+
> moves things every frame.
|
|
1678
|
+
|
|
1679
|
+
### 18.2 Install + build wiring
|
|
1680
|
+
|
|
1681
|
+
```sh
|
|
1682
|
+
pnpm add @caperjs/solid solid-js
|
|
1683
|
+
```
|
|
1684
|
+
|
|
1685
|
+
```ts
|
|
1686
|
+
// vite.config.ts
|
|
1687
|
+
import { caper } from '@caperjs/core/vite';
|
|
1688
|
+
|
|
1689
|
+
export default defineConfig({ plugins: [caper({ solid: true })] });
|
|
1690
|
+
```
|
|
1691
|
+
|
|
1692
|
+
`solid: true` makes the preset resolve `@caperjs/solid/vite` from the **app's**
|
|
1693
|
+
`node_modules` and add its Solid JSX plugin (universal renderer, `moduleName:
|
|
1694
|
+
'@caperjs/solid'`, `hot: false`). `vite-plugin-solid` ships as a dependency of
|
|
1695
|
+
`@caperjs/solid` — never install it directly. `{ solid: { include: [...] } }`
|
|
1696
|
+
narrows which files compile as JSX (default `['**/*.tsx']`). For a bare vite or
|
|
1697
|
+
vitest config, import `caperSolid()` from `@caperjs/solid/vite` instead.
|
|
1698
|
+
|
|
1699
|
+
### 18.3 tsconfig contract (all three required)
|
|
1700
|
+
|
|
1701
|
+
```jsonc
|
|
1702
|
+
{
|
|
1703
|
+
"compilerOptions": {
|
|
1704
|
+
"jsx": "preserve", // vite-plugin-solid does the real compile
|
|
1705
|
+
"jsxFactory": "CaperJSX.h", // type-lookup root only; nothing calls it
|
|
1706
|
+
"types": ["@caperjs/core/client", "@caperjs/solid/jsx"]
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
```
|
|
1710
|
+
|
|
1711
|
+
`jsxFactory` exists because a transitive `@types/react` shadows the global `JSX`
|
|
1712
|
+
namespace and `jsxImportSource` only applies in `react-jsx` mode. `caper doctor`
|
|
1713
|
+
reports a `solid-tsconfig` check with the exact missing keys whenever the app
|
|
1714
|
+
depends on `@caperjs/solid`; it walks the `extends` chain, so the three keys can
|
|
1715
|
+
live in a shared base config.
|
|
1716
|
+
|
|
1717
|
+
### 18.4 Minimal example
|
|
1718
|
+
|
|
1719
|
+
```tsx
|
|
1720
|
+
import { animated, ComposableContainer, type Composes } from '@caperjs/solid';
|
|
1721
|
+
import type { Graphics } from 'pixi.js';
|
|
1722
|
+
import { createSignal } from 'solid-js';
|
|
1723
|
+
|
|
1724
|
+
const WIDTH = 220;
|
|
1725
|
+
const HEIGHT = 22;
|
|
1726
|
+
|
|
1727
|
+
// Module-level so the reference is stable — see rule 2 in §18.6.
|
|
1728
|
+
const bg = (g: Graphics) => g.roundRect(0, 0, WIDTH, HEIGHT, 4).fill({ color: 0x22252a });
|
|
1729
|
+
const fill = (g: Graphics) => g.roundRect(0, 0, WIDTH, HEIGHT, 4).fill({ color: 0x9aa96b });
|
|
1730
|
+
|
|
1731
|
+
export class HealthBar extends ComposableContainer implements Composes {
|
|
1732
|
+
private health = createSignal(100);
|
|
1733
|
+
|
|
1734
|
+
/** Imperative API — an ordinary method that happens to write a signal. */
|
|
1735
|
+
public damage(amount: number) {
|
|
1736
|
+
this.health[1]((hp) => Math.max(0, hp - amount));
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
public compose() {
|
|
1740
|
+
const hp = this.health[0];
|
|
1741
|
+
const hpA = animated(() => hp() / 100); // glides instead of snapping
|
|
1742
|
+
|
|
1743
|
+
return (
|
|
1744
|
+
<container>
|
|
1745
|
+
<graphics draw={bg} />
|
|
1746
|
+
<graphics draw={fill} scale={{ x: hpA(), y: 1 }} />
|
|
1747
|
+
<text text={`${hp()} HP`} anchor={{ x: 0.5, y: 0.5 }} x={WIDTH / 2} y={HEIGHT / 2} />
|
|
1748
|
+
</container>
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
```
|
|
1753
|
+
|
|
1754
|
+
### 18.5 Surface
|
|
1755
|
+
|
|
1756
|
+
| Name | One-line |
|
|
1757
|
+
| --- | --- |
|
|
1758
|
+
| `Composable(Base)` | Mixin adding the `compose()` contract to any display class. |
|
|
1759
|
+
| `ComposableContainer` / `ComposableScene` | Prebuilt `Composable(Container)` / `Composable(Scene)`. |
|
|
1760
|
+
| `Composes` | The interface a subclass `implements` to declare `compose()`. |
|
|
1761
|
+
| `asComponent(Ctor, defaults?)` | Lift an existing display class into JSX; constructs once, applies props reactively (incl. `ref`). |
|
|
1762
|
+
| `useTick(fn)` | `app.ticker.add` + `onCleanup` — `update()` for function components. |
|
|
1763
|
+
| `animated(source, opts?)` | Gliding accessor: reads like a signal, eases toward its source. `opts: { duration, ease }`. |
|
|
1764
|
+
| `<AnimatedShow when enter exit>` | `<Show>` that holds children on stage until the exit animation ends. |
|
|
1765
|
+
|
|
1766
|
+
Intrinsic elements: `container`, `sprite`, `text`, `graphics`, `flexContainer`.
|
|
1767
|
+
Anything else goes through `asComponent`. Props use the same resolvers as
|
|
1768
|
+
`this.add.*`, so `scale={2}`, `scale={{ x, y }}` and `scale={[x, y]}` all behave
|
|
1769
|
+
as in factory config; `on*` props attach Pixi listeners and flip `eventMode` to
|
|
1770
|
+
`'static'`; `draw` is `<graphics>`-only.
|
|
1771
|
+
|
|
1772
|
+
`enter` / `exit` on `<AnimatedShow>` are plain GSAP vars with target properties
|
|
1773
|
+
in a `pixi: {...}` block (GSAP's PixiPlugin, which core's `GSAPPlugin` registers
|
|
1774
|
+
at bootstrap). `animated()` and `<AnimatedShow>` must be called inside a
|
|
1775
|
+
reactive owner — a component body or a `compose()`.
|
|
1776
|
+
|
|
1777
|
+
### 18.6 Authoring rules
|
|
1778
|
+
|
|
1779
|
+
1. **Signals for event-rate state; `update()` / `useTick` + refs for per-frame
|
|
1780
|
+
bulk motion.** Hundreds of per-frame bindings are fine; moving a crowd every
|
|
1781
|
+
frame still belongs in an imperative loop.
|
|
1782
|
+
2. **`draw` functions must be stable references.** Solid batches an element's
|
|
1783
|
+
dynamic props into one `!==`-guarded effect, so an inline `draw={dot(3)}`
|
|
1784
|
+
allocates a new closure whenever any prop in that batch changes and forces a
|
|
1785
|
+
full `clear()` + redraw. Hoist it.
|
|
1786
|
+
3. **Never imperatively remove or reparent a child JSX created.** Solid owns
|
|
1787
|
+
those nodes. Building extra things alongside the composed tree is fine.
|
|
1788
|
+
4. **Each `compose()` is its own reactive root.** Signals cross roots; Solid
|
|
1789
|
+
*context* does not cross the class boundary — pass props or instance fields.
|
|
1790
|
+
5. **`text` elements default to `eventMode: 'none'`** so a label cannot swallow
|
|
1791
|
+
a sibling button's tap. An `on*` prop or an explicit `eventMode` overrides it.
|
|
1792
|
+
6. **One-shot effects stay imperative via refs** — a JSX-mounted Caper container
|
|
1793
|
+
still carries the `Animated` mixin, so `bar.shake()` works (§12.3).
|
|
1794
|
+
|
|
1795
|
+
### 18.7 Scene constraint — keep `src/scenes/` JSX-free
|
|
1796
|
+
|
|
1797
|
+
Discovery parses `src/scenes/**` with `jsx: false`. **Scene files must contain
|
|
1798
|
+
no JSX.** Put the view in a sibling directory and have `compose()` return a
|
|
1799
|
+
plain function call:
|
|
1800
|
+
|
|
1801
|
+
```ts
|
|
1802
|
+
// src/scenes/HudScene.ts — plain .ts, no JSX
|
|
1803
|
+
import { SceneView } from '@/solid-demo/View';
|
|
1804
|
+
import { defineScene } from '@caperjs/core';
|
|
1805
|
+
import { ComposableScene, type Composes } from '@caperjs/solid';
|
|
1806
|
+
|
|
1807
|
+
export const scene = defineScene({ id: 'hud' });
|
|
1808
|
+
|
|
1809
|
+
export default class HudScene extends ComposableScene implements Composes {
|
|
1810
|
+
public compose() {
|
|
1811
|
+
return SceneView(this); // the JSX lives on the other side of this call
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
```
|
|
1815
|
+
|
|
1816
|
+
Canonical working example in the caper repo:
|
|
1817
|
+
`apps/kitchen-sink/src/scenes/SolidJsxScene.ts` (the JSX-free scene) plus
|
|
1818
|
+
`apps/kitchen-sink/src/solid-demo/View.tsx`, `HealthBar.tsx` (nested
|
|
1819
|
+
`compose()`) and `Orbiter.ts` (an imperative `update()` class mounted via
|
|
1820
|
+
`asComponent`). Deeper API notes live in the package's own README.
|
|
1821
|
+
|
|
1822
|
+
---
|
|
1823
|
+
|
|
1824
|
+
## 19. API reference index
|
|
1642
1825
|
|
|
1643
1826
|
Flat jump table. Name → section → one-line.
|
|
1644
1827
|
|
|
@@ -1646,8 +1829,12 @@ Flat jump table. Name → section → one-line.
|
|
|
1646
1829
|
| --- | --- | --- |
|
|
1647
1830
|
| `Application` | §3.4 | Base app class; subclass for typed plugin getters. |
|
|
1648
1831
|
| `AnimatedSprite` | §6.1, §10 | Pixi AnimatedSprite wrapper (`animations`, `animation`, `autoPlay`). |
|
|
1832
|
+
| `animated(source, opts?)` | §18.5 | `@caperjs/solid` — gliding accessor; eases toward its source. |
|
|
1833
|
+
| `<AnimatedShow>` | §18.5 | `@caperjs/solid` — `<Show>` that holds children until the exit ends. |
|
|
1834
|
+
| `asComponent(Ctor, defaults?)` | §18.5 | `@caperjs/solid` — lift a display class into JSX. |
|
|
1649
1835
|
| `Button` | §7.2 | Four-state interactive button with sounds + actions. |
|
|
1650
1836
|
| `Camera` | §10 | Camera controller for scene-level follow/constrain. |
|
|
1837
|
+
| `Composable(Base)` / `ComposableContainer` / `ComposableScene` | §18.1 | `@caperjs/solid` — the `compose()` contract. |
|
|
1651
1838
|
| `Container` | §6, §10 | Caper base container (Animated + WithSignals + Factory). |
|
|
1652
1839
|
| `create(config, el?, speak?)` | §3.3 | Programmatic app bootstrap. |
|
|
1653
1840
|
| `defineActions(contexts, map)` | §4.3 | Typed action registry. |
|
|
@@ -1675,6 +1862,7 @@ Flat jump table. Name → section → one-line.
|
|
|
1675
1862
|
| `Store` | §12.2 | Unified storage-adapter facade. |
|
|
1676
1863
|
| `Toast` / `Toaster` | §7.4 | Toast notifications. |
|
|
1677
1864
|
| `UICanvas` | §7.5 | HUD root layer. |
|
|
1865
|
+
| `useTick(fn)` | §18.5 | `@caperjs/solid` — per-frame callback inside a Solid component. |
|
|
1678
1866
|
| `app.actions(id)` | §8 | Returns the action's signal. |
|
|
1679
1867
|
| `app.assets.loadBundles(ids)` | §8, §11.5 | Load named bundles. |
|
|
1680
1868
|
| `app.assets.loadSceneAssets(scene)` | §8 | Load a scene's declared bundles. |
|
|
@@ -1693,11 +1881,11 @@ Flat jump table. Name → section → one-line.
|
|
|
1693
1881
|
| `app.store.save(adapter, key, data)` | §12.2 | Persist via storage adapter. |
|
|
1694
1882
|
| `app.store.load(adapter, key)` | §12.2 | Read via storage adapter. |
|
|
1695
1883
|
| `app.timers.delay(ms)` | §8 | Scene-scoped timer. |
|
|
1696
|
-
| `Animated.animate(props)` | §12.3 | GSAP
|
|
1884
|
+
| `Animated.animate(props)` | §12.3 | GSAP animation. |
|
|
1697
1885
|
| `Animated.shake()` / `.pulse()` / `.bob()` | §12.3 | Preset animations. |
|
|
1698
1886
|
| `this.add.*` / `this.make.*` | §6 | Factory methods on any Container subclass. |
|
|
1699
1887
|
|
|
1700
1888
|
---
|
|
1701
1889
|
|
|
1702
|
-
**Last updated:** with `@caperjs/core@0.
|
|
1890
|
+
**Last updated:** with `@caperjs/core@0.6.x` (PixiJS 8.x); version in `../package.json`. When in doubt, read the
|
|
1703
1891
|
cited framework source file — signatures and defaults are authoritative there.
|