@caperjs/core 0.7.1 → 0.7.2
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/assetpack.mjs +35 -3
- package/build/defaults.mjs +39 -1
- package/build/index.mjs +13 -2
- package/build/internal/outputMarker.mjs +52 -0
- package/cli/doctor.mjs +205 -2
- package/cli/doctor.test.mjs +522 -1
- package/cli/native.mjs +808 -0
- package/cli/native.test.mjs +1039 -0
- package/cli.mjs +4 -0
- package/extras/css/fullscreen.css +8 -0
- package/extras/llms.txt +18 -0
- package/extras/skills/caper/SKILL.md +1 -0
- package/lib/{CaptionsPlugin-BoMkCsJO.js → CaptionsPlugin-COrofpHN.js} +2 -2
- package/lib/{CaptionsPlugin-BoMkCsJO.js.map → CaptionsPlugin-COrofpHN.js.map} +1 -1
- package/lib/{DataAdapter-DHWHuN-y.js → DataAdapter-T-22-_D1.js} +2 -2
- package/lib/{DataAdapter-DHWHuN-y.js.map → DataAdapter-T-22-_D1.js.map} +1 -1
- package/lib/{DebugRenderer-9a3mO9kX.js → DebugRenderer-ZN692Pbg.js} +4 -4
- package/lib/{DebugRenderer-9a3mO9kX.js.map → DebugRenderer-ZN692Pbg.js.map} +1 -1
- package/lib/{DevToolsPlugin-TkcBq1js.js → DevToolsPlugin-CQXFoj1T.js} +3 -3
- package/lib/{DevToolsPlugin-TkcBq1js.js.map → DevToolsPlugin-CQXFoj1T.js.map} +1 -1
- package/lib/{GSAPPlugin-DLOiVK_m.js → GSAPPlugin-CNkncyXS.js} +2 -2
- package/lib/{GSAPPlugin-DLOiVK_m.js.map → GSAPPlugin-CNkncyXS.js.map} +1 -1
- package/lib/{LayoutPlugin-CEPcr4GU.js → LayoutPlugin-BuJgr9bd.js} +26 -26
- package/lib/{LayoutPlugin-CEPcr4GU.js.map → LayoutPlugin-BuJgr9bd.js.map} +1 -1
- package/lib/{ScreenDebugPlugin-CiT0viiF.js → ScreenDebugPlugin-kz9Mr6j1.js} +2 -2
- package/lib/{ScreenDebugPlugin-CiT0viiF.js.map → ScreenDebugPlugin-kz9Mr6j1.js.map} +1 -1
- package/lib/{SpinePlugin-kyeWILtF.js → SpinePlugin-DDzD-Qxd.js} +1099 -877
- package/lib/SpinePlugin-DDzD-Qxd.js.map +1 -0
- package/lib/{StatsPlugin-5So44eOq.js → StatsPlugin-tyFGzVjy.js} +2 -2
- package/lib/{StatsPlugin-5So44eOq.js.map → StatsPlugin-tyFGzVjy.js.map} +1 -1
- package/lib/{VoiceOverPlugin--QoLj6pc.js → VoiceOverPlugin-rTNz_9eb.js} +2 -2
- package/lib/{VoiceOverPlugin--QoLj6pc.js.map → VoiceOverPlugin-rTNz_9eb.js.map} +1 -1
- package/lib/caper.mjs +51 -51
- package/lib/caper.mjs.map +1 -1
- package/lib/{const-GLIgV4ZV.js → const-Cj14a5U2.js} +5 -3
- package/lib/const-Cj14a5U2.js.map +1 -0
- package/lib/plugins/FullScreenPlugin.d.ts +65 -0
- package/lib/plugins/FullScreenPlugin.d.ts.map +1 -1
- package/lib/plugins/spine/SpinePlugin.d.ts.map +1 -1
- package/lib/plugins/spine/SpinePlugin.test.d.ts +2 -0
- package/lib/plugins/spine/SpinePlugin.test.d.ts.map +1 -0
- package/lib/{registries-C5yiKJ6v.js → registries-DmNGVLAi.js} +489 -446
- package/lib/registries-DmNGVLAi.js.map +1 -0
- package/lib/ui/Button.d.ts.map +1 -1
- package/lib/utils/platform.d.ts +5 -0
- package/lib/utils/platform.d.ts.map +1 -1
- package/lib/utils/platform.test.d.ts +2 -0
- package/lib/utils/platform.test.d.ts.map +1 -0
- package/lib/utils/types.d.ts +1 -1
- package/lib/utils/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/plugins/FullScreenPlugin.test.ts +138 -0
- package/src/plugins/FullScreenPlugin.ts +132 -0
- package/src/plugins/spine/SpinePlugin.test.ts +31 -0
- package/src/plugins/spine/SpinePlugin.ts +9 -2
- package/src/ui/Button.test.ts +22 -0
- package/src/ui/Button.ts +7 -0
- package/src/utils/platform.test.ts +20 -0
- package/src/utils/platform.ts +6 -0
- package/src/utils/types.ts +5 -1
- package/src/version.ts +2 -2
- package/lib/SpinePlugin-kyeWILtF.js.map +0 -1
- package/lib/const-GLIgV4ZV.js.map +0 -1
- package/lib/registries-C5yiKJ6v.js.map +0 -1
package/build/assetpack.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { AssetPack, Logger } from '@assetpack/core';
|
|
|
2
2
|
import { pixiPipes } from '@assetpack/core/pixi';
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import { outputModeKey, shouldBypassCache, writeMarker } from './internal/outputMarker.mjs';
|
|
5
6
|
|
|
6
7
|
const cwd = process.cwd();
|
|
7
8
|
|
|
@@ -300,12 +301,39 @@ export function assetpackPlugin(manifestUrl = defaultManifestUrl, pixiPipesConfi
|
|
|
300
301
|
}
|
|
301
302
|
}
|
|
302
303
|
|
|
304
|
+
// AssetPack caches by config hash, and dev/production get separate cache
|
|
305
|
+
// files but share one output folder (vite's publicDir). A cache hit only
|
|
306
|
+
// means "my inputs are unchanged" -- it says nothing about whether the
|
|
307
|
+
// other mode overwrote the shared folder since. The marker records which
|
|
308
|
+
// mode last wrote the folder, so a mode switch forces a cache-less run
|
|
309
|
+
// instead of silently leaving the folder out of sync with the manifest.
|
|
310
|
+
function markerPath() {
|
|
311
|
+
return path.join(apConfig.cacheLocation || '.assetpack', 'caper-output-mode.json');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function prepareRun() {
|
|
315
|
+
const key = outputModeKey({ isProduction, pixiPipesConfig, manifestUrl });
|
|
316
|
+
const bypass = shouldBypassCache({ markerPath: markerPath(), outputDir: apConfig.output, key });
|
|
317
|
+
if (bypass) {
|
|
318
|
+
Logger.info(
|
|
319
|
+
'Caper assetpack plugin:: output folder was not last written by this mode, rebuilding assets without cache',
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
return { config: bypass ? { ...apConfig, cache: false } : apConfig, key };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function markRun(key) {
|
|
326
|
+
writeMarker({ markerPath: markerPath(), key, outputDir: apConfig.output });
|
|
327
|
+
}
|
|
328
|
+
|
|
303
329
|
return {
|
|
304
330
|
name: 'vite-plugin-assetpack',
|
|
305
331
|
api: {
|
|
306
332
|
runOnce: async () => {
|
|
307
333
|
await getConfig();
|
|
308
|
-
|
|
334
|
+
const { config, key } = prepareRun();
|
|
335
|
+
await new AssetPack(config).run();
|
|
336
|
+
markRun(key);
|
|
309
337
|
},
|
|
310
338
|
},
|
|
311
339
|
async configResolved(resolvedConfig) {
|
|
@@ -333,7 +361,8 @@ export function assetpackPlugin(manifestUrl = defaultManifestUrl, pixiPipesConfi
|
|
|
333
361
|
await getConfig();
|
|
334
362
|
if (mode === 'serve') {
|
|
335
363
|
if (ap) return;
|
|
336
|
-
|
|
364
|
+
const { config, key } = prepareRun();
|
|
365
|
+
ap = new AssetPack(config);
|
|
337
366
|
// Vite does not reload the page when files under publicDir change, so
|
|
338
367
|
// without this the browser keeps serving stale assets after a rebuild.
|
|
339
368
|
// One reload per completed rebuild batch; the initial pass is skipped
|
|
@@ -342,6 +371,7 @@ export function assetpackPlugin(manifestUrl = defaultManifestUrl, pixiPipesConfi
|
|
|
342
371
|
await ap.watch(() => {
|
|
343
372
|
if (initial) {
|
|
344
373
|
initial = false;
|
|
374
|
+
markRun(key);
|
|
345
375
|
return;
|
|
346
376
|
}
|
|
347
377
|
const hot = server?.environments?.client?.hot ?? server?.ws;
|
|
@@ -349,7 +379,9 @@ export function assetpackPlugin(manifestUrl = defaultManifestUrl, pixiPipesConfi
|
|
|
349
379
|
Logger.info('Caper assetpack plugin:: assets rebuilt, reloading page');
|
|
350
380
|
});
|
|
351
381
|
} else {
|
|
352
|
-
|
|
382
|
+
const { config, key } = prepareRun();
|
|
383
|
+
await new AssetPack(config).run();
|
|
384
|
+
markRun(key);
|
|
353
385
|
}
|
|
354
386
|
isBuilding = false;
|
|
355
387
|
},
|
package/build/defaults.mjs
CHANGED
|
@@ -52,6 +52,43 @@ export function readAppIdentity(root = process.cwd()) {
|
|
|
52
52
|
const isPlainObject = (value) =>
|
|
53
53
|
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
54
54
|
|
|
55
|
+
/** Deep-merge `overrides` onto `base`; plain objects recurse, everything else replaces. */
|
|
56
|
+
function overlay(base, overrides) {
|
|
57
|
+
const out = { ...base };
|
|
58
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
59
|
+
out[key] = isPlainObject(value) && isPlainObject(base[key]) ? overlay(base[key], value) : value;
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Vite config the Tauri CLI needs, keyed off the env vars it sets on `vite` /
|
|
66
|
+
* `vite build` child processes: `TAURI_ENV_PLATFORM` (always set), `TAURI_DEV_HOST`
|
|
67
|
+
* (LAN IP, only set for mobile dev on a device). Returns `{}` outside Tauri.
|
|
68
|
+
*
|
|
69
|
+
* These override caper's own server defaults (`open`/`host`) rather than merely
|
|
70
|
+
* filling gaps in them — Tauri owns the window chrome, so a browser tab popping
|
|
71
|
+
* open alongside it, or the dev server refusing a LAN connection, are both wrong.
|
|
72
|
+
* They still lose to the project's own `vite.config`, via the `fillMissing` call
|
|
73
|
+
* in `caperDefaults` below.
|
|
74
|
+
*/
|
|
75
|
+
export function tauriDefaults(env = process.env) {
|
|
76
|
+
if (!env.TAURI_ENV_PLATFORM) return {};
|
|
77
|
+
|
|
78
|
+
const host = env.TAURI_DEV_HOST;
|
|
79
|
+
return {
|
|
80
|
+
clearScreen: false,
|
|
81
|
+
envPrefix: ['VITE_', 'TAURI_ENV_'],
|
|
82
|
+
server: {
|
|
83
|
+
open: false,
|
|
84
|
+
strictPort: true,
|
|
85
|
+
host: host || false,
|
|
86
|
+
...(host ? { hmr: { protocol: 'ws', host, port: 1421 } } : {}),
|
|
87
|
+
watch: { ignored: ['**/src-tauri/**'] },
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
55
92
|
/**
|
|
56
93
|
* The subset of `defaults` that `target` does not already specify. Returns
|
|
57
94
|
* `undefined` when there is nothing to contribute, so callers can omit the key
|
|
@@ -176,5 +213,6 @@ export function caperDefaultValues(env, userConfig = {}) {
|
|
|
176
213
|
|
|
177
214
|
/** The partial config caper contributes for this project and command. */
|
|
178
215
|
export function caperDefaults(userConfig, env) {
|
|
179
|
-
|
|
216
|
+
const values = overlay(caperDefaultValues(env, userConfig), tauriDefaults());
|
|
217
|
+
return fillMissing(userConfig, values) ?? {};
|
|
180
218
|
}
|
package/build/index.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import wasm from 'vite-plugin-wasm';
|
|
|
18
18
|
import { assetpackPlugin } from './assetpack.mjs';
|
|
19
19
|
import { caperDefaults } from './defaults.mjs';
|
|
20
20
|
import { readCaperBuildFlags } from './internal/buildFlags.mjs';
|
|
21
|
+
import { logger } from './internal/util.mjs';
|
|
21
22
|
import { assetTypesPlugin } from './plugins/assetTypes.mjs';
|
|
22
23
|
import { caperConfigPlugin } from './plugins/caperConfig.mjs';
|
|
23
24
|
import { caperDevHelperPlugin } from './plugins/devHelper.mjs';
|
|
@@ -44,6 +45,16 @@ function caperPluginList({ assets = {}, pwa } = {}) {
|
|
|
44
45
|
...(pngFallback ? [] : [pngFallbackPrunePlugin({ manifestUrl })]),
|
|
45
46
|
];
|
|
46
47
|
|
|
48
|
+
// Service workers can't register on Tauri's `tauri://localhost` origin, and
|
|
49
|
+
// the PWA defaults force `base: '/'` — so under the Tauri CLI, skip the
|
|
50
|
+
// plugin (and its runtime registration snippet) entirely rather than ship a
|
|
51
|
+
// dead one.
|
|
52
|
+
const disablePwaForTauri = Boolean(pwa) && Boolean(process.env.TAURI_ENV_PLATFORM);
|
|
53
|
+
if (disablePwaForTauri) {
|
|
54
|
+
logger.info('caper: PWA disabled for this native (Tauri) build; service workers do not work on the tauri:// origin');
|
|
55
|
+
}
|
|
56
|
+
const effectivePwa = disablePwaForTauri ? undefined : pwa;
|
|
57
|
+
|
|
47
58
|
return [
|
|
48
59
|
{
|
|
49
60
|
// These must be singletons. A second copy — e.g. a @caperjs plugin
|
|
@@ -56,7 +67,7 @@ function caperPluginList({ assets = {}, pwa } = {}) {
|
|
|
56
67
|
}),
|
|
57
68
|
},
|
|
58
69
|
...(buildFlags.useWasm ? [wasm()] : []),
|
|
59
|
-
createCaperRuntimePlugin({ pwa }),
|
|
70
|
+
createCaperRuntimePlugin({ pwa: effectivePwa }),
|
|
60
71
|
createCaperViewportPlugin(),
|
|
61
72
|
viteStaticCopy({
|
|
62
73
|
// The captions plugin's bitmap font. `silent` matters: without it,
|
|
@@ -81,7 +92,7 @@ function caperPluginList({ assets = {}, pwa } = {}) {
|
|
|
81
92
|
// place that knows a project overrode it.
|
|
82
93
|
caperConfigPlugin(true, manifestUrl),
|
|
83
94
|
caperDevHelperPlugin(),
|
|
84
|
-
...(
|
|
95
|
+
...(effectivePwa ? caperPwaPlugins(effectivePwa) : []),
|
|
85
96
|
];
|
|
86
97
|
}
|
|
87
98
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects when AssetPack's output folder was last written by a different
|
|
3
|
+
* mode (dev vs. production) than the one about to run, so the caller can
|
|
4
|
+
* bypass AssetPack's cache for that one run.
|
|
5
|
+
*
|
|
6
|
+
* AssetPack caches by config hash, and dev/production get separate cache
|
|
7
|
+
* files but share one output folder (Vite's `publicDir`). A cache hit only
|
|
8
|
+
* means "my inputs are unchanged" — it says nothing about whether the other
|
|
9
|
+
* mode overwrote the shared folder since. The marker records which mode
|
|
10
|
+
* last wrote the folder so a mode switch can be detected and the cache
|
|
11
|
+
* bypassed for that one run, forcing a full rebuild.
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A stable string that differs whenever dev and production configs differ
|
|
18
|
+
* (or the pipes config / manifest url otherwise change), so it can be
|
|
19
|
+
* compared across runs without re-deriving AssetPack's own config hash.
|
|
20
|
+
*/
|
|
21
|
+
export function outputModeKey({ isProduction, pixiPipesConfig, manifestUrl }) {
|
|
22
|
+
const mode = isProduction ? 'production' : 'development';
|
|
23
|
+
return `${mode}|${JSON.stringify(pixiPipesConfig ?? {})}|${manifestUrl ?? ''}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* True when the output folder can't be trusted to match `key`: no marker,
|
|
28
|
+
* an unreadable one, a different stored key, or an empty/missing output
|
|
29
|
+
* folder (nothing there to trust either way).
|
|
30
|
+
*/
|
|
31
|
+
export function shouldBypassCache({ markerPath, outputDir, key }) {
|
|
32
|
+
let stored;
|
|
33
|
+
try {
|
|
34
|
+
stored = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
|
|
35
|
+
} catch {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (stored?.key !== key) return true;
|
|
39
|
+
|
|
40
|
+
let entries;
|
|
41
|
+
try {
|
|
42
|
+
entries = fs.readdirSync(outputDir);
|
|
43
|
+
} catch {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return entries.length === 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function writeMarker({ markerPath, key, outputDir }) {
|
|
50
|
+
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
|
|
51
|
+
fs.writeFileSync(markerPath, JSON.stringify({ key, output: outputDir }), 'utf8');
|
|
52
|
+
}
|
package/cli/doctor.mjs
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { dim, green, red, yellow } from 'kleur/colors';
|
|
2
2
|
|
|
3
|
-
import { execFile } from 'node:child_process';
|
|
3
|
+
import { execFile, execFileSync } from 'node:child_process';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import { createRequire } from 'node:module';
|
|
6
|
+
import os from 'node:os';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import { fileURLToPath } from 'node:url';
|
|
8
9
|
|
|
10
|
+
import { ANDROID_RUST_TARGETS, findMainActivity, missingRustTargets, resolveAndroidEnv, TAURI_PLUGIN_PERMISSIONS } from './native.mjs';
|
|
11
|
+
|
|
9
12
|
/**
|
|
10
13
|
* `caper doctor` — one-shot health report for a Caper app.
|
|
11
14
|
*
|
|
@@ -90,6 +93,38 @@ const push = (checks, id, status, label, hint) => {
|
|
|
90
93
|
checks.push({ id, status, label, ...(hint ? { hint } : {}) });
|
|
91
94
|
};
|
|
92
95
|
|
|
96
|
+
/** `rustc --version` output -> `"1.98.1"`, or `null` when unparseable. */
|
|
97
|
+
export function parseRustcVersion(output) {
|
|
98
|
+
const match = /rustc (\d+\.\d+\.\d+)/.exec(String(output ?? ''));
|
|
99
|
+
return match ? match[1] : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The default injectable command runner for the native-toolchain checks. */
|
|
103
|
+
const defaultDoctorRun = (cmd, args, opts = {}) => execFileSync(cmd, args, { encoding: 'utf-8', ...opts });
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* `@tauri-apps/cli`'s version, resolved by walking up from `cwd` looking for
|
|
107
|
+
* `node_modules/@tauri-apps/cli/package.json` (monorepo: it may live several
|
|
108
|
+
* levels above the app). A manual walk rather than `require.resolve`, which
|
|
109
|
+
* also considers global/module-loader search paths we don't want here.
|
|
110
|
+
*/
|
|
111
|
+
function resolveTauriCliVersion(cwd) {
|
|
112
|
+
let current = path.resolve(cwd);
|
|
113
|
+
for (;;) {
|
|
114
|
+
const candidate = path.join(current, 'node_modules/@tauri-apps/cli/package.json');
|
|
115
|
+
if (fs.existsSync(candidate)) {
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(fs.readFileSync(candidate, 'utf-8')).version;
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const parent = path.dirname(current);
|
|
123
|
+
if (parent === current) return null;
|
|
124
|
+
current = parent;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
93
128
|
/**
|
|
94
129
|
* Resolve a tsconfig `extends` entry to an absolute config file path,
|
|
95
130
|
* TypeScript-style: relative specs resolve against the extending config's
|
|
@@ -166,7 +201,10 @@ const resolveEffectiveCompilerOptions = (configFile, ancestors = new Set(), dept
|
|
|
166
201
|
return { ...inherited, ...(config.compilerOptions ?? {}) };
|
|
167
202
|
};
|
|
168
203
|
|
|
169
|
-
export async function runChecks(
|
|
204
|
+
export async function runChecks(
|
|
205
|
+
cwd,
|
|
206
|
+
{ online = true, run = defaultDoctorRun, platform = process.platform, env = process.env, homedir = os.homedir() } = {},
|
|
207
|
+
) {
|
|
170
208
|
const checks = [];
|
|
171
209
|
const installedVersion = readInstalledVersion();
|
|
172
210
|
const nodeModulesCaper = path.join(cwd, 'node_modules/@caperjs/core');
|
|
@@ -297,6 +335,171 @@ export async function runChecks(cwd, { online = true } = {}) {
|
|
|
297
335
|
}
|
|
298
336
|
}
|
|
299
337
|
|
|
338
|
+
// Native (Tauri) toolchain — only relevant to apps that ran `caper native init`.
|
|
339
|
+
const srcTauriDir = path.join(cwd, 'src-tauri');
|
|
340
|
+
if (fs.existsSync(srcTauriDir)) {
|
|
341
|
+
try {
|
|
342
|
+
const version = parseRustcVersion(run('rustc', ['--version']));
|
|
343
|
+
if (!version) {
|
|
344
|
+
push(checks, 'native-rust', 'fail', 'rustc version unreadable', 'install via rustup.rs');
|
|
345
|
+
} else if (compareVersions(version, '1.88.0') < 0) {
|
|
346
|
+
push(checks, 'native-rust', 'fail', `rustc ${version}`, 'rustup update (or brew upgrade rust)');
|
|
347
|
+
} else {
|
|
348
|
+
push(checks, 'native-rust', 'ok', `rustc ${version}`);
|
|
349
|
+
}
|
|
350
|
+
} catch {
|
|
351
|
+
push(checks, 'native-rust', 'fail', 'rustc not found', 'install via rustup.rs');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const tauriCliVersion = resolveTauriCliVersion(cwd);
|
|
355
|
+
if (!tauriCliVersion) {
|
|
356
|
+
push(checks, 'native-tauri-cli', 'fail', '@tauri-apps/cli not found', 'caper native init (or add @tauri-apps/cli@^2)');
|
|
357
|
+
} else {
|
|
358
|
+
const major = parseInt(tauriCliVersion.split('.')[0], 10);
|
|
359
|
+
push(
|
|
360
|
+
checks,
|
|
361
|
+
'native-tauri-cli',
|
|
362
|
+
major === 2 ? 'ok' : 'warn',
|
|
363
|
+
`@tauri-apps/cli ${tauriCliVersion}`,
|
|
364
|
+
major === 2 ? undefined : 'expected @tauri-apps/cli major version 2',
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Only relevant once the app actually depends on @caperjs/plugin-tauri —
|
|
369
|
+
// `caper native init` alone doesn't need the store crate or its permissions.
|
|
370
|
+
if (appDeps['@caperjs/plugin-tauri']) {
|
|
371
|
+
let cargoToml = null;
|
|
372
|
+
try {
|
|
373
|
+
cargoToml = fs.readFileSync(path.join(cwd, 'src-tauri/Cargo.toml'), 'utf-8');
|
|
374
|
+
} catch {
|
|
375
|
+
// fails below
|
|
376
|
+
}
|
|
377
|
+
if (cargoToml === null) {
|
|
378
|
+
push(checks, 'native-plugin', 'fail', 'src-tauri/Cargo.toml unreadable', 'caper native plugin');
|
|
379
|
+
} else if (!cargoToml.includes('tauri-plugin-store')) {
|
|
380
|
+
push(checks, 'native-plugin', 'fail', 'src-tauri/Cargo.toml is missing tauri-plugin-store', 'caper native plugin');
|
|
381
|
+
} else {
|
|
382
|
+
const capabilities = readJson(path.join(cwd, 'src-tauri/capabilities/default.json'));
|
|
383
|
+
if (!capabilities) {
|
|
384
|
+
push(checks, 'native-plugin', 'fail', 'src-tauri/capabilities/default.json unreadable', 'caper native plugin');
|
|
385
|
+
} else {
|
|
386
|
+
const permissions = Array.isArray(capabilities.permissions) ? capabilities.permissions : [];
|
|
387
|
+
const missing = TAURI_PLUGIN_PERMISSIONS.filter((p) => !permissions.includes(p));
|
|
388
|
+
if (missing.length) {
|
|
389
|
+
push(checks, 'native-plugin', 'warn', `missing capability permissions: ${missing.join(', ')}`, 'caper native plugin');
|
|
390
|
+
} else {
|
|
391
|
+
push(checks, 'native-plugin', 'ok', 'native plugin wiring');
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const tauriConf = readJson(path.join(cwd, 'src-tauri/tauri.conf.json'));
|
|
398
|
+
if (!tauriConf || typeof tauriConf.identifier !== 'string') {
|
|
399
|
+
push(checks, 'native-identifier', 'fail', 'tauri.conf.json identifier unreadable', 'src-tauri/tauri.conf.json is missing or not parseable');
|
|
400
|
+
} else if (tauriConf.identifier === 'com.tauri.dev') {
|
|
401
|
+
push(checks, 'native-identifier', 'fail', 'identifier is the tauri placeholder com.tauri.dev', 'set a real reverse-DNS identifier before shipping');
|
|
402
|
+
} else if (tauriConf.identifier.startsWith('dev.caper.')) {
|
|
403
|
+
push(checks, 'native-identifier', 'warn', `identifier ${tauriConf.identifier}`, 'placeholder, change before shipping');
|
|
404
|
+
} else {
|
|
405
|
+
push(checks, 'native-identifier', 'ok', `identifier ${tauriConf.identifier}`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (tauriConf?.build) {
|
|
409
|
+
const { devUrl, beforeDevCommand } = tauriConf.build;
|
|
410
|
+
const devUrlPort = typeof devUrl === 'string' ? Number(devUrl.match(/:(\d+)(?:\/|$)/)?.[1]) : NaN;
|
|
411
|
+
const beforeDevPort = typeof beforeDevCommand === 'string' ? Number(beforeDevCommand.match(/--port[= ](\d+)/)?.[1]) : NaN;
|
|
412
|
+
|
|
413
|
+
if (Number.isNaN(devUrlPort)) {
|
|
414
|
+
push(checks, 'native-dev-port', 'warn', 'devUrl port unknown', 'check src-tauri/tauri.conf.json build.devUrl');
|
|
415
|
+
} else if (!Number.isNaN(beforeDevPort) && beforeDevPort !== devUrlPort) {
|
|
416
|
+
push(checks, 'native-dev-port', 'warn', `devUrl port ${devUrlPort} differs from beforeDevCommand port ${beforeDevPort}`, 'keep both in sync');
|
|
417
|
+
} else if (devUrlPort === 3000) {
|
|
418
|
+
push(checks, 'native-dev-port', 'warn', `devUrl port ${devUrlPort}`, "collides with other Caper apps' default dev port");
|
|
419
|
+
} else {
|
|
420
|
+
push(checks, 'native-dev-port', 'ok', `devUrl port ${devUrlPort}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (platform === 'darwin') {
|
|
425
|
+
try {
|
|
426
|
+
run('xcode-select', ['-p']);
|
|
427
|
+
push(checks, 'native-xcode-clt', 'ok', 'Xcode command line tools');
|
|
428
|
+
} catch {
|
|
429
|
+
push(checks, 'native-xcode-clt', 'fail', 'Xcode command line tools missing', 'xcode-select --install');
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Android: only once `tauri android init` (or `caper native android`) has run.
|
|
434
|
+
const genAndroidDir = path.join(srcTauriDir, 'gen/android');
|
|
435
|
+
if (fs.existsSync(genAndroidDir)) {
|
|
436
|
+
const listDir = (dir) => {
|
|
437
|
+
try {
|
|
438
|
+
return fs.readdirSync(dir);
|
|
439
|
+
} catch {
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
const android = resolveAndroidEnv({ env, platform, homedir, exists: fs.existsSync, listDir });
|
|
444
|
+
|
|
445
|
+
if (android.missing.includes('rustup')) {
|
|
446
|
+
push(checks, 'native-android-rust', 'fail', 'rustup not found', 'install Rust through rustup, then caper native android');
|
|
447
|
+
} else {
|
|
448
|
+
try {
|
|
449
|
+
const missingTargets = missingRustTargets(run('rustup', ['target', 'list', '--installed'], { env: android.env }));
|
|
450
|
+
if (missingTargets.length) {
|
|
451
|
+
push(checks, 'native-android-rust', 'fail', `missing rust targets: ${missingTargets.join(', ')}`, 'caper native android');
|
|
452
|
+
} else {
|
|
453
|
+
push(checks, 'native-android-rust', 'ok', `rust targets: ${ANDROID_RUST_TARGETS.length} Android targets`);
|
|
454
|
+
}
|
|
455
|
+
} catch {
|
|
456
|
+
push(checks, 'native-android-rust', 'fail', 'rustup target list failed', 'caper native android');
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const isDir = (p) => {
|
|
461
|
+
try {
|
|
462
|
+
return fs.statSync(p).isDirectory();
|
|
463
|
+
} catch {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
for (const [id, name, install] of [
|
|
468
|
+
['native-android-ndk', 'NDK_HOME', 'install an NDK with Android Studio (SDK Manager > SDK Tools > NDK)'],
|
|
469
|
+
['native-android-java', 'JAVA_HOME', 'install Android Studio (it bundles a JDK)'],
|
|
470
|
+
]) {
|
|
471
|
+
if (env[name]) {
|
|
472
|
+
if (isDir(env[name])) push(checks, id, 'ok', `${name} ${env[name]}`);
|
|
473
|
+
else push(checks, id, 'fail', `${name} is not a directory: ${env[name]}`, install);
|
|
474
|
+
} else if (android.env[name]) {
|
|
475
|
+
push(checks, id, 'warn', `${name} not set (found ${android.env[name]})`, `export ${name}=${android.env[name]}`);
|
|
476
|
+
} else {
|
|
477
|
+
push(checks, id, 'fail', `${name} not set`, install);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
let buildRs = '';
|
|
482
|
+
try {
|
|
483
|
+
buildRs = fs.readFileSync(path.join(srcTauriDir, 'build.rs'), 'utf-8');
|
|
484
|
+
} catch {
|
|
485
|
+
// fails below
|
|
486
|
+
}
|
|
487
|
+
if (buildRs.includes('max-page-size=16384')) {
|
|
488
|
+
push(checks, 'native-android-16kb', 'ok', 'build.rs 16 KB page alignment');
|
|
489
|
+
} else {
|
|
490
|
+
push(checks, 'native-android-16kb', 'fail', 'build.rs lacks 16 KB page alignment', 'caper native android');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const mainActivityPath = findMainActivity(genAndroidDir);
|
|
494
|
+
const mainActivity = mainActivityPath ? fs.readFileSync(mainActivityPath, 'utf-8') : '';
|
|
495
|
+
if (mainActivity.includes('WindowInsetsCompat.Type.systemBars()')) {
|
|
496
|
+
push(checks, 'native-android-bars', 'ok', 'MainActivity hides the system bars');
|
|
497
|
+
} else {
|
|
498
|
+
push(checks, 'native-android-bars', 'warn', 'MainActivity does not hide the system bars', 'caper native android');
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
300
503
|
const caches = ['.assetpack', '.cache', 'dist'].filter((name) => fs.existsSync(path.join(cwd, name)));
|
|
301
504
|
push(checks, 'caches', 'ok', `caches${caches.length ? `: ${caches.join(', ')}` : ' clean'}`, caches.length ? 'rm -rf them on weird asset/name mismatches' : undefined);
|
|
302
505
|
|