@akanjs/devkit 3.0.0-beta.25 → 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/DEV_RUNTIME_KNOBS.md +9 -6
- package/akanApp/akanApp.host.ts +79 -22
- package/akanApp/devHostPolicy.test.ts +56 -11
- package/akanApp/devHostPolicy.ts +10 -10
- 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/clientEntryDiscovery.test.ts +75 -0
- package/frontendBuild/clientEntryDiscovery.ts +112 -70
- 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 +261 -0
- package/frontendBuild/csrDevModuleCompiler.ts +95 -20
- package/frontendBuild/csrDevPatcher.test.ts +234 -2
- package/frontendBuild/csrDevPatcher.ts +184 -10
- package/frontendBuild/csrDevPaths.ts +32 -11
- package/frontendBuild/csrDevResolver.test.ts +76 -2
- package/frontendBuild/csrDevResolver.ts +141 -12
- package/frontendBuild/csrDevTypes.ts +2 -0
- package/frontendBuild/devRegistryBundler.ts +18 -3
- package/frontendBuild/ssrDevBundler.ts +2 -8
- package/incrementalBuilder/incrementalBuilder.host.test.ts +34 -0
- package/incrementalBuilder/incrementalBuilder.host.ts +18 -3
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +62 -1
- package/incrementalBuilder/incrementalBuilder.proc.ts +58 -24
- package/integration/devStability.integration.test.ts +9 -2
- 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
|
@@ -165,7 +165,7 @@ class IncrementalBuilder {
|
|
|
165
165
|
this.#logger.verbose(`build-route ok routeId=${msg.routeId} newEntries=${delta.newEntries.length}`);
|
|
166
166
|
//? The builder's generation, not the route cache's: the cache's does not move when a fixed file maps to no entry,
|
|
167
167
|
//? so the rebuild's ok would carry the failure's own number.
|
|
168
|
-
this.#sendBuildStatus("route", { generation: this.#generation, ok: true, files: msg.seeds });
|
|
168
|
+
this.#sendBuildStatus("route", { generation: this.#generation, ok: true, files: msg.seeds, scope: msg.routeId });
|
|
169
169
|
return {
|
|
170
170
|
type: "build-route-res",
|
|
171
171
|
id: msg.id,
|
|
@@ -185,24 +185,30 @@ class IncrementalBuilder {
|
|
|
185
185
|
} catch (err) {
|
|
186
186
|
const errMsg = ApplicationBuildReporter.formatError(err, this.#app.workspace.workspaceRoot);
|
|
187
187
|
this.#logger.error(`build-route failed routeId=${msg.routeId}: ${errMsg}`);
|
|
188
|
-
this.#sendBuildStatus("route", {
|
|
188
|
+
this.#sendBuildStatus("route", {
|
|
189
|
+
generation: this.#generation,
|
|
190
|
+
ok: false,
|
|
191
|
+
files: msg.seeds,
|
|
192
|
+
message: errMsg,
|
|
193
|
+
scope: msg.routeId,
|
|
194
|
+
});
|
|
189
195
|
return { type: "build-route-res", id: msg.id, ok: false, error: errMsg };
|
|
190
196
|
}
|
|
191
197
|
}
|
|
192
198
|
#sendBuildStatus(
|
|
193
199
|
phase: BuildPhase,
|
|
194
|
-
{
|
|
200
|
+
{
|
|
201
|
+
generation,
|
|
202
|
+
ok,
|
|
203
|
+
files,
|
|
204
|
+
message,
|
|
205
|
+
scope,
|
|
206
|
+
}: { generation?: number; ok: boolean; files?: string[]; message?: string; scope?: string },
|
|
195
207
|
): void {
|
|
196
208
|
if (typeof generation !== "number") return;
|
|
197
209
|
BuilderChannel.emit({
|
|
198
210
|
type: "build-status",
|
|
199
|
-
data: {
|
|
200
|
-
generation,
|
|
201
|
-
phase,
|
|
202
|
-
ok,
|
|
203
|
-
files: files ?? [],
|
|
204
|
-
message,
|
|
205
|
-
},
|
|
211
|
+
data: { generation, phase, ok, files: files ?? [], message, ...(scope !== undefined ? { scope } : {}) },
|
|
206
212
|
});
|
|
207
213
|
}
|
|
208
214
|
async #enqueueFast<T>(label: string, fn: () => Promise<T>): Promise<T> {
|
|
@@ -445,11 +451,8 @@ class IncrementalBuilder {
|
|
|
445
451
|
if (ssrToWorker) needs.unshift("ssr");
|
|
446
452
|
const batch: BatchJob = { generation, needs, changedFiles: files, trace, ...(discovery ? { discovery } : {}) };
|
|
447
453
|
// A worker's registry build rewrites the directory its patcher reads, so the next save waits for it.
|
|
448
|
-
if (needs.includes("csr") || needs.includes("ssr"))
|
|
449
|
-
|
|
450
|
-
if (needs.includes("csr")) this.#patcher?.forget();
|
|
451
|
-
if (needs.includes("ssr")) this.#ssrPatcher?.forget();
|
|
452
|
-
} else void this.#workQueue.enqueueBatch(batch).catch(this.#slowLaneFailed("batch"));
|
|
454
|
+
if (needs.includes("csr") || needs.includes("ssr")) await this.#workQueue.enqueueBatch(batch);
|
|
455
|
+
else void this.#workQueue.enqueueBatch(batch).catch(this.#slowLaneFailed("batch"));
|
|
453
456
|
return;
|
|
454
457
|
}
|
|
455
458
|
if (discovery)
|
|
@@ -525,6 +528,9 @@ class IncrementalBuilder {
|
|
|
525
528
|
const hold = await ServerGraphFile.touches(await ServerGraphFile.read(this.#artifactDir), files, (file) =>
|
|
526
529
|
ServerGraphFile.clientExportsOf(file),
|
|
527
530
|
);
|
|
531
|
+
//? Ahead of this save's own discovery job in the slow lane: which client entries a page still names decides which
|
|
532
|
+
//? pending roots this patch keeps.
|
|
533
|
+
this.#discovery.invalidate?.(files);
|
|
528
534
|
return await this.#withSsrLock(
|
|
529
535
|
async () => await this.#runSsrPatcher(patcher, files, { trace, hold, batchGeneration: generation }),
|
|
530
536
|
);
|
|
@@ -532,14 +538,16 @@ class IncrementalBuilder {
|
|
|
532
538
|
|
|
533
539
|
//* Adds the entries a route build names to the registry, since the tab requires them by id. A registry that cannot
|
|
534
540
|
//* take them fails no route build: the page answers, and its tab waits for the registry while the overlay says why.
|
|
535
|
-
async #ensureSsrEntries(entries: string[]): Promise<void> {
|
|
541
|
+
async #ensureSsrEntries(entries: string[], { crashRetry = false }: { crashRetry?: boolean } = {}): Promise<void> {
|
|
536
542
|
if (entries.length === 0) return;
|
|
537
543
|
//? The boot build takes every entry the routes reach, so the first page answers now and its tab waits for the
|
|
538
544
|
//? registry (boot.json) instead; whatever that build missed is added right after it lands.
|
|
539
545
|
const arming = this.#ssrArming;
|
|
540
546
|
if (arming) {
|
|
541
547
|
void this.#runBeside(
|
|
542
|
-
arming
|
|
548
|
+
arming
|
|
549
|
+
.then(async () => await this.#ensureSsrEntries(entries, { crashRetry }))
|
|
550
|
+
.catch(this.#slowLaneFailed("ssr-ensure")),
|
|
543
551
|
);
|
|
544
552
|
return;
|
|
545
553
|
}
|
|
@@ -558,6 +566,8 @@ class IncrementalBuilder {
|
|
|
558
566
|
//? the SSR lock that save's patch waits on.
|
|
559
567
|
if (this.#ssrBroken) return;
|
|
560
568
|
if (this.#ssrCrashedAt !== null && Date.now() - this.#ssrCrashedAt < IncrementalBuilder.#crashRetryMs) {
|
|
569
|
+
//? Handed over just now with nothing run for them: the retry must find them to hand over again.
|
|
570
|
+
patcher?.releaseHandedOver();
|
|
561
571
|
this.#retryAfterCrash(this.#ssrCrashedAt, entries);
|
|
562
572
|
return;
|
|
563
573
|
}
|
|
@@ -566,6 +576,9 @@ class IncrementalBuilder {
|
|
|
566
576
|
`ssr-registry could not take a route's entries; the save that fixes it rebuilds it: ${ApplicationBuildReporter.formatError(err, this.#app.workspace.workspaceRoot)}`,
|
|
567
577
|
);
|
|
568
578
|
});
|
|
579
|
+
//? A worker that died took these entries with it and left them pending nowhere: they are tried once more after the
|
|
580
|
+
//? crash window, but a retry that crashes again waits for the next route build or save.
|
|
581
|
+
if (this.#ssrCrashedAt !== null && !crashRetry) this.#retryAfterCrash(this.#ssrCrashedAt, entries);
|
|
569
582
|
});
|
|
570
583
|
}
|
|
571
584
|
|
|
@@ -580,7 +593,9 @@ class IncrementalBuilder {
|
|
|
580
593
|
const retried = [...this.#crashRetryEntries];
|
|
581
594
|
this.#crashRetryEntries.clear();
|
|
582
595
|
if (this.shuttingDown || retried.length === 0) return;
|
|
583
|
-
void this.#runBeside(this.#ensureSsrEntries(retried)).catch(
|
|
596
|
+
void this.#runBeside(this.#ensureSsrEntries(retried, { crashRetry: true })).catch(
|
|
597
|
+
this.#slowLaneFailed("ssr-crash-retry"),
|
|
598
|
+
);
|
|
584
599
|
},
|
|
585
600
|
Math.max(0, crashedAt + IncrementalBuilder.#crashRetryMs - Date.now()),
|
|
586
601
|
);
|
|
@@ -654,8 +669,12 @@ class IncrementalBuilder {
|
|
|
654
669
|
|
|
655
670
|
// Runs the worker directly, under the SSR lock its callers hold: queueing it could wait on a batch that waits on them.
|
|
656
671
|
async #runSsrWorker(generation: number): Promise<void> {
|
|
657
|
-
|
|
658
|
-
|
|
672
|
+
let result: BuildBatchResult;
|
|
673
|
+
try {
|
|
674
|
+
result = await this.#runBatch({ generation, needs: ["ssr"], changedFiles: [] });
|
|
675
|
+
} finally {
|
|
676
|
+
this.#ssrPatcher?.forget();
|
|
677
|
+
}
|
|
659
678
|
this.#noteSsrWorker(result);
|
|
660
679
|
if (result.errors.ssr) throw new Error(result.errors.ssr);
|
|
661
680
|
}
|
|
@@ -741,9 +760,20 @@ class IncrementalBuilder {
|
|
|
741
760
|
//? The slow lane runs in order, so every batch up to this one has had its discovery job.
|
|
742
761
|
this.#discoveredThrough = Math.max(this.#discoveredThrough, work.generation);
|
|
743
762
|
// A worker writing ssr-dev must not overlap the boot build, which no longer runs in this lane.
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
763
|
+
//? Forgotten inside the lock: a route build's check waiting on it would patch the old state over the worker's.
|
|
764
|
+
try {
|
|
765
|
+
if (work.needs.includes("ssr"))
|
|
766
|
+
await this.#withSsrLock(async () => {
|
|
767
|
+
try {
|
|
768
|
+
this.#noteSsrWorker(await this.#runBatch(work));
|
|
769
|
+
} finally {
|
|
770
|
+
this.#ssrPatcher?.forget();
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
else await this.#runBatch(work);
|
|
774
|
+
} finally {
|
|
775
|
+
if (work.needs.includes("csr")) this.#patcher?.forget();
|
|
776
|
+
}
|
|
747
777
|
}
|
|
748
778
|
|
|
749
779
|
async #refreshDiscovery({ files, refresh, generation }: DiscoveryJob): Promise<void> {
|
|
@@ -762,8 +792,12 @@ class IncrementalBuilder {
|
|
|
762
792
|
if (result.cssAssets) this.#artifact = { ...this.#artifact, cssAssets: result.cssAssets };
|
|
763
793
|
// A need the worker died before reporting goes red here instead of looking silently successful.
|
|
764
794
|
if (result.crashed) {
|
|
795
|
+
const crashedNeeds = result.crashedNeeds ?? needs;
|
|
796
|
+
//? A worker that died wrote no failure for the roots handed to it: they must not wait on one that never comes.
|
|
797
|
+
if (crashedNeeds.includes("csr")) this.#patcher?.releaseHandedOver();
|
|
798
|
+
if (crashedNeeds.includes("ssr")) this.#ssrPatcher?.releaseHandedOver();
|
|
765
799
|
// `base` is not a `BuildPhase` and never comes through here: `#buildBootDeps` throws into the degraded boot.
|
|
766
|
-
for (const need of
|
|
800
|
+
for (const need of crashedNeeds)
|
|
767
801
|
if (need !== "base")
|
|
768
802
|
this.#sendBuildStatus(need, {
|
|
769
803
|
generation,
|
|
@@ -589,6 +589,7 @@ describe("dev resource budgets", () => {
|
|
|
589
589
|
// A ceiling the replacement is back over within the interval is reported, and still enforced (recycles are
|
|
590
590
|
// throttled), never disabled for the session. Whether it is back over depends on the machine's allocator, so the
|
|
591
591
|
// warning is asserted only when it is; the decision itself is unit-tested (devHostPolicy).
|
|
592
|
+
const settledFrom = host.markLog();
|
|
592
593
|
for (let i = 1; i <= 3; i++) {
|
|
593
594
|
const { mark } = await harness.editUntilSeen(host, (attempt) =>
|
|
594
595
|
harness.replaceText("ui/ClientMarker.tsx", /marker(-[\w-]+)?/, `marker-settled-${i}-${attempt}`),
|
|
@@ -596,8 +597,14 @@ describe("dev resource budgets", () => {
|
|
|
596
597
|
await host.waitForLogSince(mark, /pages-rebundle ok/, WAIT_MS).catch(() => undefined);
|
|
597
598
|
}
|
|
598
599
|
const settled = await DevStabilityHarness.builderProcess(host.proc.pid);
|
|
599
|
-
|
|
600
|
-
|
|
600
|
+
const warned = /ceiling costs about one boot build per interval/.test(host.logs.join("").slice(start));
|
|
601
|
+
//? Past the interval (a slow machine) the next report recycles instead of warning: either answer settles it.
|
|
602
|
+
if ((settled?.rssBytes ?? 0) >= 200 * MB && !warned)
|
|
603
|
+
await host.waitForLogSince(
|
|
604
|
+
settledFrom,
|
|
605
|
+
/ceiling costs about one boot build per interval|recycling builder pid=\d+|skipped: the builder fell to/,
|
|
606
|
+
WAIT_MS,
|
|
607
|
+
);
|
|
601
608
|
expect(host.logs.join("").slice(start)).not.toMatch(/no longer enforcing it this session/);
|
|
602
609
|
});
|
|
603
610
|
|
package/mobile/nativeApp.test.ts
CHANGED
|
@@ -38,6 +38,21 @@ describe("NativeApp", () => {
|
|
|
38
38
|
expect(admin.outDir("android")).toBe(path.join(appDir, ".akan/mobile/admin/native/android"));
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
test("a dev boot that came up says so in one line: platform, device, dev server, time", () => {
|
|
42
|
+
const app = { ...fakeApp(), name: "portal" } as unknown as App;
|
|
43
|
+
const startedAt = performance.now() - 12_400;
|
|
44
|
+
expect(new NativeApp(app, target()).readyLine("macos", { upstream: "http://localhost:8283", startedAt })).toMatch(
|
|
45
|
+
/^portal desktop ready · http:\/\/localhost:8283 · 12\.\ds$/,
|
|
46
|
+
);
|
|
47
|
+
expect(
|
|
48
|
+
new NativeApp(app, target({ name: "admin" })).readyLine("ios", {
|
|
49
|
+
upstream: "http://localhost:8283",
|
|
50
|
+
device: { name: "iPhone 16" },
|
|
51
|
+
startedAt,
|
|
52
|
+
}),
|
|
53
|
+
).toMatch(/^portal\/admin ios ready · iPhone 16 · http:\/\/localhost:8283 · 12\.\ds$/);
|
|
54
|
+
});
|
|
55
|
+
|
|
41
56
|
test("opens a dev build on its target's home, the CSR shell under the locale", () => {
|
|
42
57
|
expect(new NativeApp(fakeApp(), target({ indexPath: "/explore" })).startPath("en")).toBe(
|
|
43
58
|
"/en/explore?csr=true&akanMobileTarget=default&akanMobileIndexPath=%2Fexplore",
|
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",
|