@akanjs/cli 3.1.0-rc.1 → 3.1.0-rc.11
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-rkjyxgec.js} +2 -2
- package/{CodeTui-21mtze3j.js → CodeTui-h7pyj6xa.js} +1 -1
- package/TunnelShare-661cxz5e.js +14 -0
- package/{agent.command-f3gsc1gd.js → agent.command-3qxf4xcj.js} +5 -5
- package/akanCode.js +2 -2
- package/{application.command-5s780vnz.js → application.command-m1kcd7jf.js} +54 -42
- package/{applicationBuildRunner-1yaqsrdq.js → applicationBuildRunner-dj0xkrep.js} +26 -33
- package/buildBatch.proc.js +24 -15
- package/{cloud.command-yj6xb2a7.js → cloud.command-9cp1t0f5.js} +60 -67
- package/cloud.script-dgstqt1m.js +24 -0
- package/{code.command-3wkn4ja5.js → code.command-0ad6gw2a.js} +6 -6
- package/{context.command-9jjn3gcw.js → context.command-0qx447mb.js} +17 -13
- package/context.runner-1vsjztjg.js +25 -0
- package/{dependencyScanner-06z2dmrr.js → dependencyScanner-3afj2vt1.js} +1 -1
- package/{guideline.command-dtczrqpf.js → guideline.command-cv5bmrx2.js} +2 -2
- package/guidelines/agentRule/agentRule.instruction.md +3 -3
- package/guidelines/conventions/conventions.instruction.md +457 -1071
- package/guidelines/cssRule/cssRule.instruction.md +28 -7
- package/guidelines/fieldRule/fieldRule.instruction.md +62 -0
- package/guidelines/framework/framework.instruction.md +4 -7
- package/guidelines/mcpRule/mcpRule.instruction.md +43 -20
- package/guidelines/modelConstant/modelConstant.instruction.md +2 -1
- package/guidelines/modelStore/modelStore.instruction.md +9 -0
- package/guidelines/pageRule/pageRule.instruction.md +80 -0
- package/guidelines/queryRule/queryRule.instruction.md +44 -11
- package/guidelines/runtimeRule/runtimeRule.instruction.md +113 -17
- package/guidelines/ssrRule/ssrRule.instruction.md +13 -0
- package/guidelines/transportRule/transportRule.instruction.md +135 -13
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +4 -4
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +68 -7
- package/incrementalBuilder.proc.js +10 -7
- package/index-192ha4xh.js +1173 -0
- package/{index-c76k723e.js → index-1p98exm2.js} +2 -2
- package/{index-rwq3ekqc.js → index-1xj2t7cb.js} +7 -7
- package/{index-rtct7d43.js → index-20bxxyvn.js} +80 -80
- package/{index-qt22xstj.js → index-35hc95h5.js} +5 -3
- package/{index-8r4d7cmt.js → index-4mhy6qme.js} +4 -4
- package/{index-ww9bhp67.js → index-4pf8g7ff.js} +1 -1
- package/{index-2mx692qq.js → index-5edzv8fn.js} +5 -5
- package/{index-k9rrten6.js → index-5nd1wtkv.js} +92 -94
- package/index-62md4dwy.js +260 -0
- package/index-665zp9rs.js +271 -0
- package/{index-2etcv0sc.js → index-86jt2a0e.js} +1 -1
- package/{index-krq7btbt.js → index-8fp3b47h.js} +419 -418
- package/{index-c2yrsxdh.js → index-9z426msb.js} +109 -328
- package/{index-ctanya41.js → index-awbdfcf2.js} +4 -4
- package/{index-3hx1z96c.js → index-bqbf654f.js} +1 -1
- package/{index-rc5rwyps.js → index-bspm21y1.js} +71 -60
- package/index-cpb3nvma.js +667 -0
- package/{index-jx4bzvwg.js → index-d47ypebv.js} +97 -55
- package/{index-ae5pwhgs.js → index-d72hfmdm.js} +15 -13
- package/{index-e0cvze1s.js → index-fmzcs8v6.js} +4 -4
- package/{index-8ggztp8k.js → index-fyq1z6kt.js} +68 -201
- package/{index-1w2qaee2.js → index-g1mbppa9.js} +331 -259
- package/{index-gnyf1fqy.js → index-j237xtew.js} +6 -6
- package/{index-fm46xrf1.js → index-j4mkhz93.js} +3 -3
- package/{index-6jjtnhkw.js → index-jw1zm273.js} +74 -34
- package/{index-q958tqqg.js → index-m9jj544v.js} +1 -1
- package/index-mdjc93e1.js +33 -0
- package/{index-c1x0be5j.js → index-pydbnfez.js} +2 -2
- package/{index-56d2g8ms.js → index-qwa5k1j8.js} +1 -1
- package/{index-v29746vb.js → index-tamvyvj3.js} +3 -3
- package/{index-05g3jayg.js → index-trpasv9q.js} +260 -259
- package/{index-2ahj896a.js → index-tvehn2sp.js} +1 -1
- package/{index-55mynnqs.js → index-va06nr10.js} +1434 -1173
- package/index-vk0pa0mn.js +240 -0
- package/{index-aprt7pkz.js → index-w44cy4vs.js} +2271 -3369
- package/index.js +22 -22
- package/{library.command-gaga5bmq.js → library.command-erp2f56n.js} +3 -3
- package/{localRegistry.command-vsreyy42.js → localRegistry.command-y176p558.js} +15 -11
- package/{module.command-7vpf6db4.js → module.command-gs2r615e.js} +5 -5
- package/{package.command-twmvvf69.js → package.command-j10r76ax.js} +4 -4
- package/package.json +21 -21
- package/{page.command-5asq7fdf.js → page.command-4h4a8pnh.js} +3 -3
- package/{pipeline.command-d0v38572.js → pipeline.command-a2nrk26m.js} +221 -220
- package/{primitive.command-dyrs2fm8.js → primitive.command-w6t0asqz.js} +6 -6
- package/{projectTypecheck-cvtkrzp2.js → projectTypecheck-f534enqj.js} +1 -1
- package/{quality.command-6emsqpbg.js → quality.command-77x7vbbx.js} +4 -4
- package/{repair.command-8t6aegv5.js → repair.command-y6sstev7.js} +5 -5
- package/{scalar.command-bnggy1x7.js → scalar.command-20b8cy0k.js} +4 -4
- package/{subspace.command-md2teygq.js → subspace.command-5c7yasd7.js} +424 -364
- package/templates/appSample/lib/task/Task.Util.tsx +7 -7
- package/templates/appSample/lib/task/Task.Zone.tsx +1 -1
- package/templates/appSample/ui/QuantityControl.tsx +2 -2
- package/templates/crudSinglePage/page.tsx +1 -1
- package/templates/lib/cnst.ts +0 -1
- package/templates/workspaceRoot/.gitignore.template +23 -52
- package/templates/workspaceRoot/biome.json.template +1 -1
- package/{tunnel.command-jx3tdm2d.js → tunnel.command-2kw8vt9g.js} +8 -4
- package/{workflow.command-jx0wag5r.js → workflow.command-2djwmpqf.js} +10 -10
- package/{workspace.command-yxd7rr7g.js → workspace.command-6qfgq4b4.js} +38 -34
- 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-n68jx79w.js +0 -120
- package/index-pgj2rt78.js +0 -404
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
import {
|
|
3
3
|
binPlatforms,
|
|
4
4
|
ServerEnvProfile,
|
|
5
|
-
Executor
|
|
6
|
-
|
|
5
|
+
Executor,
|
|
6
|
+
WorkspaceExecutor
|
|
7
|
+
} from "./index-va06nr10.js";
|
|
7
8
|
import {
|
|
8
9
|
resolveAppId,
|
|
9
10
|
targetHtmlFilename
|
|
10
|
-
} from "./index-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import
|
|
11
|
+
} from "./index-m9jj544v.js";
|
|
12
|
+
import {
|
|
13
|
+
ExitCodeError
|
|
14
|
+
} from "./index-jw1zm273.js";
|
|
15
|
+
import {
|
|
16
|
+
CsrArtifactBuilder
|
|
17
|
+
} from "./index-665zp9rs.js";
|
|
15
18
|
|
|
16
19
|
// pkgs/@akanjs/devkit/mobile/desktopBin.ts
|
|
17
20
|
import { chmod, mkdir, rename, rm, writeFile } from "fs/promises";
|
|
@@ -138,6 +141,7 @@ import path2 from "path";
|
|
|
138
141
|
|
|
139
142
|
class NativeApi {
|
|
140
143
|
static supportedMajor = 0;
|
|
144
|
+
static minimumVersion = "0.12.0";
|
|
141
145
|
static entryOf(appDir) {
|
|
142
146
|
const workspaceEntry = NativeApi.#resolve("@akanjs/native/api", appDir);
|
|
143
147
|
if (workspaceEntry)
|
|
@@ -153,8 +157,8 @@ class NativeApi {
|
|
|
153
157
|
throw new Error(`The installed akanjs has no native build at ${entry}; update akanjs.`);
|
|
154
158
|
const api = await import(entry);
|
|
155
159
|
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.`);
|
|
160
|
+
if (major !== NativeApi.supportedMajor || Bun.semver.order(api.API_VERSION, NativeApi.minimumVersion) < 0)
|
|
161
|
+
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
162
|
return api;
|
|
159
163
|
}
|
|
160
164
|
static #resolve(specifier, from) {
|
|
@@ -166,6 +170,10 @@ class NativeApi {
|
|
|
166
170
|
}
|
|
167
171
|
}
|
|
168
172
|
|
|
173
|
+
// pkgs/@akanjs/devkit/mobile/nativeApp.ts
|
|
174
|
+
import path8 from "path";
|
|
175
|
+
import { toError } from "akanjs/common";
|
|
176
|
+
|
|
169
177
|
// pkgs/@akanjs/devkit/mobile/nativeAppLine.ts
|
|
170
178
|
class NativeAppLine {
|
|
171
179
|
static #page = /^\[page(\+)?(?:#(\d+))? (trace|verbose|debug|log|info|warn|error)\] ?(.*)$/;
|
|
@@ -224,6 +232,7 @@ class NativeAppLine {
|
|
|
224
232
|
// pkgs/@akanjs/devkit/mobile/nativeConfig.ts
|
|
225
233
|
import { readFileSync } from "fs";
|
|
226
234
|
import path3 from "path";
|
|
235
|
+
import { trimSlashes } from "akanjs/common";
|
|
227
236
|
|
|
228
237
|
// pkgs/@akanjs/devkit/mobile/usageDescriptions.ts
|
|
229
238
|
import { capitalize } from "akanjs/common";
|
|
@@ -425,7 +434,7 @@ class NativeConfig {
|
|
|
425
434
|
return url && keys ? { ...rest, url, ...keys, ...channel ? { channel } : {} } : undefined;
|
|
426
435
|
}
|
|
427
436
|
static fileNameOf(name) {
|
|
428
|
-
return name.replace(/[^A-Za-z0-9._-]/g, "-")
|
|
437
|
+
return /[^-.](?:.*[^-.])?/.exec(name.replace(/[^A-Za-z0-9._-]/g, "-"))?.[0] ?? "app";
|
|
429
438
|
}
|
|
430
439
|
static #pluginId(appPath, spec) {
|
|
431
440
|
if (NativeConfig.#isId(spec))
|
|
@@ -448,7 +457,7 @@ class NativeConfig {
|
|
|
448
457
|
return Object.assign({}, ...contributed, own);
|
|
449
458
|
}
|
|
450
459
|
static #deepLinks(target, locales) {
|
|
451
|
-
const basePath = target.basePath
|
|
460
|
+
const basePath = target.basePath ? trimSlashes(target.basePath) : "";
|
|
452
461
|
const pathPrefixes = basePath ? locales.map((locale) => `/${locale}/${basePath}`) : undefined;
|
|
453
462
|
return {
|
|
454
463
|
...target.deepLinks?.schemes?.length ? { schemes: target.deepLinks.schemes } : {},
|
|
@@ -500,9 +509,109 @@ class NativePluginFolders {
|
|
|
500
509
|
}
|
|
501
510
|
}
|
|
502
511
|
|
|
512
|
+
// pkgs/@akanjs/devkit/mobile/nativePublicEnv.ts
|
|
513
|
+
import { createHash } from "crypto";
|
|
514
|
+
class NativePublicEnv {
|
|
515
|
+
static derivedKeys = [
|
|
516
|
+
"AKAN_PUBLIC_APP_NAME",
|
|
517
|
+
"AKAN_PUBLIC_BASE_PATHS",
|
|
518
|
+
"AKAN_PUBLIC_API_PREFIX",
|
|
519
|
+
"AKAN_PUBLIC_WS_PREFIX",
|
|
520
|
+
"AKAN_PUBLIC_RENDER_ENV",
|
|
521
|
+
"AKAN_PUBLIC_DEFAULT_LOCALE",
|
|
522
|
+
"AKAN_PUBLIC_LOCALES"
|
|
523
|
+
];
|
|
524
|
+
static #keyPattern = /^AKAN_PUBLIC_[A-Z0-9_]{1,128}$/;
|
|
525
|
+
static #secretWords = ["SECRET", "TOKEN", "PASSWORD", "PRIVATE", "CREDENTIAL"];
|
|
526
|
+
static #maxKeys = 256;
|
|
527
|
+
static #maxBytes = 64 * 1024;
|
|
528
|
+
static of(env) {
|
|
529
|
+
return Object.fromEntries(Object.keys(env).filter((key) => key.startsWith("AKAN_PUBLIC_") && !NativePublicEnv.derivedKeys.includes(key)).sort().map((key) => [key, String(env[key])]));
|
|
530
|
+
}
|
|
531
|
+
static hash(publicEnv) {
|
|
532
|
+
return createHash("sha256").update(JSON.stringify(NativePublicEnv.of(publicEnv))).digest("hex");
|
|
533
|
+
}
|
|
534
|
+
static problems(publicEnv) {
|
|
535
|
+
const keys = Object.keys(publicEnv);
|
|
536
|
+
const problems = keys.flatMap((key) => {
|
|
537
|
+
const value = publicEnv[key];
|
|
538
|
+
if (!NativePublicEnv.#keyPattern.test(key))
|
|
539
|
+
return [`${key} is not an AKAN_PUBLIC_[A-Z0-9_] name`];
|
|
540
|
+
const word = NativePublicEnv.#secretWords.find((secret) => key.includes(secret));
|
|
541
|
+
if (word)
|
|
542
|
+
return [`${key} names a ${word.toLowerCase()}, which a page bundle must not carry`];
|
|
543
|
+
if (typeof value !== "string")
|
|
544
|
+
return [`${key} is not a string`];
|
|
545
|
+
if (/[\u0000-\u001f\u007f]/.test(value))
|
|
546
|
+
return [`${key} holds a control character`];
|
|
547
|
+
return [];
|
|
548
|
+
});
|
|
549
|
+
if (keys.length > NativePublicEnv.#maxKeys)
|
|
550
|
+
problems.push(`${keys.length} keys, more than ${NativePublicEnv.#maxKeys}`);
|
|
551
|
+
const bytes = Buffer.byteLength(JSON.stringify(publicEnv));
|
|
552
|
+
if (bytes > NativePublicEnv.#maxBytes)
|
|
553
|
+
problems.push(`${bytes} bytes, more than ${NativePublicEnv.#maxBytes}`);
|
|
554
|
+
return problems;
|
|
555
|
+
}
|
|
556
|
+
static diff(a, b) {
|
|
557
|
+
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);
|
|
558
|
+
}
|
|
559
|
+
static async built(app) {
|
|
560
|
+
const file = Bun.file(CsrArtifactBuilder.publicEnvPath(app));
|
|
561
|
+
if (!await file.exists())
|
|
562
|
+
return null;
|
|
563
|
+
return NativePublicEnv.of(await file.json());
|
|
564
|
+
}
|
|
565
|
+
static async recordedIn(bundleFile) {
|
|
566
|
+
const file = Bun.file(bundleFile);
|
|
567
|
+
if (!await file.exists())
|
|
568
|
+
return null;
|
|
569
|
+
const bundle = await file.json();
|
|
570
|
+
return bundle.web?.publicEnv ?? null;
|
|
571
|
+
}
|
|
572
|
+
static async load(file) {
|
|
573
|
+
const source = Bun.file(file);
|
|
574
|
+
if (!await source.exists())
|
|
575
|
+
throw new Error(`--public-env ${file} does not exist.`);
|
|
576
|
+
const json = await source.json().catch(() => null);
|
|
577
|
+
if (!json || typeof json !== "object" || Array.isArray(json))
|
|
578
|
+
throw new Error(`--public-env ${file} is not a JSON object of AKAN_PUBLIC_* values or a bundle.json.`);
|
|
579
|
+
const isBundle = json.schema === 1 && typeof json.web === "object";
|
|
580
|
+
const map = isBundle ? json.web.publicEnv : json;
|
|
581
|
+
if (!map)
|
|
582
|
+
throw new Error(`--public-env ${file} is a bundle.json from a build that recorded no public env.`);
|
|
583
|
+
const derived = Object.keys(map).filter((key) => NativePublicEnv.derivedKeys.includes(key));
|
|
584
|
+
if (derived.length)
|
|
585
|
+
throw new Error(`--public-env ${file} sets ${derived.join(", ")}, which akan.config.ts decides.`);
|
|
586
|
+
const problems = NativePublicEnv.problems(map);
|
|
587
|
+
if (problems.length)
|
|
588
|
+
throw new Error(`--public-env ${file} is refused:
|
|
589
|
+
- ${problems.join(`
|
|
590
|
+
- `)}`);
|
|
591
|
+
return NativePublicEnv.of(map);
|
|
592
|
+
}
|
|
593
|
+
static pin(publicEnv) {
|
|
594
|
+
for (const key of Object.keys(process.env))
|
|
595
|
+
if (key.startsWith("AKAN_PUBLIC_") && !NativePublicEnv.derivedKeys.includes(key))
|
|
596
|
+
delete process.env[key];
|
|
597
|
+
Object.assign(process.env, publicEnv);
|
|
598
|
+
WorkspaceExecutor.publicEnvPin = publicEnv;
|
|
599
|
+
}
|
|
600
|
+
static async problemAgainst(shipped, bundle) {
|
|
601
|
+
const [before, now] = await Promise.all([NativePublicEnv.recordedIn(shipped), NativePublicEnv.recordedIn(bundle)]);
|
|
602
|
+
if (!before)
|
|
603
|
+
return null;
|
|
604
|
+
const differs = NativePublicEnv.diff(before, now ?? {});
|
|
605
|
+
if (!differs.length)
|
|
606
|
+
return null;
|
|
607
|
+
return `public env ${differs.join(", ")} differs from the store build's (pass --public-env ${shipped} to repeat it)`;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
503
611
|
// pkgs/@akanjs/devkit/mobile/nativeWebDir.ts
|
|
504
612
|
import { cp, mkdir as mkdir2, rm as rm2, stat } from "fs/promises";
|
|
505
613
|
import path5 from "path";
|
|
614
|
+
import { trimSlashes as trimSlashes2 } from "akanjs/common";
|
|
506
615
|
|
|
507
616
|
class NativeWebDir {
|
|
508
617
|
dir;
|
|
@@ -513,13 +622,13 @@ class NativeWebDir {
|
|
|
513
622
|
static injectTarget(html, target) {
|
|
514
623
|
if (html.includes(NativeWebDir.#targetTag))
|
|
515
624
|
return html;
|
|
516
|
-
const basePath = target.basePath
|
|
625
|
+
const basePath = target.basePath ? trimSlashes2(target.basePath) : "";
|
|
517
626
|
const script = `${NativeWebDir.#targetTag}${JSON.stringify({
|
|
518
627
|
name: target.name,
|
|
519
628
|
basePath,
|
|
520
629
|
indexPath: target.indexPath
|
|
521
630
|
})};</script>`;
|
|
522
|
-
const head = /<head(?:\s[
|
|
631
|
+
const head = /<head(?:\s[^<>]*)?>/i.exec(html);
|
|
523
632
|
if (!head)
|
|
524
633
|
return `${script}
|
|
525
634
|
${html}`;
|
|
@@ -562,14 +671,14 @@ class ServerBundleSecrets {
|
|
|
562
671
|
workspaceRoot;
|
|
563
672
|
appDir;
|
|
564
673
|
static #largestFile = 64 * 1024 * 1024;
|
|
674
|
+
static #printer = new Bun.Transpiler({ loader: "js", target: "bun" });
|
|
675
|
+
static #notSource = new Set(["node_modules", "env", "private", "public", "dist"]);
|
|
565
676
|
constructor(workspaceRoot, appDir) {
|
|
566
677
|
this.workspaceRoot = workspaceRoot;
|
|
567
678
|
this.appDir = appDir;
|
|
568
679
|
}
|
|
569
680
|
async envFiles() {
|
|
570
|
-
const
|
|
571
|
-
const libs = await readdir2(libsDir).catch(() => []);
|
|
572
|
-
const dirs = [path6.join(this.appDir, "env"), ...libs.map((lib) => path6.join(libsDir, lib, "env"))];
|
|
681
|
+
const dirs = [path6.join(this.appDir, "env"), ...(await this.#libDirs()).map((lib) => path6.join(lib, "env"))];
|
|
573
682
|
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))));
|
|
574
683
|
return files.flat();
|
|
575
684
|
}
|
|
@@ -584,24 +693,52 @@ class ServerBundleSecrets {
|
|
|
584
693
|
unread.push(ServerBundleSecrets.#relative(this.workspaceRoot, file));
|
|
585
694
|
}
|
|
586
695
|
}
|
|
696
|
+
if (!secrets.length)
|
|
697
|
+
return unread;
|
|
698
|
+
const sought = secrets.map((secret) => ({ secret, needles: ServerBundleSecrets.#needles(secret.value) }));
|
|
587
699
|
const found = [];
|
|
588
700
|
for (const bundled of await ServerBundleSecrets.#files(bundleDir)) {
|
|
589
701
|
const text = await readFile(bundled, "latin1");
|
|
590
|
-
for (const secret of
|
|
591
|
-
if (
|
|
592
|
-
found.push(
|
|
702
|
+
for (const { secret, needles } of sought)
|
|
703
|
+
if (needles.some((needle) => text.includes(needle)))
|
|
704
|
+
found.push({ bundled, secret });
|
|
593
705
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
706
|
+
const coded = found.length ? await this.#coded(found.map(({ secret }) => secret.value)) : new Set;
|
|
707
|
+
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}`);
|
|
708
|
+
if (leaks.length)
|
|
709
|
+
throw new Error(`The server this app carries holds secret values of the workspace's env files, so the release stops here:
|
|
710
|
+
${[...new Set(leaks)].join(`
|
|
597
711
|
`)}
|
|
598
|
-
Build its server with a profile (native.desktop.server.profile), whose bundle carries
|
|
712
|
+
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
713
|
return unread;
|
|
600
714
|
}
|
|
715
|
+
async#coded(values) {
|
|
716
|
+
const plain = [...new Set(values)].filter((value) => !ServerEnvProfile.distinctive(value));
|
|
717
|
+
const coded = new Set;
|
|
718
|
+
if (!plain.length)
|
|
719
|
+
return coded;
|
|
720
|
+
for (const dir of [this.appDir, ...await this.#libDirs()])
|
|
721
|
+
for (const file of await ServerBundleSecrets.#sources(dir)) {
|
|
722
|
+
const text = await readFile(file, "utf8");
|
|
723
|
+
for (const value of plain)
|
|
724
|
+
if (text.includes(value))
|
|
725
|
+
coded.add(value);
|
|
726
|
+
}
|
|
727
|
+
return coded;
|
|
728
|
+
}
|
|
729
|
+
async#libDirs() {
|
|
730
|
+
const libsDir = path6.join(this.workspaceRoot, "libs");
|
|
731
|
+
return (await readdir2(libsDir).catch(() => [])).map((lib) => path6.join(libsDir, lib));
|
|
732
|
+
}
|
|
601
733
|
static #needles(value) {
|
|
602
734
|
const escaped = JSON.stringify(value).slice(1, -1);
|
|
603
735
|
const line = value.split(/\r?\n/).find((part) => part.length >= 32);
|
|
604
|
-
|
|
736
|
+
const forms = [value, escaped, ServerBundleSecrets.#printed(value), ...line ? [line] : []];
|
|
737
|
+
return [...new Set(forms.map((form) => Buffer.from(form, "utf8").toString("latin1")))];
|
|
738
|
+
}
|
|
739
|
+
static #printed(value) {
|
|
740
|
+
const code = ServerBundleSecrets.#printer.transformSync(`export default ${JSON.stringify(value)};`);
|
|
741
|
+
return /^export default (["'`])([\s\S]*)\1;?\s*$/.exec(code)?.[2] ?? value;
|
|
605
742
|
}
|
|
606
743
|
static #relative(from, to) {
|
|
607
744
|
return path6.relative(from, to).split(path6.sep).join("/");
|
|
@@ -618,6 +755,102 @@ Build its server with a profile (native.desktop.server.profile), whose bundle ca
|
|
|
618
755
|
}
|
|
619
756
|
return files;
|
|
620
757
|
}
|
|
758
|
+
static async#sources(dir) {
|
|
759
|
+
const files = [];
|
|
760
|
+
for (const entry of await readdir2(dir, { withFileTypes: true }).catch(() => [])) {
|
|
761
|
+
if (entry.name.startsWith("."))
|
|
762
|
+
continue;
|
|
763
|
+
const file = path6.join(dir, entry.name);
|
|
764
|
+
if (entry.isDirectory()) {
|
|
765
|
+
if (!ServerBundleSecrets.#notSource.has(entry.name))
|
|
766
|
+
files.push(...await ServerBundleSecrets.#sources(file));
|
|
767
|
+
} else if (/\.(?:[cm]?[jt]sx?|json)$/.test(entry.name) && !/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(entry.name))
|
|
768
|
+
files.push(file);
|
|
769
|
+
}
|
|
770
|
+
return files;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// pkgs/@akanjs/devkit/mobile/updatePackFolder.ts
|
|
775
|
+
import { mkdtemp, rm as rm3 } from "fs/promises";
|
|
776
|
+
import os from "os";
|
|
777
|
+
import path7 from "path";
|
|
778
|
+
class UpdatePackFolder {
|
|
779
|
+
static packFile = "pack.json";
|
|
780
|
+
static compatFile = "compat.json";
|
|
781
|
+
static bundleFile = "bundle.json";
|
|
782
|
+
static templateFile = "manifest.template.json";
|
|
783
|
+
static filesDir = "files";
|
|
784
|
+
static #records = [
|
|
785
|
+
UpdatePackFolder.templateFile,
|
|
786
|
+
UpdatePackFolder.bundleFile,
|
|
787
|
+
UpdatePackFolder.packFile,
|
|
788
|
+
UpdatePackFolder.compatFile
|
|
789
|
+
];
|
|
790
|
+
static #sha256 = /^[0-9a-f]{64}$/;
|
|
791
|
+
static async clear(dir) {
|
|
792
|
+
await Promise.all([UpdatePackFolder.packFile, UpdatePackFolder.compatFile].map((name) => rm3(path7.join(dir, name), { force: true })));
|
|
793
|
+
}
|
|
794
|
+
static async read(dir) {
|
|
795
|
+
const file = Bun.file(path7.join(dir, UpdatePackFolder.packFile));
|
|
796
|
+
if (!await file.exists())
|
|
797
|
+
throw new Error(`${dir} holds no ${UpdatePackFolder.packFile}: give the folder \`akan pack-update\` wrote.`);
|
|
798
|
+
const pack = await file.json();
|
|
799
|
+
if (pack.schema !== 1)
|
|
800
|
+
throw new Error(`${dir}/${UpdatePackFolder.packFile} is schema ${pack.schema}, and this akan reads schema 1.`);
|
|
801
|
+
return { ...pack, out: path7.resolve(dir) };
|
|
802
|
+
}
|
|
803
|
+
static async write({ out, ...record }) {
|
|
804
|
+
await Bun.write(path7.join(out, UpdatePackFolder.packFile), `${JSON.stringify(record, null, 2)}
|
|
805
|
+
`);
|
|
806
|
+
}
|
|
807
|
+
static async writeCompat(pack, compat) {
|
|
808
|
+
pack.compat = compat;
|
|
809
|
+
await Bun.write(path7.join(pack.out, UpdatePackFolder.compatFile), `${JSON.stringify(compat, null, 2)}
|
|
810
|
+
`);
|
|
811
|
+
}
|
|
812
|
+
static async readBundle(file) {
|
|
813
|
+
return await Bun.file(file).json();
|
|
814
|
+
}
|
|
815
|
+
static storeBuildVerdict({ out, compat }, then) {
|
|
816
|
+
if (!compat || compat.compatible)
|
|
817
|
+
return null;
|
|
818
|
+
return new ExitCodeError(`The pack in ${out} needs a new store build; it differs from the one ${compat.against} came from:
|
|
819
|
+
- ${compat.problems.join(`
|
|
820
|
+
- `)}${then ? `
|
|
821
|
+
${then}` : ""}`, 2);
|
|
822
|
+
}
|
|
823
|
+
static async withArchive(pack, use) {
|
|
824
|
+
const { templateFile, filesDir } = UpdatePackFolder;
|
|
825
|
+
const template = Bun.file(path7.join(pack.out, templateFile));
|
|
826
|
+
if (!await template.exists())
|
|
827
|
+
throw new Error(`${pack.out} holds no ${templateFile}: give the folder \`akan pack-update\` wrote.`);
|
|
828
|
+
const { files = [] } = await template.json();
|
|
829
|
+
const hashes = [...new Set(files.map((file) => file.sha256))];
|
|
830
|
+
if (!hashes.every((hash) => typeof hash === "string" && UpdatePackFolder.#sha256.test(hash)))
|
|
831
|
+
throw new Error(`${pack.out}/${templateFile} lists a file whose sha256 is not 64 hex digits.`);
|
|
832
|
+
const present = await Promise.all(UpdatePackFolder.#records.map((name) => Bun.file(path7.join(pack.out, name)).exists()));
|
|
833
|
+
const entries = [
|
|
834
|
+
...UpdatePackFolder.#records.filter((_, idx) => present[idx]),
|
|
835
|
+
...hashes.map((hash) => `${filesDir}/${hash}`)
|
|
836
|
+
];
|
|
837
|
+
const dir = await mkdtemp(path7.join(os.tmpdir(), "akan-pack-"));
|
|
838
|
+
try {
|
|
839
|
+
const tar = Bun.spawn(["tar", "-czf", "pack.tgz", "-C", pack.out, "-T", "-"], {
|
|
840
|
+
cwd: dir,
|
|
841
|
+
stdin: new Blob([entries.map((entry) => `./${entry}
|
|
842
|
+
`).join("")]),
|
|
843
|
+
stdout: "ignore",
|
|
844
|
+
stderr: "pipe"
|
|
845
|
+
});
|
|
846
|
+
const [code, stderr] = await Promise.all([tar.exited, new Response(tar.stderr).text()]);
|
|
847
|
+
if (code !== 0)
|
|
848
|
+
throw new Error(`tar of ${pack.out} failed: ${stderr.trim()}`);
|
|
849
|
+
return await use(path7.join(dir, "pack.tgz"));
|
|
850
|
+
} finally {
|
|
851
|
+
await rm3(dir, { recursive: true, force: true });
|
|
852
|
+
}
|
|
853
|
+
}
|
|
621
854
|
}
|
|
622
855
|
|
|
623
856
|
// pkgs/@akanjs/devkit/mobile/nativeApp.ts
|
|
@@ -632,14 +865,14 @@ class NativeApp {
|
|
|
632
865
|
this.app = app;
|
|
633
866
|
this.target = target;
|
|
634
867
|
this.env = env;
|
|
635
|
-
this.targetRoot =
|
|
636
|
-
this.web = new NativeWebDir(
|
|
868
|
+
this.targetRoot = path8.join(app.workspace.workspaceRoot, "dist", "native", app.name, target.name);
|
|
869
|
+
this.web = new NativeWebDir(path8.join(this.targetRoot, "web"));
|
|
637
870
|
}
|
|
638
871
|
outDir(platform) {
|
|
639
|
-
return
|
|
872
|
+
return path8.join(this.targetRoot, "build", platform);
|
|
640
873
|
}
|
|
641
874
|
devOutDir(platform) {
|
|
642
|
-
return
|
|
875
|
+
return path8.join(this.targetRoot, "dev", platform);
|
|
643
876
|
}
|
|
644
877
|
indexPath(platform) {
|
|
645
878
|
const { config } = this.target;
|
|
@@ -649,13 +882,13 @@ class NativeApp {
|
|
|
649
882
|
async assembleWeb(platform) {
|
|
650
883
|
const dist = this.app.dist.cwdPath;
|
|
651
884
|
return await this.web.assemble({ ...this.target.config, indexPath: this.indexPath(platform) }, {
|
|
652
|
-
html:
|
|
653
|
-
publicDir:
|
|
654
|
-
fontsDir:
|
|
885
|
+
html: path8.join(dist, "csr", targetHtmlFilename(this.target.config)),
|
|
886
|
+
publicDir: path8.join(dist, "public"),
|
|
887
|
+
fontsDir: path8.join(dist, ".akan", "artifact", "fonts")
|
|
655
888
|
});
|
|
656
889
|
}
|
|
657
890
|
get binDir() {
|
|
658
|
-
return
|
|
891
|
+
return path8.join(this.targetRoot, "bin");
|
|
659
892
|
}
|
|
660
893
|
async config({
|
|
661
894
|
server,
|
|
@@ -739,16 +972,17 @@ class NativeApp {
|
|
|
739
972
|
NativeApp.#assertServerPlatform(platform, server);
|
|
740
973
|
NativeApp.assertInstaller(platform, installer);
|
|
741
974
|
await this.assembleWeb(platform);
|
|
975
|
+
const publicEnv = await this.#publicEnv();
|
|
742
976
|
const { api, config } = await this.prepare(platform, server, arch);
|
|
743
977
|
const distribution = await this.#distribution(api, platform, profile, { sign, notarize }, server);
|
|
744
|
-
return await api.build({
|
|
978
|
+
return await this.#recorded(this.outDir(platform), publicEnv, api.build({
|
|
745
979
|
...this.#task(platform, config, { env: await this.#runtimeEnv(server) }),
|
|
746
980
|
profile,
|
|
747
981
|
...arch ? { arch } : {},
|
|
748
982
|
...platform === "windows" ? { windows: { ...installer ? { installer } : {}, ...distribution.windows } } : {},
|
|
749
983
|
...platform === "macos" ? { macos: { ...distribution.macos, ...installer ? { dmg: true } : {} } } : {},
|
|
750
984
|
...platform === "linux" && installer ? { linux: { appImage: true } } : {}
|
|
751
|
-
});
|
|
985
|
+
}));
|
|
752
986
|
}
|
|
753
987
|
async run(platform, { device, teamId, profile = "debug", server } = {}) {
|
|
754
988
|
NativeApp.#assertServerPlatform(platform, server);
|
|
@@ -804,22 +1038,25 @@ class NativeApp {
|
|
|
804
1038
|
}
|
|
805
1039
|
async releaseIos({ teamId, adHoc = false } = {}) {
|
|
806
1040
|
await this.assembleWeb("ios");
|
|
1041
|
+
const publicEnv = await this.#publicEnv();
|
|
807
1042
|
const { api, config } = await this.prepare("ios");
|
|
808
|
-
return await api.release({
|
|
1043
|
+
return await this.#recorded(this.outDir("ios"), publicEnv, api.release({
|
|
809
1044
|
...this.#task("ios", config, { env: await this.#runtimeEnv() }),
|
|
810
1045
|
platform: "ios",
|
|
811
1046
|
signing: { ...teamId ? { teamId } : {}, ...adHoc ? { distribution: "ad-hoc" } : {} }
|
|
812
|
-
});
|
|
1047
|
+
}));
|
|
813
1048
|
}
|
|
814
1049
|
get updatesDir() {
|
|
815
|
-
return
|
|
1050
|
+
return path8.join(this.targetRoot, "updates");
|
|
816
1051
|
}
|
|
817
1052
|
async publishUpdate(platform, {
|
|
818
1053
|
channel,
|
|
819
1054
|
server,
|
|
820
1055
|
arch,
|
|
821
1056
|
sign = false,
|
|
822
|
-
notarize = false
|
|
1057
|
+
notarize = false,
|
|
1058
|
+
revoke,
|
|
1059
|
+
resetSequence
|
|
823
1060
|
} = {}) {
|
|
824
1061
|
NativeApp.#assertServerPlatform(platform, server);
|
|
825
1062
|
this.#assertUpdates();
|
|
@@ -833,25 +1070,35 @@ class NativeApp {
|
|
|
833
1070
|
...channel ? { channel } : {},
|
|
834
1071
|
...arch ? { arch } : {},
|
|
835
1072
|
...distribution.macos ? { macos: distribution.macos } : {},
|
|
836
|
-
...distribution.windows ? { windows: distribution.windows } : {}
|
|
1073
|
+
...distribution.windows ? { windows: distribution.windows } : {},
|
|
1074
|
+
...revoke?.length ? { revoke } : {},
|
|
1075
|
+
...resetSequence ? { resetSequence } : {}
|
|
837
1076
|
});
|
|
838
1077
|
}
|
|
839
1078
|
async#distribution(api, platform, profile, request, server) {
|
|
840
1079
|
const distribution = api.requestedDistribution(platform, profile, request);
|
|
1080
|
+
if (server && profile === "release")
|
|
1081
|
+
await this.#assertServerClean(server);
|
|
841
1082
|
if (!distribution.notice) {
|
|
842
1083
|
if (distribution.unused.length && (platform === "macos" || platform === "windows"))
|
|
843
1084
|
this.app.logger.info(`${distribution.unused.join(", ")} set: this build is not signed without --sign.`);
|
|
844
1085
|
return distribution;
|
|
845
1086
|
}
|
|
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
1087
|
this.app.logger.warn(distribution.notice);
|
|
852
1088
|
return distribution;
|
|
853
1089
|
}
|
|
854
|
-
async
|
|
1090
|
+
async#assertServerClean(server) {
|
|
1091
|
+
const unread = await new ServerBundleSecrets(this.app.workspace.workspaceRoot, this.app.cwdPath).assertClean(server.dir);
|
|
1092
|
+
if (unread.length)
|
|
1093
|
+
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.`);
|
|
1094
|
+
}
|
|
1095
|
+
async assertPublishable(platform, {
|
|
1096
|
+
channel,
|
|
1097
|
+
server = false,
|
|
1098
|
+
arch,
|
|
1099
|
+
revoke,
|
|
1100
|
+
resetSequence
|
|
1101
|
+
} = {}) {
|
|
855
1102
|
this.#assertUpdates();
|
|
856
1103
|
const [api, { config }] = await Promise.all([
|
|
857
1104
|
NativeApi.load(this.app.cwdPath),
|
|
@@ -866,10 +1113,12 @@ class NativeApp {
|
|
|
866
1113
|
out: this.updatesDir,
|
|
867
1114
|
server,
|
|
868
1115
|
...channel ? { channel } : {},
|
|
869
|
-
...arch ? { arch } : {}
|
|
1116
|
+
...arch ? { arch } : {},
|
|
1117
|
+
...revoke?.length ? { revoke } : {},
|
|
1118
|
+
...resetSequence ? { resetSequence } : {}
|
|
870
1119
|
});
|
|
871
1120
|
} catch (error) {
|
|
872
|
-
throw new Error(`Native target '${this.target.name}': ${error
|
|
1121
|
+
throw new Error(`Native target '${this.target.name}': ${toError(error).message}`, {
|
|
873
1122
|
cause: error
|
|
874
1123
|
});
|
|
875
1124
|
}
|
|
@@ -888,24 +1137,26 @@ class NativeApp {
|
|
|
888
1137
|
async releaseAndroid({ formats = ["aab"] } = {}) {
|
|
889
1138
|
const signing = NativeApp.androidSigning();
|
|
890
1139
|
await this.assembleWeb("android");
|
|
1140
|
+
const publicEnv = await this.#publicEnv();
|
|
891
1141
|
const { api, config } = await this.prepare("android");
|
|
892
|
-
return await api.release({
|
|
1142
|
+
return await this.#recorded(this.outDir("android"), publicEnv, api.release({
|
|
893
1143
|
...this.#task("android", config, { env: await this.#runtimeEnv() }),
|
|
894
1144
|
platform: "android",
|
|
895
1145
|
signing,
|
|
896
1146
|
formats
|
|
897
|
-
});
|
|
1147
|
+
}));
|
|
898
1148
|
}
|
|
899
1149
|
async packUpdate(platform, { out } = {}) {
|
|
900
1150
|
await this.assembleWeb(platform);
|
|
1151
|
+
const publicEnv = await this.#publicEnv();
|
|
901
1152
|
const { api, config } = await this.prepare(platform);
|
|
902
1153
|
if (!config.updates)
|
|
903
1154
|
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 });
|
|
1155
|
+
const dir = path8.resolve(out ?? path8.join(this.updatesDir, platform));
|
|
1156
|
+
await UpdatePackFolder.clear(dir);
|
|
1157
|
+
const packed = await this.#recorded(dir, publicEnv, api.packUpdate({ ...this.#task(platform, config), platform, out: dir }));
|
|
907
1158
|
const channel = config.updates.channel ?? "production";
|
|
908
|
-
const bundle = await
|
|
1159
|
+
const bundle = await UpdatePackFolder.readBundle(path8.join(packed.out, UpdatePackFolder.bundleFile));
|
|
909
1160
|
const pack = {
|
|
910
1161
|
schema: 1,
|
|
911
1162
|
app: this.app.name,
|
|
@@ -917,24 +1168,39 @@ class NativeApp {
|
|
|
917
1168
|
appVersion: packed.manifest.version,
|
|
918
1169
|
nativeApi: packed.manifest.nativeApi ?? "",
|
|
919
1170
|
webHash: bundle.web.hash,
|
|
1171
|
+
...bundle.web.publicEnvHash ? { publicEnvHash: bundle.web.publicEnvHash } : {},
|
|
920
1172
|
files: packed.manifest.files.length,
|
|
921
1173
|
size: packed.manifest.files.reduce((sum, file) => sum + file.size, 0),
|
|
922
1174
|
out: packed.out
|
|
923
1175
|
};
|
|
924
1176
|
return { ...packed, channel, pack };
|
|
925
1177
|
}
|
|
926
|
-
|
|
927
|
-
await
|
|
1178
|
+
async#publicEnv() {
|
|
1179
|
+
const publicEnv = await NativePublicEnv.built(this.app);
|
|
1180
|
+
if (!publicEnv)
|
|
1181
|
+
return null;
|
|
1182
|
+
const problems = NativePublicEnv.problems(publicEnv);
|
|
1183
|
+
if (!problems.length)
|
|
1184
|
+
return publicEnv;
|
|
1185
|
+
const message = `The web build inlines public env an update cannot carry:
|
|
1186
|
+
- ${problems.join(`
|
|
1187
|
+
- `)}`;
|
|
1188
|
+
if (this.target.config.updates)
|
|
1189
|
+
throw new Error(message);
|
|
1190
|
+
this.app.logger.warn(`${message}
|
|
1191
|
+
bundle.json records none, so no update can be checked against this build.`);
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
async#recorded(dir, publicEnv, build) {
|
|
1195
|
+
const result = await build;
|
|
1196
|
+
const file = path8.join(dir, UpdatePackFolder.bundleFile);
|
|
1197
|
+
if (!publicEnv || !await Bun.file(file).exists())
|
|
1198
|
+
return result;
|
|
1199
|
+
const bundle = await UpdatePackFolder.readBundle(file);
|
|
1200
|
+
const web = { ...bundle.web, publicEnv, publicEnvHash: NativePublicEnv.hash(publicEnv) };
|
|
1201
|
+
await Bun.write(file, `${JSON.stringify({ ...bundle, web }, null, 2)}
|
|
928
1202
|
`);
|
|
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) };
|
|
1203
|
+
return result;
|
|
938
1204
|
}
|
|
939
1205
|
static async compareBundles(appDir, shipped, bundle) {
|
|
940
1206
|
return (await NativeApi.load(appDir)).compareBundles(shipped, bundle);
|
|
@@ -990,198 +1256,4 @@ class NativeApp {
|
|
|
990
1256
|
}
|
|
991
1257
|
}
|
|
992
1258
|
|
|
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 };
|
|
1259
|
+
export { DesktopBin, NativePublicEnv, UpdatePackFolder, NativeApp };
|