@akanjs/cli 3.1.0-rc.1 → 3.1.0-rc.3
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-kyprmy06.js → CodeAgent-ctk4fh2b.js} +2 -2
- package/{CodeTui-21mtze3j.js → CodeTui-mrxtrdzw.js} +1 -1
- package/TunnelShare-rj74jd6x.js +13 -0
- package/{agent.command-f3gsc1gd.js → agent.command-ncre6kwz.js} +5 -5
- package/akanCode.js +2 -2
- package/{application.command-5s780vnz.js → application.command-54mawreg.js} +52 -41
- package/{applicationBuildRunner-1yaqsrdq.js → applicationBuildRunner-8573zr2w.js} +16 -30
- package/buildBatch.proc.js +22 -13
- package/{cloud.command-yj6xb2a7.js → cloud.command-nyemv0m3.js} +57 -65
- package/cloud.script-ht6yb203.js +23 -0
- package/{code.command-3wkn4ja5.js → code.command-spsrzg63.js} +6 -6
- package/{context.command-9jjn3gcw.js → context.command-768zv7z4.js} +16 -13
- package/context.runner-tf1b53qq.js +24 -0
- package/{guideline.command-dtczrqpf.js → guideline.command-4ycwc51q.js} +2 -2
- package/guidelines/conventions/conventions.instruction.md +28 -15
- package/guidelines/cssRule/cssRule.instruction.md +12 -6
- package/guidelines/mcpRule/mcpRule.instruction.md +27 -11
- package/guidelines/runtimeRule/runtimeRule.instruction.md +24 -15
- package/guidelines/transportRule/transportRule.instruction.md +28 -5
- package/incrementalBuilder.proc.js +9 -6
- package/{index-2etcv0sc.js → index-00seamy5.js} +1 -1
- package/{index-jx4bzvwg.js → index-1a7nhq56.js} +92 -52
- package/{index-e0cvze1s.js → index-1nyxvz8j.js} +4 -4
- package/index-2z3kykvd.js +248 -0
- package/{index-rtct7d43.js → index-3675mvjw.js} +1 -1
- package/{index-56d2g8ms.js → index-3ddt3qjg.js} +1 -1
- package/{index-6jjtnhkw.js → index-3x7yeecy.js} +51 -9
- package/{index-ww9bhp67.js → index-4pf8g7ff.js} +1 -1
- package/index-8935h3n1.js +33 -0
- package/{index-v29746vb.js → index-8eqkxwsx.js} +3 -3
- package/{index-c2yrsxdh.js → index-91rt5bye.js} +96 -320
- package/{index-55mynnqs.js → index-bv210r6t.js} +459 -210
- package/{index-c76k723e.js → index-c4h5b5nq.js} +2 -2
- package/{index-qt22xstj.js → index-c9zjb3tw.js} +5 -3
- package/{index-c1x0be5j.js → index-dmsvd2rv.js} +2 -2
- package/index-ec080x99.js +1124 -0
- package/{index-ctanya41.js → index-fjqvd5mx.js} +4 -4
- package/{index-aprt7pkz.js → index-fvn6f5wx.js} +229 -1327
- package/{index-rc5rwyps.js → index-jj7yapwq.js} +71 -60
- package/index-kmamh5bv.js +241 -0
- package/{index-fm46xrf1.js → index-q5abh0jq.js} +3 -3
- package/{index-1w2qaee2.js → index-qf8g88j9.js} +361 -291
- package/{index-gnyf1fqy.js → index-qvghfv6v.js} +2 -2
- package/{index-ae5pwhgs.js → index-r5d6rq19.js} +10 -8
- package/{index-2mx692qq.js → index-vhfjzda3.js} +2 -2
- package/{index-8r4d7cmt.js → index-w9b71b6y.js} +4 -4
- package/{index-8ggztp8k.js → index-wanhh011.js} +42 -177
- package/{index-05g3jayg.js → index-wdze51wz.js} +1 -1
- package/index-wjqbc7sf.js +668 -0
- package/{index-rwq3ekqc.js → index-xf40a295.js} +7 -7
- package/{index-krq7btbt.js → index-xm5tmpv0.js} +3 -3
- package/index.js +22 -22
- package/{library.command-gaga5bmq.js → library.command-88ahgych.js} +3 -3
- package/{localRegistry.command-vsreyy42.js → localRegistry.command-ykea1m7b.js} +12 -9
- package/{module.command-7vpf6db4.js → module.command-zky4b7m9.js} +5 -5
- package/{package.command-twmvvf69.js → package.command-vh8pcedh.js} +3 -3
- package/package.json +2 -2
- package/{page.command-5asq7fdf.js → page.command-fyqeqxc3.js} +3 -3
- package/{pipeline.command-d0v38572.js → pipeline.command-r5t30py6.js} +220 -219
- package/{primitive.command-dyrs2fm8.js → primitive.command-a0516zfk.js} +6 -6
- package/{projectTypecheck-cvtkrzp2.js → projectTypecheck-7djd4z50.js} +1 -1
- package/{quality.command-6emsqpbg.js → quality.command-k7jcnfn0.js} +2 -2
- package/{repair.command-8t6aegv5.js → repair.command-85zg2an3.js} +5 -5
- package/{scalar.command-bnggy1x7.js → scalar.command-g5vmqwca.js} +4 -4
- package/{subspace.command-md2teygq.js → subspace.command-mp4evgsj.js} +421 -362
- package/{tunnel.command-jx3tdm2d.js → tunnel.command-kbnr99sj.js} +7 -4
- package/{workflow.command-jx0wag5r.js → workflow.command-9y9p1n8y.js} +10 -10
- package/{workspace.command-yxd7rr7g.js → workspace.command-jmec2kjg.js} +33 -30
- package/TunnelShare-q4jq08qg.js +0 -9
- package/cloud.script-h6pvcpa2.js +0 -20
- package/context.runner-atcrzz5b.js +0 -21
- package/index-6w0rbr9d.js +0 -68
- package/index-pgj2rt78.js +0 -404
|
@@ -2,20 +2,208 @@
|
|
|
2
2
|
import {
|
|
3
3
|
binPlatforms,
|
|
4
4
|
ServerEnvProfile,
|
|
5
|
-
Executor
|
|
6
|
-
|
|
5
|
+
Executor,
|
|
6
|
+
WorkspaceExecutor
|
|
7
|
+
} from "./index-bv210r6t.js";
|
|
7
8
|
import {
|
|
8
9
|
resolveAppId,
|
|
9
10
|
targetHtmlFilename
|
|
10
11
|
} from "./index-q958tqqg.js";
|
|
12
|
+
import {
|
|
13
|
+
ExitCodeError
|
|
14
|
+
} from "./index-3x7yeecy.js";
|
|
15
|
+
import {
|
|
16
|
+
CsrArtifactBuilder
|
|
17
|
+
} from "./index-2z3kykvd.js";
|
|
18
|
+
|
|
19
|
+
// pkgs/@akanjs/devkit/mobile/nativePublicEnv.ts
|
|
20
|
+
import { createHash } from "crypto";
|
|
21
|
+
class NativePublicEnv {
|
|
22
|
+
static derivedKeys = [
|
|
23
|
+
"AKAN_PUBLIC_APP_NAME",
|
|
24
|
+
"AKAN_PUBLIC_BASE_PATHS",
|
|
25
|
+
"AKAN_PUBLIC_API_PREFIX",
|
|
26
|
+
"AKAN_PUBLIC_WS_PREFIX",
|
|
27
|
+
"AKAN_PUBLIC_RENDER_ENV",
|
|
28
|
+
"AKAN_PUBLIC_DEFAULT_LOCALE",
|
|
29
|
+
"AKAN_PUBLIC_LOCALES"
|
|
30
|
+
];
|
|
31
|
+
static #keyPattern = /^AKAN_PUBLIC_[A-Z0-9_]{1,128}$/;
|
|
32
|
+
static #secretWords = ["SECRET", "TOKEN", "PASSWORD", "PRIVATE", "CREDENTIAL"];
|
|
33
|
+
static #maxKeys = 256;
|
|
34
|
+
static #maxBytes = 64 * 1024;
|
|
35
|
+
static of(env) {
|
|
36
|
+
return Object.fromEntries(Object.keys(env).filter((key) => key.startsWith("AKAN_PUBLIC_") && !NativePublicEnv.derivedKeys.includes(key)).sort().map((key) => [key, String(env[key])]));
|
|
37
|
+
}
|
|
38
|
+
static hash(publicEnv) {
|
|
39
|
+
return createHash("sha256").update(JSON.stringify(NativePublicEnv.of(publicEnv))).digest("hex");
|
|
40
|
+
}
|
|
41
|
+
static problems(publicEnv) {
|
|
42
|
+
const keys = Object.keys(publicEnv);
|
|
43
|
+
const problems = keys.flatMap((key) => {
|
|
44
|
+
const value = publicEnv[key];
|
|
45
|
+
if (!NativePublicEnv.#keyPattern.test(key))
|
|
46
|
+
return [`${key} is not an AKAN_PUBLIC_[A-Z0-9_] name`];
|
|
47
|
+
const word = NativePublicEnv.#secretWords.find((secret) => key.includes(secret));
|
|
48
|
+
if (word)
|
|
49
|
+
return [`${key} names a ${word.toLowerCase()}, which a page bundle must not carry`];
|
|
50
|
+
if (typeof value !== "string")
|
|
51
|
+
return [`${key} is not a string`];
|
|
52
|
+
if (/[\u0000-\u001f\u007f]/.test(value))
|
|
53
|
+
return [`${key} holds a control character`];
|
|
54
|
+
return [];
|
|
55
|
+
});
|
|
56
|
+
if (keys.length > NativePublicEnv.#maxKeys)
|
|
57
|
+
problems.push(`${keys.length} keys, more than ${NativePublicEnv.#maxKeys}`);
|
|
58
|
+
const bytes = Buffer.byteLength(JSON.stringify(publicEnv));
|
|
59
|
+
if (bytes > NativePublicEnv.#maxBytes)
|
|
60
|
+
problems.push(`${bytes} bytes, more than ${NativePublicEnv.#maxBytes}`);
|
|
61
|
+
return problems;
|
|
62
|
+
}
|
|
63
|
+
static diff(a, b) {
|
|
64
|
+
return [...new Set([...Object.keys(a), ...Object.keys(b)])].filter((key) => a[key] !== b[key]).sort((x, y) => x < y ? -1 : x > y ? 1 : 0);
|
|
65
|
+
}
|
|
66
|
+
static async built(app) {
|
|
67
|
+
const file = Bun.file(CsrArtifactBuilder.publicEnvPath(app));
|
|
68
|
+
if (!await file.exists())
|
|
69
|
+
return null;
|
|
70
|
+
return NativePublicEnv.of(await file.json());
|
|
71
|
+
}
|
|
72
|
+
static async recordedIn(bundleFile) {
|
|
73
|
+
const file = Bun.file(bundleFile);
|
|
74
|
+
if (!await file.exists())
|
|
75
|
+
return null;
|
|
76
|
+
const bundle = await file.json();
|
|
77
|
+
return bundle.web?.publicEnv ?? null;
|
|
78
|
+
}
|
|
79
|
+
static async load(file) {
|
|
80
|
+
const source = Bun.file(file);
|
|
81
|
+
if (!await source.exists())
|
|
82
|
+
throw new Error(`--public-env ${file} does not exist.`);
|
|
83
|
+
const json = await source.json().catch(() => null);
|
|
84
|
+
if (!json || typeof json !== "object" || Array.isArray(json))
|
|
85
|
+
throw new Error(`--public-env ${file} is not a JSON object of AKAN_PUBLIC_* values or a bundle.json.`);
|
|
86
|
+
const isBundle = json.schema === 1 && typeof json.web === "object";
|
|
87
|
+
const map = isBundle ? json.web.publicEnv : json;
|
|
88
|
+
if (!map)
|
|
89
|
+
throw new Error(`--public-env ${file} is a bundle.json from a build that recorded no public env.`);
|
|
90
|
+
const derived = Object.keys(map).filter((key) => NativePublicEnv.derivedKeys.includes(key));
|
|
91
|
+
if (derived.length)
|
|
92
|
+
throw new Error(`--public-env ${file} sets ${derived.join(", ")}, which akan.config.ts decides.`);
|
|
93
|
+
const problems = NativePublicEnv.problems(map);
|
|
94
|
+
if (problems.length)
|
|
95
|
+
throw new Error(`--public-env ${file} is refused:
|
|
96
|
+
- ${problems.join(`
|
|
97
|
+
- `)}`);
|
|
98
|
+
return NativePublicEnv.of(map);
|
|
99
|
+
}
|
|
100
|
+
static pin(publicEnv) {
|
|
101
|
+
for (const key of Object.keys(process.env))
|
|
102
|
+
if (key.startsWith("AKAN_PUBLIC_") && !NativePublicEnv.derivedKeys.includes(key))
|
|
103
|
+
delete process.env[key];
|
|
104
|
+
Object.assign(process.env, publicEnv);
|
|
105
|
+
WorkspaceExecutor.publicEnvPin = publicEnv;
|
|
106
|
+
}
|
|
107
|
+
static async problemAgainst(shipped, bundle) {
|
|
108
|
+
const [before, now] = await Promise.all([NativePublicEnv.recordedIn(shipped), NativePublicEnv.recordedIn(bundle)]);
|
|
109
|
+
if (!before)
|
|
110
|
+
return null;
|
|
111
|
+
const differs = NativePublicEnv.diff(before, now ?? {});
|
|
112
|
+
if (!differs.length)
|
|
113
|
+
return null;
|
|
114
|
+
return `public env ${differs.join(", ")} differs from the store build's (pass --public-env ${shipped} to repeat it)`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// pkgs/@akanjs/devkit/mobile/updatePackFolder.ts
|
|
119
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
120
|
+
import os from "os";
|
|
121
|
+
import path from "path";
|
|
122
|
+
class UpdatePackFolder {
|
|
123
|
+
static packFile = "pack.json";
|
|
124
|
+
static compatFile = "compat.json";
|
|
125
|
+
static bundleFile = "bundle.json";
|
|
126
|
+
static templateFile = "manifest.template.json";
|
|
127
|
+
static filesDir = "files";
|
|
128
|
+
static #records = [
|
|
129
|
+
UpdatePackFolder.templateFile,
|
|
130
|
+
UpdatePackFolder.bundleFile,
|
|
131
|
+
UpdatePackFolder.packFile,
|
|
132
|
+
UpdatePackFolder.compatFile
|
|
133
|
+
];
|
|
134
|
+
static #sha256 = /^[0-9a-f]{64}$/;
|
|
135
|
+
static async clear(dir) {
|
|
136
|
+
await Promise.all([UpdatePackFolder.packFile, UpdatePackFolder.compatFile].map((name) => rm(path.join(dir, name), { force: true })));
|
|
137
|
+
}
|
|
138
|
+
static async read(dir) {
|
|
139
|
+
const file = Bun.file(path.join(dir, UpdatePackFolder.packFile));
|
|
140
|
+
if (!await file.exists())
|
|
141
|
+
throw new Error(`${dir} holds no ${UpdatePackFolder.packFile}: give the folder \`akan pack-update\` wrote.`);
|
|
142
|
+
const pack = await file.json();
|
|
143
|
+
if (pack.schema !== 1)
|
|
144
|
+
throw new Error(`${dir}/${UpdatePackFolder.packFile} is schema ${pack.schema}, and this akan reads schema 1.`);
|
|
145
|
+
return { ...pack, out: path.resolve(dir) };
|
|
146
|
+
}
|
|
147
|
+
static async write({ out, ...record }) {
|
|
148
|
+
await Bun.write(path.join(out, UpdatePackFolder.packFile), `${JSON.stringify(record, null, 2)}
|
|
149
|
+
`);
|
|
150
|
+
}
|
|
151
|
+
static async writeCompat(pack, compat) {
|
|
152
|
+
pack.compat = compat;
|
|
153
|
+
await Bun.write(path.join(pack.out, UpdatePackFolder.compatFile), `${JSON.stringify(compat, null, 2)}
|
|
154
|
+
`);
|
|
155
|
+
}
|
|
156
|
+
static async readBundle(file) {
|
|
157
|
+
return await Bun.file(file).json();
|
|
158
|
+
}
|
|
159
|
+
static storeBuildVerdict({ out, compat }, then) {
|
|
160
|
+
if (!compat || compat.compatible)
|
|
161
|
+
return null;
|
|
162
|
+
return new ExitCodeError(`The pack in ${out} needs a new store build; it differs from the one ${compat.against} came from:
|
|
163
|
+
- ${compat.problems.join(`
|
|
164
|
+
- `)}${then ? `
|
|
165
|
+
${then}` : ""}`, 2);
|
|
166
|
+
}
|
|
167
|
+
static async withArchive(pack, use) {
|
|
168
|
+
const { templateFile, filesDir } = UpdatePackFolder;
|
|
169
|
+
const template = Bun.file(path.join(pack.out, templateFile));
|
|
170
|
+
if (!await template.exists())
|
|
171
|
+
throw new Error(`${pack.out} holds no ${templateFile}: give the folder \`akan pack-update\` wrote.`);
|
|
172
|
+
const { files = [] } = await template.json();
|
|
173
|
+
const hashes = [...new Set(files.map((file) => file.sha256))];
|
|
174
|
+
if (!hashes.every((hash) => typeof hash === "string" && UpdatePackFolder.#sha256.test(hash)))
|
|
175
|
+
throw new Error(`${pack.out}/${templateFile} lists a file whose sha256 is not 64 hex digits.`);
|
|
176
|
+
const present = await Promise.all(UpdatePackFolder.#records.map((name) => Bun.file(path.join(pack.out, name)).exists()));
|
|
177
|
+
const entries = [
|
|
178
|
+
...UpdatePackFolder.#records.filter((_, idx) => present[idx]),
|
|
179
|
+
...hashes.map((hash) => `${filesDir}/${hash}`)
|
|
180
|
+
];
|
|
181
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "akan-pack-"));
|
|
182
|
+
try {
|
|
183
|
+
const tar = Bun.spawn(["tar", "-czf", "pack.tgz", "-C", pack.out, "-T", "-"], {
|
|
184
|
+
cwd: dir,
|
|
185
|
+
stdin: new Blob([entries.map((entry) => `./${entry}
|
|
186
|
+
`).join("")]),
|
|
187
|
+
stdout: "ignore",
|
|
188
|
+
stderr: "pipe"
|
|
189
|
+
});
|
|
190
|
+
const [code, stderr] = await Promise.all([tar.exited, new Response(tar.stderr).text()]);
|
|
191
|
+
if (code !== 0)
|
|
192
|
+
throw new Error(`tar of ${pack.out} failed: ${stderr.trim()}`);
|
|
193
|
+
return await use(path.join(dir, "pack.tgz"));
|
|
194
|
+
} finally {
|
|
195
|
+
await rm(dir, { recursive: true, force: true });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
11
199
|
|
|
12
200
|
// pkgs/@akanjs/devkit/mobile/nativeApp.ts
|
|
13
|
-
import
|
|
14
|
-
import
|
|
201
|
+
import path8 from "path";
|
|
202
|
+
import { toError } from "akanjs/common";
|
|
15
203
|
|
|
16
204
|
// pkgs/@akanjs/devkit/mobile/desktopBin.ts
|
|
17
|
-
import { chmod, mkdir, rename, rm, writeFile } from "fs/promises";
|
|
18
|
-
import
|
|
205
|
+
import { chmod, mkdir, rename, rm as rm2, writeFile } from "fs/promises";
|
|
206
|
+
import path2 from "path";
|
|
19
207
|
class DesktopBin {
|
|
20
208
|
app;
|
|
21
209
|
config;
|
|
@@ -48,23 +236,23 @@ class DesktopBin {
|
|
|
48
236
|
static fileName(name, executable, platform = process.platform) {
|
|
49
237
|
if (platform !== "win32" || /\.(exe|cmd|bat|com)$/i.test(name))
|
|
50
238
|
return name;
|
|
51
|
-
return `${name}${
|
|
239
|
+
return `${name}${path2.extname(executable).toLowerCase() || ".exe"}`;
|
|
52
240
|
}
|
|
53
241
|
static downloadName(url) {
|
|
54
|
-
const extension = /(?:\.tar)?\.[A-Za-z0-9]{1,8}$/.exec(
|
|
242
|
+
const extension = /(?:\.tar)?\.[A-Za-z0-9]{1,8}$/.exec(path2.posix.basename(new URL(url).pathname))?.[0] ?? "";
|
|
55
243
|
return `download${extension.toLowerCase()}`;
|
|
56
244
|
}
|
|
57
245
|
static extractCommand(archive, dir, platform = process.platform) {
|
|
58
246
|
if (platform === "linux" && /\.zip$/i.test(archive))
|
|
59
247
|
return ["unzip", "-q", "-o", archive, "-d", dir];
|
|
60
|
-
const tar = platform === "win32" ?
|
|
248
|
+
const tar = platform === "win32" ? path2.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe") : "tar";
|
|
61
249
|
return [tar, "-xf", archive, "-C", dir];
|
|
62
250
|
}
|
|
63
251
|
static async sha256(file) {
|
|
64
252
|
return new Bun.CryptoHasher("sha256").update(await Bun.file(file).arrayBuffer()).digest("hex");
|
|
65
253
|
}
|
|
66
254
|
get #cacheDir() {
|
|
67
|
-
return
|
|
255
|
+
return path2.join(this.app.cwdPath, ".akan", "cache", "bin");
|
|
68
256
|
}
|
|
69
257
|
async stage(dir, arch = process.arch) {
|
|
70
258
|
const scanInfo = this.app.getScanInfo({ allowEmpty: true }) ?? await this.app.scan({ write: false });
|
|
@@ -74,14 +262,14 @@ class DesktopBin {
|
|
|
74
262
|
const platform = DesktopBin.platform(arch);
|
|
75
263
|
if (!platform)
|
|
76
264
|
throw new Error(`bin: this desktop app is for ${process.platform}-${arch}, and a bin names only ${binPlatforms.join(", ")}.`);
|
|
77
|
-
await
|
|
265
|
+
await rm2(dir, { recursive: true, force: true });
|
|
78
266
|
await mkdir(dir, { recursive: true });
|
|
79
267
|
for (const [name, { owner, platforms }] of chosen) {
|
|
80
268
|
const source = platforms[platform];
|
|
81
269
|
if (!source)
|
|
82
270
|
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"}).`);
|
|
83
271
|
const executable = await this.#executable(`${owner}: bin.${name}.${platform}`, source);
|
|
84
|
-
const target =
|
|
272
|
+
const target = path2.join(dir, DesktopBin.fileName(name, executable));
|
|
85
273
|
await Bun.write(target, Bun.file(executable));
|
|
86
274
|
await chmod(target, 493);
|
|
87
275
|
this.app.logger.info(`The desktop app carries ${name} from ${"url" in source ? source.url : source.path}`);
|
|
@@ -95,14 +283,14 @@ class DesktopBin {
|
|
|
95
283
|
if (!source.file)
|
|
96
284
|
return file;
|
|
97
285
|
const key = "url" in source ? source.sha256 : await DesktopBin.sha256(file);
|
|
98
|
-
const executable =
|
|
286
|
+
const executable = path2.join(await this.#extract(file, key), ...source.file.split("/"));
|
|
99
287
|
if (!await Bun.file(executable).exists())
|
|
100
288
|
throw new Error(`${at}: the archive holds no ${source.file}`);
|
|
101
289
|
return executable;
|
|
102
290
|
}
|
|
103
291
|
async#download(at, { url, sha256 }) {
|
|
104
|
-
const dir =
|
|
105
|
-
const file =
|
|
292
|
+
const dir = path2.join(this.#cacheDir, sha256);
|
|
293
|
+
const file = path2.join(dir, DesktopBin.downloadName(url));
|
|
106
294
|
if (await Bun.file(file).exists())
|
|
107
295
|
return file;
|
|
108
296
|
await mkdir(dir, { recursive: true });
|
|
@@ -119,10 +307,10 @@ class DesktopBin {
|
|
|
119
307
|
return file;
|
|
120
308
|
}
|
|
121
309
|
async#extract(archive, key) {
|
|
122
|
-
const dir =
|
|
310
|
+
const dir = path2.join(this.#cacheDir, key, "extracted");
|
|
123
311
|
if (await Bun.file(`${dir}.done`).exists())
|
|
124
312
|
return dir;
|
|
125
|
-
await
|
|
313
|
+
await rm2(dir, { recursive: true, force: true });
|
|
126
314
|
await mkdir(dir, { recursive: true });
|
|
127
315
|
const [command = "tar", ...args] = DesktopBin.extractCommand(archive, dir);
|
|
128
316
|
if (command === "unzip" && !Bun.which("unzip"))
|
|
@@ -134,10 +322,11 @@ class DesktopBin {
|
|
|
134
322
|
}
|
|
135
323
|
|
|
136
324
|
// pkgs/@akanjs/devkit/mobile/nativeApi.ts
|
|
137
|
-
import
|
|
325
|
+
import path3 from "path";
|
|
138
326
|
|
|
139
327
|
class NativeApi {
|
|
140
328
|
static supportedMajor = 0;
|
|
329
|
+
static minimumVersion = "0.12.0";
|
|
141
330
|
static entryOf(appDir) {
|
|
142
331
|
const workspaceEntry = NativeApi.#resolve("@akanjs/native/api", appDir);
|
|
143
332
|
if (workspaceEntry)
|
|
@@ -145,7 +334,7 @@ class NativeApi {
|
|
|
145
334
|
const akanjsPackage = NativeApi.#resolve("akanjs/package.json", appDir);
|
|
146
335
|
if (!akanjsPackage)
|
|
147
336
|
throw new Error(`akanjs is not installed for ${appDir}; the native build ships inside it.`);
|
|
148
|
-
return
|
|
337
|
+
return path3.join(path3.dirname(akanjsPackage), "vendor", "@akanjs", "native", "packages", "cli", "src", "api.ts");
|
|
149
338
|
}
|
|
150
339
|
static async load(appDir) {
|
|
151
340
|
const entry = NativeApi.entryOf(appDir);
|
|
@@ -153,8 +342,8 @@ class NativeApi {
|
|
|
153
342
|
throw new Error(`The installed akanjs has no native build at ${entry}; update akanjs.`);
|
|
154
343
|
const api = await import(entry);
|
|
155
344
|
const major = Number(api.API_VERSION.split(".")[0]);
|
|
156
|
-
if (major !== NativeApi.supportedMajor)
|
|
157
|
-
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.`);
|
|
345
|
+
if (major !== NativeApi.supportedMajor || Bun.semver.order(api.API_VERSION, NativeApi.minimumVersion) < 0)
|
|
346
|
+
throw new Error(`The native build API is ${api.API_VERSION}, and this akan CLI speaks ${NativeApi.minimumVersion} or a later ${NativeApi.supportedMajor}.x; install matching versions of akanjs and @akanjs/cli.`);
|
|
158
347
|
return api;
|
|
159
348
|
}
|
|
160
349
|
static #resolve(specifier, from) {
|
|
@@ -223,7 +412,7 @@ class NativeAppLine {
|
|
|
223
412
|
|
|
224
413
|
// pkgs/@akanjs/devkit/mobile/nativeConfig.ts
|
|
225
414
|
import { readFileSync } from "fs";
|
|
226
|
-
import
|
|
415
|
+
import path4 from "path";
|
|
227
416
|
|
|
228
417
|
// pkgs/@akanjs/devkit/mobile/usageDescriptions.ts
|
|
229
418
|
import { capitalize } from "akanjs/common";
|
|
@@ -305,11 +494,11 @@ class NativeConfig {
|
|
|
305
494
|
warnings.push(`Permission '${permission}' has no native plugin yet; the app ships without it.`);
|
|
306
495
|
return used;
|
|
307
496
|
});
|
|
308
|
-
const abs = (relative) =>
|
|
497
|
+
const abs = (relative) => path4.resolve(appPath, relative);
|
|
309
498
|
const updates = NativeConfig.#updates(target, env);
|
|
310
|
-
const folders = new Set(nativePlugins.map((plugin) =>
|
|
499
|
+
const folders = new Set(nativePlugins.map((plugin) => path4.resolve(plugin.dir)));
|
|
311
500
|
const folderIds = new Set(nativePlugins.map((plugin) => plugin.id));
|
|
312
|
-
const listed = (target.plugins ?? []).filter((spec) => NativeConfig.#isId(spec) ? !folderIds.has(spec) : !folders.has(
|
|
501
|
+
const listed = (target.plugins ?? []).filter((spec) => NativeConfig.#isId(spec) ? !folderIds.has(spec) : !folders.has(path4.resolve(appPath, spec)));
|
|
313
502
|
const plugins = [
|
|
314
503
|
...new Set([
|
|
315
504
|
...NativeConfig.basePlugins,
|
|
@@ -365,7 +554,7 @@ class NativeConfig {
|
|
|
365
554
|
app: {
|
|
366
555
|
id: resolveAppId(target.appId, platform),
|
|
367
556
|
name: target.appName,
|
|
368
|
-
fileName: target.fileName ?? NativeConfig.fileNameOf(
|
|
557
|
+
fileName: target.fileName ?? NativeConfig.fileNameOf(path4.basename(appPath)),
|
|
369
558
|
version: target.version,
|
|
370
559
|
build: target.buildNum
|
|
371
560
|
},
|
|
@@ -431,14 +620,14 @@ class NativeConfig {
|
|
|
431
620
|
if (NativeConfig.#isId(spec))
|
|
432
621
|
return spec;
|
|
433
622
|
try {
|
|
434
|
-
const { id } = JSON.parse(readFileSync(
|
|
623
|
+
const { id } = JSON.parse(readFileSync(path4.join(path4.resolve(appPath, spec), "native-plugin.json"), "utf8"));
|
|
435
624
|
return typeof id === "string" ? id : spec;
|
|
436
625
|
} catch {
|
|
437
626
|
return spec;
|
|
438
627
|
}
|
|
439
628
|
}
|
|
440
629
|
static #isId(spec) {
|
|
441
|
-
return !spec.startsWith(".") && !
|
|
630
|
+
return !spec.startsWith(".") && !path4.isAbsolute(spec);
|
|
442
631
|
}
|
|
443
632
|
static #compact(value) {
|
|
444
633
|
const kept = Object.entries(value).filter(([, entry]) => Array.isArray(entry) ? entry.length > 0 : entry && typeof entry === "object" ? Object.keys(entry).length > 0 : entry !== undefined);
|
|
@@ -459,23 +648,23 @@ class NativeConfig {
|
|
|
459
648
|
|
|
460
649
|
// pkgs/@akanjs/devkit/mobile/nativePluginFolders.ts
|
|
461
650
|
import { readdir } from "fs/promises";
|
|
462
|
-
import
|
|
651
|
+
import path5 from "path";
|
|
463
652
|
|
|
464
653
|
class NativePluginFolders {
|
|
465
654
|
static folder = "native";
|
|
466
655
|
static async in(root, owner) {
|
|
467
|
-
const base =
|
|
656
|
+
const base = path5.join(root, NativePluginFolders.folder);
|
|
468
657
|
const entries = await readdir(base, { withFileTypes: true }).catch(() => []);
|
|
469
658
|
const folders = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
470
659
|
return await Promise.all(folders.sort().map(async (name) => {
|
|
471
660
|
const where = `${owner}/${NativePluginFolders.folder}/${name}`;
|
|
472
|
-
const manifest = Bun.file(
|
|
661
|
+
const manifest = Bun.file(path5.join(base, name, "native-plugin.json"));
|
|
473
662
|
if (!await manifest.exists())
|
|
474
663
|
throw new Error(`${where} has no native-plugin.json; a folder in native/ is one native plugin.`);
|
|
475
664
|
const { id } = await manifest.json();
|
|
476
665
|
if (id !== name)
|
|
477
666
|
throw new Error(`${where}/native-plugin.json has id ${JSON.stringify(id)}; name the folder after its id.`);
|
|
478
|
-
return { id, dir:
|
|
667
|
+
return { id, dir: path5.join(base, name), owner };
|
|
479
668
|
}));
|
|
480
669
|
}
|
|
481
670
|
static select(own, fromLibs) {
|
|
@@ -494,15 +683,15 @@ class NativePluginFolders {
|
|
|
494
683
|
const root = app.workspace.workspaceRoot;
|
|
495
684
|
const [own, ...fromLibs] = await Promise.all([
|
|
496
685
|
NativePluginFolders.in(app.cwdPath, `apps/${app.name}`),
|
|
497
|
-
...scanInfo.getLibs().map((lib) => NativePluginFolders.in(
|
|
686
|
+
...scanInfo.getLibs().map((lib) => NativePluginFolders.in(path5.join(root, "libs", lib), `libs/${lib}`))
|
|
498
687
|
]);
|
|
499
688
|
return NativePluginFolders.select(own ?? [], fromLibs.flat());
|
|
500
689
|
}
|
|
501
690
|
}
|
|
502
691
|
|
|
503
692
|
// pkgs/@akanjs/devkit/mobile/nativeWebDir.ts
|
|
504
|
-
import { cp, mkdir as mkdir2, rm as
|
|
505
|
-
import
|
|
693
|
+
import { cp, mkdir as mkdir2, rm as rm3, stat } from "fs/promises";
|
|
694
|
+
import path6 from "path";
|
|
506
695
|
|
|
507
696
|
class NativeWebDir {
|
|
508
697
|
dir;
|
|
@@ -529,7 +718,7 @@ ${script}${html.slice(at)}`;
|
|
|
529
718
|
}
|
|
530
719
|
static #targetTag = "<script>window.__AKAN_MOBILE_TARGET__=";
|
|
531
720
|
async clear() {
|
|
532
|
-
await
|
|
721
|
+
await rm3(this.dir, { recursive: true, force: true });
|
|
533
722
|
await mkdir2(this.dir, { recursive: true });
|
|
534
723
|
}
|
|
535
724
|
async assemble(target, { html, publicDir, fontsDir }) {
|
|
@@ -539,12 +728,12 @@ ${script}${html.slice(at)}`;
|
|
|
539
728
|
if (publicDir && await NativeWebDir.#isDir(publicDir))
|
|
540
729
|
await cp(publicDir, this.dir, { recursive: true });
|
|
541
730
|
if (fontsDir && await NativeWebDir.#isDir(fontsDir))
|
|
542
|
-
await cp(fontsDir,
|
|
543
|
-
if (await NativeWebDir.#isDir(
|
|
731
|
+
await cp(fontsDir, path6.join(this.dir, "_akan", "fonts"), { recursive: true });
|
|
732
|
+
if (await NativeWebDir.#isDir(path6.join(this.dir, NativeWebDir.reservedEntry)))
|
|
544
733
|
throw new Error(`public/${NativeWebDir.reservedEntry} is reserved for the native runtime.`);
|
|
545
|
-
await Bun.write(
|
|
734
|
+
await Bun.write(path6.join(this.dir, "index.html"), NativeWebDir.injectTarget(await Bun.file(html).text(), target));
|
|
546
735
|
const files = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: this.dir, onlyFiles: true, dot: true }));
|
|
547
|
-
return files.map((file) => file.split(
|
|
736
|
+
return files.map((file) => file.split(path6.sep).join("/")).sort();
|
|
548
737
|
}
|
|
549
738
|
static async#isDir(dir) {
|
|
550
739
|
try {
|
|
@@ -557,20 +746,20 @@ ${script}${html.slice(at)}`;
|
|
|
557
746
|
|
|
558
747
|
// pkgs/@akanjs/devkit/mobile/serverBundleSecrets.ts
|
|
559
748
|
import { readdir as readdir2, readFile, stat as stat2 } from "fs/promises";
|
|
560
|
-
import
|
|
749
|
+
import path7 from "path";
|
|
561
750
|
class ServerBundleSecrets {
|
|
562
751
|
workspaceRoot;
|
|
563
752
|
appDir;
|
|
564
753
|
static #largestFile = 64 * 1024 * 1024;
|
|
754
|
+
static #printer = new Bun.Transpiler({ loader: "js", target: "bun" });
|
|
755
|
+
static #notSource = new Set(["node_modules", "env", "private", "public", "dist"]);
|
|
565
756
|
constructor(workspaceRoot, appDir) {
|
|
566
757
|
this.workspaceRoot = workspaceRoot;
|
|
567
758
|
this.appDir = appDir;
|
|
568
759
|
}
|
|
569
760
|
async envFiles() {
|
|
570
|
-
const
|
|
571
|
-
const
|
|
572
|
-
const dirs = [path6.join(this.appDir, "env"), ...libs.map((lib) => path6.join(libsDir, lib, "env"))];
|
|
573
|
-
const files = await Promise.all(dirs.map(async (dir) => (await readdir2(dir).catch(() => [])).filter((name) => /^env\.server\.[A-Za-z0-9.-]+\.ts$/.test(name) && !/\.(type|example)\.ts$/.test(name)).map((name) => path6.join(dir, name))));
|
|
761
|
+
const dirs = [path7.join(this.appDir, "env"), ...(await this.#libDirs()).map((lib) => path7.join(lib, "env"))];
|
|
762
|
+
const files = await Promise.all(dirs.map(async (dir) => (await readdir2(dir).catch(() => [])).filter((name) => /^env\.server\.[A-Za-z0-9.-]+\.ts$/.test(name) && !/\.(type|example)\.ts$/.test(name)).map((name) => path7.join(dir, name))));
|
|
574
763
|
return files.flat();
|
|
575
764
|
}
|
|
576
765
|
async assertClean(bundleDir) {
|
|
@@ -578,38 +767,66 @@ class ServerBundleSecrets {
|
|
|
578
767
|
const secrets = [];
|
|
579
768
|
for (const file of await this.envFiles()) {
|
|
580
769
|
try {
|
|
581
|
-
const value = await ServerEnvProfile.evaluate(file,
|
|
770
|
+
const value = await ServerEnvProfile.evaluate(file, path7.dirname(path7.dirname(file)));
|
|
582
771
|
secrets.push(...ServerEnvProfile.secretValues(value).map((secret) => ({ file, ...secret })));
|
|
583
772
|
} catch {
|
|
584
773
|
unread.push(ServerBundleSecrets.#relative(this.workspaceRoot, file));
|
|
585
774
|
}
|
|
586
775
|
}
|
|
776
|
+
if (!secrets.length)
|
|
777
|
+
return unread;
|
|
778
|
+
const sought = secrets.map((secret) => ({ secret, needles: ServerBundleSecrets.#needles(secret.value) }));
|
|
587
779
|
const found = [];
|
|
588
780
|
for (const bundled of await ServerBundleSecrets.#files(bundleDir)) {
|
|
589
781
|
const text = await readFile(bundled, "latin1");
|
|
590
|
-
for (const secret of
|
|
591
|
-
if (
|
|
592
|
-
found.push(
|
|
782
|
+
for (const { secret, needles } of sought)
|
|
783
|
+
if (needles.some((needle) => text.includes(needle)))
|
|
784
|
+
found.push({ bundled, secret });
|
|
593
785
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
786
|
+
const coded = found.length ? await this.#coded(found.map(({ secret }) => secret.value)) : new Set;
|
|
787
|
+
const leaks = found.filter(({ secret }) => !coded.has(secret.value)).map(({ bundled, secret }) => `${ServerBundleSecrets.#relative(bundleDir, bundled)} holds ${ServerBundleSecrets.#relative(this.workspaceRoot, secret.file)} ${secret.path}`);
|
|
788
|
+
if (leaks.length)
|
|
789
|
+
throw new Error(`The server this app carries holds secret values of the workspace's env files, so the release stops here:
|
|
790
|
+
${[...new Set(leaks)].join(`
|
|
597
791
|
`)}
|
|
598
|
-
Build its server with a profile (native.desktop.server.profile), whose bundle carries
|
|
792
|
+
Build its server with a profile (native.desktop.server.profile), whose bundle leaves the env files' secrets out. A value a profile build still holds sits in code or a file the server carries: take it out there.`);
|
|
599
793
|
return unread;
|
|
600
794
|
}
|
|
795
|
+
async#coded(values) {
|
|
796
|
+
const plain = [...new Set(values)].filter((value) => !ServerEnvProfile.distinctive(value));
|
|
797
|
+
const coded = new Set;
|
|
798
|
+
if (!plain.length)
|
|
799
|
+
return coded;
|
|
800
|
+
for (const dir of [this.appDir, ...await this.#libDirs()])
|
|
801
|
+
for (const file of await ServerBundleSecrets.#sources(dir)) {
|
|
802
|
+
const text = await readFile(file, "utf8");
|
|
803
|
+
for (const value of plain)
|
|
804
|
+
if (text.includes(value))
|
|
805
|
+
coded.add(value);
|
|
806
|
+
}
|
|
807
|
+
return coded;
|
|
808
|
+
}
|
|
809
|
+
async#libDirs() {
|
|
810
|
+
const libsDir = path7.join(this.workspaceRoot, "libs");
|
|
811
|
+
return (await readdir2(libsDir).catch(() => [])).map((lib) => path7.join(libsDir, lib));
|
|
812
|
+
}
|
|
601
813
|
static #needles(value) {
|
|
602
814
|
const escaped = JSON.stringify(value).slice(1, -1);
|
|
603
815
|
const line = value.split(/\r?\n/).find((part) => part.length >= 32);
|
|
604
|
-
|
|
816
|
+
const forms = [value, escaped, ServerBundleSecrets.#printed(value), ...line ? [line] : []];
|
|
817
|
+
return [...new Set(forms.map((form) => Buffer.from(form, "utf8").toString("latin1")))];
|
|
818
|
+
}
|
|
819
|
+
static #printed(value) {
|
|
820
|
+
const code = ServerBundleSecrets.#printer.transformSync(`export default ${JSON.stringify(value)};`);
|
|
821
|
+
return /^export default (["'`])([\s\S]*)\1;?\s*$/.exec(code)?.[2] ?? value;
|
|
605
822
|
}
|
|
606
823
|
static #relative(from, to) {
|
|
607
|
-
return
|
|
824
|
+
return path7.relative(from, to).split(path7.sep).join("/");
|
|
608
825
|
}
|
|
609
826
|
static async#files(dir) {
|
|
610
827
|
const files = [];
|
|
611
828
|
for (const entry of await readdir2(dir, { withFileTypes: true })) {
|
|
612
|
-
const file =
|
|
829
|
+
const file = path7.join(dir, entry.name);
|
|
613
830
|
if (entry.isDirectory()) {
|
|
614
831
|
if (entry.name !== "node_modules")
|
|
615
832
|
files.push(...await ServerBundleSecrets.#files(file));
|
|
@@ -618,6 +835,20 @@ Build its server with a profile (native.desktop.server.profile), whose bundle ca
|
|
|
618
835
|
}
|
|
619
836
|
return files;
|
|
620
837
|
}
|
|
838
|
+
static async#sources(dir) {
|
|
839
|
+
const files = [];
|
|
840
|
+
for (const entry of await readdir2(dir, { withFileTypes: true }).catch(() => [])) {
|
|
841
|
+
if (entry.name.startsWith("."))
|
|
842
|
+
continue;
|
|
843
|
+
const file = path7.join(dir, entry.name);
|
|
844
|
+
if (entry.isDirectory()) {
|
|
845
|
+
if (!ServerBundleSecrets.#notSource.has(entry.name))
|
|
846
|
+
files.push(...await ServerBundleSecrets.#sources(file));
|
|
847
|
+
} else if (/\.(?:[cm]?[jt]sx?|json)$/.test(entry.name) && !/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(entry.name))
|
|
848
|
+
files.push(file);
|
|
849
|
+
}
|
|
850
|
+
return files;
|
|
851
|
+
}
|
|
621
852
|
}
|
|
622
853
|
|
|
623
854
|
// pkgs/@akanjs/devkit/mobile/nativeApp.ts
|
|
@@ -632,14 +863,14 @@ class NativeApp {
|
|
|
632
863
|
this.app = app;
|
|
633
864
|
this.target = target;
|
|
634
865
|
this.env = env;
|
|
635
|
-
this.targetRoot =
|
|
636
|
-
this.web = new NativeWebDir(
|
|
866
|
+
this.targetRoot = path8.join(app.workspace.workspaceRoot, "dist", "native", app.name, target.name);
|
|
867
|
+
this.web = new NativeWebDir(path8.join(this.targetRoot, "web"));
|
|
637
868
|
}
|
|
638
869
|
outDir(platform) {
|
|
639
|
-
return
|
|
870
|
+
return path8.join(this.targetRoot, "build", platform);
|
|
640
871
|
}
|
|
641
872
|
devOutDir(platform) {
|
|
642
|
-
return
|
|
873
|
+
return path8.join(this.targetRoot, "dev", platform);
|
|
643
874
|
}
|
|
644
875
|
indexPath(platform) {
|
|
645
876
|
const { config } = this.target;
|
|
@@ -649,13 +880,13 @@ class NativeApp {
|
|
|
649
880
|
async assembleWeb(platform) {
|
|
650
881
|
const dist = this.app.dist.cwdPath;
|
|
651
882
|
return await this.web.assemble({ ...this.target.config, indexPath: this.indexPath(platform) }, {
|
|
652
|
-
html:
|
|
653
|
-
publicDir:
|
|
654
|
-
fontsDir:
|
|
883
|
+
html: path8.join(dist, "csr", targetHtmlFilename(this.target.config)),
|
|
884
|
+
publicDir: path8.join(dist, "public"),
|
|
885
|
+
fontsDir: path8.join(dist, ".akan", "artifact", "fonts")
|
|
655
886
|
});
|
|
656
887
|
}
|
|
657
888
|
get binDir() {
|
|
658
|
-
return
|
|
889
|
+
return path8.join(this.targetRoot, "bin");
|
|
659
890
|
}
|
|
660
891
|
async config({
|
|
661
892
|
server,
|
|
@@ -739,16 +970,17 @@ class NativeApp {
|
|
|
739
970
|
NativeApp.#assertServerPlatform(platform, server);
|
|
740
971
|
NativeApp.assertInstaller(platform, installer);
|
|
741
972
|
await this.assembleWeb(platform);
|
|
973
|
+
const publicEnv = await this.#publicEnv();
|
|
742
974
|
const { api, config } = await this.prepare(platform, server, arch);
|
|
743
975
|
const distribution = await this.#distribution(api, platform, profile, { sign, notarize }, server);
|
|
744
|
-
return await api.build({
|
|
976
|
+
return await this.#recorded(this.outDir(platform), publicEnv, api.build({
|
|
745
977
|
...this.#task(platform, config, { env: await this.#runtimeEnv(server) }),
|
|
746
978
|
profile,
|
|
747
979
|
...arch ? { arch } : {},
|
|
748
980
|
...platform === "windows" ? { windows: { ...installer ? { installer } : {}, ...distribution.windows } } : {},
|
|
749
981
|
...platform === "macos" ? { macos: { ...distribution.macos, ...installer ? { dmg: true } : {} } } : {},
|
|
750
982
|
...platform === "linux" && installer ? { linux: { appImage: true } } : {}
|
|
751
|
-
});
|
|
983
|
+
}));
|
|
752
984
|
}
|
|
753
985
|
async run(platform, { device, teamId, profile = "debug", server } = {}) {
|
|
754
986
|
NativeApp.#assertServerPlatform(platform, server);
|
|
@@ -804,22 +1036,25 @@ class NativeApp {
|
|
|
804
1036
|
}
|
|
805
1037
|
async releaseIos({ teamId, adHoc = false } = {}) {
|
|
806
1038
|
await this.assembleWeb("ios");
|
|
1039
|
+
const publicEnv = await this.#publicEnv();
|
|
807
1040
|
const { api, config } = await this.prepare("ios");
|
|
808
|
-
return await api.release({
|
|
1041
|
+
return await this.#recorded(this.outDir("ios"), publicEnv, api.release({
|
|
809
1042
|
...this.#task("ios", config, { env: await this.#runtimeEnv() }),
|
|
810
1043
|
platform: "ios",
|
|
811
1044
|
signing: { ...teamId ? { teamId } : {}, ...adHoc ? { distribution: "ad-hoc" } : {} }
|
|
812
|
-
});
|
|
1045
|
+
}));
|
|
813
1046
|
}
|
|
814
1047
|
get updatesDir() {
|
|
815
|
-
return
|
|
1048
|
+
return path8.join(this.targetRoot, "updates");
|
|
816
1049
|
}
|
|
817
1050
|
async publishUpdate(platform, {
|
|
818
1051
|
channel,
|
|
819
1052
|
server,
|
|
820
1053
|
arch,
|
|
821
1054
|
sign = false,
|
|
822
|
-
notarize = false
|
|
1055
|
+
notarize = false,
|
|
1056
|
+
revoke,
|
|
1057
|
+
resetSequence
|
|
823
1058
|
} = {}) {
|
|
824
1059
|
NativeApp.#assertServerPlatform(platform, server);
|
|
825
1060
|
this.#assertUpdates();
|
|
@@ -833,25 +1068,35 @@ class NativeApp {
|
|
|
833
1068
|
...channel ? { channel } : {},
|
|
834
1069
|
...arch ? { arch } : {},
|
|
835
1070
|
...distribution.macos ? { macos: distribution.macos } : {},
|
|
836
|
-
...distribution.windows ? { windows: distribution.windows } : {}
|
|
1071
|
+
...distribution.windows ? { windows: distribution.windows } : {},
|
|
1072
|
+
...revoke?.length ? { revoke } : {},
|
|
1073
|
+
...resetSequence ? { resetSequence } : {}
|
|
837
1074
|
});
|
|
838
1075
|
}
|
|
839
1076
|
async#distribution(api, platform, profile, request, server) {
|
|
840
1077
|
const distribution = api.requestedDistribution(platform, profile, request);
|
|
1078
|
+
if (server && profile === "release")
|
|
1079
|
+
await this.#assertServerClean(server);
|
|
841
1080
|
if (!distribution.notice) {
|
|
842
1081
|
if (distribution.unused.length && (platform === "macos" || platform === "windows"))
|
|
843
1082
|
this.app.logger.info(`${distribution.unused.join(", ")} set: this build is not signed without --sign.`);
|
|
844
1083
|
return distribution;
|
|
845
1084
|
}
|
|
846
|
-
if (server) {
|
|
847
|
-
const unread = await new ServerBundleSecrets(this.app.workspace.workspaceRoot, this.app.cwdPath).assertClean(server.dir);
|
|
848
|
-
if (unread.length)
|
|
849
|
-
this.app.logger.warn(`Not searched for secrets, as they could not be evaluated: ${unread.join(", ")}.`);
|
|
850
|
-
}
|
|
851
1085
|
this.app.logger.warn(distribution.notice);
|
|
852
1086
|
return distribution;
|
|
853
1087
|
}
|
|
854
|
-
async
|
|
1088
|
+
async#assertServerClean(server) {
|
|
1089
|
+
const unread = await new ServerBundleSecrets(this.app.workspace.workspaceRoot, this.app.cwdPath).assertClean(server.dir);
|
|
1090
|
+
if (unread.length)
|
|
1091
|
+
throw new Error(`The server this app carries was not searched for the secret values of ${unread.join(", ")}, which could not be evaluated with no app env set, so the release stops here.`);
|
|
1092
|
+
}
|
|
1093
|
+
async assertPublishable(platform, {
|
|
1094
|
+
channel,
|
|
1095
|
+
server = false,
|
|
1096
|
+
arch,
|
|
1097
|
+
revoke,
|
|
1098
|
+
resetSequence
|
|
1099
|
+
} = {}) {
|
|
855
1100
|
this.#assertUpdates();
|
|
856
1101
|
const [api, { config }] = await Promise.all([
|
|
857
1102
|
NativeApi.load(this.app.cwdPath),
|
|
@@ -866,10 +1111,12 @@ class NativeApp {
|
|
|
866
1111
|
out: this.updatesDir,
|
|
867
1112
|
server,
|
|
868
1113
|
...channel ? { channel } : {},
|
|
869
|
-
...arch ? { arch } : {}
|
|
1114
|
+
...arch ? { arch } : {},
|
|
1115
|
+
...revoke?.length ? { revoke } : {},
|
|
1116
|
+
...resetSequence ? { resetSequence } : {}
|
|
870
1117
|
});
|
|
871
1118
|
} catch (error) {
|
|
872
|
-
throw new Error(`Native target '${this.target.name}': ${error
|
|
1119
|
+
throw new Error(`Native target '${this.target.name}': ${toError(error).message}`, {
|
|
873
1120
|
cause: error
|
|
874
1121
|
});
|
|
875
1122
|
}
|
|
@@ -888,24 +1135,26 @@ class NativeApp {
|
|
|
888
1135
|
async releaseAndroid({ formats = ["aab"] } = {}) {
|
|
889
1136
|
const signing = NativeApp.androidSigning();
|
|
890
1137
|
await this.assembleWeb("android");
|
|
1138
|
+
const publicEnv = await this.#publicEnv();
|
|
891
1139
|
const { api, config } = await this.prepare("android");
|
|
892
|
-
return await api.release({
|
|
1140
|
+
return await this.#recorded(this.outDir("android"), publicEnv, api.release({
|
|
893
1141
|
...this.#task("android", config, { env: await this.#runtimeEnv() }),
|
|
894
1142
|
platform: "android",
|
|
895
1143
|
signing,
|
|
896
1144
|
formats
|
|
897
|
-
});
|
|
1145
|
+
}));
|
|
898
1146
|
}
|
|
899
1147
|
async packUpdate(platform, { out } = {}) {
|
|
900
1148
|
await this.assembleWeb(platform);
|
|
1149
|
+
const publicEnv = await this.#publicEnv();
|
|
901
1150
|
const { api, config } = await this.prepare(platform);
|
|
902
1151
|
if (!config.updates)
|
|
903
1152
|
throw new Error(`Native target '${this.target.name}' takes no updates: add native.updates: { url, publicKey } to akan.config.ts first.`);
|
|
904
|
-
const dir =
|
|
905
|
-
await
|
|
906
|
-
const packed = await api.packUpdate({ ...this.#task(platform, config), platform, out: dir });
|
|
1153
|
+
const dir = path8.resolve(out ?? path8.join(this.updatesDir, platform));
|
|
1154
|
+
await UpdatePackFolder.clear(dir);
|
|
1155
|
+
const packed = await this.#recorded(dir, publicEnv, api.packUpdate({ ...this.#task(platform, config), platform, out: dir }));
|
|
907
1156
|
const channel = config.updates.channel ?? "production";
|
|
908
|
-
const bundle = await
|
|
1157
|
+
const bundle = await UpdatePackFolder.readBundle(path8.join(packed.out, UpdatePackFolder.bundleFile));
|
|
909
1158
|
const pack = {
|
|
910
1159
|
schema: 1,
|
|
911
1160
|
app: this.app.name,
|
|
@@ -917,24 +1166,39 @@ class NativeApp {
|
|
|
917
1166
|
appVersion: packed.manifest.version,
|
|
918
1167
|
nativeApi: packed.manifest.nativeApi ?? "",
|
|
919
1168
|
webHash: bundle.web.hash,
|
|
1169
|
+
...bundle.web.publicEnvHash ? { publicEnvHash: bundle.web.publicEnvHash } : {},
|
|
920
1170
|
files: packed.manifest.files.length,
|
|
921
1171
|
size: packed.manifest.files.reduce((sum, file) => sum + file.size, 0),
|
|
922
1172
|
out: packed.out
|
|
923
1173
|
};
|
|
924
1174
|
return { ...packed, channel, pack };
|
|
925
1175
|
}
|
|
926
|
-
|
|
927
|
-
await
|
|
1176
|
+
async#publicEnv() {
|
|
1177
|
+
const publicEnv = await NativePublicEnv.built(this.app);
|
|
1178
|
+
if (!publicEnv)
|
|
1179
|
+
return null;
|
|
1180
|
+
const problems = NativePublicEnv.problems(publicEnv);
|
|
1181
|
+
if (!problems.length)
|
|
1182
|
+
return publicEnv;
|
|
1183
|
+
const message = `The web build inlines public env an update cannot carry:
|
|
1184
|
+
- ${problems.join(`
|
|
1185
|
+
- `)}`;
|
|
1186
|
+
if (this.target.config.updates)
|
|
1187
|
+
throw new Error(message);
|
|
1188
|
+
this.app.logger.warn(`${message}
|
|
1189
|
+
bundle.json records none, so no update can be checked against this build.`);
|
|
1190
|
+
return null;
|
|
1191
|
+
}
|
|
1192
|
+
async#recorded(dir, publicEnv, build) {
|
|
1193
|
+
const result = await build;
|
|
1194
|
+
const file = path8.join(dir, UpdatePackFolder.bundleFile);
|
|
1195
|
+
if (!publicEnv || !await Bun.file(file).exists())
|
|
1196
|
+
return result;
|
|
1197
|
+
const bundle = await UpdatePackFolder.readBundle(file);
|
|
1198
|
+
const web = { ...bundle.web, publicEnv, publicEnvHash: NativePublicEnv.hash(publicEnv) };
|
|
1199
|
+
await Bun.write(file, `${JSON.stringify({ ...bundle, web }, null, 2)}
|
|
928
1200
|
`);
|
|
929
|
-
|
|
930
|
-
static async readPack(dir) {
|
|
931
|
-
const file = Bun.file(path7.join(dir, "pack.json"));
|
|
932
|
-
if (!await file.exists())
|
|
933
|
-
throw new Error(`${dir} holds no pack.json: give the folder \`akan pack-update\` wrote.`);
|
|
934
|
-
const pack = await file.json();
|
|
935
|
-
if (pack.schema !== 1)
|
|
936
|
-
throw new Error(`${dir}/pack.json is schema ${pack.schema}, and this akan reads schema 1.`);
|
|
937
|
-
return { ...pack, out: path7.resolve(dir) };
|
|
1201
|
+
return result;
|
|
938
1202
|
}
|
|
939
1203
|
static async compareBundles(appDir, shipped, bundle) {
|
|
940
1204
|
return (await NativeApi.load(appDir)).compareBundles(shipped, bundle);
|
|
@@ -990,198 +1254,4 @@ class NativeApp {
|
|
|
990
1254
|
}
|
|
991
1255
|
}
|
|
992
1256
|
|
|
993
|
-
|
|
994
|
-
var PLACEHOLDER_APP_IDS = [
|
|
995
|
-
"com.myapp.app",
|
|
996
|
-
"com.myorg.myapp",
|
|
997
|
-
"com.example.app",
|
|
998
|
-
"com.example.myapp"
|
|
999
|
-
];
|
|
1000
|
-
var placeholderAppIdSegment = /^(example|examples|myorg|myapp|mycompany|myorganization|changeme|todo|sample|test)$/;
|
|
1001
|
-
var isPlaceholderAppId = (appId) => {
|
|
1002
|
-
const normalized = appId?.trim().toLowerCase() ?? "";
|
|
1003
|
-
if (!normalized)
|
|
1004
|
-
return true;
|
|
1005
|
-
if (PLACEHOLDER_APP_IDS.includes(normalized))
|
|
1006
|
-
return true;
|
|
1007
|
-
return normalized.split(".").some((segment) => placeholderAppIdSegment.test(segment));
|
|
1008
|
-
};
|
|
1009
|
-
// pkgs/@akanjs/devkit/mobile/desktopServerStage.ts
|
|
1010
|
-
import { cp as cp2, mkdir as mkdir3, readdir as readdir3, rm as rm4, writeFile as writeFile2 } from "fs/promises";
|
|
1011
|
-
import path8 from "path";
|
|
1012
|
-
class DesktopServerStage {
|
|
1013
|
-
app;
|
|
1014
|
-
static uncarried = new Set([
|
|
1015
|
-
"Dockerfile",
|
|
1016
|
-
"package.json",
|
|
1017
|
-
"rscWorker.js",
|
|
1018
|
-
"console.js",
|
|
1019
|
-
"console-runtime.js",
|
|
1020
|
-
"csr",
|
|
1021
|
-
"public",
|
|
1022
|
-
".akan"
|
|
1023
|
-
]);
|
|
1024
|
-
static rscRenderer = "react-server-dom-webpack";
|
|
1025
|
-
dir;
|
|
1026
|
-
constructor(app) {
|
|
1027
|
-
this.app = app;
|
|
1028
|
-
this.dir = path8.join(app.cwdPath, ".akan", "desktop", "server");
|
|
1029
|
-
}
|
|
1030
|
-
static assertCarriable(config) {
|
|
1031
|
-
if (!config.database.modes.includes("single"))
|
|
1032
|
-
throw new Error(`desktop.server in native 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(", ")}.`);
|
|
1033
|
-
}
|
|
1034
|
-
static env(config, environment, { modules = [], env = {} } = {}) {
|
|
1035
|
-
const { PORT: _port, AKAN_LOG_TO_FILE: _fileLog, ...image } = config.getProductionEnv(environment);
|
|
1036
|
-
return {
|
|
1037
|
-
...image,
|
|
1038
|
-
...env,
|
|
1039
|
-
AKAN_PUBLIC_OPERATION_MODE: "edge",
|
|
1040
|
-
AKAN_DATABASE_MODE: "single",
|
|
1041
|
-
AKAN_DATABASE_MODES: "single",
|
|
1042
|
-
AKAN_SSR: "false",
|
|
1043
|
-
AKAN_CSR: "false",
|
|
1044
|
-
AKAN_MCP: "false",
|
|
1045
|
-
AKAN_SHUTDOWN_TIMEOUT_MS: "1000",
|
|
1046
|
-
...modules.length ? { AKAN_MODULES: modules.join(",") } : {}
|
|
1047
|
-
};
|
|
1048
|
-
}
|
|
1049
|
-
static imageStepsNotice(config, carried) {
|
|
1050
|
-
const steps = typeof config.docker === "string" ? null : config.docker.preRuns.length + config.docker.postRuns.length;
|
|
1051
|
-
if (steps === 0)
|
|
1052
|
-
return null;
|
|
1053
|
-
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`;
|
|
1054
|
-
return carried.length ? {
|
|
1055
|
-
level: "info",
|
|
1056
|
-
message: `${image}; the desktop app's server runs none of them and carries only bin: ${carried.join(", ")}.`
|
|
1057
|
-
} : {
|
|
1058
|
-
level: "warn",
|
|
1059
|
-
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.`
|
|
1060
|
-
};
|
|
1061
|
-
}
|
|
1062
|
-
static packageJson(config, built, omit = []) {
|
|
1063
|
-
const otherDrivers = new Set(config.database.modes.filter((mode) => mode !== "single").flatMap((mode) => config.getDatabaseModeRuntimePackages(mode)));
|
|
1064
|
-
const dependencies = Object.entries(built.dependencies ?? {}).filter(([name]) => !omit.includes(name) && (config.externalLibs.includes(name) || name !== DesktopServerStage.rscRenderer && !otherDrivers.has(name)));
|
|
1065
|
-
return { ...built, dependencies: Object.fromEntries(dependencies) };
|
|
1066
|
-
}
|
|
1067
|
-
static async dependentsOf(nodeModules, name) {
|
|
1068
|
-
const dependents = new Set;
|
|
1069
|
-
for await (const manifest of new Bun.Glob("**/package.json").scan({ cwd: nodeModules, onlyFiles: true })) {
|
|
1070
|
-
const json = await Bun.file(path8.join(nodeModules, manifest)).json().catch(() => null);
|
|
1071
|
-
if (!json?.name || json.name === name)
|
|
1072
|
-
continue;
|
|
1073
|
-
const declared = { ...json.dependencies, ...json.optionalDependencies, ...json.peerDependencies };
|
|
1074
|
-
if (name in declared)
|
|
1075
|
-
dependents.add(json.name);
|
|
1076
|
-
}
|
|
1077
|
-
return [...dependents].sort();
|
|
1078
|
-
}
|
|
1079
|
-
static installArgs(arch) {
|
|
1080
|
-
return ["install", "--production", "--prefer-offline", ...arch && arch !== process.arch ? [`--cpu=${arch}`] : []];
|
|
1081
|
-
}
|
|
1082
|
-
async install(arch, omit = []) {
|
|
1083
|
-
const config = await this.app.getConfig();
|
|
1084
|
-
DesktopServerStage.assertCarriable(config);
|
|
1085
|
-
await rm4(this.dir, { recursive: true, force: true });
|
|
1086
|
-
await mkdir3(this.dir, { recursive: true });
|
|
1087
|
-
const built = config.getProductionPackageJson();
|
|
1088
|
-
for (const name of omit.filter((name) => !(name in (built.dependencies ?? {}))))
|
|
1089
|
-
this.app.logger.warn(`desktop.server.omit names ${name}, which the server does not depend on directly.`);
|
|
1090
|
-
await writeFile2(path8.join(this.dir, "package.json"), JSON.stringify(DesktopServerStage.packageJson(config, built, omit), null, 2));
|
|
1091
|
-
try {
|
|
1092
|
-
await this.app.spawn(process.execPath, DesktopServerStage.installArgs(arch), { cwd: this.dir });
|
|
1093
|
-
} catch (error) {
|
|
1094
|
-
throw new Error(`The desktop app's server could not install its packages in ${this.dir}: it needs the npm registry, or a Bun cache that already holds every one of them.
|
|
1095
|
-
${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
1096
|
-
}
|
|
1097
|
-
await rm4(path8.join(this.dir, "node_modules", ".bin"), { recursive: true, force: true });
|
|
1098
|
-
await this.#assertOmitted(omit);
|
|
1099
|
-
}
|
|
1100
|
-
async#assertOmitted(omit) {
|
|
1101
|
-
const nodeModules = path8.join(this.dir, "node_modules");
|
|
1102
|
-
const kept = [];
|
|
1103
|
-
for (const name of omit) {
|
|
1104
|
-
if (!await Bun.file(path8.join(nodeModules, name, "package.json")).exists())
|
|
1105
|
-
continue;
|
|
1106
|
-
const dependents = await DesktopServerStage.dependentsOf(nodeModules, name);
|
|
1107
|
-
kept.push(`${name} (needed by ${dependents.join(", ") || "a package that names it"})`);
|
|
1108
|
-
}
|
|
1109
|
-
if (kept.length)
|
|
1110
|
-
throw new Error(`desktop.server.omit leaves out packages another dependency still installs: ${kept.join("; ")}. Omit those too, or keep the package.`);
|
|
1111
|
-
}
|
|
1112
|
-
async prepare(environment, run = {}) {
|
|
1113
|
-
const config = await this.app.getConfig();
|
|
1114
|
-
const dist = this.app.dist.cwdPath;
|
|
1115
|
-
await mkdir3(this.dir, { recursive: true });
|
|
1116
|
-
for (const name of await readdir3(dist))
|
|
1117
|
-
if (!DesktopServerStage.uncarried.has(name))
|
|
1118
|
-
await cp2(path8.join(dist, name), path8.join(this.dir, name), { recursive: true });
|
|
1119
|
-
const scanInfo = this.app.getScanInfo({ allowEmpty: true }) ?? await this.app.scan({ write: false });
|
|
1120
|
-
const carried = [...DesktopBin.select(config, scanInfo.getLibs()).keys()];
|
|
1121
|
-
const notice = DesktopServerStage.imageStepsNotice(config, carried);
|
|
1122
|
-
if (notice)
|
|
1123
|
-
this.app.logger[notice.level](notice.message);
|
|
1124
|
-
return {
|
|
1125
|
-
dir: this.dir,
|
|
1126
|
-
entry: "main.js",
|
|
1127
|
-
env: DesktopServerStage.env(config, environment, run),
|
|
1128
|
-
...run.restart !== undefined ? { restart: run.restart } : {},
|
|
1129
|
-
...run.watchdog ? { watchdog: run.watchdog } : {}
|
|
1130
|
-
};
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
// pkgs/@akanjs/devkit/mobile/updateUploader.ts
|
|
1134
|
-
import path9 from "path";
|
|
1135
|
-
|
|
1136
|
-
class UpdateUploader {
|
|
1137
|
-
bucket;
|
|
1138
|
-
prefix;
|
|
1139
|
-
static #manifest = /\.json(?:\.sig)?$/;
|
|
1140
|
-
static parseTarget(target) {
|
|
1141
|
-
const match = /^s3:\/\/([^/]+)\/?(.*)$/.exec(target.trim());
|
|
1142
|
-
if (!match?.[1])
|
|
1143
|
-
throw new Error(`--upload takes s3://<bucket>/<prefix>, not ${JSON.stringify(target)}.`);
|
|
1144
|
-
return { bucket: match[1], prefix: (match[2] ?? "").replace(/\/+$/, "") };
|
|
1145
|
-
}
|
|
1146
|
-
static order(files) {
|
|
1147
|
-
const manifests = files.filter((file) => UpdateUploader.#manifest.test(file));
|
|
1148
|
-
return {
|
|
1149
|
-
releases: files.filter((file) => !UpdateUploader.#manifest.test(file)).sort((a, b) => a.localeCompare(b)),
|
|
1150
|
-
manifests: manifests.sort((a, b) => Number(a.endsWith(".json")) - Number(b.endsWith(".json")) || a.localeCompare(b))
|
|
1151
|
-
};
|
|
1152
|
-
}
|
|
1153
|
-
constructor(bucket, prefix) {
|
|
1154
|
-
this.bucket = bucket;
|
|
1155
|
-
this.prefix = prefix;
|
|
1156
|
-
}
|
|
1157
|
-
static forTarget(target) {
|
|
1158
|
-
const { bucket, prefix } = UpdateUploader.parseTarget(target);
|
|
1159
|
-
return new UpdateUploader(new Bun.S3Client({ bucket }), prefix);
|
|
1160
|
-
}
|
|
1161
|
-
async upload(dir) {
|
|
1162
|
-
const files = [];
|
|
1163
|
-
for await (const file of new Bun.Glob("**/*").scan({ cwd: dir, onlyFiles: true })) {
|
|
1164
|
-
if (!file.endsWith(".tmp"))
|
|
1165
|
-
files.push(file.split(path9.sep).join("/"));
|
|
1166
|
-
}
|
|
1167
|
-
const { releases, manifests } = UpdateUploader.order(files);
|
|
1168
|
-
const keyOf = (file) => this.prefix ? `${this.prefix}/${file}` : file;
|
|
1169
|
-
let uploaded = 0;
|
|
1170
|
-
let skipped = 0;
|
|
1171
|
-
for (const file of releases) {
|
|
1172
|
-
if (await this.bucket.exists(keyOf(file))) {
|
|
1173
|
-
skipped++;
|
|
1174
|
-
continue;
|
|
1175
|
-
}
|
|
1176
|
-
await this.bucket.write(keyOf(file), Bun.file(path9.join(dir, file)));
|
|
1177
|
-
uploaded++;
|
|
1178
|
-
}
|
|
1179
|
-
for (const file of manifests) {
|
|
1180
|
-
const type = file.endsWith(".json") ? "application/json" : "text/plain";
|
|
1181
|
-
await this.bucket.write(keyOf(file), Bun.file(path9.join(dir, file)), { type });
|
|
1182
|
-
uploaded++;
|
|
1183
|
-
}
|
|
1184
|
-
return { uploaded, skipped };
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
export { DesktopServerStage, NativeApp, isPlaceholderAppId, UpdateUploader };
|
|
1257
|
+
export { DesktopBin, NativePublicEnv, UpdatePackFolder, NativeApp };
|