@akanjs/cli 3.0.0-beta.27 → 3.0.0-beta.28
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-stamp +1 -1
- package/{CodeAgent-h19atbr3.js → CodeAgent-7acv4ah8.js} +2 -2
- package/{CodeTui-46zygq8p.js → CodeTui-9tkh34mj.js} +1 -1
- package/{agent.command-rp505zxa.js → agent.command-q9bmqrag.js} +5 -5
- package/akanCode.js +2 -2
- package/{application.command-1p0cpt6y.js → application.command-xpnkyx4z.js} +70 -8
- package/{applicationBuildRunner-r6b9rkb8.js → applicationBuildRunner-w4917tje.js} +67 -22
- package/buildBatch.proc.js +3 -3
- package/{cloud.command-ps9dabbg.js → cloud.command-7yd624qd.js} +9 -9
- package/cloud.script-25vzkz7b.js +20 -0
- package/{code.command-78qw3q3f.js → code.command-fpx4gzmy.js} +6 -6
- package/commandManifest.json +1 -1
- package/{context.command-x5f7t3gk.js → context.command-snn1k5q3.js} +13 -13
- package/context.runner-nzaf8c98.js +21 -0
- package/{guideline.command-d5qmxx29.js → guideline.command-ene55fkr.js} +2 -2
- package/guidelines/conventions/conventions.instruction.md +27 -7
- package/guidelines/runtimeRule/runtimeRule.instruction.md +107 -1
- package/incrementalBuilder.proc.js +2 -2
- package/{index-118vq9dh.js → index-0ej0sr6m.js} +225 -46
- package/{index-hzy4pe24.js → index-0pqzrbpr.js} +2 -2
- package/{index-6dt6zft6.js → index-20r2g1t3.js} +4 -4
- package/{index-xfgdmm5d.js → index-2ctqegdm.js} +2 -2
- package/{index-d65fzxvd.js → index-2s2e5qnd.js} +1 -1
- package/{index-t02g6dpn.js → index-2z30v4k5.js} +3 -3
- package/{index-vxqe24mp.js → index-52n8xr63.js} +1 -1
- package/{index-tg09x14f.js → index-9j0kykc6.js} +5 -5
- package/{index-58cyna2s.js → index-b68zfsvz.js} +1 -1
- package/{index-2zcm7fjd.js → index-ewbek95g.js} +3 -3
- package/{index-bp9b1g2r.js → index-fy42hyb9.js} +7 -3
- package/{index-zazvn45w.js → index-genegfnt.js} +4 -4
- package/{index-da72pfdv.js → index-h9ed99ax.js} +7 -7
- package/{index-per2kqy9.js → index-k7809wmg.js} +2 -2
- package/{index-j9z8vg2n.js → index-n9704w6y.js} +4 -4
- package/{index-860zjnr1.js → index-nbg9bfnr.js} +1 -1
- package/{index-5mm3g24t.js → index-qtz88kh0.js} +2 -2
- package/{index-0n9q1f23.js → index-r8gp8zk3.js} +231 -98
- package/{index-ggfhz9ab.js → index-rbeahw6j.js} +3 -3
- package/{index-56xwsgyn.js → index-rrt9pnb4.js} +2 -2
- package/{index-v4hnkqz9.js → index-ss828r32.js} +1 -1
- package/index-tfe3711h.js +947 -0
- package/{index-5kjsr2pg.js → index-ttn00pr3.js} +38 -3
- package/{index-w5pet0nq.js → index-vt64c067.js} +11 -9
- package/index.js +21 -21
- package/{library.command-sv6g1h2g.js → library.command-tz7vnwea.js} +3 -3
- package/{localRegistry.command-zppa6w7q.js → localRegistry.command-f39yk7be.js} +8 -8
- package/{module.command-ccdeder9.js → module.command-qhc9ns0s.js} +5 -5
- package/{package.command-xmw605b1.js → package.command-z3epnvas.js} +3 -3
- package/package.json +2 -2
- package/{page.command-ky7vafsn.js → page.command-exd2vakz.js} +3 -3
- package/{primitive.command-bh6j8nfs.js → primitive.command-0w6cha4j.js} +6 -6
- package/{quality.command-rqqrpe98.js → quality.command-je4kc9yv.js} +2 -2
- package/{repair.command-n9nsm9qe.js → repair.command-knvwfgtg.js} +5 -5
- package/{scalar.command-t09gmck8.js → scalar.command-60nxfchc.js} +4 -4
- package/{subspace.command-gx2cjpnv.js → subspace.command-0e53tvxk.js} +4 -4
- package/templates/server.ts +1 -1
- package/{tunnel.command-4g95vhsn.js → tunnel.command-7m2kxtat.js} +2 -2
- package/{workflow.command-b91ph8sx.js → workflow.command-j5tk13ds.js} +10 -10
- package/{workspace.command-1r29qzzk.js → workspace.command-wypqk2zb.js} +20 -20
- package/cloud.script-wrw1kz6k.js +0 -20
- package/context.runner-nfs8ksh3.js +0 -21
- package/index-y736f42m.js +0 -556
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
binPlatforms,
|
|
4
|
+
Executor
|
|
5
|
+
} from "./index-r8gp8zk3.js";
|
|
6
|
+
|
|
7
|
+
// pkgs/@akanjs/devkit/mobile/mobileTarget.ts
|
|
8
|
+
var trimSlashes = (value) => value.replace(/^\/+|\/+$/g, "");
|
|
9
|
+
var getMobileTargets = async (app) => {
|
|
10
|
+
const config = await app.getConfig();
|
|
11
|
+
return Object.entries(config.mobile.targets).map(([name, target]) => ({ name, config: target }));
|
|
12
|
+
};
|
|
13
|
+
var isTemplateOnly = ({ basePaths, mobile }) => basePaths.size > 0 && Object.values(mobile.targets).every((target) => !target.basePath);
|
|
14
|
+
var getMobileTargetChoices = async (app) => {
|
|
15
|
+
const config = await app.getConfig();
|
|
16
|
+
const targetNames = Object.keys(config.mobile.targets);
|
|
17
|
+
if (targetNames.length > 0 && !isTemplateOnly(config))
|
|
18
|
+
return targetNames;
|
|
19
|
+
return [...config.basePaths];
|
|
20
|
+
};
|
|
21
|
+
var resolveMobileTargetByBasePath = (targets, basePath) => {
|
|
22
|
+
const normalizedBasePath = trimSlashes(basePath);
|
|
23
|
+
const byBasePath = targets.find(({ config }) => config.basePath !== undefined && trimSlashes(config.basePath) === normalizedBasePath);
|
|
24
|
+
if (byBasePath)
|
|
25
|
+
return byBasePath;
|
|
26
|
+
const [template] = targets;
|
|
27
|
+
if (!template)
|
|
28
|
+
return;
|
|
29
|
+
return {
|
|
30
|
+
name: normalizedBasePath,
|
|
31
|
+
config: { ...template.config, name: normalizedBasePath, basePath: normalizedBasePath }
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
var resolveMobileTargets = async (app, selection) => {
|
|
35
|
+
const config = await app.getConfig();
|
|
36
|
+
const targets = await getMobileTargets(app);
|
|
37
|
+
if (targets.length === 0)
|
|
38
|
+
throw new Error(`No mobile targets configured for ${app.name}`);
|
|
39
|
+
if (!selection && targets.length === 1 && !isTemplateOnly(config))
|
|
40
|
+
return targets;
|
|
41
|
+
if (!selection) {
|
|
42
|
+
const choices = await getMobileTargetChoices(app);
|
|
43
|
+
if (choices.length === 1)
|
|
44
|
+
return resolveMobileTargets(app, choices[0]);
|
|
45
|
+
throw new Error(`Multiple mobile targets found for ${app.name}. Pass --target <${choices.join("|")}|all>.`);
|
|
46
|
+
}
|
|
47
|
+
if (selection === "all") {
|
|
48
|
+
if (Object.keys(config.mobile.targets).length > 1)
|
|
49
|
+
return targets;
|
|
50
|
+
const basePaths = [...config.basePaths];
|
|
51
|
+
if (basePaths.length > 1 || isTemplateOnly(config)) {
|
|
52
|
+
return basePaths.flatMap((basePath) => {
|
|
53
|
+
const resolved = resolveMobileTargetByBasePath(targets, basePath);
|
|
54
|
+
return resolved ? [resolved] : [];
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return targets;
|
|
58
|
+
}
|
|
59
|
+
const target = targets.find((candidate) => candidate.name === selection);
|
|
60
|
+
if (target)
|
|
61
|
+
return [target];
|
|
62
|
+
const basePathTarget = resolveMobileTargetByBasePath(targets, selection);
|
|
63
|
+
if (basePathTarget && config.basePaths.has(trimSlashes(selection)))
|
|
64
|
+
return [basePathTarget];
|
|
65
|
+
const choices = await getMobileTargetChoices(app);
|
|
66
|
+
throw new Error(`Mobile target '${selection}' was not found. Available: ${choices.join(", ")}`);
|
|
67
|
+
};
|
|
68
|
+
var resolveAppId = (appId, platform) => {
|
|
69
|
+
if (typeof appId === "string")
|
|
70
|
+
return appId;
|
|
71
|
+
const id = appId[platform] ?? appId.default;
|
|
72
|
+
if (!id)
|
|
73
|
+
throw new Error(`mobile.appId names no id for ${platform}; give it ${platform} or default.`);
|
|
74
|
+
return id;
|
|
75
|
+
};
|
|
76
|
+
var appIdsOf = (appId) => typeof appId === "string" ? [appId] : Object.values(appId).filter((id) => !!id);
|
|
77
|
+
var targetHtmlFilename = (target) => {
|
|
78
|
+
const basePath = trimSlashes(target.basePath ?? "");
|
|
79
|
+
return basePath ? `${basePath}.html` : "index.html";
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// pkgs/@akanjs/devkit/mobile/nativeApp.ts
|
|
83
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
84
|
+
import path6 from "path";
|
|
85
|
+
|
|
86
|
+
// pkgs/@akanjs/devkit/mobile/desktopBin.ts
|
|
87
|
+
import { chmod, mkdir, rename, rm, writeFile } from "fs/promises";
|
|
88
|
+
import path from "path";
|
|
89
|
+
class DesktopBin {
|
|
90
|
+
app;
|
|
91
|
+
config;
|
|
92
|
+
constructor(app, config) {
|
|
93
|
+
this.app = app;
|
|
94
|
+
this.config = config;
|
|
95
|
+
}
|
|
96
|
+
static platform() {
|
|
97
|
+
const platform = `${process.platform}-${process.arch}`;
|
|
98
|
+
return binPlatforms.includes(platform) ? platform : null;
|
|
99
|
+
}
|
|
100
|
+
static select(config, libs) {
|
|
101
|
+
const chosen = new Map;
|
|
102
|
+
for (const [name, platforms] of Object.entries(config.bin))
|
|
103
|
+
chosen.set(name, { owner: `apps/${config.app.name}/akan.config.ts`, platforms });
|
|
104
|
+
for (const { lib, bin } of config.libBins) {
|
|
105
|
+
if (!libs.includes(lib))
|
|
106
|
+
continue;
|
|
107
|
+
const owner = `libs/${lib}/akan.config.ts`;
|
|
108
|
+
for (const [name, platforms] of Object.entries(bin)) {
|
|
109
|
+
const held = chosen.get(name);
|
|
110
|
+
if (!held)
|
|
111
|
+
chosen.set(name, { owner, platforms });
|
|
112
|
+
else if (held.owner.startsWith("libs/") && JSON.stringify(held.platforms) !== JSON.stringify(platforms))
|
|
113
|
+
throw new Error(`${held.owner} and ${owner} declare bin.${name} differently; declare it in apps/${config.app.name}/akan.config.ts to pick one.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return chosen;
|
|
117
|
+
}
|
|
118
|
+
static fileName(name, executable, platform = process.platform) {
|
|
119
|
+
if (platform !== "win32" || /\.(exe|cmd|bat|com)$/i.test(name))
|
|
120
|
+
return name;
|
|
121
|
+
return `${name}${path.extname(executable).toLowerCase() || ".exe"}`;
|
|
122
|
+
}
|
|
123
|
+
static downloadName(url) {
|
|
124
|
+
const extension = /(?:\.tar)?\.[A-Za-z0-9]{1,8}$/.exec(path.posix.basename(new URL(url).pathname))?.[0] ?? "";
|
|
125
|
+
return `download${extension.toLowerCase()}`;
|
|
126
|
+
}
|
|
127
|
+
static extractCommand(archive, dir, platform = process.platform) {
|
|
128
|
+
if (platform === "linux" && /\.zip$/i.test(archive))
|
|
129
|
+
return ["unzip", "-q", "-o", archive, "-d", dir];
|
|
130
|
+
const tar = platform === "win32" ? path.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe") : "tar";
|
|
131
|
+
return [tar, "-xf", archive, "-C", dir];
|
|
132
|
+
}
|
|
133
|
+
static async sha256(file) {
|
|
134
|
+
return new Bun.CryptoHasher("sha256").update(await Bun.file(file).arrayBuffer()).digest("hex");
|
|
135
|
+
}
|
|
136
|
+
get #cacheDir() {
|
|
137
|
+
return path.join(this.app.cwdPath, ".akan", "cache", "bin");
|
|
138
|
+
}
|
|
139
|
+
async stage(dir) {
|
|
140
|
+
const scanInfo = this.app.getScanInfo({ allowEmpty: true }) ?? await this.app.scan({ write: false });
|
|
141
|
+
const chosen = DesktopBin.select(this.config, scanInfo.getLibs());
|
|
142
|
+
if (!chosen.size)
|
|
143
|
+
return [];
|
|
144
|
+
const platform = DesktopBin.platform();
|
|
145
|
+
if (!platform)
|
|
146
|
+
throw new Error(`bin: this computer is ${process.platform}-${process.arch}, and a bin names only ${binPlatforms.join(", ")}.`);
|
|
147
|
+
await rm(dir, { recursive: true, force: true });
|
|
148
|
+
await mkdir(dir, { recursive: true });
|
|
149
|
+
for (const [name, { owner, platforms }] of chosen) {
|
|
150
|
+
const source = platforms[platform];
|
|
151
|
+
if (!source)
|
|
152
|
+
throw new Error(`${owner}: bin.${name} names no ${platform} file, the platform this desktop app is built for (it names ${Object.keys(platforms).join(", ") || "none"}).`);
|
|
153
|
+
const executable = await this.#executable(`${owner}: bin.${name}.${platform}`, source);
|
|
154
|
+
const target = path.join(dir, DesktopBin.fileName(name, executable));
|
|
155
|
+
await Bun.write(target, Bun.file(executable));
|
|
156
|
+
await chmod(target, 493);
|
|
157
|
+
this.app.logger.info(`The desktop app carries ${name} from ${"url" in source ? source.url : source.path}`);
|
|
158
|
+
}
|
|
159
|
+
return [...chosen.keys()];
|
|
160
|
+
}
|
|
161
|
+
async#executable(at, source) {
|
|
162
|
+
const file = "url" in source ? await this.#download(at, source) : source.path;
|
|
163
|
+
if (!await Bun.file(file).exists())
|
|
164
|
+
throw new Error(`${at}: ${file} does not exist`);
|
|
165
|
+
if (!source.file)
|
|
166
|
+
return file;
|
|
167
|
+
const key = "url" in source ? source.sha256 : await DesktopBin.sha256(file);
|
|
168
|
+
const executable = path.join(await this.#extract(file, key), ...source.file.split("/"));
|
|
169
|
+
if (!await Bun.file(executable).exists())
|
|
170
|
+
throw new Error(`${at}: the archive holds no ${source.file}`);
|
|
171
|
+
return executable;
|
|
172
|
+
}
|
|
173
|
+
async#download(at, { url, sha256 }) {
|
|
174
|
+
const dir = path.join(this.#cacheDir, sha256);
|
|
175
|
+
const file = path.join(dir, DesktopBin.downloadName(url));
|
|
176
|
+
if (await Bun.file(file).exists())
|
|
177
|
+
return file;
|
|
178
|
+
await mkdir(dir, { recursive: true });
|
|
179
|
+
this.app.logger.info(`Downloading ${url}`);
|
|
180
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(600000) });
|
|
181
|
+
if (!res.ok)
|
|
182
|
+
throw new Error(`${at}: ${url} answered ${res.status}`);
|
|
183
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
184
|
+
const digest = new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
|
|
185
|
+
if (digest !== sha256)
|
|
186
|
+
throw new Error(`${at}: ${url} hashes to ${digest}, not the declared ${sha256}`);
|
|
187
|
+
await Bun.write(`${file}.partial`, bytes);
|
|
188
|
+
await rename(`${file}.partial`, file);
|
|
189
|
+
return file;
|
|
190
|
+
}
|
|
191
|
+
async#extract(archive, key) {
|
|
192
|
+
const dir = path.join(this.#cacheDir, key, "extracted");
|
|
193
|
+
if (await Bun.file(`${dir}.done`).exists())
|
|
194
|
+
return dir;
|
|
195
|
+
await rm(dir, { recursive: true, force: true });
|
|
196
|
+
await mkdir(dir, { recursive: true });
|
|
197
|
+
const [command = "tar", ...args] = DesktopBin.extractCommand(archive, dir);
|
|
198
|
+
if (command === "unzip" && !Bun.which("unzip"))
|
|
199
|
+
throw new Error(`${archive} is a zip, and this computer has no unzip: install it, or use a .tar.gz or .tar.xz.`);
|
|
200
|
+
await this.app.spawn(command, args, { cwd: dir });
|
|
201
|
+
await writeFile(`${dir}.done`, "");
|
|
202
|
+
return dir;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// pkgs/@akanjs/devkit/mobile/nativeApi.ts
|
|
207
|
+
import path2 from "path";
|
|
208
|
+
|
|
209
|
+
class NativeApi {
|
|
210
|
+
static supportedMajor = 0;
|
|
211
|
+
static entryOf(appDir) {
|
|
212
|
+
const workspaceEntry = NativeApi.#resolve("@akanjs/native/api", appDir);
|
|
213
|
+
if (workspaceEntry)
|
|
214
|
+
return workspaceEntry;
|
|
215
|
+
const akanjsPackage = NativeApi.#resolve("akanjs/package.json", appDir);
|
|
216
|
+
if (!akanjsPackage)
|
|
217
|
+
throw new Error(`akanjs is not installed for ${appDir}; the native build ships inside it.`);
|
|
218
|
+
return path2.join(path2.dirname(akanjsPackage), "vendor", "@akanjs", "native", "packages", "cli", "src", "api.ts");
|
|
219
|
+
}
|
|
220
|
+
static async load(appDir) {
|
|
221
|
+
const entry = NativeApi.entryOf(appDir);
|
|
222
|
+
if (!await Bun.file(entry).exists())
|
|
223
|
+
throw new Error(`The installed akanjs has no native build at ${entry}; update akanjs.`);
|
|
224
|
+
const api = await import(entry);
|
|
225
|
+
const major = Number(api.API_VERSION.split(".")[0]);
|
|
226
|
+
if (major !== NativeApi.supportedMajor)
|
|
227
|
+
throw new Error(`The native build API is ${api.API_VERSION}, and this akan CLI speaks ${NativeApi.supportedMajor}.x; install matching versions of akanjs and @akanjs/cli.`);
|
|
228
|
+
return api;
|
|
229
|
+
}
|
|
230
|
+
static #resolve(specifier, from) {
|
|
231
|
+
try {
|
|
232
|
+
return Bun.resolveSync(specifier, from);
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// pkgs/@akanjs/devkit/mobile/nativeAppLine.ts
|
|
240
|
+
class NativeAppLine {
|
|
241
|
+
static #page = /^\[page(\+)?(?:#(\d+))? (trace|verbose|debug|log|info|warn|error)\] ?(.*)$/;
|
|
242
|
+
static #logcat = /^([VDIWEFA])\/([^(]+?)\s*\(\s*\d+\):\s?(.*)$/;
|
|
243
|
+
static #named = /^\[([^\]\s]+)\] ([\s\S]*)$/;
|
|
244
|
+
static #server = /^\[server\] /;
|
|
245
|
+
static #serverLevel = /\b(TRACE|VERBOSE|DEBUG|INFO|WARN|ERROR)\b/;
|
|
246
|
+
static #logcatLevels = {
|
|
247
|
+
V: "verbose",
|
|
248
|
+
D: "debug",
|
|
249
|
+
I: "info",
|
|
250
|
+
W: "warn",
|
|
251
|
+
E: "error",
|
|
252
|
+
F: "error",
|
|
253
|
+
A: "error"
|
|
254
|
+
};
|
|
255
|
+
static read(line, { verbose = false } = {}) {
|
|
256
|
+
const logcat = NativeAppLine.#logcat.exec(line);
|
|
257
|
+
if (logcat) {
|
|
258
|
+
const [, priority = "I", tag = "", text = ""] = logcat;
|
|
259
|
+
const level = NativeAppLine.#logcatLevels[priority] ?? "info";
|
|
260
|
+
if (tag === "AkanNativeConsole")
|
|
261
|
+
return NativeAppLine.#pageLine(level, text, { continuation: false });
|
|
262
|
+
if (tag === "AkanNative")
|
|
263
|
+
return NativeAppLine.#hostLine(`[akan-native] ${text}`, level);
|
|
264
|
+
return { level, message: `[${tag}] ${text}` };
|
|
265
|
+
}
|
|
266
|
+
const page = NativeAppLine.#page.exec(line);
|
|
267
|
+
if (page) {
|
|
268
|
+
const [, continuation, window, level = "info", text = ""] = page;
|
|
269
|
+
return NativeAppLine.#pageLine(level === "log" ? "info" : level, text, {
|
|
270
|
+
continuation: continuation === "+",
|
|
271
|
+
window
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (line.startsWith("[akan-native"))
|
|
275
|
+
return NativeAppLine.#hostLine(line, "info");
|
|
276
|
+
if (NativeAppLine.#server.test(line)) {
|
|
277
|
+
const word = NativeAppLine.#serverLevel.exec(Bun.stripANSI(line))?.[1];
|
|
278
|
+
return { level: word ? word.toLowerCase() : "info", message: line };
|
|
279
|
+
}
|
|
280
|
+
return { level: verbose ? "info" : "debug", message: line };
|
|
281
|
+
}
|
|
282
|
+
static #pageLine(level, text, { continuation, window }) {
|
|
283
|
+
if (continuation)
|
|
284
|
+
return { level, message: ` ${text}` };
|
|
285
|
+
const named = NativeAppLine.#named.exec(text);
|
|
286
|
+
const scope = `page${window ? `#${window}` : ""}${named ? `:${named[1]}` : ""}`;
|
|
287
|
+
return { level, message: `[${scope}] ${named ? named[2] : text}` };
|
|
288
|
+
}
|
|
289
|
+
static #hostLine(line, level) {
|
|
290
|
+
return { level: /\bpages from\b/.test(line) ? "debug" : level, message: line };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// pkgs/@akanjs/devkit/mobile/nativeConfig.ts
|
|
295
|
+
import { readFileSync } from "fs";
|
|
296
|
+
import path3 from "path";
|
|
297
|
+
|
|
298
|
+
// pkgs/@akanjs/devkit/mobile/usageDescriptions.ts
|
|
299
|
+
import { capitalize } from "akanjs/common";
|
|
300
|
+
var iosUsageDescriptionKeys = {
|
|
301
|
+
cameraUsageDescription: ["NSCameraUsageDescription"],
|
|
302
|
+
photoAddUsageDescription: ["NSPhotoLibraryAddUsageDescription"],
|
|
303
|
+
photoUsageDescription: ["NSPhotoLibraryUsageDescription"],
|
|
304
|
+
contactsUsageDescription: ["NSContactsUsageDescription"],
|
|
305
|
+
locationAlwaysUsageDescription: ["NSLocationAlwaysAndWhenInUseUsageDescription", "NSLocationAlwaysUsageDescription"],
|
|
306
|
+
locationWhenInUseUsageDescription: ["NSLocationWhenInUseUsageDescription"],
|
|
307
|
+
microphoneUsageDescription: ["NSMicrophoneUsageDescription"],
|
|
308
|
+
speechRecognitionUsageDescription: ["NSSpeechRecognitionUsageDescription"]
|
|
309
|
+
};
|
|
310
|
+
var toIosInfoPlistUsageDescriptions = (descriptions) => Object.fromEntries(Object.entries(descriptions).flatMap(([key, value]) => {
|
|
311
|
+
const plistKeys = Object.hasOwn(iosUsageDescriptionKeys, key) ? iosUsageDescriptionKeys[key] : [`NS${capitalize(key)}`];
|
|
312
|
+
return plistKeys.map((plistKey) => [plistKey, value]);
|
|
313
|
+
}));
|
|
314
|
+
|
|
315
|
+
// pkgs/@akanjs/devkit/mobile/nativeConfig.ts
|
|
316
|
+
class NativeConfig {
|
|
317
|
+
static basePlugins = [
|
|
318
|
+
"app",
|
|
319
|
+
"app-state",
|
|
320
|
+
"browser",
|
|
321
|
+
"opener",
|
|
322
|
+
"auth-session",
|
|
323
|
+
"device",
|
|
324
|
+
"dialog",
|
|
325
|
+
"keyboard",
|
|
326
|
+
"preferences",
|
|
327
|
+
"secure-storage",
|
|
328
|
+
"haptics"
|
|
329
|
+
];
|
|
330
|
+
static builtinContributions = {
|
|
331
|
+
camera: {
|
|
332
|
+
permission: "camera",
|
|
333
|
+
plugins: ["camera"],
|
|
334
|
+
usageDescriptions: {
|
|
335
|
+
cameraUsageDescription: "$(PRODUCT_NAME) requires access to the camera to take photos.",
|
|
336
|
+
photoAddUsageDescription: "$(PRODUCT_NAME) requires access to the photo library to take photos.",
|
|
337
|
+
photoUsageDescription: "$(PRODUCT_NAME) requires access to the photo library to take photos."
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
contacts: {
|
|
341
|
+
permission: "contacts",
|
|
342
|
+
plugins: ["contacts"],
|
|
343
|
+
usageDescriptions: {
|
|
344
|
+
contactsUsageDescription: "$(PRODUCT_NAME) requires access to the contacts to find people you know."
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
location: {
|
|
348
|
+
permission: "location",
|
|
349
|
+
plugins: ["geolocation"],
|
|
350
|
+
usageDescriptions: {
|
|
351
|
+
locationAlwaysUsageDescription: "$(PRODUCT_NAME) requires access to the location to get the user's location.",
|
|
352
|
+
locationWhenInUseUsageDescription: "$(PRODUCT_NAME) requires access to the location to get the user's location."
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
push: { permission: "push", plugins: ["push"] },
|
|
356
|
+
speech: { permission: "speech" }
|
|
357
|
+
};
|
|
358
|
+
static build({
|
|
359
|
+
appPath,
|
|
360
|
+
target,
|
|
361
|
+
webDir,
|
|
362
|
+
contributions,
|
|
363
|
+
locales,
|
|
364
|
+
env,
|
|
365
|
+
platform,
|
|
366
|
+
desktopServer,
|
|
367
|
+
nativePlugins = [],
|
|
368
|
+
desktopBin
|
|
369
|
+
}) {
|
|
370
|
+
const warnings = [];
|
|
371
|
+
const applied = (target.permissions ?? []).flatMap((permission) => {
|
|
372
|
+
const claimed = contributions.filter((contribution) => contribution.permission === permission);
|
|
373
|
+
const used = claimed.length ? claimed : [NativeConfig.builtinContributions[permission]];
|
|
374
|
+
if (!used.some((contribution) => contribution.plugins?.length))
|
|
375
|
+
warnings.push(`Permission '${permission}' has no native plugin yet; the app ships without it.`);
|
|
376
|
+
return used;
|
|
377
|
+
});
|
|
378
|
+
const abs = (relative) => path3.resolve(appPath, relative);
|
|
379
|
+
const updates = NativeConfig.#updates(target, env);
|
|
380
|
+
const folders = new Set(nativePlugins.map((plugin) => path3.resolve(plugin.dir)));
|
|
381
|
+
const folderIds = new Set(nativePlugins.map((plugin) => plugin.id));
|
|
382
|
+
const listed = (target.native?.plugins ?? []).filter((spec) => NativeConfig.#isId(spec) ? !folderIds.has(spec) : !folders.has(path3.resolve(appPath, spec)));
|
|
383
|
+
const plugins = [
|
|
384
|
+
...new Set([
|
|
385
|
+
...NativeConfig.basePlugins,
|
|
386
|
+
...target.deepLinks?.schemes?.length ? ["single-instance"] : [],
|
|
387
|
+
...applied.flatMap((contribution) => contribution.plugins ?? []),
|
|
388
|
+
...desktopServer ? ["single-instance"] : [],
|
|
389
|
+
...updates ? ["updates"] : [],
|
|
390
|
+
...listed,
|
|
391
|
+
...nativePlugins.map((plugin) => plugin.dir)
|
|
392
|
+
])
|
|
393
|
+
];
|
|
394
|
+
const pluginIds = plugins.map((spec) => nativePlugins.find((plugin) => plugin.dir === spec)?.id ?? NativeConfig.#pluginId(appPath, spec));
|
|
395
|
+
const usageDescriptions = Object.fromEntries(Object.entries(toIosInfoPlistUsageDescriptions(Object.assign({}, ...applied.map((contribution) => contribution.usageDescriptions ?? {})))).map(([key, text]) => [key, text.replaceAll("$(PRODUCT_NAME)", target.appName)]));
|
|
396
|
+
const ios = NativeConfig.#compact({
|
|
397
|
+
infoPlist: NativeConfig.#merged(applied.map((c) => c.infoPlist), target.native?.ios?.infoPlist),
|
|
398
|
+
entitlements: NativeConfig.#merged(applied.map((c) => c.entitlements), target.native?.ios?.entitlements)
|
|
399
|
+
});
|
|
400
|
+
const manifest = [
|
|
401
|
+
...[...new Set(applied.flatMap((c) => c.androidPermissions ?? []))].map((name) => `<uses-permission android:name="android.permission.${name}" />`),
|
|
402
|
+
...[...new Set(applied.flatMap((c) => c.androidFeatures ?? []))].map((name) => `<uses-feature android:name="${name}" android:required="false" />`),
|
|
403
|
+
...target.native?.android?.manifest ?? []
|
|
404
|
+
];
|
|
405
|
+
const android = NativeConfig.#compact({
|
|
406
|
+
manifest,
|
|
407
|
+
application: target.native?.android?.application ?? [],
|
|
408
|
+
activity: target.native?.android?.activity ?? []
|
|
409
|
+
});
|
|
410
|
+
const resources = Object.entries(target.files ?? {}).map(([to, from]) => ({ from: abs(from), to }));
|
|
411
|
+
const native = NativeConfig.#compact({ ios, android, resources });
|
|
412
|
+
const googleServices = target.native?.android?.googleServices;
|
|
413
|
+
const pushAndroid = target.native?.push?.android;
|
|
414
|
+
const desktop = NativeConfig.#compact({
|
|
415
|
+
recovery: target.native?.desktop?.recovery,
|
|
416
|
+
screenCapture: target.native?.desktop?.screenCapture,
|
|
417
|
+
window: NativeConfig.#compact({ ...target.native?.desktop?.window }),
|
|
418
|
+
server: desktopServer,
|
|
419
|
+
bin: desktopBin
|
|
420
|
+
});
|
|
421
|
+
const config = {
|
|
422
|
+
app: {
|
|
423
|
+
id: resolveAppId(target.appId, platform),
|
|
424
|
+
name: target.appName,
|
|
425
|
+
fileName: target.fileName ?? NativeConfig.fileNameOf(path3.basename(appPath)),
|
|
426
|
+
version: target.version,
|
|
427
|
+
build: target.buildNum
|
|
428
|
+
},
|
|
429
|
+
web: { dir: webDir },
|
|
430
|
+
plugins,
|
|
431
|
+
capabilities: [
|
|
432
|
+
{
|
|
433
|
+
identifier: "app",
|
|
434
|
+
description: `The plugins ${target.appName} ships, each with its default permissions`,
|
|
435
|
+
permissions: pluginIds.map((id) => `${id}:default`)
|
|
436
|
+
}
|
|
437
|
+
],
|
|
438
|
+
...Object.keys(usageDescriptions).length ? { usageDescriptions } : {},
|
|
439
|
+
...target.deepLinks?.schemes?.length || target.deepLinks?.domains?.length ? { deepLinks: NativeConfig.#deepLinks(target, locales) } : {},
|
|
440
|
+
...native ? { native } : {},
|
|
441
|
+
...updates ? { updates } : {},
|
|
442
|
+
...pushAndroid ? {
|
|
443
|
+
push: {
|
|
444
|
+
android: { ...pushAndroid, ...pushAndroid.smallIcon ? { smallIcon: abs(pushAndroid.smallIcon) } : {} }
|
|
445
|
+
}
|
|
446
|
+
} : {},
|
|
447
|
+
...target.native?.privacy ? { privacy: target.native.privacy } : {},
|
|
448
|
+
android: {
|
|
449
|
+
debugAppIdSuffix: ".debug",
|
|
450
|
+
...googleServices ? { googleServices: abs(googleServices) } : {},
|
|
451
|
+
...target.native?.android?.autoplay ? { autoplay: true } : {}
|
|
452
|
+
},
|
|
453
|
+
keyboard: { resize: "none" },
|
|
454
|
+
...desktop ? { desktop } : {},
|
|
455
|
+
...target.assets?.icon ? { icon: NativeConfig.#icon(target.assets.icon, abs) } : {},
|
|
456
|
+
...target.assets?.splash ? { splash: NativeConfig.#splash(target.assets.splash, abs) } : {}
|
|
457
|
+
};
|
|
458
|
+
return { config, warnings };
|
|
459
|
+
}
|
|
460
|
+
static #icon(icon, abs) {
|
|
461
|
+
return typeof icon === "string" ? abs(icon) : { ...icon, image: abs(icon.image) };
|
|
462
|
+
}
|
|
463
|
+
static #splash(splash, abs) {
|
|
464
|
+
if (typeof splash === "string")
|
|
465
|
+
return { image: abs(splash) };
|
|
466
|
+
return { ...splash, ...splash.image ? { image: abs(splash.image) } : {} };
|
|
467
|
+
}
|
|
468
|
+
static #updates(target, env) {
|
|
469
|
+
const { url, publicKey, channel = env, ...rest } = target.updates ?? {};
|
|
470
|
+
return url && publicKey ? { ...rest, url, publicKey, ...channel ? { channel } : {} } : undefined;
|
|
471
|
+
}
|
|
472
|
+
static fileNameOf(name) {
|
|
473
|
+
return name.replace(/[^A-Za-z0-9._-]/g, "-").replace(/^[-.]+|[-.]+$/g, "") || "app";
|
|
474
|
+
}
|
|
475
|
+
static #pluginId(appPath, spec) {
|
|
476
|
+
if (NativeConfig.#isId(spec))
|
|
477
|
+
return spec;
|
|
478
|
+
try {
|
|
479
|
+
const { id } = JSON.parse(readFileSync(path3.join(path3.resolve(appPath, spec), "native-plugin.json"), "utf8"));
|
|
480
|
+
return typeof id === "string" ? id : spec;
|
|
481
|
+
} catch {
|
|
482
|
+
return spec;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
static #isId(spec) {
|
|
486
|
+
return !spec.startsWith(".") && !path3.isAbsolute(spec);
|
|
487
|
+
}
|
|
488
|
+
static #compact(value) {
|
|
489
|
+
const kept = Object.entries(value).filter(([, entry]) => Array.isArray(entry) ? entry.length > 0 : entry && typeof entry === "object" ? Object.keys(entry).length > 0 : entry !== undefined);
|
|
490
|
+
return kept.length ? Object.fromEntries(kept) : undefined;
|
|
491
|
+
}
|
|
492
|
+
static #merged(contributed, own) {
|
|
493
|
+
return Object.assign({}, ...contributed, own);
|
|
494
|
+
}
|
|
495
|
+
static #deepLinks(target, locales) {
|
|
496
|
+
const basePath = target.basePath?.replace(/^\/+|\/+$/g, "");
|
|
497
|
+
const pathPrefixes = basePath ? locales.map((locale) => `/${locale}/${basePath}`) : undefined;
|
|
498
|
+
return {
|
|
499
|
+
...target.deepLinks?.schemes?.length ? { schemes: target.deepLinks.schemes } : {},
|
|
500
|
+
...target.deepLinks?.domains?.length ? { domains: target.deepLinks.domains.map((host) => pathPrefixes ? { host, pathPrefixes } : host) } : {}
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// pkgs/@akanjs/devkit/mobile/nativePluginFolders.ts
|
|
506
|
+
import { readdir } from "fs/promises";
|
|
507
|
+
import path4 from "path";
|
|
508
|
+
|
|
509
|
+
class NativePluginFolders {
|
|
510
|
+
static folder = "native";
|
|
511
|
+
static async in(root, owner) {
|
|
512
|
+
const base = path4.join(root, NativePluginFolders.folder);
|
|
513
|
+
const entries = await readdir(base, { withFileTypes: true }).catch(() => []);
|
|
514
|
+
const folders = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
515
|
+
return await Promise.all(folders.sort().map(async (name) => {
|
|
516
|
+
const where = `${owner}/${NativePluginFolders.folder}/${name}`;
|
|
517
|
+
const manifest = Bun.file(path4.join(base, name, "native-plugin.json"));
|
|
518
|
+
if (!await manifest.exists())
|
|
519
|
+
throw new Error(`${where} has no native-plugin.json; a folder in native/ is one native plugin.`);
|
|
520
|
+
const { id } = await manifest.json();
|
|
521
|
+
if (id !== name)
|
|
522
|
+
throw new Error(`${where}/native-plugin.json has id ${JSON.stringify(id)}; name the folder after its id.`);
|
|
523
|
+
return { id, dir: path4.join(base, name), owner };
|
|
524
|
+
}));
|
|
525
|
+
}
|
|
526
|
+
static select(own, fromLibs) {
|
|
527
|
+
const chosen = new Map(own.map((plugin) => [plugin.id, plugin]));
|
|
528
|
+
for (const plugin of fromLibs) {
|
|
529
|
+
const held = chosen.get(plugin.id);
|
|
530
|
+
if (!held)
|
|
531
|
+
chosen.set(plugin.id, plugin);
|
|
532
|
+
else if (held.owner.startsWith("libs/"))
|
|
533
|
+
throw new Error(`${held.owner} and ${plugin.owner} both hold native plugin "${plugin.id}"; give the app its own native/${plugin.id} to pick one.`);
|
|
534
|
+
}
|
|
535
|
+
return [...chosen.values()];
|
|
536
|
+
}
|
|
537
|
+
static async of(app) {
|
|
538
|
+
const scanInfo = app.getScanInfo({ allowEmpty: true }) ?? await app.scan({ write: false });
|
|
539
|
+
const root = app.workspace.workspaceRoot;
|
|
540
|
+
const [own, ...fromLibs] = await Promise.all([
|
|
541
|
+
NativePluginFolders.in(app.cwdPath, `apps/${app.name}`),
|
|
542
|
+
...scanInfo.getLibs().map((lib) => NativePluginFolders.in(path4.join(root, "libs", lib), `libs/${lib}`))
|
|
543
|
+
]);
|
|
544
|
+
return NativePluginFolders.select(own ?? [], fromLibs.flat());
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// pkgs/@akanjs/devkit/mobile/nativeWebDir.ts
|
|
549
|
+
import { cp, mkdir as mkdir2, rm as rm2, stat } from "fs/promises";
|
|
550
|
+
import path5 from "path";
|
|
551
|
+
|
|
552
|
+
class NativeWebDir {
|
|
553
|
+
dir;
|
|
554
|
+
static reservedEntry = "__akan_native";
|
|
555
|
+
constructor(dir) {
|
|
556
|
+
this.dir = dir;
|
|
557
|
+
}
|
|
558
|
+
static injectTarget(html, target) {
|
|
559
|
+
if (html.includes(NativeWebDir.#targetTag))
|
|
560
|
+
return html;
|
|
561
|
+
const basePath = target.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
|
|
562
|
+
const script = `${NativeWebDir.#targetTag}${JSON.stringify({
|
|
563
|
+
name: target.name,
|
|
564
|
+
basePath,
|
|
565
|
+
indexPath: target.indexPath
|
|
566
|
+
})};</script>`;
|
|
567
|
+
const head = /<head(?:\s[^>]*)?>/i.exec(html);
|
|
568
|
+
if (!head)
|
|
569
|
+
return `${script}
|
|
570
|
+
${html}`;
|
|
571
|
+
const at = head.index + head[0].length;
|
|
572
|
+
return `${html.slice(0, at)}
|
|
573
|
+
${script}${html.slice(at)}`;
|
|
574
|
+
}
|
|
575
|
+
static #targetTag = "<script>window.__AKAN_MOBILE_TARGET__=";
|
|
576
|
+
async assemble(target, { html, publicDir, fontsDir }) {
|
|
577
|
+
if (!await Bun.file(html).exists())
|
|
578
|
+
throw new Error(`CSR html for mobile target '${target.name}' not found: ${html}`);
|
|
579
|
+
await rm2(this.dir, { recursive: true, force: true });
|
|
580
|
+
await mkdir2(this.dir, { recursive: true });
|
|
581
|
+
if (publicDir && await NativeWebDir.#isDir(publicDir))
|
|
582
|
+
await cp(publicDir, this.dir, { recursive: true });
|
|
583
|
+
if (fontsDir && await NativeWebDir.#isDir(fontsDir))
|
|
584
|
+
await cp(fontsDir, path5.join(this.dir, "_akan", "fonts"), { recursive: true });
|
|
585
|
+
if (await NativeWebDir.#isDir(path5.join(this.dir, NativeWebDir.reservedEntry)))
|
|
586
|
+
throw new Error(`public/${NativeWebDir.reservedEntry} is reserved for the native runtime.`);
|
|
587
|
+
await Bun.write(path5.join(this.dir, "index.html"), NativeWebDir.injectTarget(await Bun.file(html).text(), target));
|
|
588
|
+
const files = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: this.dir, onlyFiles: true, dot: true }));
|
|
589
|
+
return files.map((file) => file.split(path5.sep).join("/")).sort();
|
|
590
|
+
}
|
|
591
|
+
static async#isDir(dir) {
|
|
592
|
+
try {
|
|
593
|
+
return (await stat(dir)).isDirectory();
|
|
594
|
+
} catch {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// pkgs/@akanjs/devkit/mobile/nativeApp.ts
|
|
601
|
+
class NativeApp {
|
|
602
|
+
app;
|
|
603
|
+
target;
|
|
604
|
+
env;
|
|
605
|
+
targetRoot;
|
|
606
|
+
web;
|
|
607
|
+
#boot = null;
|
|
608
|
+
constructor(app, target, env) {
|
|
609
|
+
this.app = app;
|
|
610
|
+
this.target = target;
|
|
611
|
+
this.env = env;
|
|
612
|
+
this.targetRoot = path6.join(app.cwdPath, ".akan", "mobile", target.name);
|
|
613
|
+
this.web = new NativeWebDir(path6.join(this.targetRoot, "web"));
|
|
614
|
+
}
|
|
615
|
+
outDir(platform) {
|
|
616
|
+
return path6.join(this.targetRoot, "native", platform);
|
|
617
|
+
}
|
|
618
|
+
async assembleWeb() {
|
|
619
|
+
const dist = this.app.dist.cwdPath;
|
|
620
|
+
return await this.web.assemble(this.target.config, {
|
|
621
|
+
html: path6.join(dist, "csr", targetHtmlFilename(this.target.config)),
|
|
622
|
+
publicDir: path6.join(dist, "public"),
|
|
623
|
+
fontsDir: path6.join(dist, ".akan", "artifact", "fonts")
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
get binDir() {
|
|
627
|
+
return path6.join(this.targetRoot, "bin");
|
|
628
|
+
}
|
|
629
|
+
async config({
|
|
630
|
+
server,
|
|
631
|
+
platform,
|
|
632
|
+
stageBin = true
|
|
633
|
+
}) {
|
|
634
|
+
const [appConfig, plugins, nativePlugins] = await Promise.all([
|
|
635
|
+
this.app.getConfig(),
|
|
636
|
+
this.app.collectPlugins(),
|
|
637
|
+
NativePluginFolders.of(this.app)
|
|
638
|
+
]);
|
|
639
|
+
const desktop = platform === "macos" || platform === "windows" || platform === "linux";
|
|
640
|
+
const carried = desktop && stageBin ? await new DesktopBin(this.app, appConfig).stage(this.binDir) : [];
|
|
641
|
+
return NativeConfig.build({
|
|
642
|
+
appPath: this.app.cwdPath,
|
|
643
|
+
target: this.target.config,
|
|
644
|
+
webDir: this.web.dir,
|
|
645
|
+
contributions: plugins.flatMap((plugin) => plugin.native ? [plugin.native] : []),
|
|
646
|
+
locales: appConfig.i18n.locales,
|
|
647
|
+
env: this.env,
|
|
648
|
+
platform,
|
|
649
|
+
nativePlugins,
|
|
650
|
+
...server ? { desktopServer: server } : {},
|
|
651
|
+
...carried.length ? { desktopBin: this.binDir } : {}
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
async prepare(platform, server) {
|
|
655
|
+
const [api, { config, warnings }] = await Promise.all([
|
|
656
|
+
NativeApi.load(this.app.cwdPath),
|
|
657
|
+
this.config({ platform, ...server ? { server } : {} })
|
|
658
|
+
]);
|
|
659
|
+
for (const warning of warnings)
|
|
660
|
+
this.app.logger.warn(warning);
|
|
661
|
+
const problems = api.validateConfig(config, { appDir: this.app.cwdPath });
|
|
662
|
+
if (problems.length)
|
|
663
|
+
throw new Error(`Mobile target '${this.target.name}' makes an invalid native config:
|
|
664
|
+
- ${problems.join(`
|
|
665
|
+
- `)}`);
|
|
666
|
+
return { api, config };
|
|
667
|
+
}
|
|
668
|
+
#appLine = (line) => {
|
|
669
|
+
const read = NativeAppLine.read(line, { verbose: Executor.verbose });
|
|
670
|
+
if (this.#boot)
|
|
671
|
+
this.#boot.lines.push(read);
|
|
672
|
+
else
|
|
673
|
+
this.app.logger[read.level](read.message);
|
|
674
|
+
};
|
|
675
|
+
#task(platform, config) {
|
|
676
|
+
return {
|
|
677
|
+
appDir: this.app.cwdPath,
|
|
678
|
+
config,
|
|
679
|
+
platform,
|
|
680
|
+
outDir: this.outDir(platform),
|
|
681
|
+
envFiles: false,
|
|
682
|
+
skipWebBuild: true,
|
|
683
|
+
log: (event) => {
|
|
684
|
+
if (event.level === "error")
|
|
685
|
+
this.app.logger.error(event.message);
|
|
686
|
+
else if (event.level === "warn")
|
|
687
|
+
this.app.logger.warn(event.message);
|
|
688
|
+
else if (this.#boot)
|
|
689
|
+
this.#boot.steps.push(event.message);
|
|
690
|
+
else if (event.level === "tool")
|
|
691
|
+
this.app.logger.verbose(event.message);
|
|
692
|
+
else
|
|
693
|
+
this.app.logger.info(event.message);
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
async build(platform, { profile = "release", server, installer = false } = {}) {
|
|
698
|
+
NativeApp.#assertServerPlatform(platform, server);
|
|
699
|
+
if (installer && platform !== "windows")
|
|
700
|
+
throw new Error(`An installer is built for Windows, not for ${platform}.`);
|
|
701
|
+
await this.assembleWeb();
|
|
702
|
+
const { api, config } = await this.prepare(platform, server);
|
|
703
|
+
return await api.build({
|
|
704
|
+
...this.#task(platform, config),
|
|
705
|
+
profile,
|
|
706
|
+
...installer ? { windows: { installer } } : {}
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
async run(platform, { device, teamId, profile = "debug", server } = {}) {
|
|
710
|
+
NativeApp.#assertServerPlatform(platform, server);
|
|
711
|
+
await this.assembleWeb();
|
|
712
|
+
const { api, config } = await this.prepare(platform, server);
|
|
713
|
+
return await api.run({
|
|
714
|
+
...this.#task(platform, config),
|
|
715
|
+
profile,
|
|
716
|
+
onLine: this.#appLine,
|
|
717
|
+
...device ? { device } : {},
|
|
718
|
+
...teamId ? { ios: { signing: { teamId } } } : {}
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
async dev(platform, { upstream, lang, device, teamId }) {
|
|
722
|
+
const startedAt = performance.now();
|
|
723
|
+
const boot = { steps: [], lines: [] };
|
|
724
|
+
this.#boot = boot;
|
|
725
|
+
try {
|
|
726
|
+
await mkdir3(this.web.dir, { recursive: true });
|
|
727
|
+
const [{ api, config }, { api: routes }] = await Promise.all([this.prepare(platform), this.app.getConfig()]);
|
|
728
|
+
const session = await api.dev({
|
|
729
|
+
...this.#task(platform, config),
|
|
730
|
+
upstream,
|
|
731
|
+
hmrPath: "/_akan/hmr",
|
|
732
|
+
wsPaths: [`${routes.prefix}${routes.websocketPrefix}`],
|
|
733
|
+
reversePorts: [Number(new URL(upstream).port)],
|
|
734
|
+
onLine: this.#appLine,
|
|
735
|
+
startPath: this.startPath(lang),
|
|
736
|
+
...device ? { device } : {},
|
|
737
|
+
...teamId ? { ios: { signing: { teamId } } } : {}
|
|
738
|
+
});
|
|
739
|
+
this.#boot = null;
|
|
740
|
+
this.app.logger.info(this.readyLine(platform, { upstream, device: session.device, startedAt }));
|
|
741
|
+
for (const step of [...boot.steps, `pages through ${session.gateway}`])
|
|
742
|
+
this.app.logger.debug(step);
|
|
743
|
+
for (const line of boot.lines)
|
|
744
|
+
this.app.logger[line.level](line.message);
|
|
745
|
+
return session;
|
|
746
|
+
} catch (error) {
|
|
747
|
+
this.#boot = null;
|
|
748
|
+
for (const step of boot.steps)
|
|
749
|
+
this.app.logger.info(step);
|
|
750
|
+
for (const line of boot.lines)
|
|
751
|
+
this.app.logger[line.level === "debug" ? "info" : line.level](line.message);
|
|
752
|
+
throw error;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
readyLine(platform, { upstream, device, startedAt }) {
|
|
756
|
+
const label = platform === "ios" || platform === "android" ? platform : "desktop";
|
|
757
|
+
const target = this.target.name === "default" ? "" : `/${this.target.name}`;
|
|
758
|
+
const seconds = ((performance.now() - startedAt) / 1000).toFixed(1);
|
|
759
|
+
return [`${this.app.name}${target} ${label} ready`, ...device ? [device.name] : [], upstream, `${seconds}s`].join(" \xB7 ");
|
|
760
|
+
}
|
|
761
|
+
async releaseIos({ teamId, adHoc = false } = {}) {
|
|
762
|
+
await this.assembleWeb();
|
|
763
|
+
const { api, config } = await this.prepare("ios");
|
|
764
|
+
return await api.release({
|
|
765
|
+
...this.#task("ios", config),
|
|
766
|
+
platform: "ios",
|
|
767
|
+
signing: { ...teamId ? { teamId } : {}, ...adHoc ? { distribution: "ad-hoc" } : {} }
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
get updatesDir() {
|
|
771
|
+
return path6.join(this.targetRoot, "updates");
|
|
772
|
+
}
|
|
773
|
+
async publishUpdate(platform, { channel, server } = {}) {
|
|
774
|
+
NativeApp.#assertServerPlatform(platform, server);
|
|
775
|
+
if (!this.target.config.updates)
|
|
776
|
+
throw new Error(`Mobile target '${this.target.name}' has no updates: { url, publicKey } in akan.config.ts; \`akan update-keygen ${this.app.name}\` prints the key.`);
|
|
777
|
+
await this.assembleWeb();
|
|
778
|
+
const { api, config } = await this.prepare(platform, server);
|
|
779
|
+
return await api.publishUpdate({
|
|
780
|
+
...this.#task(platform, config),
|
|
781
|
+
platform,
|
|
782
|
+
out: this.updatesDir,
|
|
783
|
+
...channel ? { channel } : {}
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
async updateKeygen(platform) {
|
|
787
|
+
const [api, { config }] = await Promise.all([
|
|
788
|
+
NativeApi.load(this.app.cwdPath),
|
|
789
|
+
this.config({ platform, stageBin: false })
|
|
790
|
+
]);
|
|
791
|
+
return api.updateKeygen({ config });
|
|
792
|
+
}
|
|
793
|
+
async releaseAndroid({ formats = ["aab"] } = {}) {
|
|
794
|
+
const signing = NativeApp.androidSigning();
|
|
795
|
+
await this.assembleWeb();
|
|
796
|
+
const { api, config } = await this.prepare("android");
|
|
797
|
+
return await api.release({ ...this.#task("android", config), platform: "android", signing, formats });
|
|
798
|
+
}
|
|
799
|
+
async packUpdate(platform, { out } = {}) {
|
|
800
|
+
await this.assembleWeb();
|
|
801
|
+
const { api, config } = await this.prepare(platform);
|
|
802
|
+
if (!config.updates)
|
|
803
|
+
throw new Error(`Mobile target '${this.target.name}' takes no updates: add mobile.updates: { url, publicKey } to akan.config.ts first.`);
|
|
804
|
+
const packed = await api.packUpdate({
|
|
805
|
+
...this.#task(platform, config),
|
|
806
|
+
platform,
|
|
807
|
+
out: out ?? path6.join(this.targetRoot, "updates", platform)
|
|
808
|
+
});
|
|
809
|
+
return { ...packed, channel: config.updates.channel ?? "production" };
|
|
810
|
+
}
|
|
811
|
+
static async compareBundles(appDir, shipped, bundle) {
|
|
812
|
+
return (await NativeApi.load(appDir)).compareBundles(shipped, bundle);
|
|
813
|
+
}
|
|
814
|
+
static androidSigning(env = process.env) {
|
|
815
|
+
const keystore = env.MYAPP_RELEASE_STORE_FILE;
|
|
816
|
+
const storePassword = env.MYAPP_RELEASE_STORE_PASSWORD;
|
|
817
|
+
const alias = env.MYAPP_RELEASE_KEY_ALIAS;
|
|
818
|
+
const keyPassword = env.MYAPP_RELEASE_KEY_PASSWORD;
|
|
819
|
+
if (!keystore || !storePassword || !alias) {
|
|
820
|
+
const missing = Object.entries({
|
|
821
|
+
MYAPP_RELEASE_STORE_FILE: keystore,
|
|
822
|
+
MYAPP_RELEASE_STORE_PASSWORD: storePassword,
|
|
823
|
+
MYAPP_RELEASE_KEY_ALIAS: alias
|
|
824
|
+
}).flatMap(([key, value]) => value ? [] : [key]);
|
|
825
|
+
throw new Error(`An Android release is signed with the upload key; set ${missing.join(", ")} in the environment.`);
|
|
826
|
+
}
|
|
827
|
+
return { keystore, storePassword, alias, ...keyPassword ? { keyPassword } : {} };
|
|
828
|
+
}
|
|
829
|
+
startPath(lang) {
|
|
830
|
+
const basePath = this.target.config.basePath?.replace(/^\/+|\/+$/g, "");
|
|
831
|
+
const params = new URLSearchParams({ csr: "true", akanMobileTarget: this.target.name });
|
|
832
|
+
if (basePath)
|
|
833
|
+
params.set("akanMobileBasePath", basePath);
|
|
834
|
+
if (this.target.config.indexPath)
|
|
835
|
+
params.set("akanMobileIndexPath", this.target.config.indexPath);
|
|
836
|
+
const home = [lang, basePath, this.target.config.indexPath].flatMap((part) => (part ?? "").split("/")).filter((segment) => segment.length > 0);
|
|
837
|
+
return `/${home.join("/")}?${params}`;
|
|
838
|
+
}
|
|
839
|
+
static #assertServerPlatform(platform, server) {
|
|
840
|
+
if (server && (platform === "ios" || platform === "android"))
|
|
841
|
+
throw new Error(`Only a desktop app carries its server; ${platform} cannot run one.`);
|
|
842
|
+
}
|
|
843
|
+
static desktopPlatform(host = process.platform) {
|
|
844
|
+
if (host === "darwin")
|
|
845
|
+
return "macos";
|
|
846
|
+
if (host === "win32")
|
|
847
|
+
return "windows";
|
|
848
|
+
if (host === "linux")
|
|
849
|
+
return "linux";
|
|
850
|
+
throw new Error(`A desktop app builds on macOS, Windows or Linux, not on ${host}.`);
|
|
851
|
+
}
|
|
852
|
+
static async devices(appDir, platform) {
|
|
853
|
+
return await (await NativeApi.load(appDir)).devices({ platform });
|
|
854
|
+
}
|
|
855
|
+
static async doctor(appDir, platforms) {
|
|
856
|
+
return await (await NativeApi.load(appDir)).doctor({ platforms });
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// pkgs/@akanjs/devkit/mobile/placeholderAppId.ts
|
|
861
|
+
var PLACEHOLDER_APP_IDS = [
|
|
862
|
+
"com.myapp.app",
|
|
863
|
+
"com.myorg.myapp",
|
|
864
|
+
"com.example.app",
|
|
865
|
+
"com.example.myapp"
|
|
866
|
+
];
|
|
867
|
+
var placeholderAppIdSegment = /^(example|examples|myorg|myapp|mycompany|myorganization|changeme|todo|sample|test)$/;
|
|
868
|
+
var isPlaceholderAppId = (appId) => {
|
|
869
|
+
const normalized = appId?.trim().toLowerCase() ?? "";
|
|
870
|
+
if (!normalized)
|
|
871
|
+
return true;
|
|
872
|
+
if (PLACEHOLDER_APP_IDS.includes(normalized))
|
|
873
|
+
return true;
|
|
874
|
+
return normalized.split(".").some((segment) => placeholderAppIdSegment.test(segment));
|
|
875
|
+
};
|
|
876
|
+
// pkgs/@akanjs/devkit/mobile/desktopServerStage.ts
|
|
877
|
+
import { cp as cp2, mkdir as mkdir4, readdir as readdir2, rm as rm3, writeFile as writeFile2 } from "fs/promises";
|
|
878
|
+
import path7 from "path";
|
|
879
|
+
class DesktopServerStage {
|
|
880
|
+
app;
|
|
881
|
+
static skippedFiles = new Set(["rscWorker.js", "console.js", "console-runtime.js"]);
|
|
882
|
+
static rscRenderer = "react-server-dom-webpack";
|
|
883
|
+
dir;
|
|
884
|
+
constructor(app) {
|
|
885
|
+
this.app = app;
|
|
886
|
+
this.dir = path7.join(app.cwdPath, ".akan", "desktop", "server");
|
|
887
|
+
}
|
|
888
|
+
static assertCarriable(config) {
|
|
889
|
+
if (!config.database.modes.includes("single"))
|
|
890
|
+
throw new Error(`native.desktop.server puts the server in the app, where only database mode single runs (no Redis or Postgres); apps/${config.app.name}/akan.config.ts declares ${config.database.modes.join(", ")}.`);
|
|
891
|
+
}
|
|
892
|
+
static env(config, environment) {
|
|
893
|
+
const { PORT: _port, AKAN_LOG_TO_FILE: _fileLog, ...image } = config.getProductionEnv(environment);
|
|
894
|
+
return {
|
|
895
|
+
...image,
|
|
896
|
+
AKAN_PUBLIC_OPERATION_MODE: "edge",
|
|
897
|
+
AKAN_DATABASE_MODE: "single",
|
|
898
|
+
AKAN_DATABASE_MODES: "single",
|
|
899
|
+
AKAN_SSR: "false",
|
|
900
|
+
AKAN_CSR: "false",
|
|
901
|
+
AKAN_MCP: "false",
|
|
902
|
+
AKAN_SHUTDOWN_TIMEOUT_MS: "1000"
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
static imageStepsNotice(config, carried) {
|
|
906
|
+
const steps = typeof config.docker === "string" ? null : config.docker.preRuns.length + config.docker.postRuns.length;
|
|
907
|
+
if (steps === 0)
|
|
908
|
+
return null;
|
|
909
|
+
const image = steps === null ? `apps/${config.app.name}/akan.config.ts writes its own Dockerfile` : `The image runs ${steps} docker step${steps === 1 ? "" : "s"} from the app and its libs`;
|
|
910
|
+
return carried.length ? {
|
|
911
|
+
level: "info",
|
|
912
|
+
message: `${image}; the desktop app's server runs none of them and carries only bin: ${carried.join(", ")}.`
|
|
913
|
+
} : {
|
|
914
|
+
level: "warn",
|
|
915
|
+
message: `${image}, and a desktop app's server runs none of them: declare in bin the executables its code spawns, or those calls fail on the user's computer.`
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
static packageJson(config, built) {
|
|
919
|
+
const otherDrivers = new Set(config.database.modes.filter((mode) => mode !== "single").flatMap((mode) => config.getDatabaseModeRuntimePackages(mode)));
|
|
920
|
+
const dependencies = Object.entries(built.dependencies ?? {}).filter(([name]) => config.externalLibs.includes(name) || name !== DesktopServerStage.rscRenderer && !otherDrivers.has(name));
|
|
921
|
+
return { ...built, dependencies: Object.fromEntries(dependencies) };
|
|
922
|
+
}
|
|
923
|
+
async prepare(environment) {
|
|
924
|
+
const config = await this.app.getConfig();
|
|
925
|
+
DesktopServerStage.assertCarriable(config);
|
|
926
|
+
const dist = this.app.dist.cwdPath;
|
|
927
|
+
await rm3(this.dir, { recursive: true, force: true });
|
|
928
|
+
await mkdir4(this.dir, { recursive: true });
|
|
929
|
+
const entries = await readdir2(dist, { withFileTypes: true });
|
|
930
|
+
for (const entry of entries) {
|
|
931
|
+
const carried = entry.isFile() && (entry.name.endsWith(".js") && !DesktopServerStage.skippedFiles.has(entry.name) || entry.name === "akan.build.json") || entry.isDirectory() && entry.name === "private";
|
|
932
|
+
if (carried)
|
|
933
|
+
await cp2(path7.join(dist, entry.name), path7.join(this.dir, entry.name), { recursive: true });
|
|
934
|
+
}
|
|
935
|
+
const built = await Bun.file(path7.join(dist, "package.json")).json();
|
|
936
|
+
await writeFile2(path7.join(this.dir, "package.json"), JSON.stringify(DesktopServerStage.packageJson(config, built), null, 2));
|
|
937
|
+
await this.app.spawn(process.execPath, ["install", "--production"], { cwd: this.dir });
|
|
938
|
+
await rm3(path7.join(this.dir, "node_modules", ".bin"), { recursive: true, force: true });
|
|
939
|
+
const scanInfo = this.app.getScanInfo({ allowEmpty: true }) ?? await this.app.scan({ write: false });
|
|
940
|
+
const carried = [...DesktopBin.select(config, scanInfo.getLibs()).keys()];
|
|
941
|
+
const notice = DesktopServerStage.imageStepsNotice(config, carried);
|
|
942
|
+
if (notice)
|
|
943
|
+
this.app.logger[notice.level](notice.message);
|
|
944
|
+
return { dir: this.dir, entry: "main.js", env: DesktopServerStage.env(config, environment) };
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
export { DesktopServerStage, getMobileTargets, getMobileTargetChoices, resolveMobileTargets, resolveAppId, appIdsOf, NativeApp, isPlaceholderAppId };
|