@akanjs/devkit 3.0.0-beta.28 → 3.0.0-beta.29
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/akanApp/akanApp.host.test.ts +55 -0
- package/akanApp/akanApp.host.ts +2 -0
- package/mobile/desktopServerStage.test.ts +76 -1
- package/mobile/desktopServerStage.ts +34 -16
- package/mobile/nativeApp.test.ts +30 -0
- package/mobile/nativeApp.ts +56 -10
- package/mobile/nativeWebDir.ts +6 -2
- package/package.json +2 -2
- package/transforms/serverEnvPlugin.test.ts +3 -0
- package/transforms/serverEnvPlugin.ts +1 -1
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { IncrementalBuilderHost } from "../incrementalBuilder";
|
|
4
|
+
import { createTempApp, isolateEnv, tempRoots, writeText } from "../testHelpers";
|
|
2
5
|
import { AkanAppHost } from "./akanApp.host";
|
|
6
|
+
import type { DevHostEvent } from "./devHostPolicy";
|
|
3
7
|
|
|
4
8
|
describe("readProcessRssBytes", () => {
|
|
5
9
|
test("reads this process's own rss", async () => {
|
|
@@ -14,3 +18,54 @@ describe("readProcessRssBytes", () => {
|
|
|
14
18
|
expect(await AkanAppHost.readProcessRssBytes(2_147_483_646)).toBeNull();
|
|
15
19
|
});
|
|
16
20
|
});
|
|
21
|
+
|
|
22
|
+
describe("a gateway whose replica crash-loops", () => {
|
|
23
|
+
isolateEnv({ AKAN_PUBLIC_REPO_NAME: "repo", AKAN_PUBLIC_SERVE_DOMAIN: "localhost", AKAN_PUBLIC_ENV: "local" });
|
|
24
|
+
const track = tempRoots();
|
|
25
|
+
const crashLoop = "Backend replica 0/federation failed 3 consecutive boots; waiting for a code change to retry: boom";
|
|
26
|
+
const readyBuilder = {
|
|
27
|
+
status: "ready",
|
|
28
|
+
patcherOff: false,
|
|
29
|
+
start: ({ onReady }: { onReady?: () => void }) => onReady?.(),
|
|
30
|
+
send: () => true,
|
|
31
|
+
stop: () => undefined,
|
|
32
|
+
} as unknown as IncrementalBuilderHost;
|
|
33
|
+
|
|
34
|
+
test("fails the dev server at once, and leaves it running for the fix", async () => {
|
|
35
|
+
const { root, app } = track(await createTempApp("demo"));
|
|
36
|
+
await writeText(
|
|
37
|
+
path.join(root, "apps/demo/main.ts"),
|
|
38
|
+
[
|
|
39
|
+
`process.send?.({ type: "build-status", data: { generation: -1, phase: "backend", ok: false, files: [], message: ${JSON.stringify(crashLoop)} } });`,
|
|
40
|
+
"setInterval(() => undefined, 1_000);",
|
|
41
|
+
"",
|
|
42
|
+
].join("\n"),
|
|
43
|
+
);
|
|
44
|
+
const create = IncrementalBuilderHost.create;
|
|
45
|
+
IncrementalBuilderHost.create = async () => readyBuilder;
|
|
46
|
+
const events: DevHostEvent[] = [];
|
|
47
|
+
const failed = Promise.withResolvers<void>();
|
|
48
|
+
const host = new AkanAppHost(app, {
|
|
49
|
+
env: { ...process.env } as Record<string, string>,
|
|
50
|
+
onDevEvent: (event) => {
|
|
51
|
+
events.push(event);
|
|
52
|
+
if ("state" in event && event.state === "failed") failed.resolve();
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
let beforeStop: DevHostEvent[] = [];
|
|
56
|
+
try {
|
|
57
|
+
await host.start();
|
|
58
|
+
await failed.promise;
|
|
59
|
+
await Bun.sleep(300);
|
|
60
|
+
beforeStop = [...events];
|
|
61
|
+
} finally {
|
|
62
|
+
await host.stop();
|
|
63
|
+
IncrementalBuilderHost.create = create;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
expect(beforeStop).toEqual([
|
|
67
|
+
{ app: "demo", state: "starting" },
|
|
68
|
+
{ app: "demo", state: "failed", detail: crashLoop },
|
|
69
|
+
]);
|
|
70
|
+
});
|
|
71
|
+
});
|
package/akanApp/akanApp.host.ts
CHANGED
|
@@ -245,6 +245,8 @@ export class AkanAppHost {
|
|
|
245
245
|
message: msg.data.message,
|
|
246
246
|
});
|
|
247
247
|
this.#sendOrQueueBuildStatus(status);
|
|
248
|
+
//? A replica crash loop leaves the gateway up, waiting for an edit: no exit reaches the host's own give-up.
|
|
249
|
+
if (!status.ok) this.#emitDevEvent("failed", status.message);
|
|
248
250
|
return;
|
|
249
251
|
}
|
|
250
252
|
if (backendMsgTypeSet.has(msg.type)) this.#sendToBuilder(msg);
|
|
@@ -2,7 +2,8 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { AkanAppConfig } from "../akanConfig";
|
|
5
|
-
import {
|
|
5
|
+
import type { App } from "../commandDecorators";
|
|
6
|
+
import { tempDirs, writeText as write } from "../testHelpers";
|
|
6
7
|
import type { PackageJson } from "../types";
|
|
7
8
|
import { DesktopServerStage } from "./desktopServerStage";
|
|
8
9
|
import { NativeApi } from "./nativeApi";
|
|
@@ -85,6 +86,80 @@ describe("DesktopServerStage", () => {
|
|
|
85
86
|
});
|
|
86
87
|
});
|
|
87
88
|
|
|
89
|
+
const stageApp = async (spawn: (...args: unknown[]) => Promise<string> = async () => "") => {
|
|
90
|
+
const root = await makeTempRoot();
|
|
91
|
+
const dist = path.join(root, "dist/apps/portal");
|
|
92
|
+
const config = appConfig();
|
|
93
|
+
const app = {
|
|
94
|
+
cwdPath: path.join(root, "apps/portal"),
|
|
95
|
+
dist: { cwdPath: dist },
|
|
96
|
+
getConfig: async () => config,
|
|
97
|
+
getScanInfo: () => ({ getLibs: () => [] }),
|
|
98
|
+
logger: { info: () => undefined, warn: () => undefined },
|
|
99
|
+
spawn,
|
|
100
|
+
} as unknown as App;
|
|
101
|
+
return { dist, app, stage: new DesktopServerStage(app) };
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
test("carries everything the backend build wrote beside main.js but what only the image reads", async () => {
|
|
105
|
+
const { dist, stage } = await stageApp();
|
|
106
|
+
const carried = [
|
|
107
|
+
"main.js",
|
|
108
|
+
"server.js",
|
|
109
|
+
"chunk-5eap2n9b.js",
|
|
110
|
+
"akan.build.json",
|
|
111
|
+
"better-sqlite3-a1b2c3d4.node",
|
|
112
|
+
"engine-e5f6a7b8.wasm",
|
|
113
|
+
"model-c9d0e1f2.bin",
|
|
114
|
+
"private/service-account.json",
|
|
115
|
+
];
|
|
116
|
+
const imageOnly = [
|
|
117
|
+
"Dockerfile",
|
|
118
|
+
"package.json",
|
|
119
|
+
"rscWorker.js",
|
|
120
|
+
"console.js",
|
|
121
|
+
"console-runtime.js",
|
|
122
|
+
"csr/index.html",
|
|
123
|
+
"public/favicon.ico",
|
|
124
|
+
".akan/artifact/base-artifact.json",
|
|
125
|
+
];
|
|
126
|
+
for (const file of [...carried, ...imageOnly]) await write(path.join(dist, file), file);
|
|
127
|
+
|
|
128
|
+
await stage.prepare("main");
|
|
129
|
+
|
|
130
|
+
const staged = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: stage.dir, dot: true }));
|
|
131
|
+
expect(staged.map((file) => file.split(path.sep).join("/")).sort()).toEqual(carried.sort());
|
|
132
|
+
expect(await Bun.file(path.join(stage.dir, "better-sqlite3-a1b2c3d4.node")).text()).toBe(
|
|
133
|
+
"better-sqlite3-a1b2c3d4.node",
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("installs from the config alone, offline when Bun's cache holds the packages", async () => {
|
|
138
|
+
const spawned: unknown[][] = [];
|
|
139
|
+
const { stage } = await stageApp(async (...args) => {
|
|
140
|
+
spawned.push(args);
|
|
141
|
+
return "";
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await stage.install();
|
|
145
|
+
|
|
146
|
+
expect(spawned).toEqual([[process.execPath, ["install", "--production", "--prefer-offline"], { cwd: stage.dir }]]);
|
|
147
|
+
const installed = (await Bun.file(path.join(stage.dir, "package.json")).json()) as PackageJson;
|
|
148
|
+
expect(Object.keys(installed.dependencies ?? {}).sort()).toEqual(["react", "react-dom"]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("an install that fails says it needs the registry or a Bun cache that holds the packages", async () => {
|
|
152
|
+
const { stage } = await stageApp(async () => {
|
|
153
|
+
throw new Error("error: GET https://registry.npmjs.org/scheduler - ConnectionRefused");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const failed = stage.install();
|
|
157
|
+
|
|
158
|
+
await expect(failed).rejects.toThrow(
|
|
159
|
+
"it needs the npm registry, or a Bun cache that already holds every one of them.\nerror: GET https://registry.npmjs.org/scheduler",
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
88
163
|
test("refuses an app whose database modes leave out single", () => {
|
|
89
164
|
expect(() => DesktopServerStage.assertCarriable(appConfig({ database: { modes: ["cluster"] } }))).toThrow(
|
|
90
165
|
"only database mode single runs",
|
|
@@ -16,8 +16,18 @@ export interface DesktopServerBundle {
|
|
|
16
16
|
//* `bun install --production` does. It is staged outside dist: installing there would put host packages into the
|
|
17
17
|
//* image's `COPY . .`.
|
|
18
18
|
export class DesktopServerStage {
|
|
19
|
-
//?
|
|
20
|
-
|
|
19
|
+
//? An API-only server starts neither the RSC worker nor the akan console and serves no CSR, public/ or SSR artifact,
|
|
20
|
+
//? and `install` wrote its own package.json. The rest of dist is the server's: its chunks, a `.node`, a `.wasm`.
|
|
21
|
+
static readonly uncarried = new Set([
|
|
22
|
+
"Dockerfile",
|
|
23
|
+
"package.json",
|
|
24
|
+
"rscWorker.js",
|
|
25
|
+
"console.js",
|
|
26
|
+
"console-runtime.js",
|
|
27
|
+
"csr",
|
|
28
|
+
"public",
|
|
29
|
+
".akan",
|
|
30
|
+
]);
|
|
21
31
|
//? Only the RSC worker loads it, and its peer webpack makes 35 of the 43 MB an install would add.
|
|
22
32
|
static readonly rscRenderer = "react-server-dom-webpack";
|
|
23
33
|
|
|
@@ -88,29 +98,37 @@ export class DesktopServerStage {
|
|
|
88
98
|
return { ...built, dependencies: Object.fromEntries(dependencies) };
|
|
89
99
|
}
|
|
90
100
|
|
|
91
|
-
|
|
101
|
+
//* The packages come from the config alone, so they install before `akan build` runs: a machine that cannot install
|
|
102
|
+
//* them stops at once instead of after the whole build.
|
|
103
|
+
async install() {
|
|
92
104
|
const config = await this.app.getConfig();
|
|
93
105
|
DesktopServerStage.assertCarriable(config);
|
|
94
|
-
const dist = this.app.dist.cwdPath;
|
|
95
106
|
await rm(this.dir, { recursive: true, force: true });
|
|
96
107
|
await mkdir(this.dir, { recursive: true });
|
|
97
|
-
const entries = await readdir(dist, { withFileTypes: true });
|
|
98
|
-
for (const entry of entries) {
|
|
99
|
-
const carried =
|
|
100
|
-
(entry.isFile() &&
|
|
101
|
-
((entry.name.endsWith(".js") && !DesktopServerStage.skippedFiles.has(entry.name)) ||
|
|
102
|
-
entry.name === "akan.build.json")) ||
|
|
103
|
-
(entry.isDirectory() && entry.name === "private");
|
|
104
|
-
if (carried) await cp(path.join(dist, entry.name), path.join(this.dir, entry.name), { recursive: true });
|
|
105
|
-
}
|
|
106
|
-
const built = (await Bun.file(path.join(dist, "package.json")).json()) as PackageJson;
|
|
107
108
|
await writeFile(
|
|
108
109
|
path.join(this.dir, "package.json"),
|
|
109
|
-
JSON.stringify(DesktopServerStage.packageJson(config,
|
|
110
|
+
JSON.stringify(DesktopServerStage.packageJson(config, config.getProductionPackageJson()), null, 2),
|
|
110
111
|
);
|
|
111
|
-
|
|
112
|
+
try {
|
|
113
|
+
await this.app.spawn(process.execPath, ["install", "--production", "--prefer-offline"], { cwd: this.dir });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`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.\n${error instanceof Error ? error.message : String(error)}`,
|
|
117
|
+
{ cause: error },
|
|
118
|
+
);
|
|
119
|
+
}
|
|
112
120
|
//? Launcher links only: a link out of the app bundle breaks its signature, and the server runs none of them.
|
|
113
121
|
await rm(path.join(this.dir, "node_modules", ".bin"), { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
//? Copies the build beside what `install` put in the stage, so it runs after both.
|
|
125
|
+
async prepare(environment: MobileEnv): Promise<DesktopServerBundle> {
|
|
126
|
+
const config = await this.app.getConfig();
|
|
127
|
+
const dist = this.app.dist.cwdPath;
|
|
128
|
+
await mkdir(this.dir, { recursive: true });
|
|
129
|
+
for (const name of await readdir(dist))
|
|
130
|
+
if (!DesktopServerStage.uncarried.has(name))
|
|
131
|
+
await cp(path.join(dist, name), path.join(this.dir, name), { recursive: true });
|
|
114
132
|
const scanInfo = this.app.getScanInfo({ allowEmpty: true }) ?? (await this.app.scan({ write: false }));
|
|
115
133
|
const carried = [...DesktopBin.select(config, scanInfo.getLibs()).keys()];
|
|
116
134
|
const notice = DesktopServerStage.imageStepsNotice(config, carried);
|
package/mobile/nativeApp.test.ts
CHANGED
|
@@ -3,9 +3,12 @@ import path from "node:path";
|
|
|
3
3
|
import type { AkanPlugin } from "akanjs";
|
|
4
4
|
import type { AkanMobileTargetConfig } from "../akanConfig";
|
|
5
5
|
import type { App } from "../commandDecorators";
|
|
6
|
+
import { tempDirs, writeText } from "../testHelpers";
|
|
6
7
|
import { NativeApp } from "./nativeApp";
|
|
7
8
|
import { NativeConfig } from "./nativeConfig";
|
|
8
9
|
|
|
10
|
+
const makeTempRoot = tempDirs("akan-native-app-");
|
|
11
|
+
|
|
9
12
|
const target = (config: Partial<AkanMobileTargetConfig> = {}) => ({
|
|
10
13
|
name: config.name ?? "default",
|
|
11
14
|
config: {
|
|
@@ -39,6 +42,33 @@ describe("NativeApp", () => {
|
|
|
39
42
|
expect(admin.web.dir).toBe(path.join(appDir, ".akan/mobile/admin/web"));
|
|
40
43
|
expect(admin.outDir("ios")).toBe(path.join(appDir, ".akan/mobile/admin/native/ios"));
|
|
41
44
|
expect(admin.outDir("android")).toBe(path.join(appDir, ".akan/mobile/admin/native/android"));
|
|
45
|
+
expect(admin.devOutDir("macos")).toBe(path.join(appDir, ".akan/mobile/admin/dev/macos"));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("a dev build bundles no page a release build left, and builds apart from the release it would empty", async () => {
|
|
49
|
+
const app = {
|
|
50
|
+
...fakeApp(),
|
|
51
|
+
cwdPath: await makeTempRoot(),
|
|
52
|
+
logger: { info: () => undefined, debug: () => undefined, warn: () => undefined },
|
|
53
|
+
} as unknown as App;
|
|
54
|
+
const nativeApp = new NativeApp(app, target());
|
|
55
|
+
await writeText(path.join(nativeApp.web.dir, "index.html"), "<html>the last release</html>");
|
|
56
|
+
const seen: { outDir?: string; leftover: boolean }[] = [];
|
|
57
|
+
nativeApp.prepare = async () =>
|
|
58
|
+
({
|
|
59
|
+
config: {},
|
|
60
|
+
api: {
|
|
61
|
+
dev: async ({ outDir }: { outDir?: string }) => {
|
|
62
|
+
seen.push({ outDir, leftover: await Bun.file(path.join(nativeApp.web.dir, "index.html")).exists() });
|
|
63
|
+
return { gateway: "http://127.0.0.1:9", exited: Promise.resolve(0), stop: async () => undefined };
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
}) as never;
|
|
67
|
+
|
|
68
|
+
await nativeApp.dev("windows", { upstream: "http://localhost:8282", lang: "en" });
|
|
69
|
+
|
|
70
|
+
expect(seen).toEqual([{ outDir: nativeApp.devOutDir("windows"), leftover: false }]);
|
|
71
|
+
expect(path.relative(nativeApp.outDir("windows"), nativeApp.devOutDir("windows"))).toStartWith("..");
|
|
42
72
|
});
|
|
43
73
|
|
|
44
74
|
test("a dev boot that came up says so in one line: platform, device, dev server, time", () => {
|
package/mobile/nativeApp.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { mkdir } from "node:fs/promises";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import type { AkanNativeConfig } from "@akanjs/native/config";
|
|
4
3
|
import type { MobileEnv } from "akanjs";
|
|
@@ -70,6 +69,11 @@ export class NativeApp {
|
|
|
70
69
|
return path.join(this.targetRoot, "native", platform);
|
|
71
70
|
}
|
|
72
71
|
|
|
72
|
+
//? Apart from the builds: a desktop build empties its folder first, which would take a release and its installer.
|
|
73
|
+
devOutDir(platform: NativePlatform) {
|
|
74
|
+
return path.join(this.targetRoot, "dev", platform);
|
|
75
|
+
}
|
|
76
|
+
|
|
73
77
|
/** The web root, from the production build the mobile commands run first. */
|
|
74
78
|
async assembleWeb() {
|
|
75
79
|
const dist = this.app.dist.cwdPath;
|
|
@@ -123,12 +127,16 @@ export class NativeApp {
|
|
|
123
127
|
this.config({ platform, ...(server ? { server } : {}) }),
|
|
124
128
|
]);
|
|
125
129
|
for (const warning of warnings) this.app.logger.warn(warning);
|
|
130
|
+
this.#assertValid(api, config);
|
|
131
|
+
return { api, config };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#assertValid(api: NativeBuildApiModule, config: AkanNativeConfig) {
|
|
126
135
|
const problems = api.validateConfig(config, { appDir: this.app.cwdPath });
|
|
127
136
|
if (problems.length)
|
|
128
137
|
throw new Error(
|
|
129
138
|
`Mobile target '${this.target.name}' makes an invalid native config:\n- ${problems.join("\n- ")}`,
|
|
130
139
|
);
|
|
131
|
-
return { api, config };
|
|
132
140
|
}
|
|
133
141
|
|
|
134
142
|
//? The app's own output: its console (mirrored by a dev build), the simulator log stream or logcat.
|
|
@@ -139,12 +147,12 @@ export class NativeApp {
|
|
|
139
147
|
};
|
|
140
148
|
|
|
141
149
|
//* `AKAN_PUBLIC_*` is already inlined into the CSR bundle, so the runtime reads no .env file of its own.
|
|
142
|
-
#task(platform: NativePlatform, config: AkanNativeConfig): TaskOptions {
|
|
150
|
+
#task(platform: NativePlatform, config: AkanNativeConfig, outDir = this.outDir(platform)): TaskOptions {
|
|
143
151
|
return {
|
|
144
152
|
appDir: this.app.cwdPath,
|
|
145
153
|
config,
|
|
146
154
|
platform,
|
|
147
|
-
outDir
|
|
155
|
+
outDir,
|
|
148
156
|
envFiles: false,
|
|
149
157
|
skipWebBuild: true,
|
|
150
158
|
log: (event) => {
|
|
@@ -192,10 +200,11 @@ export class NativeApp {
|
|
|
192
200
|
const boot: NativeDevBoot = { steps: [], lines: [] };
|
|
193
201
|
this.#boot = boot;
|
|
194
202
|
try {
|
|
195
|
-
|
|
203
|
+
//? The runtime bundles an index.html it finds here as the dev build's page, and a release build leaves one.
|
|
204
|
+
await this.web.clear();
|
|
196
205
|
const [{ api, config }, { api: routes }] = await Promise.all([this.prepare(platform), this.app.getConfig()]);
|
|
197
206
|
const session = await api.dev({
|
|
198
|
-
...this.#task(platform, config),
|
|
207
|
+
...this.#task(platform, config, this.devOutDir(platform)),
|
|
199
208
|
upstream,
|
|
200
209
|
hmrPath: "/_akan/hmr",
|
|
201
210
|
//? A dev page calls its own origin (akanjs baseEnv), so the gateway relays the socket it opens for the API too.
|
|
@@ -253,10 +262,7 @@ export class NativeApp {
|
|
|
253
262
|
{ channel, server }: { channel?: string; server?: DesktopServerBundle } = {},
|
|
254
263
|
) {
|
|
255
264
|
NativeApp.#assertServerPlatform(platform, server);
|
|
256
|
-
|
|
257
|
-
throw new Error(
|
|
258
|
-
`Mobile target '${this.target.name}' has no updates: { url, publicKey } in akan.config.ts; \`akan update-keygen ${this.app.name}\` prints the key.`,
|
|
259
|
-
);
|
|
265
|
+
this.#assertUpdates();
|
|
260
266
|
await this.assembleWeb();
|
|
261
267
|
const { api, config } = await this.prepare(platform, server);
|
|
262
268
|
return await api.publishUpdate({
|
|
@@ -267,6 +273,46 @@ export class NativeApp {
|
|
|
267
273
|
});
|
|
268
274
|
}
|
|
269
275
|
|
|
276
|
+
/**
|
|
277
|
+
* What publishing refuses once the release is built: no updates settings, an invalid config, no signing key or
|
|
278
|
+
* another app's, and a desktop release whose server presence is not its channel's previous release's.
|
|
279
|
+
*/
|
|
280
|
+
async assertPublishable(
|
|
281
|
+
platform: NativePlatform,
|
|
282
|
+
{ channel, server = false }: { channel?: string; server?: boolean } = {},
|
|
283
|
+
) {
|
|
284
|
+
this.#assertUpdates();
|
|
285
|
+
const [api, { config }] = await Promise.all([
|
|
286
|
+
NativeApi.load(this.app.cwdPath),
|
|
287
|
+
this.config({ platform, stageBin: false }),
|
|
288
|
+
]);
|
|
289
|
+
this.#assertValid(api, config);
|
|
290
|
+
try {
|
|
291
|
+
api.checkPublishUpdate({
|
|
292
|
+
appDir: this.app.cwdPath,
|
|
293
|
+
config,
|
|
294
|
+
platform,
|
|
295
|
+
out: this.updatesDir,
|
|
296
|
+
server,
|
|
297
|
+
...(channel ? { channel } : {}),
|
|
298
|
+
});
|
|
299
|
+
} catch (error) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
`Mobile target '${this.target.name}': ${error instanceof Error ? error.message : String(error)}`,
|
|
302
|
+
{
|
|
303
|
+
cause: error,
|
|
304
|
+
},
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#assertUpdates() {
|
|
310
|
+
if (!this.target.config.updates)
|
|
311
|
+
throw new Error(
|
|
312
|
+
`Mobile target '${this.target.name}' has no updates: { url, publicKey } in akan.config.ts; \`akan update-keygen ${this.app.name}\` prints the key.`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
270
316
|
/** The key update releases of this target's app id on `platform` are signed with: made once, then read. */
|
|
271
317
|
async updateKeygen(platform: NativePlatform) {
|
|
272
318
|
const [api, { config }] = await Promise.all([
|
package/mobile/nativeWebDir.ts
CHANGED
|
@@ -34,12 +34,16 @@ export class NativeWebDir {
|
|
|
34
34
|
}
|
|
35
35
|
static readonly #targetTag = "<script>window.__AKAN_MOBILE_TARGET__=";
|
|
36
36
|
|
|
37
|
+
async clear() {
|
|
38
|
+
await rm(this.dir, { recursive: true, force: true });
|
|
39
|
+
await mkdir(this.dir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
/** Rebuilds the folder from scratch and answers its files as web paths: relative, `/`-separated and sorted. */
|
|
38
43
|
async assemble(target: AkanMobileTargetConfig, { html, publicDir, fontsDir }: NativeWebDirSources) {
|
|
39
44
|
if (!(await Bun.file(html).exists()))
|
|
40
45
|
throw new Error(`CSR html for mobile target '${target.name}' not found: ${html}`);
|
|
41
|
-
await
|
|
42
|
-
await mkdir(this.dir, { recursive: true });
|
|
46
|
+
await this.clear();
|
|
43
47
|
if (publicDir && (await NativeWebDir.#isDir(publicDir))) await cp(publicDir, this.dir, { recursive: true });
|
|
44
48
|
if (fontsDir && (await NativeWebDir.#isDir(fontsDir)))
|
|
45
49
|
await cp(fontsDir, path.join(this.dir, "_akan", "fonts"), { recursive: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.29",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@earendil-works/pi-tui": "0.86.1",
|
|
48
48
|
"@inquirer/prompts": "^8.4.3",
|
|
49
49
|
"@tailwindcss/node": "^4.3.0",
|
|
50
|
-
"akanjs": "3.0.0-beta.
|
|
50
|
+
"akanjs": "3.0.0-beta.29",
|
|
51
51
|
"chalk": "^5.6.2",
|
|
52
52
|
"commander": "^14.0.3",
|
|
53
53
|
"dayjs": "^1.11.20",
|
|
@@ -76,6 +76,9 @@ describe("createServerEnvPlugin", () => {
|
|
|
76
76
|
expect(proc.stderr.toString()).toContain(
|
|
77
77
|
"env/env.server.develop.ts is not in this build: `akan build` bundles the server env of AKAN_PUBLIC_ENV=main alone",
|
|
78
78
|
);
|
|
79
|
+
expect(proc.stderr.toString()).toContain(
|
|
80
|
+
"To run develop, build for it: AKAN_PUBLIC_ENV=develop in the workspace's .env, which outranks the shell, or --env develop on a desktop or mobile build.",
|
|
81
|
+
);
|
|
79
82
|
});
|
|
80
83
|
|
|
81
84
|
test("a custom branch is its own environment", async () => {
|
|
@@ -26,7 +26,7 @@ export const createServerEnvPlugin = ({ envDir, environment, environments }: Ser
|
|
|
26
26
|
build.onLoad({ filter }, async (args) => {
|
|
27
27
|
const name = path.basename(args.path, ".ts").slice("env.server.".length);
|
|
28
28
|
const { exports } = new Bun.Transpiler({ loader: "ts" }).scan(await Bun.file(args.path).text());
|
|
29
|
-
const message = `env/env.server.${name}.ts is not in this build: \`akan build\` bundles the server env of AKAN_PUBLIC_ENV=${environment} alone, the environment its image runs.
|
|
29
|
+
const message = `env/env.server.${name}.ts is not in this build: \`akan build\` bundles the server env of AKAN_PUBLIC_ENV=${environment} alone, the environment its image runs. To run ${name}, build for it: AKAN_PUBLIC_ENV=${name} in the workspace's .env, which outranks the shell, or --env ${name} on a desktop or mobile build.`;
|
|
30
30
|
return {
|
|
31
31
|
loader: "js",
|
|
32
32
|
contents: [
|