@caperjs/core 0.7.3 → 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 +105 -11
- package/cli/native.test.mjs +184 -0
- package/extras/llms.txt +26 -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 +159 -162
- package/lib/caper.mjs.map +1 -1
- package/lib/{const-BClauBNo.js → const-Bca54QyV.js} +251 -251
- package/lib/{const-BClauBNo.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/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/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/build/index.mjs
CHANGED
|
@@ -92,7 +92,7 @@ function caperPluginList({ assets = {}, pwa } = {}) {
|
|
|
92
92
|
// place that knows a project overrode it.
|
|
93
93
|
caperConfigPlugin(true, manifestUrl),
|
|
94
94
|
caperDevHelperPlugin(),
|
|
95
|
-
...(effectivePwa ? caperPwaPlugins(effectivePwa) : []),
|
|
95
|
+
...(effectivePwa ? caperPwaPlugins(effectivePwa, { orientation: buildFlags.orientation }) : []),
|
|
96
96
|
];
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Build flags read out of `caper.config.ts` before anything can execute it. See
|
|
3
|
+
* the comment on `readCaperBuildFlags` for why this is an AST parse.
|
|
4
4
|
*/
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { AST_NODE_TYPES, findConfigObject, parse } from './ast.mjs';
|
|
8
|
+
import { resolveOrientation } from '../../src/utils/orientation.js';
|
|
8
9
|
import { cwd } from './util.mjs';
|
|
9
10
|
|
|
10
11
|
export function readCaperBuildFlags() {
|
|
11
|
-
const flags = { useWasm: false };
|
|
12
|
+
const flags = { useWasm: false, orientation: undefined };
|
|
12
13
|
const configPath = path.resolve(cwd, 'caper.config.ts');
|
|
13
14
|
if (!fs.existsSync(configPath)) return flags;
|
|
14
15
|
|
|
@@ -22,6 +23,14 @@ export function readCaperBuildFlags() {
|
|
|
22
23
|
|
|
23
24
|
for (const prop of configObject.properties) {
|
|
24
25
|
if (prop.type !== AST_NODE_TYPES.Property || prop.key?.type !== AST_NODE_TYPES.Identifier) continue;
|
|
26
|
+
if (prop.key.name === 'orientation') {
|
|
27
|
+
try {
|
|
28
|
+
flags.orientation = readOrientationLock(prop.value);
|
|
29
|
+
} catch {
|
|
30
|
+
// silent here, like every other build flag; the schema reports it
|
|
31
|
+
}
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
25
34
|
if (!(prop.key.name in flags)) continue;
|
|
26
35
|
if (prop.value?.type === AST_NODE_TYPES.Literal && typeof prop.value.value === 'boolean') {
|
|
27
36
|
flags[prop.key.name] = prop.value.value;
|
|
@@ -30,21 +39,57 @@ export function readCaperBuildFlags() {
|
|
|
30
39
|
return flags;
|
|
31
40
|
}
|
|
32
41
|
|
|
42
|
+
/** A property's value node when it is a string literal, else `undefined`. */
|
|
43
|
+
function stringLiteral(node) {
|
|
44
|
+
return node?.type === AST_NODE_TYPES.Literal && typeof node.value === 'string' ? node.value : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
* The lock out of an `orientation` value node: the string literal itself, or
|
|
49
|
+
* the `lock` string literal of an object literal (`overlay` is never read: it
|
|
50
|
+
* may hold a function). Normalized and validated by `resolveOrientation`.
|
|
51
|
+
*/
|
|
52
|
+
function readOrientationLock(node) {
|
|
53
|
+
if (node?.type === AST_NODE_TYPES.ObjectExpression) {
|
|
54
|
+
const lockProp = node.properties.find(
|
|
55
|
+
(p) => p.type === AST_NODE_TYPES.Property && p.key?.type === AST_NODE_TYPES.Identifier && p.key.name === 'lock',
|
|
56
|
+
);
|
|
57
|
+
if (!lockProp) throw new Error("orientation needs a lock: { lock: 'portrait' | 'landscape' }");
|
|
58
|
+
const lock = stringLiteral(lockProp.value);
|
|
59
|
+
if (lock === undefined) throw new Error("orientation.lock must be written as a string literal, 'portrait' or 'landscape'");
|
|
60
|
+
return resolveOrientation({ lock }).lock;
|
|
61
|
+
}
|
|
62
|
+
const value = stringLiteral(node);
|
|
63
|
+
if (value === undefined) {
|
|
64
|
+
throw new Error("orientation must be written as a string literal ('portrait' or 'landscape') or { lock: '...' }");
|
|
65
|
+
}
|
|
66
|
+
return resolveOrientation(value).lock;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The orientation lock from `<root>/caper.config.ts`, for the native CLI
|
|
71
|
+
* commands and `caper doctor`: `orientation: 'portrait'` or
|
|
72
|
+
* `orientation: { lock: 'portrait', ... }`. Same AST parse as
|
|
73
|
+
* `readCaperBuildFlags`, so nothing is executed and no DOM stub is needed, but
|
|
74
|
+
* strict where that one is silent: it throws when the file does not parse,
|
|
75
|
+
* when no `defineConfig({...})` object literal is found, or when the lock is
|
|
76
|
+
* not `'portrait'` / `'landscape'` written as a string literal. Callers turn
|
|
77
|
+
* the throw into a warning.
|
|
46
78
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* broken references the user needs to see).
|
|
79
|
+
* @param {string} root
|
|
80
|
+
* @returns {'portrait' | 'landscape' | undefined} undefined when there is no config file or no `orientation` key
|
|
50
81
|
*/
|
|
82
|
+
export function readConfigOrientation(root) {
|
|
83
|
+
const configPath = path.resolve(root, 'caper.config.ts');
|
|
84
|
+
if (!fs.existsSync(configPath)) return undefined;
|
|
85
|
+
|
|
86
|
+
const configObject = findConfigObject(parse(fs.readFileSync(configPath, 'utf-8')));
|
|
87
|
+
if (configObject?.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
88
|
+
throw new Error('no defineConfig({ ... }) object literal found');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const prop = configObject.properties.find(
|
|
92
|
+
(p) => p.type === AST_NODE_TYPES.Property && p.key?.type === AST_NODE_TYPES.Identifier && p.key.name === 'orientation',
|
|
93
|
+
);
|
|
94
|
+
return prop ? readOrientationLock(prop.value) : undefined;
|
|
95
|
+
}
|
|
@@ -17,6 +17,55 @@ export const pluginConfigSchema = z.union([
|
|
|
17
17
|
]),
|
|
18
18
|
]);
|
|
19
19
|
|
|
20
|
+
const ORIENTATION_LOCK_MESSAGE = "orientation must be 'portrait', 'landscape', or { lock: 'portrait' | 'landscape', overlay? }";
|
|
21
|
+
|
|
22
|
+
const orientationLockSchema = z.enum(['portrait', 'landscape'], { error: ORIENTATION_LOCK_MESSAGE });
|
|
23
|
+
|
|
24
|
+
const orientationObjectSchema = z.strictObject(
|
|
25
|
+
{
|
|
26
|
+
lock: z.enum(['portrait', 'landscape'], {
|
|
27
|
+
error: (issue) =>
|
|
28
|
+
issue.input === undefined
|
|
29
|
+
? "orientation.lock is required: 'portrait' or 'landscape'"
|
|
30
|
+
: "orientation.lock must be 'portrait' or 'landscape'",
|
|
31
|
+
}),
|
|
32
|
+
overlay: z
|
|
33
|
+
.union(
|
|
34
|
+
[
|
|
35
|
+
z.literal(false),
|
|
36
|
+
z.strictObject({
|
|
37
|
+
text: z.string().optional(),
|
|
38
|
+
background: z.string().optional(),
|
|
39
|
+
color: z.string().optional(),
|
|
40
|
+
fontFamily: z.string().optional(),
|
|
41
|
+
className: z.string().optional(),
|
|
42
|
+
element: z.function().optional(),
|
|
43
|
+
}),
|
|
44
|
+
],
|
|
45
|
+
{
|
|
46
|
+
error:
|
|
47
|
+
'orientation.overlay must be false or an object of text, background, color, fontFamily, className (strings) and element (a function)',
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
.optional(),
|
|
51
|
+
},
|
|
52
|
+
{ error: (issue) => (issue.code === 'unrecognized_keys' ? `orientation only takes lock and overlay (got ${issue.keys.join(', ')})` : undefined) },
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* `orientation` is a string shorthand or `{ lock, overlay? }`. Validated by
|
|
57
|
+
* picking the branch from the value's type rather than with a `z.union`, whose
|
|
58
|
+
* single "Invalid input" would hide the object branch's precise messages.
|
|
59
|
+
*/
|
|
60
|
+
const orientationSchema = z.any().superRefine((value, ctx) => {
|
|
61
|
+
const schema = value !== null && typeof value === 'object' ? orientationObjectSchema : orientationLockSchema;
|
|
62
|
+
const result = schema.safeParse(value);
|
|
63
|
+
if (result.success) return;
|
|
64
|
+
for (const issue of result.error.issues) {
|
|
65
|
+
ctx.addIssue({ code: 'custom', message: issue.message, path: issue.path });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
20
69
|
export const caperConfigSchema = z
|
|
21
70
|
.object({
|
|
22
71
|
id: z.string().min(1).optional(),
|
|
@@ -50,6 +99,10 @@ export const caperConfigSchema = z
|
|
|
50
99
|
useHash: z.boolean().optional(),
|
|
51
100
|
// Build-time only — read by readCaperBuildFlags(), no runtime effect.
|
|
52
101
|
useWasm: z.boolean().optional(),
|
|
102
|
+
// `'portrait' | 'landscape' | { lock, overlay? }`. Read by the native CLI
|
|
103
|
+
// (`caper native init|android`), the PWA manifest default, and at runtime by
|
|
104
|
+
// the `orientation` plugin.
|
|
105
|
+
orientation: orientationSchema.optional(),
|
|
53
106
|
showStats: z.boolean().optional(),
|
|
54
107
|
showSceneDebugMenu: z.boolean().optional(),
|
|
55
108
|
resizeToContainer: z.boolean().optional(),
|
package/build/plugins/pwa.mjs
CHANGED
|
@@ -129,8 +129,12 @@ function defaultWorkbox() {
|
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
/**
|
|
133
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Caper's vite-plugin-pwa defaults.
|
|
134
|
+
*
|
|
135
|
+
* @param {{ orientation?: 'portrait' | 'landscape' }} [caperConfig] `orientation` from caper.config.ts, if set
|
|
136
|
+
*/
|
|
137
|
+
export function defaultPwaOptions({ orientation } = {}) {
|
|
134
138
|
const app = readAppIdentity();
|
|
135
139
|
|
|
136
140
|
return {
|
|
@@ -162,7 +166,7 @@ export function defaultPwaOptions() {
|
|
|
162
166
|
background_color: '#000000',
|
|
163
167
|
display: 'fullscreen',
|
|
164
168
|
display_override: ['fullscreen', 'standalone', 'minimal-ui'],
|
|
165
|
-
orientation: 'any',
|
|
169
|
+
orientation: orientation ?? 'any',
|
|
166
170
|
categories: ['games', 'entertainment'],
|
|
167
171
|
},
|
|
168
172
|
workbox: defaultWorkbox(),
|
|
@@ -204,10 +208,11 @@ function caperPwaDefaultsPlugin(options, projectSetIcons) {
|
|
|
204
208
|
* defaults, producing the options vite-plugin-pwa is given.
|
|
205
209
|
*
|
|
206
210
|
* @param {object} pwa Project options, merged over `defaultPwaOptions()`.
|
|
211
|
+
* @param {{ orientation?: 'portrait' | 'landscape' }} [caperConfig] build flags read out of caper.config.ts
|
|
207
212
|
*/
|
|
208
|
-
export function resolvePwaOptions(pwa) {
|
|
213
|
+
export function resolvePwaOptions(pwa, caperConfig = {}) {
|
|
209
214
|
const { autoRegister: _autoRegister, update = 'prompt', ...projectOptions } = pwa;
|
|
210
|
-
const options = deepMerge(defaultPwaOptions(), projectOptions);
|
|
215
|
+
const options = deepMerge(defaultPwaOptions(caperConfig), projectOptions);
|
|
211
216
|
|
|
212
217
|
// `update` is caper's plain-language spelling of registerType. 'prompt' and
|
|
213
218
|
// 'manual' both wait for the user — they differ only in who draws the UI, which
|
|
@@ -222,10 +227,11 @@ export function resolvePwaOptions(pwa) {
|
|
|
222
227
|
|
|
223
228
|
/**
|
|
224
229
|
* @param {object} pwa Project options, merged over `defaultPwaOptions()`.
|
|
230
|
+
* @param {{ orientation?: 'portrait' | 'landscape' }} [caperConfig] build flags read out of caper.config.ts
|
|
225
231
|
* @returns {import('vite').PluginOption[]}
|
|
226
232
|
*/
|
|
227
|
-
export function caperPwaPlugins(pwa) {
|
|
228
|
-
const { options, projectSetIcons } = resolvePwaOptions(pwa);
|
|
233
|
+
export function caperPwaPlugins(pwa, caperConfig = {}) {
|
|
234
|
+
const { options, projectSetIcons } = resolvePwaOptions(pwa, caperConfig);
|
|
229
235
|
|
|
230
236
|
return [caperPwaDefaultsPlugin(options, projectSetIcons), VitePWA(options)];
|
|
231
237
|
}
|
package/cli/doctor.mjs
CHANGED
|
@@ -7,7 +7,17 @@ import os from 'node:os';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
ANDROID_MANIFEST,
|
|
12
|
+
ANDROID_RUST_TARGETS,
|
|
13
|
+
ANDROID_SCREEN_ORIENTATION,
|
|
14
|
+
findMainActivity,
|
|
15
|
+
missingRustTargets,
|
|
16
|
+
readManifestOrientation,
|
|
17
|
+
readOrientation,
|
|
18
|
+
resolveAndroidEnv,
|
|
19
|
+
TAURI_PLUGIN_PERMISSIONS,
|
|
20
|
+
} from './native.mjs';
|
|
11
21
|
|
|
12
22
|
/**
|
|
13
23
|
* `caper doctor` — one-shot health report for a Caper app.
|
|
@@ -203,7 +213,14 @@ const resolveEffectiveCompilerOptions = (configFile, ancestors = new Set(), dept
|
|
|
203
213
|
|
|
204
214
|
export async function runChecks(
|
|
205
215
|
cwd,
|
|
206
|
-
{
|
|
216
|
+
{
|
|
217
|
+
online = true,
|
|
218
|
+
run = defaultDoctorRun,
|
|
219
|
+
platform = process.platform,
|
|
220
|
+
env = process.env,
|
|
221
|
+
homedir = os.homedir(),
|
|
222
|
+
loadConfig = undefined,
|
|
223
|
+
} = {},
|
|
207
224
|
) {
|
|
208
225
|
const checks = [];
|
|
209
226
|
const installedVersion = readInstalledVersion();
|
|
@@ -497,6 +514,40 @@ export async function runChecks(
|
|
|
497
514
|
} else {
|
|
498
515
|
push(checks, 'native-android-bars', 'warn', 'MainActivity does not hide the system bars', 'caper native android');
|
|
499
516
|
}
|
|
517
|
+
|
|
518
|
+
const { orientation, warning: orientationWarning } = await readOrientation(cwd, loadConfig);
|
|
519
|
+
let manifest = '';
|
|
520
|
+
try {
|
|
521
|
+
manifest = fs.readFileSync(path.join(srcTauriDir, ANDROID_MANIFEST), 'utf-8');
|
|
522
|
+
} catch {
|
|
523
|
+
// no lock found; compared below
|
|
524
|
+
}
|
|
525
|
+
const locked = readManifestOrientation(manifest);
|
|
526
|
+
const expected = ANDROID_SCREEN_ORIENTATION[orientation];
|
|
527
|
+
if (orientationWarning) {
|
|
528
|
+
push(checks, 'native-android-orientation', 'warn', 'could not read caper.config.ts orientation', "write it as orientation: 'portrait' or 'landscape'");
|
|
529
|
+
} else if (!expected && !locked) {
|
|
530
|
+
push(checks, 'native-android-orientation', 'ok', 'no orientation lock');
|
|
531
|
+
} else if (expected === locked) {
|
|
532
|
+
push(checks, 'native-android-orientation', 'ok', `orientation ${orientation} (AndroidManifest ${locked})`);
|
|
533
|
+
} else if (!expected) {
|
|
534
|
+
// `caper native android` leaves the manifest alone when orientation is unset, so it is not the fix here.
|
|
535
|
+
push(
|
|
536
|
+
checks,
|
|
537
|
+
'native-android-orientation',
|
|
538
|
+
'warn',
|
|
539
|
+
`AndroidManifest locks orientation to ${locked}, caper.config.ts sets none`,
|
|
540
|
+
'set orientation in caper.config.ts, or remove android:screenOrientation from AndroidManifest.xml',
|
|
541
|
+
);
|
|
542
|
+
} else {
|
|
543
|
+
push(
|
|
544
|
+
checks,
|
|
545
|
+
'native-android-orientation',
|
|
546
|
+
'warn',
|
|
547
|
+
`caper.config.ts orientation ${orientation}, AndroidManifest ${locked ? `locks ${locked}` : 'has no lock'}`,
|
|
548
|
+
'caper native android',
|
|
549
|
+
);
|
|
550
|
+
}
|
|
500
551
|
}
|
|
501
552
|
}
|
|
502
553
|
|
package/cli/doctor.test.mjs
CHANGED
|
@@ -712,8 +712,9 @@ describe('runChecks native-android rows', () => {
|
|
|
712
712
|
|
|
713
713
|
const allTargets = 'aarch64-linux-android\narmv7-linux-androideabi\ni686-linux-android\nx86_64-linux-android\n';
|
|
714
714
|
|
|
715
|
-
function check(cwd, env, responses = { 'rustup target list --installed': allTargets }) {
|
|
715
|
+
function check(cwd, env, responses = { 'rustup target list --installed': allTargets }, loadConfig = () => ({})) {
|
|
716
716
|
return runChecks(cwd, {
|
|
717
|
+
loadConfig,
|
|
717
718
|
online: false,
|
|
718
719
|
platform: 'linux',
|
|
719
720
|
env,
|
|
@@ -814,4 +815,82 @@ describe('runChecks native-android rows', () => {
|
|
|
814
815
|
expect(pageSize.hint).toMatch(/caper native android/);
|
|
815
816
|
expect(find(checks, 'native-android-bars').status).toBe('warn');
|
|
816
817
|
});
|
|
818
|
+
|
|
819
|
+
describe('native-android-orientation', () => {
|
|
820
|
+
const MANIFEST = '<manifest>\n <application>\n <activity\n android:name=".MainActivity"\n android:exported="true">\n </activity>\n </application>\n</manifest>\n';
|
|
821
|
+
const manifestPath = (cwd) => path.join(cwd, 'src-tauri/gen/android/app/src/main/AndroidManifest.xml');
|
|
822
|
+
const writeManifest = (cwd, value) =>
|
|
823
|
+
fs.writeFileSync(
|
|
824
|
+
manifestPath(cwd),
|
|
825
|
+
value ? MANIFEST.replace('android:name=".MainActivity"', `android:name=".MainActivity"\n android:screenOrientation="${value}"`) : MANIFEST,
|
|
826
|
+
'utf-8',
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
async function orientationRow(cwd, loadConfig) {
|
|
830
|
+
const { bin, ndk, java } = fs.existsSync(path.join(cwd, 'bin')) ? { bin: path.join(cwd, 'bin'), ndk: path.join(cwd, 'ndk'), java: path.join(cwd, 'java') } : scaffoldAndroid(cwd);
|
|
831
|
+
return find(await check(cwd, { PATH: bin, NDK_HOME: ndk, JAVA_HOME: java }, undefined, loadConfig), 'native-android-orientation');
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
it('is absent when src-tauri/gen/android does not exist', async () => {
|
|
835
|
+
const cwd = makeTempDir();
|
|
836
|
+
writeTauriConf(cwd);
|
|
837
|
+
writeTauriCli(cwd, '2.11.4');
|
|
838
|
+
const checks = await check(cwd, {}, undefined, () => ({ orientation: 'portrait' }));
|
|
839
|
+
expect(find(checks, 'native-android-orientation')).toBeUndefined();
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it('is ok with no orientation lock when neither side sets one', async () => {
|
|
843
|
+
const cwd = makeTempDir();
|
|
844
|
+
scaffoldAndroid(cwd);
|
|
845
|
+
writeManifest(cwd);
|
|
846
|
+
const row = await orientationRow(cwd, () => ({}));
|
|
847
|
+
expect(row.status).toBe('ok');
|
|
848
|
+
expect(row.label).toMatch(/no orientation lock/);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it('is ok when the manifest matches caper.config', async () => {
|
|
852
|
+
const cwd = makeTempDir();
|
|
853
|
+
scaffoldAndroid(cwd);
|
|
854
|
+
writeManifest(cwd, 'sensorLandscape');
|
|
855
|
+
expect((await orientationRow(cwd, () => ({ orientation: 'landscape' }))).status).toBe('ok');
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it('warns with the caper native android hint on a mismatch', async () => {
|
|
859
|
+
const cwd = makeTempDir();
|
|
860
|
+
scaffoldAndroid(cwd);
|
|
861
|
+
writeManifest(cwd, 'portrait');
|
|
862
|
+
const row = await orientationRow(cwd, () => ({ orientation: 'landscape' }));
|
|
863
|
+
expect(row.status).toBe('warn');
|
|
864
|
+
expect(row.hint).toBe('caper native android');
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
it('warns when caper.config sets an orientation the manifest lacks', async () => {
|
|
868
|
+
const cwd = makeTempDir();
|
|
869
|
+
scaffoldAndroid(cwd);
|
|
870
|
+
writeManifest(cwd);
|
|
871
|
+
const row = await orientationRow(cwd, () => ({ orientation: 'portrait' }));
|
|
872
|
+
expect(row.status).toBe('warn');
|
|
873
|
+
expect(row.hint).toBe('caper native android');
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
it('warns when the manifest has a lock but caper.config sets none', async () => {
|
|
877
|
+
const cwd = makeTempDir();
|
|
878
|
+
scaffoldAndroid(cwd);
|
|
879
|
+
writeManifest(cwd, 'portrait');
|
|
880
|
+
const row = await orientationRow(cwd, () => ({}));
|
|
881
|
+
expect(row.status).toBe('warn');
|
|
882
|
+
expect(row.hint).toMatch(/caper\.config\.ts/);
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it('warns when caper.config orientation cannot be read', async () => {
|
|
886
|
+
const cwd = makeTempDir();
|
|
887
|
+
scaffoldAndroid(cwd);
|
|
888
|
+
writeManifest(cwd);
|
|
889
|
+
const row = await orientationRow(cwd, () => {
|
|
890
|
+
throw new Error('boom');
|
|
891
|
+
});
|
|
892
|
+
expect(row.status).toBe('warn');
|
|
893
|
+
expect(row.label).toMatch(/could not read caper\.config\.ts orientation/);
|
|
894
|
+
});
|
|
895
|
+
});
|
|
817
896
|
});
|
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,
|
|
@@ -130,10 +132,37 @@ function renderCommand({ cmd, args }) {
|
|
|
130
132
|
return [cmd, ...args].join(' ');
|
|
131
133
|
}
|
|
132
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
|
+
|
|
133
160
|
/** Returns a new tauri.conf.json: sets `identifier` and window 0's title/size; leaves the rest untouched. */
|
|
134
|
-
export function patchTauriConfig(conf, { identifier, title }) {
|
|
161
|
+
export function patchTauriConfig(conf, { identifier, title, orientation }) {
|
|
135
162
|
const windows = Array.isArray(conf.app?.windows) ? [...conf.app.windows] : [];
|
|
136
|
-
|
|
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 };
|
|
137
166
|
|
|
138
167
|
return {
|
|
139
168
|
...conf,
|
|
@@ -208,9 +237,9 @@ function writeJson(file, data, indent) {
|
|
|
208
237
|
*
|
|
209
238
|
* @param {string} cwd
|
|
210
239
|
* @param {{ identifier?: string, port?: number, icon?: string }} [opts]
|
|
211
|
-
* @param {{ run?: typeof defaultRun }} [deps]
|
|
240
|
+
* @param {{ run?: typeof defaultRun, loadConfig?: typeof defaultLoadConfig }} [deps]
|
|
212
241
|
*/
|
|
213
|
-
export async function initNative(cwd, opts = {}, { run = defaultRun } = {}) {
|
|
242
|
+
export async function initNative(cwd, opts = {}, { run = defaultRun, loadConfig = defaultLoadConfig } = {}) {
|
|
214
243
|
const pkgPath = path.join(cwd, 'package.json');
|
|
215
244
|
if (!fs.existsSync(pkgPath)) {
|
|
216
245
|
throw new Error('no package.json found in this directory.');
|
|
@@ -268,9 +297,13 @@ export async function initNative(cwd, opts = {}, { run = defaultRun } = {}) {
|
|
|
268
297
|
];
|
|
269
298
|
run(EXEC_RUNNER[pm], initArgs, { cwd });
|
|
270
299
|
|
|
300
|
+
const warnings = [];
|
|
301
|
+
const { orientation, warning } = await readOrientation(cwd, loadConfig);
|
|
302
|
+
if (warning) warnings.push(warning);
|
|
303
|
+
|
|
271
304
|
const tauriConfPath = path.join(srcTauriDir, 'tauri.conf.json');
|
|
272
305
|
const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, 'utf-8'));
|
|
273
|
-
writeJson(tauriConfPath, patchTauriConfig(tauriConf, { identifier, title }), 2);
|
|
306
|
+
writeJson(tauriConfPath, patchTauriConfig(tauriConf, { identifier, title, orientation }), 2);
|
|
274
307
|
|
|
275
308
|
// Re-read: the package manager rewrote package.json when it added @tauri-apps/cli,
|
|
276
309
|
// and patching the copy read above would silently drop that devDependency.
|
|
@@ -281,7 +314,7 @@ export async function initNative(cwd, opts = {}, { run = defaultRun } = {}) {
|
|
|
281
314
|
run(EXEC_RUNNER[pm], ['tauri', 'icon', opts.icon], { cwd });
|
|
282
315
|
}
|
|
283
316
|
|
|
284
|
-
return { status: 'ok', name, title, identifier, port, pm };
|
|
317
|
+
return { status: 'ok', name, title, identifier, port, pm, orientation, warnings };
|
|
285
318
|
}
|
|
286
319
|
|
|
287
320
|
/**
|
|
@@ -541,6 +574,38 @@ export function findMainActivity(genAndroidDir) {
|
|
|
541
574
|
return walk(path.join(genAndroidDir, 'app/src/main/java'));
|
|
542
575
|
}
|
|
543
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
|
+
|
|
544
609
|
/** Returns a new package.json: adds `native:android:dev`/`native:android:build` scripts only where absent. */
|
|
545
610
|
export function patchAndroidScripts(pkg) {
|
|
546
611
|
const scripts = { ...(pkg.scripts ?? {}) };
|
|
@@ -572,13 +637,15 @@ const ANDROID_MISSING_HELP = {
|
|
|
572
637
|
* The I/O core of `caper native android`: the Android steps from
|
|
573
638
|
* docs/wiki/native-tauri.md: installs the Rust targets, runs
|
|
574
639
|
* `tauri android init --ci`, adds the 16 KB page-alignment link arg to
|
|
575
|
-
* `build.rs`, hides the system bars in `MainActivity.kt`,
|
|
576
|
-
* `
|
|
577
|
-
*
|
|
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.
|
|
578
645
|
*
|
|
579
646
|
* @param {string} cwd
|
|
580
647
|
* @param {object} [opts] unused today; kept for symmetry with `initNative`
|
|
581
|
-
* @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]
|
|
582
649
|
*/
|
|
583
650
|
export async function androidNative(
|
|
584
651
|
cwd,
|
|
@@ -591,6 +658,7 @@ export async function androidNative(
|
|
|
591
658
|
homedir = os.homedir(),
|
|
592
659
|
exists = fs.existsSync,
|
|
593
660
|
listDir = defaultListDir,
|
|
661
|
+
loadConfig = defaultLoadConfig,
|
|
594
662
|
} = {},
|
|
595
663
|
) {
|
|
596
664
|
const pkgPath = path.join(cwd, 'package.json');
|
|
@@ -650,6 +718,24 @@ export async function androidNative(
|
|
|
650
718
|
}
|
|
651
719
|
}
|
|
652
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
|
+
|
|
653
739
|
const pkgRaw = fs.readFileSync(pkgPath, 'utf-8');
|
|
654
740
|
const pkg = JSON.parse(pkgRaw);
|
|
655
741
|
const patchedPkg = patchAndroidScripts(pkg);
|
|
@@ -661,11 +747,14 @@ export async function androidNative(
|
|
|
661
747
|
const buildRsPatched = buildRsPlan === 'write';
|
|
662
748
|
return {
|
|
663
749
|
status: 'ok',
|
|
664
|
-
changed:
|
|
750
|
+
changed:
|
|
751
|
+
addedTargets.length > 0 || ranAndroidInit || buildRsPatched || mainActivityPatched || orientationPatched || addedScripts.length > 0,
|
|
665
752
|
addedTargets,
|
|
666
753
|
ranAndroidInit,
|
|
667
754
|
buildRsPatched,
|
|
668
755
|
mainActivityPatched,
|
|
756
|
+
orientation,
|
|
757
|
+
orientationPatched,
|
|
669
758
|
addedScripts,
|
|
670
759
|
env: {
|
|
671
760
|
ANDROID_HOME: resolved.env.ANDROID_HOME,
|
|
@@ -739,6 +828,8 @@ async function runInit(args) {
|
|
|
739
828
|
console.log(` ${yellow('identifier:')} ${result.identifier}${result.identifier.startsWith(DEFAULT_IDENTIFIER_PREFIX) ? dim(' (placeholder — change before shipping)') : ''}`);
|
|
740
829
|
console.log(` ${yellow('dev port:')} ${result.port}`);
|
|
741
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}`));
|
|
742
833
|
console.log(`\n Run ${cyan('npx caper doctor')} to check your native toolchain.`);
|
|
743
834
|
console.log(` The first ${cyan('native:build')} compiles Rust — expect a few minutes.`);
|
|
744
835
|
}
|
|
@@ -783,6 +874,9 @@ async function runAndroid() {
|
|
|
783
874
|
if (result.ranAndroidInit) console.log(` ${yellow('ran:')} tauri android init --ci`);
|
|
784
875
|
if (result.buildRsPatched) console.log(` ${yellow('build.rs:')} 16 KB page alignment added to src-tauri/build.rs`);
|
|
785
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
|
+
}
|
|
786
880
|
if (result.addedScripts.length) console.log(` ${yellow('scripts:')} ${result.addedScripts.join(', ')}`);
|
|
787
881
|
}
|
|
788
882
|
for (const warning of result.warnings) console.log(yellow(` ⚠ ${warning}`));
|