@caperjs/core 0.7.2 → 0.7.4
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.mjs +1 -1
- package/build/internal/buildFlags.mjs +63 -18
- package/build/internal/schema.mjs +53 -0
- package/build/plugins/pwa.mjs +13 -7
- package/cli/doctor.mjs +53 -2
- package/cli/doctor.test.mjs +80 -1
- package/cli/native.mjs +126 -15
- package/cli/native.test.mjs +219 -3
- package/extras/llms.txt +35 -3
- package/lib/{CaptionsPlugin-COrofpHN.js → CaptionsPlugin-CAgQBcA1.js} +7 -7
- package/lib/{CaptionsPlugin-COrofpHN.js.map → CaptionsPlugin-CAgQBcA1.js.map} +1 -1
- package/lib/{DataAdapter-T-22-_D1.js → DataAdapter-CbyqWMo-.js} +5 -5
- package/lib/{DataAdapter-T-22-_D1.js.map → DataAdapter-CbyqWMo-.js.map} +1 -1
- package/lib/{DebugRenderer-ZN692Pbg.js → DebugRenderer-Bdkpyyt6.js} +2 -2
- package/lib/{DebugRenderer-ZN692Pbg.js.map → DebugRenderer-Bdkpyyt6.js.map} +1 -1
- package/lib/{DevToolsPlugin-CQXFoj1T.js → DevToolsPlugin-Cq5HHN9K.js} +2 -2
- package/lib/{DevToolsPlugin-CQXFoj1T.js.map → DevToolsPlugin-Cq5HHN9K.js.map} +1 -1
- package/lib/{GSAPPlugin-CNkncyXS.js → GSAPPlugin-D-ujhVYo.js} +2 -2
- package/lib/{GSAPPlugin-CNkncyXS.js.map → GSAPPlugin-D-ujhVYo.js.map} +1 -1
- package/lib/{LayoutPlugin-BuJgr9bd.js → LayoutPlugin-DqbwBm-X.js} +4 -4
- package/lib/{LayoutPlugin-BuJgr9bd.js.map → LayoutPlugin-DqbwBm-X.js.map} +1 -1
- package/lib/{ScreenDebugPlugin-kz9Mr6j1.js → ScreenDebugPlugin-CyT8UxEa.js} +3 -3
- package/lib/{ScreenDebugPlugin-kz9Mr6j1.js.map → ScreenDebugPlugin-CyT8UxEa.js.map} +1 -1
- package/lib/{SpinePlugin-DDzD-Qxd.js → SpinePlugin-DyXJyA3x.js} +2 -2
- package/lib/{SpinePlugin-DDzD-Qxd.js.map → SpinePlugin-DyXJyA3x.js.map} +1 -1
- package/lib/{StatsPlugin-tyFGzVjy.js → StatsPlugin-CEhgzWby.js} +4 -4
- package/lib/{StatsPlugin-tyFGzVjy.js.map → StatsPlugin-CEhgzWby.js.map} +1 -1
- package/lib/{VoiceOverPlugin-rTNz_9eb.js → VoiceOverPlugin-CSpNZjpJ.js} +2 -2
- package/lib/{VoiceOverPlugin-rTNz_9eb.js.map → VoiceOverPlugin-CSpNZjpJ.js.map} +1 -1
- package/lib/caper.mjs +126 -129
- package/lib/caper.mjs.map +1 -1
- package/lib/{const-Cj14a5U2.js → const-Bca54QyV.js} +235 -227
- package/lib/{const-Cj14a5U2.js.map → const-Bca54QyV.js.map} +1 -1
- package/lib/core/Application.d.ts +4 -0
- package/lib/core/Application.d.ts.map +1 -1
- package/lib/core/interfaces/IApplicationOptions.d.ts +21 -0
- package/lib/core/interfaces/IApplicationOptions.d.ts.map +1 -1
- package/lib/plugins/OrientationPlugin.d.ts +57 -0
- package/lib/plugins/OrientationPlugin.d.ts.map +1 -0
- package/lib/plugins/OrientationPlugin.test.d.ts +2 -0
- package/lib/plugins/OrientationPlugin.test.d.ts.map +1 -0
- package/lib/plugins/defaults.d.ts.map +1 -1
- package/lib/plugins/index.d.ts +1 -0
- package/lib/plugins/index.d.ts.map +1 -1
- package/lib/{registries-DmNGVLAi.js → registries-C1nPl8B3.js} +593 -502
- package/lib/registries-C1nPl8B3.js.map +1 -0
- package/lib/ui/UICanvas.d.ts +10 -2
- package/lib/ui/UICanvas.d.ts.map +1 -1
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.d.ts.map +1 -1
- package/lib/utils/orientation.d.ts +44 -0
- package/lib/utils/orientation.test.d.ts +2 -0
- package/lib/utils/orientation.test.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/core/Application.ts +10 -0
- package/src/core/interfaces/IApplicationOptions.ts +21 -0
- package/src/plugins/OrientationPlugin.test.ts +252 -0
- package/src/plugins/OrientationPlugin.ts +178 -0
- package/src/plugins/defaults.ts +6 -0
- package/src/plugins/index.ts +1 -0
- package/src/ui/UICanvas.test.ts +23 -1
- package/src/ui/UICanvas.ts +22 -3
- package/src/utils/index.ts +1 -0
- package/src/utils/orientation.d.ts +44 -0
- package/src/utils/orientation.js +37 -0
- package/src/utils/orientation.test.ts +30 -0
- package/src/version.ts +1 -1
- package/lib/registries-DmNGVLAi.js.map +0 -1
package/cli/native.mjs
CHANGED
|
@@ -6,6 +6,8 @@ import os from 'node:os';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import process from 'node:process';
|
|
8
8
|
|
|
9
|
+
import { resolveOrientation } from '../src/utils/orientation.js';
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* `caper native init` — one-shot Tauri v2 scaffolding for a Caper app: adds
|
|
11
13
|
* `@tauri-apps/cli`, runs `tauri init --ci` with a pinned identifier/port,
|
|
@@ -18,6 +20,16 @@ const IDENTIFIER_RE = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9-]*)+$/;
|
|
|
18
20
|
const PLACEHOLDER_IDENTIFIER = 'com.tauri.dev';
|
|
19
21
|
const DEFAULT_IDENTIFIER_PREFIX = 'dev.caper.';
|
|
20
22
|
|
|
23
|
+
/** A window/app title from a package name: strip an npm scope, split on `-`/`_`/`.`, capitalize each word. */
|
|
24
|
+
export function displayTitle(packageName) {
|
|
25
|
+
return String(packageName ?? '')
|
|
26
|
+
.replace(/^@[^/]+\//, '')
|
|
27
|
+
.split(/[-_.\s]+/)
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
.map((word) => word[0].toUpperCase() + word.slice(1))
|
|
30
|
+
.join(' ');
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
/** Strip an npm scope, lowercase, collapse non `[a-z0-9]` runs to `-`, trim `-`. */
|
|
22
34
|
export function appSlug(packageName) {
|
|
23
35
|
return String(packageName ?? '')
|
|
@@ -120,10 +132,37 @@ function renderCommand({ cmd, args }) {
|
|
|
120
132
|
return [cmd, ...args].join(' ');
|
|
121
133
|
}
|
|
122
134
|
|
|
135
|
+
/** The `caper.config.ts` orientation values, and the `android:screenOrientation` each one becomes. */
|
|
136
|
+
export const ANDROID_SCREEN_ORIENTATION = Object.freeze({ portrait: 'portrait', landscape: 'sensorLandscape' });
|
|
137
|
+
|
|
138
|
+
/** The default injectable `loadConfig`: the subset of `caper.config.ts` the native commands read. */
|
|
139
|
+
async function defaultLoadConfig(cwd) {
|
|
140
|
+
// Imported lazily: build/ pulls in the oxc parser and vite, which only these commands need.
|
|
141
|
+
const { readConfigOrientation } = await import('../build/internal/buildFlags.mjs');
|
|
142
|
+
return { orientation: readConfigOrientation(cwd) };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The orientation lock from `caper.config.ts` through `loadConfig` (either form
|
|
147
|
+
* of `orientation`, normalized by `resolveOrientation`), or a warning when it
|
|
148
|
+
* can't be read: a broken or unusual config must never stop a native command.
|
|
149
|
+
*
|
|
150
|
+
* @returns {Promise<{ orientation?: 'portrait' | 'landscape', warning?: string }>}
|
|
151
|
+
*/
|
|
152
|
+
export async function readOrientation(cwd, loadConfig = defaultLoadConfig) {
|
|
153
|
+
try {
|
|
154
|
+
return { orientation: resolveOrientation((await loadConfig(cwd))?.orientation)?.lock };
|
|
155
|
+
} catch (err) {
|
|
156
|
+
return { warning: `could not read caper.config.ts orientation (${err.message}), so orientation was skipped.` };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
123
160
|
/** Returns a new tauri.conf.json: sets `identifier` and window 0's title/size; leaves the rest untouched. */
|
|
124
|
-
export function patchTauriConfig(conf, { identifier, title }) {
|
|
161
|
+
export function patchTauriConfig(conf, { identifier, title, orientation }) {
|
|
125
162
|
const windows = Array.isArray(conf.app?.windows) ? [...conf.app.windows] : [];
|
|
126
|
-
|
|
163
|
+
// 450x800 keeps a portrait window, title bar included, on a 13" laptop screen.
|
|
164
|
+
const size = orientation === 'portrait' ? { width: 450, height: 800 } : { width: 1280, height: 720 };
|
|
165
|
+
windows[0] = { ...(windows[0] ?? {}), title, ...size };
|
|
127
166
|
|
|
128
167
|
return {
|
|
129
168
|
...conf,
|
|
@@ -198,9 +237,9 @@ function writeJson(file, data, indent) {
|
|
|
198
237
|
*
|
|
199
238
|
* @param {string} cwd
|
|
200
239
|
* @param {{ identifier?: string, port?: number, icon?: string }} [opts]
|
|
201
|
-
* @param {{ run?: typeof defaultRun }} [deps]
|
|
240
|
+
* @param {{ run?: typeof defaultRun, loadConfig?: typeof defaultLoadConfig }} [deps]
|
|
202
241
|
*/
|
|
203
|
-
export async function initNative(cwd, opts = {}, { run = defaultRun } = {}) {
|
|
242
|
+
export async function initNative(cwd, opts = {}, { run = defaultRun, loadConfig = defaultLoadConfig } = {}) {
|
|
204
243
|
const pkgPath = path.join(cwd, 'package.json');
|
|
205
244
|
if (!fs.existsSync(pkgPath)) {
|
|
206
245
|
throw new Error('no package.json found in this directory.');
|
|
@@ -219,7 +258,7 @@ export async function initNative(cwd, opts = {}, { run = defaultRun } = {}) {
|
|
|
219
258
|
// vars can instead reflect an unrelated ancestor process (e.g. a monorepo
|
|
220
259
|
// task runner). Read the name straight from the package.json we already have.
|
|
221
260
|
const name = pkg.name ?? 'app';
|
|
222
|
-
const title = opts.title ?? name;
|
|
261
|
+
const title = opts.title ?? displayTitle(name);
|
|
223
262
|
const slug = appSlug(pkg.name ?? name);
|
|
224
263
|
|
|
225
264
|
const identifier = opts.identifier ?? defaultIdentifier(slug);
|
|
@@ -244,7 +283,7 @@ export async function initNative(cwd, opts = {}, { run = defaultRun } = {}) {
|
|
|
244
283
|
'init',
|
|
245
284
|
'--ci',
|
|
246
285
|
'--app-name',
|
|
247
|
-
|
|
286
|
+
title,
|
|
248
287
|
'--window-title',
|
|
249
288
|
title,
|
|
250
289
|
'--frontend-dist',
|
|
@@ -258,17 +297,24 @@ export async function initNative(cwd, opts = {}, { run = defaultRun } = {}) {
|
|
|
258
297
|
];
|
|
259
298
|
run(EXEC_RUNNER[pm], initArgs, { cwd });
|
|
260
299
|
|
|
300
|
+
const warnings = [];
|
|
301
|
+
const { orientation, warning } = await readOrientation(cwd, loadConfig);
|
|
302
|
+
if (warning) warnings.push(warning);
|
|
303
|
+
|
|
261
304
|
const tauriConfPath = path.join(srcTauriDir, 'tauri.conf.json');
|
|
262
305
|
const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, 'utf-8'));
|
|
263
|
-
writeJson(tauriConfPath, patchTauriConfig(tauriConf, { identifier, title }), 2);
|
|
306
|
+
writeJson(tauriConfPath, patchTauriConfig(tauriConf, { identifier, title, orientation }), 2);
|
|
264
307
|
|
|
265
|
-
|
|
308
|
+
// Re-read: the package manager rewrote package.json when it added @tauri-apps/cli,
|
|
309
|
+
// and patching the copy read above would silently drop that devDependency.
|
|
310
|
+
const freshPkgRaw = fs.readFileSync(pkgPath, 'utf-8');
|
|
311
|
+
writeJson(pkgPath, patchPackageScripts(JSON.parse(freshPkgRaw)), detectIndent(freshPkgRaw));
|
|
266
312
|
|
|
267
313
|
if (opts.icon) {
|
|
268
314
|
run(EXEC_RUNNER[pm], ['tauri', 'icon', opts.icon], { cwd });
|
|
269
315
|
}
|
|
270
316
|
|
|
271
|
-
return { status: 'ok', name, title, identifier, port, pm };
|
|
317
|
+
return { status: 'ok', name, title, identifier, port, pm, orientation, warnings };
|
|
272
318
|
}
|
|
273
319
|
|
|
274
320
|
/**
|
|
@@ -528,6 +574,38 @@ export function findMainActivity(genAndroidDir) {
|
|
|
528
574
|
return walk(path.join(genAndroidDir, 'app/src/main/java'));
|
|
529
575
|
}
|
|
530
576
|
|
|
577
|
+
/** Where `tauri android init` writes the app manifest, relative to `src-tauri/`. */
|
|
578
|
+
export const ANDROID_MANIFEST = 'gen/android/app/src/main/AndroidManifest.xml';
|
|
579
|
+
|
|
580
|
+
const MAIN_ACTIVITY_TAG_RE = /<activity\b[^>]*\bandroid:name="\.MainActivity"[^>]*>/;
|
|
581
|
+
const SCREEN_ORIENTATION_RE = /(\bandroid:screenOrientation=")([^"]*)(")/;
|
|
582
|
+
|
|
583
|
+
/** The MainActivity's `android:screenOrientation` in an AndroidManifest.xml, or `undefined`. */
|
|
584
|
+
export function readManifestOrientation(xml) {
|
|
585
|
+
return String(xml ?? '').match(MAIN_ACTIVITY_TAG_RE)?.[0].match(SCREEN_ORIENTATION_RE)?.[2];
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Sets `android:screenOrientation` on the `.MainActivity` element for a
|
|
590
|
+
* `caper.config.ts` orientation (see `ANDROID_SCREEN_ORIENTATION`), replacing a
|
|
591
|
+
* different value or adding the attribute under `android:name` with its
|
|
592
|
+
* indentation. Returns `xml` itself when nothing changes, including when
|
|
593
|
+
* `orientation` is unset or there is no MainActivity element.
|
|
594
|
+
*/
|
|
595
|
+
export function patchManifestOrientation(xml, orientation) {
|
|
596
|
+
const value = ANDROID_SCREEN_ORIENTATION[orientation];
|
|
597
|
+
const tag = value ? xml.match(MAIN_ACTIVITY_TAG_RE)?.[0] : undefined;
|
|
598
|
+
if (!tag) return xml;
|
|
599
|
+
|
|
600
|
+
let patched;
|
|
601
|
+
if (SCREEN_ORIENTATION_RE.test(tag)) {
|
|
602
|
+
patched = tag.replace(SCREEN_ORIENTATION_RE, `$1${value}$3`);
|
|
603
|
+
} else {
|
|
604
|
+
patched = tag.replace(/(\s+)(android:name="\.MainActivity")/, `$1$2$1android:screenOrientation="${value}"`);
|
|
605
|
+
}
|
|
606
|
+
return patched === tag ? xml : xml.replace(tag, patched);
|
|
607
|
+
}
|
|
608
|
+
|
|
531
609
|
/** Returns a new package.json: adds `native:android:dev`/`native:android:build` scripts only where absent. */
|
|
532
610
|
export function patchAndroidScripts(pkg) {
|
|
533
611
|
const scripts = { ...(pkg.scripts ?? {}) };
|
|
@@ -559,13 +637,15 @@ const ANDROID_MISSING_HELP = {
|
|
|
559
637
|
* The I/O core of `caper native android`: the Android steps from
|
|
560
638
|
* docs/wiki/native-tauri.md: installs the Rust targets, runs
|
|
561
639
|
* `tauri android init --ci`, adds the 16 KB page-alignment link arg to
|
|
562
|
-
* `build.rs`, hides the system bars in `MainActivity.kt`,
|
|
563
|
-
* `
|
|
564
|
-
*
|
|
640
|
+
* `build.rs`, hides the system bars in `MainActivity.kt`, locks the
|
|
641
|
+
* MainActivity to `caper.config.ts`'s `orientation` in `AndroidManifest.xml`
|
|
642
|
+
* (only when it is set), and adds `native:android:*` scripts. No console
|
|
643
|
+
* output; the CLI wrapper below owns presentation. Idempotent: a second run
|
|
644
|
+
* makes zero `run` calls and zero writes.
|
|
565
645
|
*
|
|
566
646
|
* @param {string} cwd
|
|
567
647
|
* @param {object} [opts] unused today; kept for symmetry with `initNative`
|
|
568
|
-
* @param {{ run?: typeof defaultRun, exec?: typeof defaultExec, env?: Record<string, string>, platform?: string, homedir?: string, exists?: (p: string) => boolean, listDir?: (p: string) => string[] }} [deps]
|
|
648
|
+
* @param {{ run?: typeof defaultRun, exec?: typeof defaultExec, env?: Record<string, string>, platform?: string, homedir?: string, exists?: (p: string) => boolean, listDir?: (p: string) => string[], loadConfig?: typeof defaultLoadConfig }} [deps]
|
|
569
649
|
*/
|
|
570
650
|
export async function androidNative(
|
|
571
651
|
cwd,
|
|
@@ -578,6 +658,7 @@ export async function androidNative(
|
|
|
578
658
|
homedir = os.homedir(),
|
|
579
659
|
exists = fs.existsSync,
|
|
580
660
|
listDir = defaultListDir,
|
|
661
|
+
loadConfig = defaultLoadConfig,
|
|
581
662
|
} = {},
|
|
582
663
|
) {
|
|
583
664
|
const pkgPath = path.join(cwd, 'package.json');
|
|
@@ -637,6 +718,24 @@ export async function androidNative(
|
|
|
637
718
|
}
|
|
638
719
|
}
|
|
639
720
|
|
|
721
|
+
// Unset means no lock: the manifest is left alone, never rewritten to "unspecified".
|
|
722
|
+
let orientationPatched = false;
|
|
723
|
+
const { orientation, warning: orientationWarning } = await readOrientation(cwd, loadConfig);
|
|
724
|
+
if (orientationWarning) warnings.push(orientationWarning);
|
|
725
|
+
if (orientation) {
|
|
726
|
+
const manifestPath = path.join(srcTauriDir, ANDROID_MANIFEST);
|
|
727
|
+
const xml = fs.existsSync(manifestPath) ? fs.readFileSync(manifestPath, 'utf-8') : null;
|
|
728
|
+
const patched = xml === null ? null : patchManifestOrientation(xml, orientation);
|
|
729
|
+
if (patched === null || readManifestOrientation(patched) !== ANDROID_SCREEN_ORIENTATION[orientation]) {
|
|
730
|
+
warnings.push(
|
|
731
|
+
`could not find the MainActivity in src-tauri/${ANDROID_MANIFEST}, so the ${orientation} orientation was not applied. Set android:screenOrientation="${ANDROID_SCREEN_ORIENTATION[orientation]}" on it by hand.`,
|
|
732
|
+
);
|
|
733
|
+
} else if (patched !== xml) {
|
|
734
|
+
fs.writeFileSync(manifestPath, patched, 'utf-8');
|
|
735
|
+
orientationPatched = true;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
640
739
|
const pkgRaw = fs.readFileSync(pkgPath, 'utf-8');
|
|
641
740
|
const pkg = JSON.parse(pkgRaw);
|
|
642
741
|
const patchedPkg = patchAndroidScripts(pkg);
|
|
@@ -648,11 +747,14 @@ export async function androidNative(
|
|
|
648
747
|
const buildRsPatched = buildRsPlan === 'write';
|
|
649
748
|
return {
|
|
650
749
|
status: 'ok',
|
|
651
|
-
changed:
|
|
750
|
+
changed:
|
|
751
|
+
addedTargets.length > 0 || ranAndroidInit || buildRsPatched || mainActivityPatched || orientationPatched || addedScripts.length > 0,
|
|
652
752
|
addedTargets,
|
|
653
753
|
ranAndroidInit,
|
|
654
754
|
buildRsPatched,
|
|
655
755
|
mainActivityPatched,
|
|
756
|
+
orientation,
|
|
757
|
+
orientationPatched,
|
|
656
758
|
addedScripts,
|
|
657
759
|
env: {
|
|
658
760
|
ANDROID_HOME: resolved.env.ANDROID_HOME,
|
|
@@ -666,7 +768,7 @@ export async function androidNative(
|
|
|
666
768
|
}
|
|
667
769
|
|
|
668
770
|
function printUsage() {
|
|
669
|
-
console.error(red('Usage: caper native init [--identifier <id>] [--port <n>] [--icon <png>]'));
|
|
771
|
+
console.error(red('Usage: caper native init [--title <name>] [--identifier <id>] [--port <n>] [--icon <png>]'));
|
|
670
772
|
console.error(red(' caper native plugin'));
|
|
671
773
|
console.error(red(' caper native android'));
|
|
672
774
|
}
|
|
@@ -687,6 +789,10 @@ function parseInitArgs(args) {
|
|
|
687
789
|
const next = args[++i];
|
|
688
790
|
if (!next) throw new Error('Missing value for --icon');
|
|
689
791
|
opts.icon = next;
|
|
792
|
+
} else if (arg === '--title') {
|
|
793
|
+
const next = args[++i];
|
|
794
|
+
if (!next) throw new Error('Missing value for --title');
|
|
795
|
+
opts.title = next;
|
|
690
796
|
} else {
|
|
691
797
|
throw new Error(`Unknown option: ${arg}`);
|
|
692
798
|
}
|
|
@@ -722,6 +828,8 @@ async function runInit(args) {
|
|
|
722
828
|
console.log(` ${yellow('identifier:')} ${result.identifier}${result.identifier.startsWith(DEFAULT_IDENTIFIER_PREFIX) ? dim(' (placeholder — change before shipping)') : ''}`);
|
|
723
829
|
console.log(` ${yellow('dev port:')} ${result.port}`);
|
|
724
830
|
console.log(` ${yellow('scripts:')} native:dev, native:build`);
|
|
831
|
+
if (result.orientation) console.log(` ${yellow('window:')} ${result.orientation === 'portrait' ? '450x800' : '1280x720'} (${result.orientation}, from caper.config.ts)`);
|
|
832
|
+
for (const warning of result.warnings) console.log(yellow(` ⚠ ${warning}`));
|
|
725
833
|
console.log(`\n Run ${cyan('npx caper doctor')} to check your native toolchain.`);
|
|
726
834
|
console.log(` The first ${cyan('native:build')} compiles Rust — expect a few minutes.`);
|
|
727
835
|
}
|
|
@@ -766,6 +874,9 @@ async function runAndroid() {
|
|
|
766
874
|
if (result.ranAndroidInit) console.log(` ${yellow('ran:')} tauri android init --ci`);
|
|
767
875
|
if (result.buildRsPatched) console.log(` ${yellow('build.rs:')} 16 KB page alignment added to src-tauri/build.rs`);
|
|
768
876
|
if (result.mainActivityPatched) console.log(` ${yellow('MainActivity.kt:')} system bars hidden`);
|
|
877
|
+
if (result.orientationPatched) {
|
|
878
|
+
console.log(` ${yellow('AndroidManifest.xml:')} ${result.orientation} orientation (android:screenOrientation="${ANDROID_SCREEN_ORIENTATION[result.orientation]}")`);
|
|
879
|
+
}
|
|
769
880
|
if (result.addedScripts.length) console.log(` ${yellow('scripts:')} ${result.addedScripts.join(', ')}`);
|
|
770
881
|
}
|
|
771
882
|
for (const warning of result.warnings) console.log(yellow(` ⚠ ${warning}`));
|
package/cli/native.test.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
7
8
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
8
9
|
|
|
9
10
|
import {
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
commandsFor,
|
|
15
16
|
defaultIdentifier,
|
|
16
17
|
defaultPort,
|
|
18
|
+
displayTitle,
|
|
17
19
|
findMainActivity,
|
|
18
20
|
initNative,
|
|
19
21
|
isValidIdentifier,
|
|
@@ -23,15 +25,23 @@ import {
|
|
|
23
25
|
parsePort,
|
|
24
26
|
patchAndroidScripts,
|
|
25
27
|
patchCapabilities,
|
|
28
|
+
patchManifestOrientation,
|
|
26
29
|
patchPackageScripts,
|
|
27
30
|
patchTauriConfig,
|
|
28
31
|
planBuildRs,
|
|
29
32
|
planMainActivity,
|
|
33
|
+
readManifestOrientation,
|
|
30
34
|
renderMainActivity,
|
|
31
35
|
resolveAndroidEnv,
|
|
32
36
|
TAURI_PLUGIN_PERMISSIONS,
|
|
33
37
|
} from './native.mjs';
|
|
34
38
|
|
|
39
|
+
// A copy of the manifest `tauri android init` generated for kitchen-sink.
|
|
40
|
+
const MANIFEST_FIXTURE = fs.readFileSync(
|
|
41
|
+
path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../test/fixtures/android/AndroidManifest.xml'),
|
|
42
|
+
'utf-8',
|
|
43
|
+
);
|
|
44
|
+
|
|
35
45
|
let tempDir = null;
|
|
36
46
|
|
|
37
47
|
afterEach(() => {
|
|
@@ -46,6 +56,14 @@ function makeTempDir() {
|
|
|
46
56
|
return tempDir;
|
|
47
57
|
}
|
|
48
58
|
|
|
59
|
+
describe('displayTitle', () => {
|
|
60
|
+
it('title-cases a package name into a window title', () => {
|
|
61
|
+
expect(displayTitle('rhodora')).toBe('Rhodora');
|
|
62
|
+
expect(displayTitle('my-game')).toBe('My Game');
|
|
63
|
+
expect(displayTitle('@studio/space_rocks')).toBe('Space Rocks');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
49
67
|
describe('appSlug', () => {
|
|
50
68
|
it('strips a scope, lowercases, and collapses non-alnum runs to a hyphen', () => {
|
|
51
69
|
expect(appSlug('@acme/My Game')).toBe('my-game');
|
|
@@ -244,6 +262,16 @@ describe('patchTauriConfig', () => {
|
|
|
244
262
|
expect(patched.app.windows[0].resizable).toBe(true);
|
|
245
263
|
});
|
|
246
264
|
|
|
265
|
+
it('uses a 450x800 window for a portrait game', () => {
|
|
266
|
+
const patched = patchTauriConfig(conf, { identifier: 'dev.caper.my-game', title: 'My Game', orientation: 'portrait' });
|
|
267
|
+
expect(patched.app.windows[0]).toMatchObject({ width: 450, height: 800 });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('keeps 1280x720 for a landscape game', () => {
|
|
271
|
+
const patched = patchTauriConfig(conf, { identifier: 'dev.caper.my-game', title: 'My Game', orientation: 'landscape' });
|
|
272
|
+
expect(patched.app.windows[0]).toMatchObject({ width: 1280, height: 720 });
|
|
273
|
+
});
|
|
274
|
+
|
|
247
275
|
it('does not mutate the original object', () => {
|
|
248
276
|
patchTauriConfig(conf, { identifier: 'dev.caper.my-game', title: 'My Game' });
|
|
249
277
|
expect(conf.identifier).toBe('com.tauri.dev');
|
|
@@ -315,6 +343,56 @@ describe('initNative', () => {
|
|
|
315
343
|
};
|
|
316
344
|
}
|
|
317
345
|
|
|
346
|
+
it('keeps the @tauri-apps/cli devDependency the package manager just added', async () => {
|
|
347
|
+
const dir = makeTempDir();
|
|
348
|
+
scaffoldApp(dir);
|
|
349
|
+
const calls = [];
|
|
350
|
+
const tauriRun = stubbedRun(calls);
|
|
351
|
+
// Like a real `pnpm add -D`, write the devDependency into package.json.
|
|
352
|
+
const run = (cmd, args, opts) => {
|
|
353
|
+
if (args[0] === 'add') {
|
|
354
|
+
const pkgPath = path.join(opts.cwd, 'package.json');
|
|
355
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
356
|
+
pkg.devDependencies = { ...pkg.devDependencies, '@tauri-apps/cli': '^2.12.1' };
|
|
357
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
358
|
+
}
|
|
359
|
+
return tauriRun(cmd, args, opts);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
await initNative(dir, {}, { run });
|
|
363
|
+
|
|
364
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
|
|
365
|
+
expect(pkg.devDependencies['@tauri-apps/cli']).toBe('^2.12.1');
|
|
366
|
+
expect(pkg.scripts['native:build']).toBe('tauri build');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('sizes the window from caper.config orientation', async () => {
|
|
370
|
+
const dir = makeTempDir();
|
|
371
|
+
scaffoldApp(dir);
|
|
372
|
+
|
|
373
|
+
const result = await initNative(dir, {}, { run: stubbedRun([]), loadConfig: () => ({ orientation: 'portrait' }) });
|
|
374
|
+
|
|
375
|
+
expect(result.warnings).toEqual([]);
|
|
376
|
+
const conf = JSON.parse(fs.readFileSync(path.join(dir, 'src-tauri/tauri.conf.json'), 'utf-8'));
|
|
377
|
+
expect(conf.app.windows[0]).toMatchObject({ width: 450, height: 800 });
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('warns and keeps 1280x720 when caper.config orientation cannot be read', async () => {
|
|
381
|
+
const dir = makeTempDir();
|
|
382
|
+
scaffoldApp(dir);
|
|
383
|
+
const loadConfig = () => {
|
|
384
|
+
throw new Error('boom');
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const result = await initNative(dir, {}, { run: stubbedRun([]), loadConfig });
|
|
388
|
+
|
|
389
|
+
expect(result.status).toBe('ok');
|
|
390
|
+
expect(result.warnings).toHaveLength(1);
|
|
391
|
+
expect(result.warnings[0]).toMatch(/could not read caper\.config\.ts orientation/);
|
|
392
|
+
const conf = JSON.parse(fs.readFileSync(path.join(dir, 'src-tauri/tauri.conf.json'), 'utf-8'));
|
|
393
|
+
expect(conf.app.windows[0]).toMatchObject({ width: 1280, height: 720 });
|
|
394
|
+
});
|
|
395
|
+
|
|
318
396
|
it('does nothing and makes zero run calls when src-tauri already exists', async () => {
|
|
319
397
|
const dir = makeTempDir();
|
|
320
398
|
scaffoldApp(dir);
|
|
@@ -360,9 +438,9 @@ describe('initNative', () => {
|
|
|
360
438
|
'init',
|
|
361
439
|
'--ci',
|
|
362
440
|
'--app-name',
|
|
363
|
-
'
|
|
441
|
+
'My Game',
|
|
364
442
|
'--window-title',
|
|
365
|
-
'
|
|
443
|
+
'My Game',
|
|
366
444
|
'--frontend-dist',
|
|
367
445
|
'../dist',
|
|
368
446
|
'--dev-url',
|
|
@@ -375,7 +453,7 @@ describe('initNative', () => {
|
|
|
375
453
|
|
|
376
454
|
const conf = JSON.parse(fs.readFileSync(path.join(dir, 'src-tauri/tauri.conf.json'), 'utf-8'));
|
|
377
455
|
expect(conf.identifier).toBe('dev.caper.mygame');
|
|
378
|
-
expect(conf.app.windows[0]).toMatchObject({ title: '
|
|
456
|
+
expect(conf.app.windows[0]).toMatchObject({ title: 'My Game', width: 1280, height: 720 });
|
|
379
457
|
expect(conf.app.security.csp).toBeNull();
|
|
380
458
|
expect(conf.bundle).toEqual({ active: true, targets: ['app'] });
|
|
381
459
|
|
|
@@ -891,6 +969,7 @@ describe('androidNative', () => {
|
|
|
891
969
|
const dir = path.join(opts.cwd, 'src-tauri/gen/android/app/src/main/java/x/y/z');
|
|
892
970
|
fs.mkdirSync(dir, { recursive: true });
|
|
893
971
|
fs.writeFileSync(path.join(dir, 'MainActivity.kt'), STOCK_MAIN_ACTIVITY, 'utf-8');
|
|
972
|
+
fs.writeFileSync(path.join(opts.cwd, 'src-tauri/gen/android/app/src/main/AndroidManifest.xml'), MANIFEST_FIXTURE, 'utf-8');
|
|
894
973
|
} else {
|
|
895
974
|
throw new Error(`unstubbed run: ${cmd} ${args.join(' ')}`);
|
|
896
975
|
}
|
|
@@ -989,6 +1068,103 @@ describe('androidNative', () => {
|
|
|
989
1068
|
expect(snapshot(dir)).toEqual(before);
|
|
990
1069
|
});
|
|
991
1070
|
|
|
1071
|
+
const manifestPath = (dir) => path.join(dir, 'src-tauri/gen/android/app/src/main/AndroidManifest.xml');
|
|
1072
|
+
|
|
1073
|
+
it('leaves AndroidManifest.xml alone when caper.config sets no orientation', async () => {
|
|
1074
|
+
const dir = makeTempDir();
|
|
1075
|
+
const deps = scaffoldAndroidApp(dir);
|
|
1076
|
+
|
|
1077
|
+
const result = await androidNative(dir, {}, { ...stubs({ installed: [] }, []), ...deps, loadConfig: () => ({}) });
|
|
1078
|
+
|
|
1079
|
+
expect(result.orientation).toBeUndefined();
|
|
1080
|
+
expect(result.orientationPatched).toBe(false);
|
|
1081
|
+
expect(result.warnings).toEqual([]);
|
|
1082
|
+
expect(fs.readFileSync(manifestPath(dir), 'utf-8')).toBe(MANIFEST_FIXTURE);
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
it('locks MainActivity to caper.config orientation', async () => {
|
|
1086
|
+
const dir = makeTempDir();
|
|
1087
|
+
const deps = scaffoldAndroidApp(dir);
|
|
1088
|
+
|
|
1089
|
+
const result = await androidNative(dir, {}, { ...stubs({ installed: [] }, []), ...deps, loadConfig: () => ({ orientation: 'portrait' }) });
|
|
1090
|
+
|
|
1091
|
+
expect(result.orientation).toBe('portrait');
|
|
1092
|
+
expect(result.orientationPatched).toBe(true);
|
|
1093
|
+
expect(readManifestOrientation(fs.readFileSync(manifestPath(dir), 'utf-8'))).toBe('portrait');
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
it('applies a changed orientation on the next run, then makes zero writes', async () => {
|
|
1097
|
+
const dir = makeTempDir();
|
|
1098
|
+
const deps = scaffoldAndroidApp(dir);
|
|
1099
|
+
const state = { installed: [] };
|
|
1100
|
+
await androidNative(dir, {}, { ...stubs(state, []), ...deps, loadConfig: () => ({ orientation: 'portrait' }) });
|
|
1101
|
+
|
|
1102
|
+
const landscape = { ...deps, loadConfig: () => ({ orientation: 'landscape' }) };
|
|
1103
|
+
const changed = await androidNative(dir, {}, { ...stubs(state, []), ...landscape });
|
|
1104
|
+
expect(changed.changed).toBe(true);
|
|
1105
|
+
expect(changed.orientationPatched).toBe(true);
|
|
1106
|
+
expect(readManifestOrientation(fs.readFileSync(manifestPath(dir), 'utf-8'))).toBe('sensorLandscape');
|
|
1107
|
+
|
|
1108
|
+
const before = snapshot(dir);
|
|
1109
|
+
const calls = [];
|
|
1110
|
+
const again = await androidNative(dir, {}, { ...stubs(state, calls), ...landscape });
|
|
1111
|
+
expect(calls.filter((c) => c.kind === 'run')).toHaveLength(0);
|
|
1112
|
+
expect(again.changed).toBe(false);
|
|
1113
|
+
expect(again.orientationPatched).toBe(false);
|
|
1114
|
+
expect(snapshot(dir)).toEqual(before);
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
it('reads the lock from the object form of orientation', async () => {
|
|
1118
|
+
const dir = makeTempDir();
|
|
1119
|
+
const deps = scaffoldAndroidApp(dir);
|
|
1120
|
+
const loadConfig = () => ({ orientation: { lock: 'landscape', overlay: false } });
|
|
1121
|
+
|
|
1122
|
+
const result = await androidNative(dir, {}, { ...stubs({ installed: [] }, []), ...deps, loadConfig });
|
|
1123
|
+
|
|
1124
|
+
expect(result.orientation).toBe('landscape');
|
|
1125
|
+
expect(readManifestOrientation(fs.readFileSync(manifestPath(dir), 'utf-8'))).toBe('sensorLandscape');
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
it('warns and skips orientation when caper.config orientation is invalid', async () => {
|
|
1129
|
+
const dir = makeTempDir();
|
|
1130
|
+
const deps = scaffoldAndroidApp(dir);
|
|
1131
|
+
|
|
1132
|
+
const result = await androidNative(dir, {}, { ...stubs({ installed: [] }, []), ...deps, loadConfig: () => ({ orientation: { lock: 'any' } }) });
|
|
1133
|
+
|
|
1134
|
+
expect(result.orientationPatched).toBe(false);
|
|
1135
|
+
expect(result.warnings[0]).toMatch(/could not read caper\.config\.ts orientation/);
|
|
1136
|
+
expect(fs.readFileSync(manifestPath(dir), 'utf-8')).toBe(MANIFEST_FIXTURE);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
it('warns and skips orientation when caper.config cannot be read', async () => {
|
|
1140
|
+
const dir = makeTempDir();
|
|
1141
|
+
const deps = scaffoldAndroidApp(dir);
|
|
1142
|
+
const loadConfig = () => {
|
|
1143
|
+
throw new Error('boom');
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
const result = await androidNative(dir, {}, { ...stubs({ installed: [] }, []), ...deps, loadConfig });
|
|
1147
|
+
|
|
1148
|
+
expect(result.status).toBe('ok');
|
|
1149
|
+
expect(result.orientationPatched).toBe(false);
|
|
1150
|
+
expect(result.warnings).toHaveLength(1);
|
|
1151
|
+
expect(result.warnings[0]).toMatch(/could not read caper\.config\.ts orientation/);
|
|
1152
|
+
expect(fs.readFileSync(manifestPath(dir), 'utf-8')).toBe(MANIFEST_FIXTURE);
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
it('warns when orientation is set but AndroidManifest.xml is missing', async () => {
|
|
1156
|
+
const dir = makeTempDir();
|
|
1157
|
+
const deps = scaffoldAndroidApp(dir);
|
|
1158
|
+
await androidNative(dir, {}, { ...stubs({ installed: [] }, []), ...deps, loadConfig: () => ({}) });
|
|
1159
|
+
fs.rmSync(manifestPath(dir));
|
|
1160
|
+
|
|
1161
|
+
const result = await androidNative(dir, {}, { ...stubs({ installed: [...ANDROID_RUST_TARGETS] }, []), ...deps, loadConfig: () => ({ orientation: 'portrait' }) });
|
|
1162
|
+
|
|
1163
|
+
expect(result.orientationPatched).toBe(false);
|
|
1164
|
+
expect(result.warnings).toHaveLength(1);
|
|
1165
|
+
expect(result.warnings[0]).toMatch(/AndroidManifest\.xml/);
|
|
1166
|
+
});
|
|
1167
|
+
|
|
992
1168
|
it('writes the build.rs template when build.rs is missing', async () => {
|
|
993
1169
|
const dir = makeTempDir();
|
|
994
1170
|
const deps = scaffoldAndroidApp(dir);
|
|
@@ -1023,6 +1199,46 @@ describe('androidNative', () => {
|
|
|
1023
1199
|
});
|
|
1024
1200
|
});
|
|
1025
1201
|
|
|
1202
|
+
describe('patchManifestOrientation', () => {
|
|
1203
|
+
const withAttr = (value) => MANIFEST_FIXTURE.replace('android:name=".MainActivity"', `android:name=".MainActivity"\n android:screenOrientation="${value}"`);
|
|
1204
|
+
|
|
1205
|
+
it('reads no orientation from the stock manifest', () => {
|
|
1206
|
+
expect(readManifestOrientation(MANIFEST_FIXTURE)).toBeUndefined();
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
it('adds android:screenOrientation under android:name, matching its indentation', () => {
|
|
1210
|
+
const patched = patchManifestOrientation(MANIFEST_FIXTURE, 'portrait');
|
|
1211
|
+
expect(patched).toBe(withAttr('portrait'));
|
|
1212
|
+
expect(readManifestOrientation(patched)).toBe('portrait');
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
it('maps landscape to sensorLandscape', () => {
|
|
1216
|
+
expect(patchManifestOrientation(MANIFEST_FIXTURE, 'landscape')).toBe(withAttr('sensorLandscape'));
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
it('returns the same string when the value already matches', () => {
|
|
1220
|
+
const xml = withAttr('portrait');
|
|
1221
|
+
expect(patchManifestOrientation(xml, 'portrait')).toBe(xml);
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
it('replaces a different existing value', () => {
|
|
1225
|
+
expect(patchManifestOrientation(withAttr('landscape'), 'portrait')).toBe(withAttr('portrait'));
|
|
1226
|
+
expect(patchManifestOrientation(withAttr('portrait'), 'landscape')).toBe(withAttr('sensorLandscape'));
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
it('returns the same string when orientation is unset', () => {
|
|
1230
|
+
const xml = withAttr('portrait');
|
|
1231
|
+
expect(patchManifestOrientation(xml, undefined)).toBe(xml);
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
it('only touches the MainActivity element', () => {
|
|
1235
|
+
const xml = MANIFEST_FIXTURE.replace('<provider', '<activity android:name=".Other" />\n\n <provider');
|
|
1236
|
+
const patched = patchManifestOrientation(xml, 'portrait');
|
|
1237
|
+
expect(patched).toContain('<activity android:name=".Other" />');
|
|
1238
|
+
expect(patched.match(/screenOrientation/g)).toHaveLength(1);
|
|
1239
|
+
});
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1026
1242
|
describe('findMainActivity', () => {
|
|
1027
1243
|
it('finds MainActivity.kt anywhere under the java source root', () => {
|
|
1028
1244
|
const dir = makeTempDir();
|
package/extras/llms.txt
CHANGED
|
@@ -219,9 +219,9 @@ subcommands run from the app root.
|
|
|
219
219
|
| `caper doctor [--offline] [--json]` | One-screen health check: installed vs latest caper, npm vs linked engine (and whether the linked build is stale), generated types present/fresh, asset manifest, agent pointers, peer deps, caches. Exit 1 on any ✗. |
|
|
220
220
|
| `caper agent init [--dir <skillsDir>]` | Install the shipped `caper` agent skill (default `.claude/skills/caper/SKILL.md`) and upsert a pointer block into `AGENTS.md`/`CLAUDE.md`. Re-run after upgrading caper. |
|
|
221
221
|
| `caper agent probe <url> [--action n[=json]]… [--until "<js predicate>"] [--wait ms] [--screenshot p] [--headed] [--json]` | Drive a **running** app headlessly through the automation bridge (§17) in a Chromium you own: boots, sends actions, waits on state, returns context/state/log/errors (+ screenshot). Needs `playwright` in the app's devDependencies. |
|
|
222
|
-
| `caper native init [--identifier <id>] [--port <n>] [--icon <png>]` | One-shot Tauri v2 scaffolding: adds `@tauri-apps/cli`, runs `tauri init --ci`, pins a `dev.caper.*`-style identifier + a stable non-3000 dev port, adds `native:dev`/`native:build` scripts. No-ops with "already initialized" if `src-tauri/` exists. |
|
|
222
|
+
| `caper native init [--title <name>] [--identifier <id>] [--port <n>] [--icon <png>]` | One-shot Tauri v2 scaffolding: adds `@tauri-apps/cli`, runs `tauri init --ci`, names the app and window after the package (title-cased: `my-game` → "My Game"; `--title` overrides), pins a `dev.caper.*`-style identifier + a stable non-3000 dev port, opens a 1280x720 window (450x800 when `caper.config.ts` sets `orientation: 'portrait'`), adds `native:dev`/`native:build` scripts. No-ops with "already initialized" if `src-tauri/` exists. |
|
|
223
223
|
| `caper native plugin` | Wires an already-`native init`'d app up for `@caperjs/plugin-tauri`: adds its JS deps, runs `tauri add store`, and patches the window permissions it needs into `capabilities/default.json`. Fails with "run `caper native init` first" if `src-tauri/` is missing; idempotent otherwise. |
|
|
224
|
-
| `caper native android` | Sets an already-`native init`'d app up for Android: finds the SDK/NDK/JDK/rustup, adds the four Android Rust targets, runs `tauri android init --ci`, adds 16 KB page alignment to `build.rs`, hides the system bars in `MainActivity.kt`, adds `native:android:dev`/`native:android:build` scripts. Idempotent; warns instead of overwriting files you changed. |
|
|
224
|
+
| `caper native android` | Sets an already-`native init`'d app up for Android: finds the SDK/NDK/JDK/rustup, adds the four Android Rust targets, runs `tauri android init --ci`, adds 16 KB page alignment to `build.rs`, hides the system bars in `MainActivity.kt`, locks the activity to `caper.config.ts`'s `orientation` (if set; re-run after changing it), adds `native:android:dev`/`native:android:build` scripts. Idempotent; warns instead of overwriting files you changed. |
|
|
225
225
|
| `caper update` | Update Caper to latest. |
|
|
226
226
|
| `caper install` | Install peer deps (rarely needed; normally automatic). |
|
|
227
227
|
| `caper vo generate [inputDir] [outputDir]` | Voiceover CSV generation from locales. Defaults: `./src/locales` → `./src/assets/audio/vo/csv`. |
|
|
@@ -236,7 +236,10 @@ exists. For Android, run `caper native android` once, then
|
|
|
236
236
|
`pnpm native:android:dev` (device or emulator) or
|
|
237
237
|
`pnpm native:android:build --debug --apk --target aarch64`, with `NDK_HOME` and
|
|
238
238
|
`JAVA_HOME` exported; doctor adds `native-android-*` rows (Rust targets, NDK,
|
|
239
|
-
JDK, 16 KB alignment, hidden system bars) once `src-tauri/gen/android/` exists.
|
|
239
|
+
JDK, 16 KB alignment, hidden system bars, and orientation matching `caper.config.ts`) once `src-tauri/gen/android/` exists.
|
|
240
|
+
Set `orientation: 'portrait' | 'landscape'` in `caper.config.ts` rather than editing
|
|
241
|
+
`AndroidManifest.xml` by hand: `caper native android` writes it there (`portrait` or
|
|
242
|
+
`sensorLandscape`) and doctor flags a mismatch. Under the Tauri CLI (detected via `TAURI_ENV_PLATFORM`), the preset
|
|
240
243
|
configures the dev server for you — no extra `vite.config.ts` needed — and
|
|
241
244
|
disables the PWA plugin, since service workers don't work on the `tauri://`
|
|
242
245
|
origin. Check `isTauri` (exported from `@caperjs/core`, alongside
|
|
@@ -480,9 +483,29 @@ Every allowed key in `caper.config.ts`. Defer shape details to
|
|
|
480
483
|
| `resolution?: number` | DPI scale factor. |
|
|
481
484
|
| `splash?: SplashOptions` | Splash screen config. |
|
|
482
485
|
| `logger?: LoggerMode` | Verbosity. |
|
|
486
|
+
| `orientation?: 'portrait' \| 'landscape' \| { lock, overlay? }` | Lock the game to one orientation on every target. Unset means no lock. The string is shorthand for `{ lock: '...' }`. Write the lock as a string literal (the build and CLI read it by parsing the file). `caper native android` writes it into `AndroidManifest.xml`, `caper native init` opens a 450x800 window for portrait, the PWA manifest's `orientation` defaults to it, and in a mobile browser (not desktop, not Tauri) a "rotate your device" overlay covers and pauses the game while the device is held the wrong way. |
|
|
487
|
+
| `orientation.overlay?: false \| { text?, background?, color?, fontFamily?, className?, element? }` | Restyle that overlay; `element: () => HTMLElement` replaces it, `false` turns off the overlay and its pause (`app.orientation.onMismatchChanged` / `app.orientation.mismatched` still report). |
|
|
483
488
|
|
|
484
489
|
Plus all standard PixiJS `ApplicationOptions` (`antialias`, `width`, `height`, etc.).
|
|
485
490
|
|
|
491
|
+
Orientation overlay, customized or replaced:
|
|
492
|
+
|
|
493
|
+
```ts
|
|
494
|
+
// shorthand: lock to portrait with the default overlay
|
|
495
|
+
export default defineConfig({ id: 'my-game', orientation: 'portrait' });
|
|
496
|
+
|
|
497
|
+
// object form: same lock, custom overlay
|
|
498
|
+
export default defineConfig({
|
|
499
|
+
id: 'my-game',
|
|
500
|
+
orientation: { lock: 'portrait', overlay: { text: 'Hold your phone upright to play' } },
|
|
501
|
+
// also: background, color, fontFamily, className (style it in your CSS),
|
|
502
|
+
// element: () => myOwnElement, or overlay: false to draw nothing and react yourself
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// with overlay: false
|
|
506
|
+
app.orientation.onMismatchChanged.connect((mismatched) => (mismatched ? app.pause() : app.resume()));
|
|
507
|
+
```
|
|
508
|
+
|
|
486
509
|
Screen viewport debugging is available through the built-in `screenDebug` overlay.
|
|
487
510
|
Open the game URL in a normal browser tab with `?caper-screen-debug=1` to enable it.
|
|
488
511
|
Then relaunch the installed PWA; the same-origin setting persists without an address bar.
|
|
@@ -834,6 +857,15 @@ then push toasts to it. Also available as `app.popups.toast(...)`.
|
|
|
834
857
|
|
|
835
858
|
A FlexContainer preset for app-size-bound HUDs. Use as the root of in-game UI.
|
|
836
859
|
|
|
860
|
+
It pads by the device safe-area insets (notch, camera hole, gesture bar) by
|
|
861
|
+
default. `useSafeArea: false` turns that off; `{ top: false }` turns off one
|
|
862
|
+
edge, e.g. to let corner buttons sit in a punch-hole phone's camera strip:
|
|
863
|
+
|
|
864
|
+
```ts
|
|
865
|
+
import { isAndroid, isTauri } from '@caperjs/core';
|
|
866
|
+
const hud = this.add.uiCanvas({ useAppSize: true, useSafeArea: isTauri && isAndroid ? { top: false } : true });
|
|
867
|
+
```
|
|
868
|
+
|
|
837
869
|
### 7.6 `Joystick` — virtual stick
|
|
838
870
|
|
|
839
871
|
For touch input. Wires into the `controls.touch.joystick` bindings set via
|