@akanjs/devkit 3.0.0-alpha.88 → 3.0.0-alpha.89
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 +27 -0
- package/akanApp/akanApp.host.ts +49 -17
- package/akanApp/devHostPolicy.ts +30 -0
- package/akanConfig/akanConfig.ts +11 -0
- package/akanConfig/types.ts +1 -0
- package/appSelectionMemory.ts +28 -0
- package/codegenLock.test.ts +137 -0
- package/codegenLock.ts +136 -0
- package/commandDecorators/argMeta.ts +9 -1
- package/commandDecorators/command.ts +64 -1
- package/commandDecorators/helpFormatter.ts +4 -0
- package/executors.ts +18 -11
- package/fileSys.ts +19 -1
- package/frontendBuild/autoImportSync.ts +3 -2
- package/frontendBuild/devGeneratedIndexSync.ts +3 -2
- package/incrementalBuilder/incrementalBuilder.host.ts +50 -6
- package/incrementalBuilder/incrementalBuilder.proc.ts +9 -2
- package/index.ts +0 -2
- package/package.json +2 -4
- package/ui/MultiScrollList.tsx +0 -242
- package/ui/ScrollList.tsx +0 -105
- package/ui/index.ts +0 -2
- package/useStdoutDimensions.ts +0 -20
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
checkbox as inquirerCheckbox,
|
|
4
|
+
confirm as inquirerConfirm,
|
|
5
|
+
input as inquirerInput,
|
|
6
|
+
select as inquirerSelect,
|
|
7
|
+
} from "@inquirer/prompts";
|
|
3
8
|
import { Logger } from "akanjs/common";
|
|
4
9
|
import chalk from "chalk";
|
|
5
10
|
import { type Command, program } from "commander";
|
|
11
|
+
import { AppSelectionMemory } from "../appSelectionMemory";
|
|
6
12
|
import { AppExecutor, Executor, LibExecutor, ModuleExecutor, PkgExecutor, WorkspaceExecutor } from "../executors";
|
|
7
13
|
// Import the owning modules directly, never the root barrel: `..` re-exports all 41 devkit modules,
|
|
8
14
|
// so a barrel import here drags ink, @trapezedev/project, ssh2, @langchain/* and the cloud stack into
|
|
@@ -103,6 +109,7 @@ const prompts = async () => await import("@inquirer/prompts");
|
|
|
103
109
|
const select = ((config, context) => prompts().then((m) => m.select(config, context))) as typeof inquirerSelect;
|
|
104
110
|
const confirm = ((config, context) => prompts().then((m) => m.confirm(config, context))) as typeof inquirerConfirm;
|
|
105
111
|
const input = ((config, context) => prompts().then((m) => m.input(config, context))) as typeof inquirerInput;
|
|
112
|
+
const checkbox = ((config, context) => prompts().then((m) => m.checkbox(config, context))) as typeof inquirerCheckbox;
|
|
106
113
|
|
|
107
114
|
/**
|
|
108
115
|
* Rejects a value that is not one of the declared choices, in commander's own wording.
|
|
@@ -208,6 +215,46 @@ const assertCurrentDirectoryIsWorkspaceRoot = async () => {
|
|
|
208
215
|
);
|
|
209
216
|
};
|
|
210
217
|
|
|
218
|
+
/** A variadic positional arrives as an array, and each entry may itself be a comma-separated list. */
|
|
219
|
+
const parseAppNameList = (value: string | string[] | undefined): string[] =>
|
|
220
|
+
(Array.isArray(value) ? value : value === undefined ? [] : [value])
|
|
221
|
+
.flatMap((entry) => entry.split(","))
|
|
222
|
+
.map((entry) => entry.trim())
|
|
223
|
+
.filter(Boolean);
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The `Apps` token: named apps, `all`, or a checkbox when the command line names none. Kept out of
|
|
227
|
+
* `getInternalArgumentValue` because it is the only internal arg whose positional is variadic, so it is
|
|
228
|
+
* the only one whose raw value is an array.
|
|
229
|
+
*/
|
|
230
|
+
export const getAppsArgumentValue = async (
|
|
231
|
+
value: string | string[] | undefined,
|
|
232
|
+
workspace: WorkspaceExecutor,
|
|
233
|
+
): Promise<AppExecutor[]> => {
|
|
234
|
+
const appNames = await workspace.getApps();
|
|
235
|
+
if (appNames.length === 0) throw new Error("No apps found in this workspace (apps/<appName>/akan.config.ts)");
|
|
236
|
+
const requested = parseAppNameList(value);
|
|
237
|
+
if (requested.includes("all")) return appNames.map((name) => AppExecutor.from(workspace, name));
|
|
238
|
+
if (requested.length) {
|
|
239
|
+
const unknown = requested.filter((name) => !appNames.includes(name));
|
|
240
|
+
if (unknown.length)
|
|
241
|
+
throw new Error(
|
|
242
|
+
`Unknown app${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}. Available: ${appNames.join(", ")}, all`,
|
|
243
|
+
);
|
|
244
|
+
return [...new Set(requested)].map((name) => AppExecutor.from(workspace, name));
|
|
245
|
+
}
|
|
246
|
+
if (appNames.length === 1 && appNames[0]) return [AppExecutor.from(workspace, appNames[0])];
|
|
247
|
+
const remembered = new Set(await AppSelectionMemory.read(workspace.workspaceRoot));
|
|
248
|
+
const picked = await checkbox<string>({
|
|
249
|
+
message: "Select the apps to run (space to toggle, enter to confirm)",
|
|
250
|
+
choices: appNames.map((name) => ({ name, value: name, checked: remembered.has(name) })),
|
|
251
|
+
// Enter with nothing ticked is a mis-keypress far more often than an intent to run nothing.
|
|
252
|
+
required: true,
|
|
253
|
+
});
|
|
254
|
+
await AppSelectionMemory.write(workspace.workspaceRoot, picked);
|
|
255
|
+
return picked.map((name) => AppExecutor.from(workspace, name));
|
|
256
|
+
};
|
|
257
|
+
|
|
211
258
|
export const getInternalArgumentValue = async (
|
|
212
259
|
argMeta: InternalArgMeta,
|
|
213
260
|
value: string | undefined,
|
|
@@ -353,6 +400,11 @@ It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
|
|
|
353
400
|
`[sys-name:module-name]`,
|
|
354
401
|
`${argMeta.type} in this workspace (apps|libs)/<sys-name>/lib/<module-name>`,
|
|
355
402
|
);
|
|
403
|
+
} else if (argMeta.type === "Apps") {
|
|
404
|
+
programCommand = programCommand.argument(
|
|
405
|
+
`[apps...]`,
|
|
406
|
+
`apps in this workspace apps/<appName>, or all (space- or comma-separated; omit to pick interactively)`,
|
|
407
|
+
);
|
|
356
408
|
} else {
|
|
357
409
|
const sysType = argMeta.type.toLowerCase();
|
|
358
410
|
programCommand = programCommand.argument(
|
|
@@ -381,6 +433,11 @@ It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
|
|
|
381
433
|
commandArgs[argMeta.idx] = await getOptionValue(argMeta, opt, commandContext);
|
|
382
434
|
else if (argMeta.type === "Argument")
|
|
383
435
|
commandArgs[argMeta.idx] = await getArgumentValue(argMeta, cmdArgs[argMeta.idx] as string);
|
|
436
|
+
else if (argMeta.type === "Apps")
|
|
437
|
+
commandArgs[argMeta.idx] = await getAppsArgumentValue(
|
|
438
|
+
cmdArgs[argMeta.idx] as string | string[] | undefined,
|
|
439
|
+
workspace,
|
|
440
|
+
);
|
|
384
441
|
else
|
|
385
442
|
commandArgs[argMeta.idx] = await getInternalArgumentValue(
|
|
386
443
|
argMeta as InternalArgMeta,
|
|
@@ -390,6 +447,12 @@ It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
|
|
|
390
447
|
// set app name to env
|
|
391
448
|
if (commandArgs[argMeta.idx] instanceof AppExecutor)
|
|
392
449
|
process.env.AKAN_PUBLIC_APP_NAME = (commandArgs[argMeta.idx] as AppExecutor).name;
|
|
450
|
+
//? Only when exactly one app resolved. Publishing a name while several are in play would make
|
|
451
|
+
//? every env-derived answer in this process belong to whichever app happened to be last.
|
|
452
|
+
else if (Array.isArray(commandArgs[argMeta.idx])) {
|
|
453
|
+
const apps = commandArgs[argMeta.idx] as AppExecutor[];
|
|
454
|
+
if (apps.length === 1 && apps[0]) process.env.AKAN_PUBLIC_APP_NAME = apps[0].name;
|
|
455
|
+
}
|
|
393
456
|
assignCommandContext(commandContext, argMeta, commandArgs[argMeta.idx]);
|
|
394
457
|
if ((opt as { verbose?: boolean }).verbose) Executor.setVerbose(true);
|
|
395
458
|
}
|
|
@@ -159,6 +159,7 @@ export const formatCommandHelp = (command: CommandCls, key: string) => {
|
|
|
159
159
|
.map((arg) => {
|
|
160
160
|
if (arg.type === "Workspace") return "";
|
|
161
161
|
if (arg.type === "Module") return "[sys:module]";
|
|
162
|
+
if (arg.type === "Apps") return "[apps...]";
|
|
162
163
|
if (arg.type === "Argument") {
|
|
163
164
|
return `[${camelToKebabCase(arg.name)}]`;
|
|
164
165
|
}
|
|
@@ -192,6 +193,9 @@ export const formatCommandHelp = (command: CommandCls, key: string) => {
|
|
|
192
193
|
} else if (arg.type === "Module") {
|
|
193
194
|
argName = "sys:module";
|
|
194
195
|
argDesc = "Module in format: app-name:module-name or lib-name:module-name";
|
|
196
|
+
} else if (arg.type === "Apps") {
|
|
197
|
+
argName = "apps...";
|
|
198
|
+
argDesc = "App names, space- or comma-separated, or all. Omit to pick them interactively";
|
|
195
199
|
} else {
|
|
196
200
|
argName = arg.type.toLowerCase();
|
|
197
201
|
argDesc = `${arg.type} name in this workspace`;
|
package/executors.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
} from "./agentsIndex";
|
|
42
42
|
import { AkanAppConfig, AkanLibConfig, decreaseBuildNum, increaseBuildNum } from "./akanConfig";
|
|
43
43
|
import { getRootBoundarySegments, isRootBoundarySegments } from "./artifact/implicitRootLayout";
|
|
44
|
+
import { CodegenLock } from "./codegenLock";
|
|
44
45
|
import { FileSys } from "./fileSys";
|
|
45
46
|
import { getDirname } from "./getDirname";
|
|
46
47
|
import { Linter } from "./linter";
|
|
@@ -1136,18 +1137,22 @@ export class SysExecutor extends Executor {
|
|
|
1136
1137
|
: await LibInfo.fromExecutor(this as unknown as LibExecutor, {
|
|
1137
1138
|
refresh,
|
|
1138
1139
|
});
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1140
|
+
//* `writeLib` regenerates every dependency lib's barrels, and each mounting app's `akan start`
|
|
1141
|
+
//* regenerates the same ones — so this region races the other dev servers in the workspace and the
|
|
1142
|
+
//* builders that watch what it writes.
|
|
1143
|
+
if (write)
|
|
1144
|
+
await CodegenLock.run(this.workspace.workspaceRoot, `scan:${this.name}`, async () => {
|
|
1145
|
+
await Promise.all(this.#getScanTemplateTasks(scanInfo));
|
|
1146
|
+
await this.writeJson(`akan.${this.type}.json`, scanInfo.getScanResult());
|
|
1147
|
+
if (this.type === "lib") this.#updateDependencies(scanInfo);
|
|
1143
1148
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1149
|
+
if (writeLib) {
|
|
1150
|
+
const libInfos = [...scanInfo.getLibInfos().values()];
|
|
1151
|
+
await this.#updateDependencies(scanInfo);
|
|
1152
|
+
await Promise.all(libInfos.flatMap((libInfo) => libInfo.exec.#getScanTemplateTasks(libInfo)));
|
|
1153
|
+
}
|
|
1154
|
+
await this.syncAgentsIndex(scanInfo);
|
|
1155
|
+
});
|
|
1151
1156
|
this.#scanInfo = scanInfo;
|
|
1152
1157
|
return scanInfo;
|
|
1153
1158
|
}
|
|
@@ -1405,6 +1410,8 @@ export class AppExecutor extends SysExecutor {
|
|
|
1405
1410
|
const databaseMode = process.env.AKAN_DATABASE_MODE ?? akanConfig.defaultDatabaseMode ?? "single";
|
|
1406
1411
|
const routeEnv = {
|
|
1407
1412
|
AKAN_PUBLIC_BASE_PATHS: [...akanConfig.basePaths].join(","),
|
|
1413
|
+
AKAN_PUBLIC_API_PREFIX: akanConfig.api.prefix,
|
|
1414
|
+
AKAN_PUBLIC_WS_PREFIX: akanConfig.api.websocketPrefix,
|
|
1408
1415
|
AKAN_DATABASE_MODE: databaseMode,
|
|
1409
1416
|
};
|
|
1410
1417
|
Object.assign(process.env, routeEnv);
|
package/fileSys.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { lstat, stat } from "node:fs/promises";
|
|
1
|
+
import { lstat, rename, rm, stat } from "node:fs/promises";
|
|
2
2
|
import { Logger } from "akanjs/common";
|
|
3
3
|
|
|
4
4
|
export class FileSys {
|
|
@@ -39,6 +39,24 @@ export class FileSys {
|
|
|
39
39
|
static async writeText(path: string, content: string) {
|
|
40
40
|
return await Bun.file(path).write(content);
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Replaces a file in one `rename`, so a concurrent reader sees either the old bytes or the new ones.
|
|
44
|
+
* Generated barrels are written by every dev server watching the same `libs/` tree while those same
|
|
45
|
+
* servers' watchers are reading them, and a half-written barrel reads back as a user edit.
|
|
46
|
+
*
|
|
47
|
+
* The temp file is a sibling because `rename` is only atomic within a filesystem, and it lands in a
|
|
48
|
+
* watched directory — the `.tmp` suffix is what `HmrChangeClassifier` ignores it by.
|
|
49
|
+
*/
|
|
50
|
+
static async writeTextAtomic(filePath: string, content: string) {
|
|
51
|
+
const temp = `${filePath}.${process.pid}.${Date.now().toString(36)}.tmp`;
|
|
52
|
+
try {
|
|
53
|
+
await Bun.write(temp, content);
|
|
54
|
+
await rename(temp, filePath);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
await rm(temp, { force: true }).catch(() => undefined);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
42
60
|
static async writeJson(path: string, content: object) {
|
|
43
61
|
return await Bun.file(path).write(`${JSON.stringify(content, null, 2)}\n`);
|
|
44
62
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { readdir, readFile, stat
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import ts from "typescript";
|
|
4
|
+
import { FileSys } from "../fileSys";
|
|
4
5
|
|
|
5
6
|
//* Auto-import daemon: a source-editing sibling of DevGeneratedIndexSync. When a domain file changes,
|
|
6
7
|
//* framework symbols that are used but not imported (e.g. `Int` in a *.constant.ts, `fetch` in a *.store.ts)
|
|
@@ -267,7 +268,7 @@ export class AutoImportSync {
|
|
|
267
268
|
const resolveExtra = await this.#extraResolverFor(abs, ctx);
|
|
268
269
|
const next = transformSource(source, abs, ctx, resolveExtra);
|
|
269
270
|
if (next === null || next === source) return false;
|
|
270
|
-
await
|
|
271
|
+
await FileSys.writeTextAtomic(abs, next);
|
|
271
272
|
return true;
|
|
272
273
|
}
|
|
273
274
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile, rm, stat
|
|
1
|
+
import { mkdir, readdir, readFile, rm, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { FileSys } from "../fileSys";
|
|
3
4
|
|
|
4
5
|
const BARREL_FACETS = new Set(["common", "srvkit", "ui", "webkit", "plugin"]);
|
|
5
6
|
const FACET_SOURCE_FILE_RE = /\.(ts|tsx)$/;
|
|
@@ -101,7 +102,7 @@ export class DevGeneratedIndexSync {
|
|
|
101
102
|
const current = await readFile(indexPath, "utf8").catch(() => null);
|
|
102
103
|
if (current === content) return false;
|
|
103
104
|
await mkdir(dir, { recursive: true });
|
|
104
|
-
await
|
|
105
|
+
await FileSys.writeTextAtomic(indexPath, content);
|
|
105
106
|
return true;
|
|
106
107
|
}
|
|
107
108
|
|
|
@@ -14,11 +14,25 @@ const builderMsgTypeSet = new Set<BuilderMessage["type"]>([
|
|
|
14
14
|
"build-status",
|
|
15
15
|
"builder-metrics",
|
|
16
16
|
]);
|
|
17
|
+
/**
|
|
18
|
+
* Where a dev child process writes. `"pipe"` is not only about keeping a TUI's frame clean: a Bun child
|
|
19
|
+
* that inherits the controlling terminal snapshots its termios at spawn and writes that snapshot back
|
|
20
|
+
* when it exits, so a builder spawned while something holds the terminal raw turns it raw again on the
|
|
21
|
+
* first recycle. See the XXX comment on `Spinner.oraOptions`.
|
|
22
|
+
*/
|
|
23
|
+
export type DevStdioMode = "inherit" | "pipe";
|
|
24
|
+
|
|
17
25
|
interface IncrementalBuilderHostOptions {
|
|
18
26
|
app: App;
|
|
19
27
|
entry: string;
|
|
20
28
|
env: Record<string, string>;
|
|
29
|
+
stdio?: DevStdioMode;
|
|
21
30
|
onMessage: (message: BuilderMessage) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Required when `stdio` is `"pipe"`: an undrained pipe fills and blocks the builder's next write.
|
|
33
|
+
* Text arrives as decoded chunks, not lines — a consumer that needs lines splits them itself.
|
|
34
|
+
*/
|
|
35
|
+
onOutput?: (kind: "stdout" | "stderr", text: string) => void;
|
|
22
36
|
}
|
|
23
37
|
|
|
24
38
|
/**
|
|
@@ -63,7 +77,9 @@ export class IncrementalBuilderHost {
|
|
|
63
77
|
app: App;
|
|
64
78
|
ready = false;
|
|
65
79
|
readonly #onMessage: (message: BuilderMessage) => void;
|
|
66
|
-
#
|
|
80
|
+
readonly #stdio: DevStdioMode;
|
|
81
|
+
readonly #onOutput: ((kind: "stdout" | "stderr", text: string) => void) | null;
|
|
82
|
+
#proc: Bun.Subprocess<"ignore", "inherit" | "pipe", "inherit" | "pipe"> | null = null;
|
|
67
83
|
#status: IncrementalBuilderStatus = "stopped";
|
|
68
84
|
#restartAttempts = 0;
|
|
69
85
|
#restartTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -83,11 +99,13 @@ export class IncrementalBuilderHost {
|
|
|
83
99
|
*/
|
|
84
100
|
readonly #inFlight = new Map<number, "build-route" | "build-csr">();
|
|
85
101
|
#startOptions: IncrementalBuilderStartOptions = {};
|
|
86
|
-
constructor({ app, entry, env, onMessage }: IncrementalBuilderHostOptions) {
|
|
102
|
+
constructor({ app, entry, env, stdio = "inherit", onMessage, onOutput }: IncrementalBuilderHostOptions) {
|
|
87
103
|
this.app = app;
|
|
88
104
|
this.entry = entry;
|
|
89
105
|
this.env = env;
|
|
106
|
+
this.#stdio = stdio;
|
|
90
107
|
this.#onMessage = onMessage;
|
|
108
|
+
this.#onOutput = onOutput ?? null;
|
|
91
109
|
}
|
|
92
110
|
get status() {
|
|
93
111
|
return this.#status;
|
|
@@ -115,11 +133,11 @@ export class IncrementalBuilderHost {
|
|
|
115
133
|
// the flag is what tells it to re-announce what it booted with.
|
|
116
134
|
const afterRecycle = this.#spawnAfterRecycle;
|
|
117
135
|
this.#spawnAfterRecycle = false;
|
|
118
|
-
let proc!: Bun.Subprocess<"ignore", "inherit", "inherit">;
|
|
136
|
+
let proc!: Bun.Subprocess<"ignore", "inherit" | "pipe", "inherit" | "pipe">;
|
|
119
137
|
proc = Bun.spawn(["bun", this.entry], {
|
|
120
138
|
cwd: this.app.cwdPath,
|
|
121
139
|
env: { ...this.env, AKAN_WATCH: "1", ...(afterRecycle ? { AKAN_BUILDER_ANNOUNCE_BOOT: "1" } : {}) },
|
|
122
|
-
stdio: ["ignore",
|
|
140
|
+
stdio: ["ignore", this.#stdio, this.#stdio],
|
|
123
141
|
ipc: (msg: BuilderMessage) => {
|
|
124
142
|
if (this.#proc !== proc) return;
|
|
125
143
|
if (!msg || typeof msg !== "object") return;
|
|
@@ -167,8 +185,25 @@ export class IncrementalBuilderHost {
|
|
|
167
185
|
},
|
|
168
186
|
});
|
|
169
187
|
this.#proc = proc;
|
|
188
|
+
// Re-attached per spawn, not once per host: a recycle or a crash-restart brings new pipes.
|
|
189
|
+
if (this.#stdio === "pipe" && this.#onOutput) {
|
|
190
|
+
void this.#drain(proc.stdout as unknown as ReadableStream<Uint8Array> | undefined, "stdout");
|
|
191
|
+
void this.#drain(proc.stderr as unknown as ReadableStream<Uint8Array> | undefined, "stderr");
|
|
192
|
+
}
|
|
170
193
|
this.logger.verbose(`builder spawned pid=${proc.pid} entry=${this.entry}${isRestart ? " restart=1" : ""}`);
|
|
171
194
|
}
|
|
195
|
+
async #drain(stream: ReadableStream<Uint8Array> | undefined | null, kind: "stdout" | "stderr") {
|
|
196
|
+
if (!stream) return;
|
|
197
|
+
const decoder = new TextDecoder();
|
|
198
|
+
try {
|
|
199
|
+
for await (const chunk of stream) {
|
|
200
|
+
const text = decoder.decode(chunk, { stream: true });
|
|
201
|
+
if (text) this.#onOutput?.(kind, text);
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
// The stream closes when the builder exits; nothing further to surface here.
|
|
205
|
+
}
|
|
206
|
+
}
|
|
172
207
|
#scheduleRestart() {
|
|
173
208
|
if (this.#manualStop || this.#restartTimer) return;
|
|
174
209
|
this.#status = "restarting";
|
|
@@ -278,7 +313,15 @@ export class IncrementalBuilderHost {
|
|
|
278
313
|
this.ready = false;
|
|
279
314
|
this.#status = "stopped";
|
|
280
315
|
}
|
|
281
|
-
static async create(
|
|
316
|
+
static async create(
|
|
317
|
+
app: App,
|
|
318
|
+
env: Record<string, string>,
|
|
319
|
+
onMessage: (message: BuilderMessage) => void,
|
|
320
|
+
{
|
|
321
|
+
stdio = "inherit",
|
|
322
|
+
onOutput,
|
|
323
|
+
}: { stdio?: DevStdioMode; onOutput?: (kind: "stdout" | "stderr", text: string) => void } = {},
|
|
324
|
+
) {
|
|
282
325
|
const candidates = [
|
|
283
326
|
path.join(app.workspace.workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts"),
|
|
284
327
|
path.join(
|
|
@@ -289,7 +332,8 @@ export class IncrementalBuilderHost {
|
|
|
289
332
|
path.join(import.meta.dir, "incrementalBuilder.proc.ts"),
|
|
290
333
|
];
|
|
291
334
|
for (const c of candidates)
|
|
292
|
-
if (await Bun.file(c).exists())
|
|
335
|
+
if (await Bun.file(c).exists())
|
|
336
|
+
return new IncrementalBuilderHost({ app, entry: c, env, stdio, onMessage, onOutput });
|
|
293
337
|
throw new Error(`[cli] frontend builder entry not found; looked in: ${candidates.join(", ")}`);
|
|
294
338
|
}
|
|
295
339
|
}
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
// barrel reaches `cssCompiler`/`ssrBaseArtifactBuilder`, which pull tailwindcss + @tailwindcss/node
|
|
5
5
|
// (~40MB) into a process that then holds them for the whole dev session. Phase 2 moved css compilation
|
|
6
6
|
// into the batch worker, so this process has no use for them — `entryModuleGraph.test.ts` keeps it that way.
|
|
7
|
+
import { CodegenLock } from "@akanjs/devkit/codegenLock";
|
|
7
8
|
import type { App } from "@akanjs/devkit/commandDecorators";
|
|
8
9
|
import { AppExecutor, type PageRoot, WorkspaceExecutor } from "@akanjs/devkit/executors";
|
|
9
10
|
import { AutoImportSync } from "@akanjs/devkit/frontendBuild/autoImportSync";
|
|
@@ -299,11 +300,17 @@ class IncrementalBuilder {
|
|
|
299
300
|
//* Insert framework imports that are used but omitted (e.g. `Int` in *.constant.ts, `fetch` in
|
|
300
301
|
//* *.store.ts) before regenerating barrels. Edits land on files already in this batch, so they
|
|
301
302
|
//* rebuild in this same generation; the write is idempotent so it does not re-trigger the watcher.
|
|
302
|
-
const autoImport = await
|
|
303
|
+
const [autoImport, indexSync] = await CodegenLock.run(
|
|
304
|
+
this.#app.workspace.workspaceRoot,
|
|
305
|
+
`hmr-batch:${this.#app.name}`,
|
|
306
|
+
async () => {
|
|
307
|
+
const auto = await this.#autoImportSync.syncForBatch(batch.files);
|
|
308
|
+
return [auto, await this.#generatedIndexSync.syncForBatch(batch.files)] as const;
|
|
309
|
+
},
|
|
310
|
+
);
|
|
303
311
|
for (const error of autoImport.errors) this.#logger.error(error);
|
|
304
312
|
if (autoImport.changedFiles.length > 0)
|
|
305
313
|
this.#logger.verbose(`[auto-import] inserted imports into ${autoImport.changedFiles.length} file(s)`);
|
|
306
|
-
const indexSync = await this.#generatedIndexSync.syncForBatch(batch.files);
|
|
307
314
|
//* Both passes above write source files, and this generation's build consumes what they wrote. Hand
|
|
308
315
|
//* them to the watcher so its verification scan does not read them back as a user edit and spend a
|
|
309
316
|
//* second generation rebuilding identical content.
|
package/index.ts
CHANGED
|
@@ -45,7 +45,5 @@ export type * from "./spinner";
|
|
|
45
45
|
export type * from "./transforms";
|
|
46
46
|
export type * from "./typeChecker";
|
|
47
47
|
export type * from "./types";
|
|
48
|
-
export type * from "./ui";
|
|
49
48
|
export type * from "./uploadRelease";
|
|
50
|
-
export type * from "./useStdoutDimensions";
|
|
51
49
|
export type * from "./workflow";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.89",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
"./incrementalBuilder": "./incrementalBuilder/index.ts",
|
|
34
34
|
"./mobile": "./mobile/index.ts",
|
|
35
35
|
"./transforms": "./transforms/index.ts",
|
|
36
|
-
"./ui": "./ui/index.ts",
|
|
37
36
|
"./workflow": "./workflow/index.ts",
|
|
38
37
|
"./*.ts": "./*.ts",
|
|
39
38
|
"./*": "./*.ts"
|
|
@@ -45,14 +44,13 @@
|
|
|
45
44
|
"@langchain/openai": "^1.4.6",
|
|
46
45
|
"@tailwindcss/node": "^4.3.0",
|
|
47
46
|
"@trapezedev/project": "^7.1.4",
|
|
48
|
-
"akanjs": "3.0.0-alpha.
|
|
47
|
+
"akanjs": "3.0.0-alpha.89",
|
|
49
48
|
"chalk": "^5.6.2",
|
|
50
49
|
"commander": "^14.0.3",
|
|
51
50
|
"dayjs": "^1.11.20",
|
|
52
51
|
"fontaine": "^0.8.0",
|
|
53
52
|
"fonteditor-core": "^2.6.3",
|
|
54
53
|
"ignore": "^7.0.5",
|
|
55
|
-
"ink": "^6.8.0",
|
|
56
54
|
"ora": "^9.4.0",
|
|
57
55
|
"ssh2": "^1.17.0",
|
|
58
56
|
"subset-font": "^2.5.0",
|