@akanjs/devkit 3.0.0-beta.26 → 3.0.0-beta.27
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/csrE2e/csrE2eHarness.fixture.ts +55 -5
- package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
- package/csrE2e/registryVendor.e2e.test.ts +221 -0
- package/frontendBuild/csrDevArtifactWriter.test.ts +8 -0
- package/frontendBuild/csrDevArtifactWriter.ts +4 -0
- package/frontendBuild/csrDevBundler.ts +1 -1
- package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
- package/frontendBuild/csrDevFactoryCheck.ts +39 -0
- package/frontendBuild/csrDevModuleCompiler.test.ts +178 -6
- package/frontendBuild/csrDevModuleCompiler.ts +89 -18
- package/frontendBuild/csrDevPaths.ts +1 -0
- package/frontendBuild/csrDevResolver.ts +89 -9
- package/frontendBuild/ssrDevBundler.ts +1 -1
- package/mobile/nativeApp.test.ts +15 -0
- package/mobile/nativeApp.ts +54 -14
- package/mobile/nativeAppLine.test.ts +62 -0
- package/mobile/nativeAppLine.ts +63 -0
- package/package.json +2 -2
package/mobile/nativeApp.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdir } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { AkanNativeConfig } from "@akanjs/native/config";
|
|
4
4
|
import type { App } from "../commandDecorators";
|
|
5
|
+
import { Executor } from "../executors";
|
|
5
6
|
import {
|
|
6
7
|
type DesktopPlatform,
|
|
7
8
|
type MobilePlatform,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
targetHtmlFilename,
|
|
11
12
|
} from "./mobileTarget";
|
|
12
13
|
import { NativeApi, type NativeBuildApiModule } from "./nativeApi";
|
|
14
|
+
import { NativeAppLine, type NativeAppLineRead } from "./nativeAppLine";
|
|
13
15
|
import { NativeConfig } from "./nativeConfig";
|
|
14
16
|
import { NativeWebDir } from "./nativeWebDir";
|
|
15
17
|
|
|
@@ -24,6 +26,11 @@ export interface NativeRunOptions {
|
|
|
24
26
|
teamId?: string;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
interface NativeDevBoot {
|
|
30
|
+
steps: string[];
|
|
31
|
+
lines: NativeAppLineRead[];
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
export interface NativeDevOptions extends NativeRunOptions {
|
|
28
35
|
/** The akan dev server the gateway proxies. */
|
|
29
36
|
upstream: string;
|
|
@@ -35,6 +42,8 @@ export interface NativeDevOptions extends NativeRunOptions {
|
|
|
35
42
|
export class NativeApp {
|
|
36
43
|
readonly targetRoot: string;
|
|
37
44
|
readonly web: NativeWebDir;
|
|
45
|
+
//? A dev boot holds its steps and the app's first lines back: one line says it is up, all of it when it failed.
|
|
46
|
+
#boot: NativeDevBoot | null = null;
|
|
38
47
|
|
|
39
48
|
constructor(
|
|
40
49
|
readonly app: App,
|
|
@@ -83,7 +92,9 @@ export class NativeApp {
|
|
|
83
92
|
|
|
84
93
|
//? The app's own output: its console (mirrored by a dev build), the simulator log stream or logcat.
|
|
85
94
|
#appLine = (line: string) => {
|
|
86
|
-
|
|
95
|
+
const read = NativeAppLine.read(line, { verbose: Executor.verbose });
|
|
96
|
+
if (this.#boot) this.#boot.lines.push(read);
|
|
97
|
+
else this.app.logger[read.level](read.message);
|
|
87
98
|
};
|
|
88
99
|
|
|
89
100
|
//* `AKAN_PUBLIC_*` is already inlined into the CSR bundle, so the runtime reads no .env file of its own.
|
|
@@ -98,6 +109,7 @@ export class NativeApp {
|
|
|
98
109
|
log: (event) => {
|
|
99
110
|
if (event.level === "error") this.app.logger.error(event.message);
|
|
100
111
|
else if (event.level === "warn") this.app.logger.warn(event.message);
|
|
112
|
+
else if (this.#boot) this.#boot.steps.push(event.message);
|
|
101
113
|
else if (event.level === "tool") this.app.logger.verbose(event.message);
|
|
102
114
|
else this.app.logger.info(event.message);
|
|
103
115
|
},
|
|
@@ -128,19 +140,47 @@ export class NativeApp {
|
|
|
128
140
|
//* The gateway serves the page and its HMR socket on the app origin; API calls go to the dev server itself (baseEnv),
|
|
129
141
|
//* which an Android device reaches through the reversed port.
|
|
130
142
|
async dev(platform: NativePlatform, { upstream, lang, device, teamId }: NativeDevOptions) {
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
const startedAt = performance.now();
|
|
144
|
+
const boot: NativeDevBoot = { steps: [], lines: [] };
|
|
145
|
+
this.#boot = boot;
|
|
146
|
+
try {
|
|
147
|
+
await mkdir(this.web.dir, { recursive: true });
|
|
148
|
+
const [{ api, config }, { api: routes }] = await Promise.all([this.prepare(), this.app.getConfig()]);
|
|
149
|
+
const session = await api.dev({
|
|
150
|
+
...this.#task(platform, config),
|
|
151
|
+
upstream,
|
|
152
|
+
hmrPath: "/_akan/hmr",
|
|
153
|
+
//? A dev page calls its own origin (akanjs baseEnv), so the gateway relays the socket it opens for the API too.
|
|
154
|
+
wsPaths: [`${routes.prefix}${routes.websocketPrefix}`],
|
|
155
|
+
onLine: this.#appLine,
|
|
156
|
+
startPath: this.startPath(lang),
|
|
157
|
+
...(device ? { device } : {}),
|
|
158
|
+
...(teamId ? { ios: { signing: { teamId } } } : {}),
|
|
159
|
+
});
|
|
160
|
+
this.#boot = null;
|
|
161
|
+
this.app.logger.info(this.readyLine(platform, { upstream, device: session.device, startedAt }));
|
|
162
|
+
for (const step of [...boot.steps, `pages through ${session.gateway}`]) this.app.logger.debug(step);
|
|
163
|
+
for (const line of boot.lines) this.app.logger[line.level](line.message);
|
|
164
|
+
return session;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
this.#boot = null;
|
|
167
|
+
for (const step of boot.steps) this.app.logger.info(step);
|
|
168
|
+
for (const line of boot.lines) this.app.logger[line.level === "debug" ? "info" : line.level](line.message);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** What a dev boot that came up says, and all it says: `cmdc ios ready · iPhone 16 · http://localhost:8283 · 12.4s`. */
|
|
174
|
+
readyLine(
|
|
175
|
+
platform: NativePlatform,
|
|
176
|
+
{ upstream, device, startedAt }: { upstream: string; device?: { name: string }; startedAt: number },
|
|
177
|
+
) {
|
|
178
|
+
const label = platform === "ios" || platform === "android" ? platform : "desktop";
|
|
179
|
+
const target = this.target.name === "default" ? "" : `/${this.target.name}`;
|
|
180
|
+
const seconds = ((performance.now() - startedAt) / 1000).toFixed(1);
|
|
181
|
+
return [`${this.app.name}${target} ${label} ready`, ...(device ? [device.name] : []), upstream, `${seconds}s`].join(
|
|
182
|
+
" · ",
|
|
183
|
+
);
|
|
144
184
|
}
|
|
145
185
|
|
|
146
186
|
async releaseIos({ teamId, adHoc = false }: { teamId?: string; adHoc?: boolean } = {}) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { NativeAppLine } from "./nativeAppLine";
|
|
3
|
+
|
|
4
|
+
describe("NativeAppLine", () => {
|
|
5
|
+
test("a page logger line keeps its level and its logger's name", () => {
|
|
6
|
+
expect(NativeAppLine.read("[page warn] [WsClient] WebSocket message process failed")).toEqual({
|
|
7
|
+
level: "warn",
|
|
8
|
+
message: "[page:WsClient] WebSocket message process failed",
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("a bare console call keeps its level, `log` reading as info", () => {
|
|
13
|
+
expect(NativeAppLine.read("[page log] hello")).toEqual({ level: "info", message: "[page] hello" });
|
|
14
|
+
expect(NativeAppLine.read("[page debug] [akan:frame:x:1] csr.render {}")).toEqual({
|
|
15
|
+
level: "debug",
|
|
16
|
+
message: "[page:akan:frame:x:1] csr.render {}",
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("a continuation line follows its message's level without a prefix of its own", () => {
|
|
21
|
+
expect(NativeAppLine.read("[page+ error] at run (app.js:1:2)")).toEqual({
|
|
22
|
+
level: "error",
|
|
23
|
+
message: " at run (app.js:1:2)",
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("a second window is named, the first is not", () => {
|
|
28
|
+
expect(NativeAppLine.read("[page#2 info] [App] ready")).toEqual({ level: "info", message: "[page#2:App] ready" });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("logcat lines read by tag and priority", () => {
|
|
32
|
+
expect(NativeAppLine.read("W/AkanNativeConsole( 1234): [WsClient] failed")).toEqual({
|
|
33
|
+
level: "warn",
|
|
34
|
+
message: "[page:WsClient] failed",
|
|
35
|
+
});
|
|
36
|
+
expect(NativeAppLine.read("E/AndroidRuntime( 1234): FATAL EXCEPTION: main")).toEqual({
|
|
37
|
+
level: "error",
|
|
38
|
+
message: "[AndroidRuntime] FATAL EXCEPTION: main",
|
|
39
|
+
});
|
|
40
|
+
expect(
|
|
41
|
+
NativeAppLine.read("I/AkanNative( 1234): pages from http://10.0.2.2:5000 (akan-native dev --hmr)").level,
|
|
42
|
+
).toBe("debug");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("the host's own lines stay visible but for where its pages come from", () => {
|
|
46
|
+
expect(NativeAppLine.read("[akan-native] opened in the browser: https://x.test")).toEqual({
|
|
47
|
+
level: "info",
|
|
48
|
+
message: "[akan-native] opened in the browser: https://x.test",
|
|
49
|
+
});
|
|
50
|
+
expect(
|
|
51
|
+
NativeAppLine.read("[akan-native native] pages from http://127.0.0.1:6000 (akan-native dev --hmr)").level,
|
|
52
|
+
).toBe("debug");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("a line no page or host wrote is kept for debug, or for info when verbose", () => {
|
|
56
|
+
expect(NativeAppLine.read("WebContent[123] Could not signal service")).toEqual({
|
|
57
|
+
level: "debug",
|
|
58
|
+
message: "WebContent[123] Could not signal service",
|
|
59
|
+
});
|
|
60
|
+
expect(NativeAppLine.read("WebContent[123] Could not signal service", { verbose: true }).level).toBe("info");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { LogLevel } from "akanjs/common";
|
|
2
|
+
|
|
3
|
+
export interface NativeAppLineRead {
|
|
4
|
+
level: LogLevel;
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
//* A line a native dev build prints — the page console its host mirrors, the simulator's console, logcat — read back
|
|
9
|
+
//* at the level the page logged it, so the terminal shows it once, stamped by the app's own logger.
|
|
10
|
+
export class NativeAppLine {
|
|
11
|
+
//? The host's grammar (desktop host.ts, iOS AkanNativeShell.swift): `[page<+><#window> <level>] text`, where `+`
|
|
12
|
+
//? marks a continuation line of a multi-line message and `#n` any window but the first.
|
|
13
|
+
static readonly #page = /^\[page(\+)?(?:#(\d+))? (trace|verbose|debug|log|info|warn|error)\] ?(.*)$/;
|
|
14
|
+
static readonly #logcat = /^([VDIWEFA])\/([^(]+?)\s*\(\s*\d+\):\s?(.*)$/;
|
|
15
|
+
static readonly #named = /^\[([^\]\s]+)\] ([\s\S]*)$/;
|
|
16
|
+
static readonly #logcatLevels: { [priority: string]: LogLevel } = {
|
|
17
|
+
V: "verbose",
|
|
18
|
+
D: "debug",
|
|
19
|
+
I: "info",
|
|
20
|
+
W: "warn",
|
|
21
|
+
E: "error",
|
|
22
|
+
F: "error",
|
|
23
|
+
A: "error",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** `verbose` keeps the lines no page or host wrote (WebKit, the simulator) at info instead of debug. */
|
|
27
|
+
static read(line: string, { verbose = false }: { verbose?: boolean } = {}): NativeAppLineRead {
|
|
28
|
+
const logcat = NativeAppLine.#logcat.exec(line);
|
|
29
|
+
if (logcat) {
|
|
30
|
+
const [, priority = "I", tag = "", text = ""] = logcat;
|
|
31
|
+
const level = NativeAppLine.#logcatLevels[priority] ?? "info";
|
|
32
|
+
if (tag === "AkanNativeConsole") return NativeAppLine.#pageLine(level, text, { continuation: false });
|
|
33
|
+
if (tag === "AkanNative") return NativeAppLine.#hostLine(`[akan-native] ${text}`, level);
|
|
34
|
+
return { level, message: `[${tag}] ${text}` };
|
|
35
|
+
}
|
|
36
|
+
const page = NativeAppLine.#page.exec(line);
|
|
37
|
+
if (page) {
|
|
38
|
+
const [, continuation, window, level = "info", text = ""] = page;
|
|
39
|
+
return NativeAppLine.#pageLine(level === "log" ? "info" : (level as LogLevel), text, {
|
|
40
|
+
continuation: continuation === "+",
|
|
41
|
+
window,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (line.startsWith("[akan-native")) return NativeAppLine.#hostLine(line, "info");
|
|
45
|
+
return { level: verbose ? "info" : "debug", message: line };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static #pageLine(
|
|
49
|
+
level: LogLevel,
|
|
50
|
+
text: string,
|
|
51
|
+
{ continuation, window }: { continuation: boolean; window?: string },
|
|
52
|
+
): NativeAppLineRead {
|
|
53
|
+
if (continuation) return { level, message: ` ${text}` };
|
|
54
|
+
const named = NativeAppLine.#named.exec(text);
|
|
55
|
+
const scope = `page${window ? `#${window}` : ""}${named ? `:${named[1]}` : ""}`;
|
|
56
|
+
return { level, message: `[${scope}] ${named ? named[2] : text}` };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
//? The host's own lines are about the host, not the app: where its pages come from is boot detail.
|
|
60
|
+
static #hostLine(line: string, level: LogLevel): NativeAppLineRead {
|
|
61
|
+
return { level: /\bpages from\b/.test(line) ? "debug" : level, message: line };
|
|
62
|
+
}
|
|
63
|
+
}
|
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.27",
|
|
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.27",
|
|
51
51
|
"chalk": "^5.6.2",
|
|
52
52
|
"commander": "^14.0.3",
|
|
53
53
|
"dayjs": "^1.11.20",
|