@akanjs/devkit 3.0.0-beta.19 → 3.0.0-beta.20
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/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +1 -4
- package/agentsIndex.test.ts +4 -7
- package/agentsIndex.ts +6 -22
- package/akanApp/BackendImportGraph.test.ts +8 -22
- package/akanApp/BackendImportGraph.ts +1 -20
- package/akanApp/akanApp.host.test.ts +1 -3
- package/akanApp/akanApp.host.ts +64 -250
- package/akanApp/devHostPolicy.test.ts +10 -34
- package/akanApp/devHostPolicy.ts +12 -98
- package/akanConfig/akanConfig.test.ts +30 -39
- package/akanConfig/akanConfig.ts +40 -136
- package/akanContext.ts +106 -159
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.ts +25 -59
- package/applicationReleasePackager.ts +24 -44
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +3 -19
- package/artifact/implicitRootLayout.ts +34 -91
- package/artifact/routeSeedIndex.ts +11 -44
- package/biomeBase.ts +2 -5
- package/capacitorApp.test.ts +4 -14
- package/capacitorApp.ts +86 -235
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +33 -57
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +3 -2
- package/cloud/globalConfig.ts +64 -31
- package/codeAgent/agent/AkanCodeServices.ts +15 -57
- package/codeAgent/agent/AkanContextFiles.ts +5 -28
- package/codeAgent/agent/CodeAgent.ts +33 -108
- package/codeAgent/agent/CodeAgentAsks.ts +7 -30
- package/codeAgent/agent/CodeAgentEventMapper.ts +9 -52
- package/codeAgent/agent/CodeAgentGate.ts +8 -34
- package/codeAgent/agent/CodeAgentProxy.ts +4 -13
- package/codeAgent/agent/CodeAgentRpcHost.ts +3 -16
- package/codeAgent/agent/CodeAgentRpcListener.ts +2 -10
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +4 -15
- package/codeAgent/agent/CodeAgentSuspended.ts +1 -8
- package/codeAgent/agent/CodeAgentUi.ts +2 -7
- package/codeAgent/agent/CodeMailbox.ts +7 -34
- package/codeAgent/agent/CodeSessionFork.ts +4 -19
- package/codeAgent/agent/CodeSessionIndex.ts +5 -31
- package/codeAgent/agent/SubagentPool.ts +48 -58
- package/codeAgent/agent/akanCodeModel.ts +5 -39
- package/codeAgent/agent/akanCodePaths.ts +4 -24
- package/codeAgent/agent/akanContextFiles.test.ts +0 -2
- package/codeAgent/agent/akanSystemPrompt.ts +1 -9
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +5 -28
- package/codeAgent/agent/codeMailbox.test.ts +0 -4
- package/codeAgent/agent/codeSessionFork.test.ts +0 -2
- package/codeAgent/agent/codeSessionIndex.test.ts +9 -4
- package/codeAgent/agent/consoleToStderr.ts +2 -12
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +3 -18
- package/codeAgent/feedback/PreviewChrome.ts +5 -29
- package/codeAgent/feedback/PreviewFeedback.ts +2 -9
- package/codeAgent/feedback/PreviewView.ts +3 -15
- package/codeAgent/feedback/TurnFeedback.ts +2 -17
- package/codeAgent/feedback/VerifyFeedback.ts +1 -7
- package/codeAgent/feedback/previewChrome.test.ts +1 -1
- package/codeAgent/index.ts +2 -10
- package/codeAgent/skills/akan-code/SKILL.md +12 -4
- package/codeAgent/tools/AkanCli.ts +4 -16
- package/codeAgent/tools/AkanCodePlugins.ts +21 -43
- package/codeAgent/tools/AkanEditScope.ts +2 -12
- package/codeAgent/tools/AkanEnvKeys.ts +3 -11
- package/codeAgent/tools/AkanToolPack.ts +6 -28
- package/codeAgent/tools/AkanVerifier.ts +1 -8
- package/codeAgent/tools/AskToolPack.ts +3 -15
- package/codeAgent/tools/MailToolPack.ts +0 -9
- package/codeAgent/tools/McpClient.ts +3 -24
- package/codeAgent/tools/McpOAuth.ts +6 -41
- package/codeAgent/tools/McpOAuthLoopback.ts +2 -15
- package/codeAgent/tools/McpServerConfig.ts +7 -26
- package/codeAgent/tools/McpSignIn.ts +46 -49
- package/codeAgent/tools/McpTokenStore.ts +6 -17
- package/codeAgent/tools/McpToolPack.ts +107 -35
- package/codeAgent/tools/SessionToolPack.ts +1 -11
- package/codeAgent/tools/WebToolPack.ts +1 -7
- package/codeAgent/tools/akanCodePlugins.test.ts +231 -63
- package/codeAgent/tools/askToolPack.test.ts +0 -3
- package/codeAgent/tools/mcpOAuth.test.ts +315 -27
- package/codeAgent/tools/mcpServerConfig.test.ts +21 -14
- package/codegenLock.test.ts +4 -13
- package/codegenLock.ts +3 -17
- package/commandDecorators/argMeta.ts +1 -5
- package/commandDecorators/camelToKebabCase.ts +1 -0
- package/commandDecorators/command.ts +44 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +1 -9
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +66 -144
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +27 -78
- package/devkitUtils.test.ts +47 -19
- package/executors.test.ts +114 -60
- package/executors.ts +165 -484
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +1 -1
- package/fileSys.ts +6 -8
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +14 -38
- package/frontendBuild/autoImportSync.ts +15 -67
- package/frontendBuild/buildRouteClient.test.ts +10 -36
- package/frontendBuild/bundleDefine.ts +12 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.ts +6 -41
- package/frontendBuild/csrArtifactBuilder.ts +6 -25
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +29 -78
- package/frontendBuild/cssImportResolver.ts +5 -18
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/fontOptimizer.test.ts +5 -13
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +27 -52
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +7 -31
- package/frontendBuild/hmrWatcher.ts +14 -71
- package/frontendBuild/pagesBundleBuilder.ts +4 -29
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/routeClientBuilder.ts +7 -47
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/sourceMtimeIndex.test.ts +37 -94
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
- package/frontendBuild/styleContract.ts +1 -8
- package/frontendBuild/themeValidator.ts +4 -26
- package/frontendBuild/watchRootResolver.test.ts +23 -42
- package/frontendBuild/watchRootResolver.ts +5 -27
- package/getRelatedCnsts.ts +12 -92
- package/incrementalBuilder/buildBatch.proc.ts +22 -57
- package/incrementalBuilder/buildBatchProtocol.ts +8 -36
- package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
- package/incrementalBuilder/buildBatchRunner.ts +10 -31
- package/incrementalBuilder/builderChannel.test.ts +24 -49
- package/incrementalBuilder/builderChannel.ts +6 -37
- package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
- package/incrementalBuilder/builderRequestRouter.ts +5 -24
- package/incrementalBuilder/devWatchBatch.test.ts +14 -18
- package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
- package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +48 -161
- package/index.ts +3 -15
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +47 -194
- package/integration/devStabilityHarness.test.ts +9 -29
- package/integration/devStabilityHarness.ts +65 -330
- package/integration/ssrMemoryProbe.ts +41 -167
- package/libSource.test.ts +4 -15
- package/libSource.ts +19 -31
- package/lint/gritRules.test.ts +6 -23
- package/linter.test.ts +1 -2
- package/linter.ts +22 -115
- package/mobile/mobileTarget.ts +14 -13
- package/package.json +2 -2
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +3 -14
- package/platformTest/IdleWatchedProcess.test.ts +5 -13
- package/platformTest/IdleWatchedProcess.ts +1 -5
- package/platformTest/LinuxDockerTarget.ts +20 -35
- package/platformTest/PlatformTestRun.test.ts +5 -9
- package/platformTest/PlatformTestTarget.ts +24 -2
- package/platformTest/TestSnapshot.test.ts +6 -14
- package/platformTest/TestSnapshot.ts +1 -5
- package/platformTest/WindowsSshTarget.ts +6 -22
- package/prompter.ts +3 -6
- package/qualityScanner.test.ts +6 -24
- package/qualityScanner.ts +196 -284
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.ts +7 -30
- package/scanInfo.ts +82 -214
- package/selectModel.ts +1 -2
- package/semver.ts +3 -5
- package/slicePlanner.test.ts +6 -24
- package/slicePlanner.ts +11 -32
- package/spinner.test.ts +3 -8
- package/spinner.ts +4 -16
- package/sshTunnel.ts +1 -6
- package/ssrScanner.test.ts +1 -6
- package/ssrScanner.ts +103 -113
- package/subspace.test.ts +6 -25
- package/subspace.ts +31 -109
- package/subspaceConfig.ts +3 -11
- package/testHelpers.ts +49 -34
- package/transforms/asyncDefaultExportDetector.ts +3 -11
- package/transforms/barrelAnalyzer.ts +43 -135
- package/transforms/barrelImportsPlugin.ts +38 -175
- package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
- package/transforms/externalizeFrameworkPlugin.ts +13 -87
- package/transforms/moduleSyntax.ts +32 -0
- package/transforms/rscUseClientTransform.ts +35 -20
- package/transforms/transforms.test.ts +153 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +2 -22
- package/typeChecker.ts +7 -63
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +85 -146
- package/workflow/index.ts +0 -1
- package/workflow/moduleIndex.test.ts +4 -15
- package/workflow/moduleIndex.ts +9 -49
- package/workflow/plan.ts +18 -25
- package/workflow/render.ts +80 -96
- package/workflow/source.ts +33 -56
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.ts +4 -19
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
package/executors.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type SpawnOptions,
|
|
8
8
|
spawn,
|
|
9
9
|
} from "node:child_process";
|
|
10
|
-
import { readFileSync } from "node:fs";
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
11
|
import {
|
|
12
12
|
copyFile,
|
|
13
13
|
cp as cpEntry,
|
|
@@ -43,14 +43,12 @@ import {
|
|
|
43
43
|
import { AkanAppConfig, AkanLibConfig, decreaseBuildNum, increaseBuildNum } from "./akanConfig";
|
|
44
44
|
import { getRootBoundarySegments, isRootBoundarySegments } from "./artifact/implicitRootLayout";
|
|
45
45
|
import { CodegenLock } from "./codegenLock";
|
|
46
|
-
import { FileSys } from "./fileSys";
|
|
47
|
-
import { getDirname } from "./getDirname";
|
|
46
|
+
import { FileSys, getDirname } from "./fileSys";
|
|
48
47
|
import { Linter } from "./linter";
|
|
49
48
|
import { resolveRepoName } from "./repoIdentity";
|
|
50
49
|
import { AppInfo, LibInfo, PkgInfo, WorkspaceInfo } from "./scanInfo";
|
|
51
50
|
import { Spinner } from "./spinner";
|
|
52
|
-
// Type-only:
|
|
53
|
-
// the resident module graph.
|
|
51
|
+
// Type-only: `getTypeChecker` loads it on demand to keep `typescript` out of the resident module graph.
|
|
54
52
|
import type { TypeChecker } from "./typeChecker";
|
|
55
53
|
import type { FileContent, PackageJson, TsConfigJson } from "./types";
|
|
56
54
|
|
|
@@ -63,39 +61,11 @@ export interface PageRoot {
|
|
|
63
61
|
keyPrefix: string;
|
|
64
62
|
}
|
|
65
63
|
|
|
66
|
-
const staticTemplateFileExtensions = new Set(
|
|
67
|
-
".avif"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
".eot",
|
|
72
|
-
".gif",
|
|
73
|
-
".html",
|
|
74
|
-
".ico",
|
|
75
|
-
".jpeg",
|
|
76
|
-
".jpg",
|
|
77
|
-
".js",
|
|
78
|
-
".json",
|
|
79
|
-
".map",
|
|
80
|
-
".md",
|
|
81
|
-
".mjs",
|
|
82
|
-
".mp3",
|
|
83
|
-
".mp4",
|
|
84
|
-
".ogg",
|
|
85
|
-
".otf",
|
|
86
|
-
".pdf",
|
|
87
|
-
".png",
|
|
88
|
-
".svg",
|
|
89
|
-
".ttf",
|
|
90
|
-
".txt",
|
|
91
|
-
".wasm",
|
|
92
|
-
".wav",
|
|
93
|
-
".webm",
|
|
94
|
-
".webp",
|
|
95
|
-
".woff",
|
|
96
|
-
".woff2",
|
|
97
|
-
".xml",
|
|
98
|
-
]);
|
|
64
|
+
const staticTemplateFileExtensions = new Set(
|
|
65
|
+
".avif .bmp .cjs .css .eot .gif .html .ico .jpeg .jpg .js .json .map .md .mjs .mp3 .mp4 .ogg .otf .pdf .png .svg"
|
|
66
|
+
.split(" ")
|
|
67
|
+
.concat(".ttf .txt .wasm .wav .webm .webp .woff .woff2 .xml".split(" ")),
|
|
68
|
+
);
|
|
99
69
|
|
|
100
70
|
//? A backslash is a path separator on Windows but an escape in a POSIX shell, so only there does it force quoting.
|
|
101
71
|
const plainCommandArgPattern = process.platform === "win32" ? /^[\w@%+=:,./\\-]+$/ : /^[\w@%+=:,./-]+$/;
|
|
@@ -158,7 +128,7 @@ export const execEmoji = {
|
|
|
158
128
|
pkg: "📦",
|
|
159
129
|
dist: "💿",
|
|
160
130
|
module: "⚙️",
|
|
161
|
-
default: "✈️",
|
|
131
|
+
default: "✈️",
|
|
162
132
|
};
|
|
163
133
|
|
|
164
134
|
const parseEnvFile = (envPath: string): Record<string, string> => {
|
|
@@ -186,6 +156,11 @@ const parseEnvFile = (envPath: string): Record<string, string> => {
|
|
|
186
156
|
return env;
|
|
187
157
|
};
|
|
188
158
|
|
|
159
|
+
const withCapitalizedDict = (dict: { [key: string]: string } = {}) => ({
|
|
160
|
+
...dict,
|
|
161
|
+
...Object.fromEntries(Object.entries(dict).map(([key, value]) => [capitalize(key), capitalize(value)])),
|
|
162
|
+
});
|
|
163
|
+
|
|
189
164
|
export class Executor {
|
|
190
165
|
static verbose = false;
|
|
191
166
|
static setVerbose(verbose: boolean) {
|
|
@@ -211,158 +186,53 @@ export class Executor {
|
|
|
211
186
|
#stderr(data: Buffer) {
|
|
212
187
|
Logger.raw(chalk.red(data.toString()));
|
|
213
188
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
189
|
+
#settle(
|
|
190
|
+
proc: ChildProcess,
|
|
191
|
+
target: { command: string; args?: string[]; cwd: string },
|
|
192
|
+
{ onClose = false, keepLogs = false, redStderr = false } = {},
|
|
193
|
+
): Promise<{ code: number | null; signal: NodeJS.Signals | null; stdout: string }> {
|
|
217
194
|
let stdout = "";
|
|
218
195
|
let stderr = "";
|
|
219
196
|
proc.stdout?.on("data", (data: Buffer) => {
|
|
220
197
|
stdout += data.toString();
|
|
198
|
+
if (keepLogs) this.logs.push(data.toString());
|
|
221
199
|
this.#stdout(data);
|
|
222
200
|
});
|
|
223
201
|
proc.stderr?.on("data", (data: Buffer) => {
|
|
224
202
|
stderr += data.toString();
|
|
225
|
-
this
|
|
203
|
+
if (keepLogs) this.logs.push(data.toString());
|
|
204
|
+
if (redStderr) this.#stderr(data);
|
|
205
|
+
else this.#stdout(data); // 정상로그도 stderr로 나옴
|
|
226
206
|
});
|
|
227
207
|
return new Promise((resolve, reject) => {
|
|
228
|
-
proc.on("error", (
|
|
229
|
-
reject(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
stdout,
|
|
236
|
-
stderr,
|
|
237
|
-
cause: error,
|
|
238
|
-
}),
|
|
239
|
-
);
|
|
240
|
-
});
|
|
241
|
-
proc.on("exit", (code, signal) => {
|
|
242
|
-
if (code || signal)
|
|
243
|
-
reject(
|
|
244
|
-
new CommandExecutionError({
|
|
245
|
-
command,
|
|
246
|
-
cwd,
|
|
247
|
-
code,
|
|
248
|
-
signal,
|
|
249
|
-
stdout,
|
|
250
|
-
stderr,
|
|
251
|
-
}),
|
|
252
|
-
);
|
|
253
|
-
else resolve({ code, signal });
|
|
208
|
+
proc.on("error", (cause) =>
|
|
209
|
+
reject(new CommandExecutionError({ ...target, code: null, signal: null, stdout, stderr, cause })),
|
|
210
|
+
);
|
|
211
|
+
proc.on(onClose ? "close" : "exit", (code: number | null, signal: NodeJS.Signals | null) => {
|
|
212
|
+
if ((onClose ? code !== 0 : code) || signal)
|
|
213
|
+
reject(new CommandExecutionError({ ...target, code, signal, stdout, stderr }));
|
|
214
|
+
else resolve({ code, signal, stdout });
|
|
254
215
|
});
|
|
255
216
|
});
|
|
256
217
|
}
|
|
218
|
+
exec(command: string, options: ExecOptions = {}): Promise<unknown> {
|
|
219
|
+
const target = { command, cwd: options.cwd?.toString() ?? this.cwdPath };
|
|
220
|
+
const proc = exec(command, { cwd: this.cwdPath, ...options });
|
|
221
|
+
return this.#settle(proc, target).then(({ code, signal }) => ({ code, signal }));
|
|
222
|
+
}
|
|
257
223
|
|
|
258
224
|
spawn(command: string, args: string[] = [], options: SpawnOptions = {}): Promise<string> {
|
|
259
|
-
const
|
|
260
|
-
const proc = spawn(command, args, {
|
|
261
|
-
|
|
262
|
-
// stdio: "inherit",
|
|
263
|
-
...options,
|
|
264
|
-
});
|
|
265
|
-
let stdout = "";
|
|
266
|
-
let stderr = "";
|
|
267
|
-
|
|
268
|
-
proc.stdout?.on("data", (data: Buffer) => {
|
|
269
|
-
stdout += data.toString();
|
|
270
|
-
this.logs.push(data.toString());
|
|
271
|
-
this.#stdout(data);
|
|
272
|
-
});
|
|
273
|
-
proc.stderr?.on("data", (data: Buffer) => {
|
|
274
|
-
stderr += data.toString();
|
|
275
|
-
this.logs.push(data.toString());
|
|
276
|
-
this.#stdout(data); // 정상로그도 stderr로 나옴
|
|
277
|
-
});
|
|
278
|
-
return new Promise((resolve, reject) => {
|
|
279
|
-
proc.on("error", (error) => {
|
|
280
|
-
reject(
|
|
281
|
-
new CommandExecutionError({
|
|
282
|
-
command,
|
|
283
|
-
args,
|
|
284
|
-
cwd,
|
|
285
|
-
code: null,
|
|
286
|
-
signal: null,
|
|
287
|
-
stdout,
|
|
288
|
-
stderr,
|
|
289
|
-
cause: error,
|
|
290
|
-
}),
|
|
291
|
-
);
|
|
292
|
-
});
|
|
293
|
-
proc.on("close", (code, signal) => {
|
|
294
|
-
if (code !== 0 || signal)
|
|
295
|
-
reject(
|
|
296
|
-
new CommandExecutionError({
|
|
297
|
-
command,
|
|
298
|
-
args,
|
|
299
|
-
cwd,
|
|
300
|
-
code,
|
|
301
|
-
signal,
|
|
302
|
-
stdout,
|
|
303
|
-
stderr,
|
|
304
|
-
}),
|
|
305
|
-
);
|
|
306
|
-
else resolve(stdout);
|
|
307
|
-
});
|
|
308
|
-
});
|
|
225
|
+
const target = { command, args, cwd: options.cwd?.toString() ?? this.cwdPath };
|
|
226
|
+
const proc = spawn(command, args, { cwd: this.cwdPath, ...options });
|
|
227
|
+
return this.#settle(proc, target, { onClose: true, keepLogs: true }).then(({ stdout }) => stdout);
|
|
309
228
|
}
|
|
310
229
|
spawnSync(command: string, args: string[] = [], options: SpawnOptions = {}): ChildProcess {
|
|
311
|
-
|
|
312
|
-
cwd: this.cwdPath,
|
|
313
|
-
// stdio: "inherit",
|
|
314
|
-
...options,
|
|
315
|
-
});
|
|
316
|
-
return proc;
|
|
230
|
+
return spawn(command, args, { cwd: this.cwdPath, ...options });
|
|
317
231
|
}
|
|
318
|
-
fork(modulePath: string, args: string[] = [], options: ForkOptions = {}) {
|
|
319
|
-
const
|
|
320
|
-
const proc = fork(modulePath, args, {
|
|
321
|
-
|
|
322
|
-
// stdio: ["ignore", "inherit", "inherit", "ipc"],
|
|
323
|
-
...options,
|
|
324
|
-
});
|
|
325
|
-
let stdout = "";
|
|
326
|
-
let stderr = "";
|
|
327
|
-
proc.stdout?.on("data", (data: Buffer) => {
|
|
328
|
-
stdout += data.toString();
|
|
329
|
-
this.#stdout(data);
|
|
330
|
-
});
|
|
331
|
-
proc.stderr?.on("data", (data: Buffer) => {
|
|
332
|
-
stderr += data.toString();
|
|
333
|
-
this.#stderr(data);
|
|
334
|
-
});
|
|
335
|
-
return new Promise((resolve, reject) => {
|
|
336
|
-
proc.on("error", (error) => {
|
|
337
|
-
reject(
|
|
338
|
-
new CommandExecutionError({
|
|
339
|
-
command: modulePath,
|
|
340
|
-
args,
|
|
341
|
-
cwd,
|
|
342
|
-
code: null,
|
|
343
|
-
signal: null,
|
|
344
|
-
stdout,
|
|
345
|
-
stderr,
|
|
346
|
-
cause: error,
|
|
347
|
-
}),
|
|
348
|
-
);
|
|
349
|
-
});
|
|
350
|
-
proc.on("exit", (code, signal) => {
|
|
351
|
-
if (code || signal)
|
|
352
|
-
reject(
|
|
353
|
-
new CommandExecutionError({
|
|
354
|
-
command: modulePath,
|
|
355
|
-
args,
|
|
356
|
-
cwd,
|
|
357
|
-
code,
|
|
358
|
-
signal,
|
|
359
|
-
stdout,
|
|
360
|
-
stderr,
|
|
361
|
-
}),
|
|
362
|
-
);
|
|
363
|
-
else resolve({ code, signal });
|
|
364
|
-
});
|
|
365
|
-
});
|
|
232
|
+
fork(modulePath: string, args: string[] = [], options: ForkOptions = {}): Promise<unknown> {
|
|
233
|
+
const target = { command: modulePath, args, cwd: options.cwd?.toString() ?? this.cwdPath };
|
|
234
|
+
const proc = fork(modulePath, args, { cwd: this.cwdPath, ...options });
|
|
235
|
+
return this.#settle(proc, target, { redStderr: true }).then(({ code, signal }) => ({ code, signal }));
|
|
366
236
|
}
|
|
367
237
|
getPath(filePath: string) {
|
|
368
238
|
if (path.isAbsolute(filePath)) return filePath;
|
|
@@ -403,16 +273,14 @@ export class Executor {
|
|
|
403
273
|
}
|
|
404
274
|
async getFilesAndDirs(dirPath: string): Promise<{ files: string[]; dirs: string[] }> {
|
|
405
275
|
const fullDirPath = this.getPath(dirPath);
|
|
406
|
-
const
|
|
407
|
-
const files = Array.from(
|
|
408
|
-
const
|
|
409
|
-
const allEntries = Array.from(dirGlob.scanSync({ cwd: fullDirPath, onlyFiles: false }));
|
|
276
|
+
const glob = new Bun.Glob("*");
|
|
277
|
+
const files = Array.from(glob.scanSync({ cwd: fullDirPath, onlyFiles: true }));
|
|
278
|
+
const allEntries = Array.from(glob.scanSync({ cwd: fullDirPath, onlyFiles: false }));
|
|
410
279
|
const dirs = allEntries.filter((entry) => !files.includes(entry));
|
|
411
280
|
return { files, dirs };
|
|
412
281
|
}
|
|
413
282
|
async exists(filePath: string) {
|
|
414
|
-
|
|
415
|
-
return await FileSys.exists(readPath);
|
|
283
|
+
return await FileSys.exists(this.getPath(filePath));
|
|
416
284
|
}
|
|
417
285
|
async remove(filePath: string) {
|
|
418
286
|
const readPath = this.getPath(filePath);
|
|
@@ -421,8 +289,7 @@ export class Executor {
|
|
|
421
289
|
return this;
|
|
422
290
|
}
|
|
423
291
|
async removeDir(dirPath: string) {
|
|
424
|
-
//* XXX: `path.join(…, ".")` drops a trailing
|
|
425
|
-
//* and wipes its target, while `rm -rf link` only unlinks it.
|
|
292
|
+
//* XXX: `path.join(…, ".")` drops a trailing slash: `rm -rf link/` wipes the target, `rm -rf link` only unlinks.
|
|
426
293
|
const readPath = path.join(this.getPath(dirPath), ".");
|
|
427
294
|
if (await FileSys.entryExists(readPath)) await rm(readPath, { recursive: true, force: true });
|
|
428
295
|
this.logger.verbose(`Remove directory ${readPath}`);
|
|
@@ -436,8 +303,7 @@ export class Executor {
|
|
|
436
303
|
const writePath = this.getPath(filePath);
|
|
437
304
|
const dir = path.dirname(writePath);
|
|
438
305
|
if (!(await FileSys.dirExists(dir))) await mkdir(dir, { recursive: true });
|
|
439
|
-
//? Biome
|
|
440
|
-
//? loses a byte to `akan lint` and takes it back on the next `akan sync` — a permanent one-line git diff.
|
|
306
|
+
//? Biome ends every .json with a newline; without one, `akan lint` and `akan sync` would flip-flop the file.
|
|
441
307
|
let contentStr = typeof content === "string" ? content : `${JSON.stringify(content, null, 2)}\n`;
|
|
442
308
|
|
|
443
309
|
if (await FileSys.fileExists(writePath)) {
|
|
@@ -464,12 +330,10 @@ export class Executor {
|
|
|
464
330
|
return { filePath, content };
|
|
465
331
|
}
|
|
466
332
|
async readFile(filePath: string) {
|
|
467
|
-
|
|
468
|
-
return await FileSys.readText(readPath);
|
|
333
|
+
return await FileSys.readText(this.getPath(filePath));
|
|
469
334
|
}
|
|
470
335
|
async readJson(filePath: string) {
|
|
471
|
-
|
|
472
|
-
return await FileSys.readJson<object>(readPath);
|
|
336
|
+
return await FileSys.readJson<object>(this.getPath(filePath));
|
|
473
337
|
}
|
|
474
338
|
async cp(srcPath: string, destPath: string, { dereference = false }: { dereference?: boolean } = {}) {
|
|
475
339
|
const src = this.getPath(srcPath);
|
|
@@ -477,9 +341,8 @@ export class Executor {
|
|
|
477
341
|
if (!(await FileSys.exists(src))) return;
|
|
478
342
|
const isDirectory = (await stat(src)).isDirectory();
|
|
479
343
|
if (!(await FileSys.exists(dest)) && isDirectory) await mkdir(dest, { recursive: true });
|
|
480
|
-
//* `cp -r` keeps symlinks on GNU
|
|
481
|
-
//*
|
|
482
|
-
//* symlink there needs a privilege most accounts lack, so it always lands as real files.
|
|
344
|
+
//* `cp -r` keeps symlinks on GNU but follows them on macOS; Windows has no `cp -r <dir>/.` and rarely the
|
|
345
|
+
//* privilege a symlink needs, so it always copies real files.
|
|
483
346
|
if (dereference || process.platform === "win32")
|
|
484
347
|
await cpEntry(src, dest, { recursive: isDirectory, dereference: true, force: true });
|
|
485
348
|
else await $`cp -r ${src}${isDirectory ? "/." : ""} ${dest}`;
|
|
@@ -503,19 +366,10 @@ export class Executor {
|
|
|
503
366
|
#tsconfig: TsConfigJson | null = null;
|
|
504
367
|
async getTsConfig(pathname = "tsconfig.json", { refresh }: { refresh?: boolean } = {}): Promise<TsConfigJson> {
|
|
505
368
|
if (this.#tsconfig && !refresh) return this.#tsconfig;
|
|
506
|
-
|
|
369
|
+
let tsconfig = (await this.readJson(pathname)) as TsConfigJson;
|
|
507
370
|
if (tsconfig.extends) {
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
...extendsTsconfig,
|
|
511
|
-
...tsconfig,
|
|
512
|
-
compilerOptions: {
|
|
513
|
-
...extendsTsconfig.compilerOptions,
|
|
514
|
-
...tsconfig.compilerOptions,
|
|
515
|
-
},
|
|
516
|
-
} as TsConfigJson;
|
|
517
|
-
this.#tsconfig = result;
|
|
518
|
-
return result;
|
|
371
|
+
const base = await this.getTsConfig(tsconfig.extends, { refresh });
|
|
372
|
+
tsconfig = { ...base, ...tsconfig, compilerOptions: { ...base.compilerOptions, ...tsconfig.compilerOptions } };
|
|
519
373
|
}
|
|
520
374
|
this.#tsconfig = tsconfig;
|
|
521
375
|
return tsconfig;
|
|
@@ -563,6 +417,11 @@ export class Executor {
|
|
|
563
417
|
dict: { [key: string]: string } = {},
|
|
564
418
|
options: { [key: string]: unknown } = {},
|
|
565
419
|
): Promise<FileContent | null> {
|
|
420
|
+
const convertPath = (target: string) =>
|
|
421
|
+
Object.entries(dict).reduce(
|
|
422
|
+
(converted, [key, value]) => converted.replace(new RegExp(`__${key}__`, "g"), value),
|
|
423
|
+
target,
|
|
424
|
+
);
|
|
566
425
|
if (targetPath.endsWith(".ts") || targetPath.endsWith(".tsx")) {
|
|
567
426
|
const getContent = (await import(templatePath)) as {
|
|
568
427
|
default: (
|
|
@@ -575,32 +434,20 @@ export class Executor {
|
|
|
575
434
|
if (result === null) return null;
|
|
576
435
|
const filename = typeof result === "object" ? result.filename : path.basename(targetPath).replace(".js", ".ts");
|
|
577
436
|
const content = typeof result === "object" ? result.content : result;
|
|
578
|
-
const
|
|
579
|
-
const convertedTargetPath = Object.entries(dict).reduce(
|
|
580
|
-
(path, [key, value]) => path.replace(new RegExp(`__${key}__`, "g"), value),
|
|
581
|
-
`${dirname}/${filename}`,
|
|
582
|
-
);
|
|
437
|
+
const convertedTargetPath = convertPath(`${path.dirname(targetPath)}/${filename}`);
|
|
583
438
|
this.logger.verbose(`Apply template ${templatePath} to ${convertedTargetPath}`);
|
|
584
439
|
return this.writeFile(convertedTargetPath, content, { overwrite });
|
|
585
440
|
} else if (targetPath.endsWith(".template")) {
|
|
586
441
|
const content = await FileSys.readText(templatePath);
|
|
587
|
-
const convertedTargetPath =
|
|
588
|
-
(path, [key, value]) => path.replace(new RegExp(`__${key}__`, "g"), value),
|
|
589
|
-
targetPath.slice(0, -9),
|
|
590
|
-
);
|
|
442
|
+
const convertedTargetPath = convertPath(targetPath.slice(0, -9));
|
|
591
443
|
const convertedContent = Object.entries(dict).reduce(
|
|
592
444
|
(data, [key, value]) => data.replace(new RegExp(`<%= ${key} %>`, "g"), value),
|
|
593
445
|
content,
|
|
594
446
|
);
|
|
595
447
|
this.logger.verbose(`Apply template ${templatePath} to ${convertedTargetPath}`);
|
|
596
|
-
return this.writeFile(convertedTargetPath, convertedContent, {
|
|
597
|
-
overwrite,
|
|
598
|
-
});
|
|
448
|
+
return this.writeFile(convertedTargetPath, convertedContent, { overwrite });
|
|
599
449
|
} else if (staticTemplateFileExtensions.has(path.extname(targetPath).toLowerCase())) {
|
|
600
|
-
const convertedTargetPath =
|
|
601
|
-
(path, [key, value]) => path.replace(new RegExp(`__${key}__`, "g"), value),
|
|
602
|
-
targetPath,
|
|
603
|
-
);
|
|
450
|
+
const convertedTargetPath = convertPath(targetPath);
|
|
604
451
|
const writePath = this.getPath(convertedTargetPath);
|
|
605
452
|
const dirname = path.dirname(writePath);
|
|
606
453
|
if (!(await FileSys.dirExists(dirname))) await mkdir(dirname, { recursive: true });
|
|
@@ -626,13 +473,11 @@ export class Executor {
|
|
|
626
473
|
}): Promise<FileContent[]> {
|
|
627
474
|
const templateRoot = await this.#resolveTemplateRoot();
|
|
628
475
|
const templatePath = `${templateRoot}${template ? `/${template}` : ""}`;
|
|
629
|
-
|
|
630
|
-
if ((await stat(prefixTemplatePath)).isFile()) {
|
|
631
|
-
const filename = path.basename(prefixTemplatePath);
|
|
476
|
+
if ((await stat(templatePath)).isFile()) {
|
|
632
477
|
const fileContent = await this.#applyTemplateFile(
|
|
633
478
|
{
|
|
634
|
-
templatePath
|
|
635
|
-
targetPath: path.join(basePath,
|
|
479
|
+
templatePath,
|
|
480
|
+
targetPath: path.join(basePath, path.basename(templatePath)),
|
|
636
481
|
scanInfo,
|
|
637
482
|
overwrite,
|
|
638
483
|
},
|
|
@@ -690,25 +535,13 @@ export class Executor {
|
|
|
690
535
|
options?: { [key: string]: unknown };
|
|
691
536
|
overwrite?: boolean;
|
|
692
537
|
}): Promise<FileContent[]> {
|
|
693
|
-
const
|
|
694
|
-
...(options.dict ?? {}),
|
|
695
|
-
...Object.fromEntries(
|
|
696
|
-
Object.entries(options.dict ?? {}).map(([key, value]) => [capitalize(key), capitalize(value)]),
|
|
697
|
-
),
|
|
698
|
-
};
|
|
699
|
-
const fileContents = await this._applyTemplate({ ...options, dict });
|
|
538
|
+
const fileContents = await this._applyTemplate({ ...options, dict: withCapitalizedDict(options.dict) });
|
|
700
539
|
await this.#formatAppliedTemplate(fileContents);
|
|
701
540
|
return fileContents;
|
|
702
541
|
}
|
|
703
542
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
* a model named Task and wrong for one named Zoo, and `organizeImports` fails `biome check` — so a
|
|
707
|
-
* scaffold that is not formatted on the way out is red for most model names, whatever the template says.
|
|
708
|
-
*
|
|
709
|
-
* Best-effort: `create-akan-workspace` scaffolds before `bun install`, so there is no local Biome binary
|
|
710
|
-
* and often no config above the target yet. An unformatted file is a lint fix; a failed scaffold is not.
|
|
711
|
-
*/
|
|
543
|
+
// A template cannot sort the identifiers it emits (`Task` vs `Zoo`). Best-effort: `create-akan-workspace`
|
|
544
|
+
// scaffolds before `bun install`, with no local Biome binary yet.
|
|
712
545
|
async #formatAppliedTemplate(fileContents: FileContent[]) {
|
|
713
546
|
const filePaths = fileContents
|
|
714
547
|
.map((fileContent) => fileContent.filePath)
|
|
@@ -721,9 +554,7 @@ export class Executor {
|
|
|
721
554
|
this.logger.verbose(`Skipped formatting scaffolded files: ${err instanceof Error ? err.message : String(err)}`);
|
|
722
555
|
}
|
|
723
556
|
}
|
|
724
|
-
// Async so `typescript` (~65MB resident)
|
|
725
|
-
// not by every process that imports an executor. `typeCheckAsync` below runs in a subprocess and
|
|
726
|
-
// never touches this path.
|
|
557
|
+
// Async so `typescript` (~65MB resident) loads only in the commands that typecheck.
|
|
727
558
|
async getTypeChecker() {
|
|
728
559
|
const { TypeChecker } = await import("./typeChecker");
|
|
729
560
|
this.typeChecker ??= new TypeChecker(this);
|
|
@@ -789,10 +620,10 @@ export class Executor {
|
|
|
789
620
|
filePath: string,
|
|
790
621
|
{ fix = false, dryRun = false }: { fix?: boolean; dryRun?: boolean } = {},
|
|
791
622
|
): Promise<{
|
|
792
|
-
results: unknown[];
|
|
623
|
+
results: unknown[];
|
|
793
624
|
message: string;
|
|
794
|
-
errors: unknown[];
|
|
795
|
-
warnings: unknown[];
|
|
625
|
+
errors: unknown[];
|
|
626
|
+
warnings: unknown[];
|
|
796
627
|
}> {
|
|
797
628
|
const path = this.getPath(filePath);
|
|
798
629
|
const linter = this.getLinter();
|
|
@@ -819,7 +650,6 @@ export class WorkspaceExecutor extends Executor {
|
|
|
819
650
|
this.repoName = repoName;
|
|
820
651
|
}
|
|
821
652
|
|
|
822
|
-
static #execs = new Map<string, WorkspaceExecutor>();
|
|
823
653
|
static fromRoot({
|
|
824
654
|
workspaceRoot = process.cwd(),
|
|
825
655
|
repoName = resolveRepoName(workspaceRoot),
|
|
@@ -827,7 +657,7 @@ export class WorkspaceExecutor extends Executor {
|
|
|
827
657
|
workspaceRoot?: string;
|
|
828
658
|
repoName?: string;
|
|
829
659
|
} = {}) {
|
|
830
|
-
return
|
|
660
|
+
return new WorkspaceExecutor({ workspaceRoot, repoName });
|
|
831
661
|
}
|
|
832
662
|
static getBaseDevEnv(envPath?: string) {
|
|
833
663
|
// Bun auto-loads .env, so we use process.env directly
|
|
@@ -876,11 +706,9 @@ export class WorkspaceExecutor extends Executor {
|
|
|
876
706
|
return await WorkspaceInfo.fromExecutor(this);
|
|
877
707
|
}
|
|
878
708
|
async getApps() {
|
|
879
|
-
if (!(await FileSys.dirExists(`${this.workspaceRoot}/apps`))) return [];
|
|
880
709
|
return await this.#getDirHasFile(`${this.workspaceRoot}/apps`, "akan.config.ts");
|
|
881
710
|
}
|
|
882
711
|
async getLibs() {
|
|
883
|
-
if (!(await FileSys.dirExists(`${this.workspaceRoot}/libs`))) return [];
|
|
884
712
|
return await this.#getDirHasFile(`${this.workspaceRoot}/libs`, "akan.config.ts");
|
|
885
713
|
}
|
|
886
714
|
async getSyss() {
|
|
@@ -888,7 +716,6 @@ export class WorkspaceExecutor extends Executor {
|
|
|
888
716
|
return [appNames, libNames] as [string[], string[]];
|
|
889
717
|
}
|
|
890
718
|
async getPkgs() {
|
|
891
|
-
if (!(await FileSys.dirExists(`${this.workspaceRoot}/pkgs`))) return [];
|
|
892
719
|
return await this.#getDirHasFile(`${this.workspaceRoot}/pkgs`, "package.json");
|
|
893
720
|
}
|
|
894
721
|
async getExecs() {
|
|
@@ -951,13 +778,7 @@ export class WorkspaceExecutor extends Executor {
|
|
|
951
778
|
async hasChanges() {
|
|
952
779
|
return !!(await this.spawn("git", ["status", "--porcelain"])).trim();
|
|
953
780
|
}
|
|
954
|
-
/**
|
|
955
|
-
* Workspace-relative paths git knows about, sorted. `untracked` adds files that exist but are not
|
|
956
|
-
* committed yet, still honoring `.gitignore` — which is what a freshly copied library looks like.
|
|
957
|
-
*
|
|
958
|
-
* Reading the file set from git is what keeps generated barrels, the `page/**` and `public/libs`
|
|
959
|
-
* symlinks, env values and the lockfile out of it without any caller restating that list.
|
|
960
|
-
*/
|
|
781
|
+
/** Sorted workspace-relative paths; `untracked` adds uncommitted files, still honoring `.gitignore`. */
|
|
961
782
|
async listGitFiles(paths: string[], { untracked = false }: { untracked?: boolean } = {}) {
|
|
962
783
|
if (!paths.length) return [];
|
|
963
784
|
const mode = untracked ? ["--cached", "--others", "--exclude-standard"] : ["--cached"];
|
|
@@ -968,6 +789,7 @@ export class WorkspaceExecutor extends Executor {
|
|
|
968
789
|
.sort();
|
|
969
790
|
}
|
|
970
791
|
async #getDirHasFile(basePath: string, targetFilename: string) {
|
|
792
|
+
if (!(await FileSys.dirExists(basePath))) return [];
|
|
971
793
|
const AVOID_DIRS = ["node_modules", "dist", "public", "webkit"];
|
|
972
794
|
const getDirs = async (dirname: string, maxDepth = 3, results: string[] = [], prefix = "") => {
|
|
973
795
|
const dirs = await this.readdir(dirname);
|
|
@@ -975,8 +797,7 @@ export class WorkspaceExecutor extends Executor {
|
|
|
975
797
|
dirs.map(async (dir) => {
|
|
976
798
|
if (AVOID_DIRS.includes(dir)) return;
|
|
977
799
|
const dirPath = path.join(dirname, dir);
|
|
978
|
-
//*
|
|
979
|
-
//* this runs for `getApps`, so throwing here would break every command until it is repaired.
|
|
800
|
+
//* Not `stat`: a dangling symlink would throw, and this walk backs `getApps`, i.e. every command.
|
|
980
801
|
if (await FileSys.dirExists(dirPath)) {
|
|
981
802
|
const hasTargetFile = await FileSys.fileExists(path.join(dirPath, targetFilename));
|
|
982
803
|
if (hasTargetFile) results.push(`${prefix}${dir}`);
|
|
@@ -986,50 +807,34 @@ export class WorkspaceExecutor extends Executor {
|
|
|
986
807
|
);
|
|
987
808
|
return results;
|
|
988
809
|
};
|
|
989
|
-
return await getDirs(basePath);
|
|
810
|
+
return (await getDirs(basePath)).sort((a, b) => a.localeCompare(b));
|
|
990
811
|
}
|
|
991
812
|
|
|
992
|
-
async
|
|
813
|
+
async #fromEverySys<T>(read: (sys: SysExecutor) => Promise<T[]>): Promise<T[]> {
|
|
993
814
|
const [appNames, libNames] = await this.getSyss();
|
|
994
|
-
const
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
).flat(),
|
|
1001
|
-
];
|
|
1002
|
-
return scalarConstantExampleFiles;
|
|
815
|
+
const fromApps = await Promise.all(appNames.map((appName) => read(AppExecutor.from(this, appName))));
|
|
816
|
+
const fromLibs = await Promise.all(libNames.map((libName) => read(LibExecutor.from(this, libName))));
|
|
817
|
+
return [...fromApps.flat(), ...fromLibs.flat()];
|
|
818
|
+
}
|
|
819
|
+
async getScalarConstantFiles() {
|
|
820
|
+
return await this.#fromEverySys((sys) => sys.getScalarConstantFiles());
|
|
1003
821
|
}
|
|
1004
822
|
async getConstantFiles() {
|
|
1005
|
-
|
|
1006
|
-
const moduleConstantExampleFiles = [
|
|
1007
|
-
...(await Promise.all(appNames.map((appName) => AppExecutor.from(this, appName).getConstantFiles()))).flat(),
|
|
1008
|
-
...(await Promise.all(libNames.map((libName) => LibExecutor.from(this, libName).getConstantFiles()))).flat(),
|
|
1009
|
-
];
|
|
1010
|
-
return moduleConstantExampleFiles;
|
|
823
|
+
return await this.#fromEverySys((sys) => sys.getConstantFiles());
|
|
1011
824
|
}
|
|
1012
825
|
async getDictionaryFiles() {
|
|
1013
|
-
|
|
1014
|
-
const moduleDictionaryExampleFiles = [
|
|
1015
|
-
...(await Promise.all(appNames.map((appName) => AppExecutor.from(this, appName).getDictionaryFiles()))).flat(),
|
|
1016
|
-
...(await Promise.all(libNames.map((libName) => LibExecutor.from(this, libName).getDictionaryFiles()))).flat(),
|
|
1017
|
-
];
|
|
1018
|
-
return moduleDictionaryExampleFiles;
|
|
826
|
+
return await this.#fromEverySys((sys) => sys.getDictionaryFiles());
|
|
1019
827
|
}
|
|
1020
828
|
async getViewFiles() {
|
|
1021
|
-
|
|
1022
|
-
const viewExampleFiles = [
|
|
1023
|
-
...(await Promise.all(appNames.map((appName) => AppExecutor.from(this, appName).getViewsSourceCode()))).flat(),
|
|
1024
|
-
...(await Promise.all(libNames.map((libName) => LibExecutor.from(this, libName).getViewsSourceCode()))).flat(),
|
|
1025
|
-
];
|
|
1026
|
-
return viewExampleFiles;
|
|
829
|
+
return await this.#fromEverySys((sys) => sys.getViewsSourceCode());
|
|
1027
830
|
}
|
|
1028
831
|
}
|
|
1029
832
|
|
|
1030
|
-
interface
|
|
833
|
+
interface NamedExecutorOptions {
|
|
1031
834
|
workspace?: WorkspaceExecutor;
|
|
1032
835
|
name: string;
|
|
836
|
+
}
|
|
837
|
+
interface SysExecutorOptions extends NamedExecutorOptions {
|
|
1033
838
|
type: "app" | "lib";
|
|
1034
839
|
}
|
|
1035
840
|
|
|
@@ -1098,29 +903,12 @@ export class SysExecutor extends Executor {
|
|
|
1098
903
|
options: { exec: this, facet },
|
|
1099
904
|
}),
|
|
1100
905
|
),
|
|
1101
|
-
...
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
}),
|
|
1108
|
-
),
|
|
1109
|
-
...scanInfo.getServiceModules().map((model) =>
|
|
1110
|
-
this._applyTemplate({
|
|
1111
|
-
basePath: `lib/_${model}`,
|
|
1112
|
-
template: "moduleRoot",
|
|
1113
|
-
scanInfo,
|
|
1114
|
-
dict: { model, Model: capitalize(model) },
|
|
1115
|
-
}),
|
|
1116
|
-
),
|
|
1117
|
-
...scanInfo.getScalarModules().map((model) =>
|
|
1118
|
-
this._applyTemplate({
|
|
1119
|
-
basePath: `lib/__scalar/${model}`,
|
|
1120
|
-
template: "moduleRoot",
|
|
1121
|
-
scanInfo,
|
|
1122
|
-
dict: { model, Model: capitalize(model) },
|
|
1123
|
-
}),
|
|
906
|
+
...[
|
|
907
|
+
...scanInfo.getDatabaseModules().map((model) => [model, `lib/${model}`]),
|
|
908
|
+
...scanInfo.getServiceModules().map((model) => [model, `lib/_${model}`]),
|
|
909
|
+
...scanInfo.getScalarModules().map((model) => [model, `lib/__scalar/${model}`]),
|
|
910
|
+
].map(([model, basePath]) =>
|
|
911
|
+
this._applyTemplate({ basePath, template: "moduleRoot", scanInfo, dict: { model, Model: capitalize(model) } }),
|
|
1124
912
|
),
|
|
1125
913
|
];
|
|
1126
914
|
}
|
|
@@ -1142,9 +930,7 @@ export class SysExecutor extends Executor {
|
|
|
1142
930
|
: await LibInfo.fromExecutor(this as unknown as LibExecutor, {
|
|
1143
931
|
refresh,
|
|
1144
932
|
});
|
|
1145
|
-
//* `writeLib`
|
|
1146
|
-
//* regenerates the same ones — so this region races the other dev servers in the workspace and the
|
|
1147
|
-
//* builders that watch what it writes.
|
|
933
|
+
//* Locked: `writeLib` rewrites dependency-lib barrels that other dev servers regenerate and watch too.
|
|
1148
934
|
if (write)
|
|
1149
935
|
await CodegenLock.run(this.workspace.workspaceRoot, `scan:${this.name}`, async () => {
|
|
1150
936
|
await Promise.all(this.#getScanTemplateTasks(scanInfo));
|
|
@@ -1161,11 +947,7 @@ export class SysExecutor extends Executor {
|
|
|
1161
947
|
this.#scanInfo = scanInfo;
|
|
1162
948
|
return scanInfo;
|
|
1163
949
|
}
|
|
1164
|
-
/**
|
|
1165
|
-
* 스코프 에이전트 색인(apps|libs/<name>/AGENTS.md) 재생성 — own + 의존 lib 레시피만 싣는다(프레임워크
|
|
1166
|
-
* 레시피는 루트 AGENTS.md 소관). scan(write) 경로에 물려 있어 sync/build/start 어디를 지나도 갱신되고,
|
|
1167
|
-
* `akan lint` 가 같은 렌더 결과와 비교해 신선도를 강제한다. 마커 밖 내용은 사용자 소유라 보존한다.
|
|
1168
|
-
*/
|
|
950
|
+
/** Rewrites only the marker block of the scoped AGENTS.md; content outside the markers belongs to the user. */
|
|
1169
951
|
async syncAgentsIndex(scanInfo?: AppInfo | LibInfo) {
|
|
1170
952
|
const info = scanInfo ?? (await this.scan({ write: false }));
|
|
1171
953
|
const scope = { type: this.type, name: this.name };
|
|
@@ -1220,69 +1002,49 @@ export class SysExecutor extends Executor {
|
|
|
1220
1002
|
}
|
|
1221
1003
|
|
|
1222
1004
|
async getDatabaseModules() {
|
|
1223
|
-
|
|
1224
|
-
.filter((name) => !name.startsWith("_") && !name.
|
|
1225
|
-
.filter((name) =>
|
|
1226
|
-
return databaseModules;
|
|
1005
|
+
return (await this.readdir("lib"))
|
|
1006
|
+
.filter((name) => !name.startsWith("_") && !name.endsWith(".ts"))
|
|
1007
|
+
.filter((name) => existsSync(`${this.cwdPath}/lib/${name}/${name}.constant.ts`));
|
|
1227
1008
|
}
|
|
1228
1009
|
|
|
1229
1010
|
async getServiceModules() {
|
|
1230
|
-
|
|
1011
|
+
return (await this.readdir("lib"))
|
|
1231
1012
|
.filter((name) => name.startsWith("_") && !name.startsWith("__"))
|
|
1232
|
-
.filter((name) =>
|
|
1233
|
-
return serviceModules;
|
|
1013
|
+
.filter((name) => existsSync(`${this.cwdPath}/lib/${name}/${name.slice(1)}.service.ts`));
|
|
1234
1014
|
}
|
|
1235
1015
|
|
|
1236
1016
|
async getScalarModules() {
|
|
1237
|
-
|
|
1017
|
+
return (await this.readdir("lib/__scalar"))
|
|
1238
1018
|
.filter((name) => !name.startsWith("_"))
|
|
1239
|
-
.filter((name) =>
|
|
1240
|
-
return scalarModules;
|
|
1019
|
+
.filter((name) => existsSync(`${this.cwdPath}/lib/__scalar/${name}/${name}.constant.ts`));
|
|
1241
1020
|
}
|
|
1242
1021
|
|
|
1022
|
+
async #getComponentModules(role: "View" | "Unit" | "Template") {
|
|
1023
|
+
return (await this.readdir("lib"))
|
|
1024
|
+
.filter((name) => !name.startsWith("_") && !name.endsWith(".ts"))
|
|
1025
|
+
.filter((name) => existsSync(`${this.cwdPath}/lib/${name}/${capitalize(name)}.${role}.tsx`));
|
|
1026
|
+
}
|
|
1027
|
+
async #getComponentSources(role: "View" | "Unit" | "Template") {
|
|
1028
|
+
const modules = await this.#getComponentModules(role);
|
|
1029
|
+
return Promise.all(modules.map((name) => this.getLocalFile(`lib/${name}/${capitalize(name)}.${role}.tsx`)));
|
|
1030
|
+
}
|
|
1243
1031
|
async getViewComponents() {
|
|
1244
|
-
|
|
1245
|
-
.filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts"))
|
|
1246
|
-
.filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${capitalize(name)}.View.tsx`).exists());
|
|
1247
|
-
return viewComponents;
|
|
1032
|
+
return await this.#getComponentModules("View");
|
|
1248
1033
|
}
|
|
1249
|
-
|
|
1250
1034
|
async getUnitComponents() {
|
|
1251
|
-
|
|
1252
|
-
.filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts"))
|
|
1253
|
-
.filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${capitalize(name)}.Unit.tsx`).exists());
|
|
1254
|
-
return unitComponents;
|
|
1035
|
+
return await this.#getComponentModules("Unit");
|
|
1255
1036
|
}
|
|
1256
1037
|
async getTemplateComponents() {
|
|
1257
|
-
|
|
1258
|
-
.filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts"))
|
|
1259
|
-
.filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${capitalize(name)}.Template.tsx`).exists());
|
|
1260
|
-
return templateComponents;
|
|
1038
|
+
return await this.#getComponentModules("Template");
|
|
1261
1039
|
}
|
|
1262
|
-
|
|
1263
1040
|
async getViewsSourceCode() {
|
|
1264
|
-
|
|
1265
|
-
return Promise.all(
|
|
1266
|
-
viewComponents.map((viewComponent) =>
|
|
1267
|
-
this.getLocalFile(`lib/${viewComponent}/${capitalize(viewComponent)}.View.tsx`),
|
|
1268
|
-
),
|
|
1269
|
-
);
|
|
1041
|
+
return await this.#getComponentSources("View");
|
|
1270
1042
|
}
|
|
1271
1043
|
async getUnitsSourceCode() {
|
|
1272
|
-
|
|
1273
|
-
return Promise.all(
|
|
1274
|
-
unitComponents.map((unitComponent) =>
|
|
1275
|
-
this.getLocalFile(`lib/${unitComponent}/${capitalize(unitComponent)}.Unit.tsx`),
|
|
1276
|
-
),
|
|
1277
|
-
);
|
|
1044
|
+
return await this.#getComponentSources("Unit");
|
|
1278
1045
|
}
|
|
1279
1046
|
async getTemplatesSourceCode() {
|
|
1280
|
-
|
|
1281
|
-
return Promise.all(
|
|
1282
|
-
templateComponents.map((templateComponent) =>
|
|
1283
|
-
this.getLocalFile(`lib/${templateComponent}/${capitalize(templateComponent)}.Template.tsx`),
|
|
1284
|
-
),
|
|
1285
|
-
);
|
|
1047
|
+
return await this.#getComponentSources("Template");
|
|
1286
1048
|
}
|
|
1287
1049
|
|
|
1288
1050
|
async getScalarConstantFiles() {
|
|
@@ -1297,7 +1059,9 @@ export class SysExecutor extends Executor {
|
|
|
1297
1059
|
async getScalarDictionaryFiles() {
|
|
1298
1060
|
const scalarModules = await this.getScalarModules();
|
|
1299
1061
|
return Promise.all(
|
|
1300
|
-
scalarModules.map((scalarModule) =>
|
|
1062
|
+
scalarModules.map((scalarModule) =>
|
|
1063
|
+
this.getLocalFile(`lib/__scalar/${scalarModule}/${scalarModule}.dictionary.ts`),
|
|
1064
|
+
),
|
|
1301
1065
|
);
|
|
1302
1066
|
}
|
|
1303
1067
|
|
|
@@ -1331,30 +1095,15 @@ export class SysExecutor extends Executor {
|
|
|
1331
1095
|
dict?: { [key: string]: string };
|
|
1332
1096
|
overwrite?: boolean;
|
|
1333
1097
|
}): Promise<FileContent[]> {
|
|
1334
|
-
const dict = {
|
|
1335
|
-
...(options.dict ?? {}),
|
|
1336
|
-
...Object.fromEntries(
|
|
1337
|
-
Object.entries(options.dict ?? {}).map(([key, value]) => [capitalize(key), capitalize(value)]),
|
|
1338
|
-
),
|
|
1339
|
-
};
|
|
1340
1098
|
const scanInfo = await this.scan();
|
|
1341
|
-
const fileContents = await this._applyTemplate({
|
|
1342
|
-
...options,
|
|
1343
|
-
scanInfo,
|
|
1344
|
-
dict,
|
|
1345
|
-
});
|
|
1099
|
+
const fileContents = await this._applyTemplate({ ...options, scanInfo, dict: withCapitalizedDict(options.dict) });
|
|
1346
1100
|
await this.scan();
|
|
1347
1101
|
return fileContents;
|
|
1348
1102
|
}
|
|
1349
1103
|
}
|
|
1350
1104
|
|
|
1351
|
-
interface AppExecutorOptions {
|
|
1352
|
-
workspace?: WorkspaceExecutor;
|
|
1353
|
-
name: string;
|
|
1354
|
-
}
|
|
1355
1105
|
export class AppExecutor extends SysExecutor {
|
|
1356
|
-
// `typescript`
|
|
1357
|
-
// ever imports an executor. Route validation is the sole consumer here and is already async.
|
|
1106
|
+
// Lazy: the validator pulls in `typescript` (~65MB resident).
|
|
1358
1107
|
static #routeSourceValidator: typeof import("./routeSourceValidator").RouteSourceValidator | null = null;
|
|
1359
1108
|
static async #getRouteSourceValidator() {
|
|
1360
1109
|
AppExecutor.#routeSourceValidator ??= (await import("./routeSourceValidator")).RouteSourceValidator;
|
|
@@ -1362,29 +1111,17 @@ export class AppExecutor extends SysExecutor {
|
|
|
1362
1111
|
}
|
|
1363
1112
|
dist: Executor;
|
|
1364
1113
|
override emoji = execEmoji.app;
|
|
1365
|
-
constructor({ workspace, name }:
|
|
1114
|
+
constructor({ workspace, name }: NamedExecutorOptions) {
|
|
1366
1115
|
super({ workspace, name, type: "app" });
|
|
1367
1116
|
this.dist = new Executor(`dist/${name}`, `${this.workspace.workspaceRoot}/dist/apps/${name}`);
|
|
1368
1117
|
}
|
|
1369
|
-
static #execs = new Map<string, AppExecutor>();
|
|
1370
1118
|
static from(executor: SysExecutor | WorkspaceExecutor, name: string) {
|
|
1371
|
-
|
|
1372
|
-
if (exec) return exec;
|
|
1373
|
-
else if (executor instanceof WorkspaceExecutor) return new AppExecutor({ workspace: executor, name });
|
|
1374
|
-
else return new AppExecutor({ workspace: executor.workspace, name });
|
|
1119
|
+
return new AppExecutor({ workspace: executor instanceof WorkspaceExecutor ? executor : executor.workspace, name });
|
|
1375
1120
|
}
|
|
1376
1121
|
getEnv() {
|
|
1377
1122
|
return WorkspaceExecutor.getBaseDevEnv().env;
|
|
1378
1123
|
}
|
|
1379
|
-
/**
|
|
1380
|
-
* This app's dev port, derived from its position in the sorted `apps/` listing so several apps can run
|
|
1381
|
-
* at once without colliding.
|
|
1382
|
-
*
|
|
1383
|
-
* `AKAN_DEV_PORT` pins it instead, because the derived value *moves*: the index shifts whenever any other
|
|
1384
|
-
* app directory appears or disappears, and a dev host recomputes this on every restart. So a session that
|
|
1385
|
-
* adds an app relands its dev server on a different port, and anything that reserved a port relative to
|
|
1386
|
-
* the old one is left pointing at nothing.
|
|
1387
|
-
*/
|
|
1124
|
+
/** `AKAN_DEV_PORT` pins it; the derived port moves whenever an app directory appears or disappears. */
|
|
1388
1125
|
async getDevPort() {
|
|
1389
1126
|
const pinned = Number(process.env.AKAN_DEV_PORT);
|
|
1390
1127
|
if (Number.isInteger(pinned) && pinned > 0 && pinned <= 65_535) return pinned;
|
|
@@ -1395,10 +1132,8 @@ export class AppExecutor extends SysExecutor {
|
|
|
1395
1132
|
return basePort + appIndex + portOffset;
|
|
1396
1133
|
}
|
|
1397
1134
|
getCommandEnv(env: Record<string, string> = {}): Record<string, string> {
|
|
1398
|
-
const
|
|
1399
|
-
const
|
|
1400
|
-
const PORT = (basePort + portOffset).toString();
|
|
1401
|
-
const AKAN_PUBLIC_SERVER_PORT = portOffset ? (8282 + portOffset).toString() : undefined;
|
|
1135
|
+
const { portOffset } = WorkspaceExecutor.getBaseDevEnv();
|
|
1136
|
+
const PORT = (8282 + portOffset).toString();
|
|
1402
1137
|
return {
|
|
1403
1138
|
...process.env,
|
|
1404
1139
|
AKAN_PUBLIC_APP_NAME: this.name,
|
|
@@ -1406,11 +1141,10 @@ export class AppExecutor extends SysExecutor {
|
|
|
1406
1141
|
NODE_NO_WARNINGS: "1",
|
|
1407
1142
|
PORT,
|
|
1408
1143
|
AKAN_PUBLIC_CLIENT_PORT: PORT,
|
|
1409
|
-
...(
|
|
1144
|
+
...(portOffset ? { AKAN_PUBLIC_SERVER_PORT: PORT } : {}),
|
|
1410
1145
|
...env,
|
|
1411
1146
|
};
|
|
1412
1147
|
}
|
|
1413
|
-
/** The database mode a command runs this app in, and the modes its build carries — the same rule the server boots by. */
|
|
1414
1148
|
async getDatabaseModeEnv() {
|
|
1415
1149
|
const akanConfig = await this.getConfig();
|
|
1416
1150
|
return {
|
|
@@ -1428,33 +1162,27 @@ export class AppExecutor extends SysExecutor {
|
|
|
1428
1162
|
};
|
|
1429
1163
|
Object.assign(process.env, routeEnv);
|
|
1430
1164
|
if (type === "build") {
|
|
1431
|
-
//* `scanSync`
|
|
1432
|
-
//* generated against and typechecked. Drop the cache so the build phases re-read it without them.
|
|
1165
|
+
//* `scanSync` read the route set unfiltered; drop the cache so build phases re-read it without dev-only routes.
|
|
1433
1166
|
this.#excludeDevOnlyPages = true;
|
|
1434
1167
|
this.#pageKeys = null;
|
|
1435
1168
|
await this.dist.removeDir(this.dist.cwdPath);
|
|
1436
1169
|
await Promise.all([this.dist.mkdir("private"), this.dist.mkdir("public")]);
|
|
1437
|
-
//* Lib assets are symlinks
|
|
1438
|
-
//*
|
|
1439
|
-
//* `public/` has exactly one reader — the web router's catch-all — so an api-only build leaves it out.
|
|
1170
|
+
//* Lib assets are symlinks, which neither the docker context nor the release tarball follows out of dist.
|
|
1171
|
+
//* Only the web router reads `public/`, so an api-only build leaves it out.
|
|
1440
1172
|
await Promise.all([
|
|
1441
1173
|
this.cp("private", `${this.dist.cwdPath}/private`, { dereference: true }),
|
|
1442
1174
|
...(akanConfig.web.ssr ? [this.cp("public", `${this.dist.cwdPath}/public`, { dereference: true })] : []),
|
|
1443
1175
|
]);
|
|
1444
1176
|
} else {
|
|
1445
1177
|
await this.removeDir(".akan");
|
|
1446
|
-
//? `
|
|
1447
|
-
//? builder is also the file watcher, so switching it off would take server-code HMR with it.
|
|
1178
|
+
//? `akan start` keeps the full dev surface: the incremental builder is also the file watcher.
|
|
1448
1179
|
if (!akanConfig.web.ssr || !akanConfig.web.csr)
|
|
1449
1180
|
this.logger.verbose(
|
|
1450
1181
|
`akan.config.ts disables web.${!akanConfig.web.ssr ? "ssr" : "csr"}; \`akan start\` still serves it, \`akan build\` will not`,
|
|
1451
1182
|
);
|
|
1452
1183
|
}
|
|
1453
|
-
//?
|
|
1454
|
-
//?
|
|
1455
|
-
//? and a dev server that believes it is production serves from the production route cache, whose every
|
|
1456
|
-
//? route throws because a dev artifact carries no routes manifest. Written into `process.env` and not only
|
|
1457
|
-
//? into the child env because the builder runs here and bakes NODE_ENV into the bundles it emits.
|
|
1184
|
+
//? Bun auto-loads `.env`, so NODE_ENV=production can arrive unexported and send the dev server to the production
|
|
1185
|
+
//? route cache. Set on `process.env` because the builder runs here and bakes NODE_ENV into its bundles.
|
|
1458
1186
|
if (type === "start") process.env.NODE_ENV = "development";
|
|
1459
1187
|
const devPort = type === "start" ? (await this.getDevPort()).toString() : undefined;
|
|
1460
1188
|
const env = this.getCommandEnv({
|
|
@@ -1462,15 +1190,8 @@ export class AppExecutor extends SysExecutor {
|
|
|
1462
1190
|
...routeEnv,
|
|
1463
1191
|
...(devPort ? { PORT: devPort, AKAN_PUBLIC_CLIENT_PORT: devPort, AKAN_PUBLIC_SERVER_PORT: devPort } : {}),
|
|
1464
1192
|
});
|
|
1465
|
-
// `
|
|
1466
|
-
//
|
|
1467
|
-
// which filters to AKAN_PUBLIC_*) sees AKAN_PUBLIC_APP_NAME — otherwise SSR throws
|
|
1468
|
-
// "environment variable AKAN_PUBLIC_APP_NAME is required". Only AKAN_PUBLIC_* is baked into bundles, so
|
|
1469
|
-
// this does not leak non-public env.
|
|
1470
|
-
// The port keys are this machine's dev allocation, and `define` turns an `AKAN_PUBLIC_*` into a literal the
|
|
1471
|
-
// artifact can never be run with a different value for — a baked port would outrank the `PORT` the container
|
|
1472
|
-
// is started with and send every SSR self-call to a port nothing bound. The operation mode likewise: one image
|
|
1473
|
-
// serves an edge site and a cloud cluster, and a shell's `local` would pin every artifact to it.
|
|
1193
|
+
// `build` runs its phases in this process, so the env is published here (only AKAN_PUBLIC_* reaches bundles).
|
|
1194
|
+
// Ports and the operation mode are dropped: `define` would bake them in and outrank the container's own values.
|
|
1474
1195
|
if (type === "build") {
|
|
1475
1196
|
const buildEnv = { ...env };
|
|
1476
1197
|
delete buildEnv.AKAN_PUBLIC_CLIENT_PORT;
|
|
@@ -1485,14 +1206,15 @@ export class AppExecutor extends SysExecutor {
|
|
|
1485
1206
|
getPublicEnv(...patterns: string[]) {
|
|
1486
1207
|
if (this.#publicEnv) return this.#publicEnv;
|
|
1487
1208
|
const searchPatterns = [...patterns, "AKAN_PUBLIC_*"];
|
|
1488
|
-
const regexes = searchPatterns.map(
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1209
|
+
const regexes = searchPatterns.map(
|
|
1210
|
+
(pattern) =>
|
|
1211
|
+
new RegExp(
|
|
1212
|
+
`^${pattern
|
|
1213
|
+
.split("*")
|
|
1214
|
+
.map((part) => part.replace(/[.+^${}()|[\]\\?]/g, "\\$&"))
|
|
1215
|
+
.join(".*")}$`,
|
|
1216
|
+
),
|
|
1217
|
+
);
|
|
1496
1218
|
const publicEnv: Record<string, string> = {};
|
|
1497
1219
|
for (const [k, v] of Object.entries(process.env)) {
|
|
1498
1220
|
if (typeof v !== "string") continue;
|
|
@@ -1505,14 +1227,11 @@ export class AppExecutor extends SysExecutor {
|
|
|
1505
1227
|
#akanConfig: AkanAppConfig | null = null;
|
|
1506
1228
|
override async getConfig({ refresh }: { refresh?: boolean } = {}) {
|
|
1507
1229
|
if (this.#akanConfig && !refresh) return this.#akanConfig;
|
|
1508
|
-
// A refresh means the config file may have been edited; bust the import cache so the fresh
|
|
1509
|
-
// module is evaluated instead of Bun's cached instance.
|
|
1510
1230
|
this.#akanConfig = await AkanAppConfig.from(this, { bustImportCache: refresh });
|
|
1511
1231
|
return this.#akanConfig;
|
|
1512
1232
|
}
|
|
1513
1233
|
|
|
1514
1234
|
#pageKeys: string[] | null = null;
|
|
1515
|
-
/** Set once `prepareCommand("build")` runs, so every later consumer of the route set agrees on it. */
|
|
1516
1235
|
#excludeDevOnlyPages = false;
|
|
1517
1236
|
async getPageKeys({ refresh }: { refresh?: boolean } = {}): Promise<string[]> {
|
|
1518
1237
|
if (this.#pageKeys && !refresh) return this.#pageKeys;
|
|
@@ -1551,8 +1270,7 @@ export class AppExecutor extends SysExecutor {
|
|
|
1551
1270
|
const fromLib = !!root.keyPrefix;
|
|
1552
1271
|
const routeId = `${parsed.kind}:${parsed.pattern}`;
|
|
1553
1272
|
const owner = owners.get(routeId);
|
|
1554
|
-
//* App-owned routes
|
|
1555
|
-
//* collision once a synced lib is involved, where neither side can see the other.
|
|
1273
|
+
//* App-owned routes may collide (two groups, one pattern); a synced lib cannot see the other side.
|
|
1556
1274
|
if (owner && (owner.fromLib || fromLib)) {
|
|
1557
1275
|
throw new Error(
|
|
1558
1276
|
`[route-convention] duplicate ${parsed.kind} route "${parsed.pattern}" in app "${this.name}":\n- ${owner.absPath}\n- ${absPath}`,
|
|
@@ -1571,8 +1289,7 @@ export class AppExecutor extends SysExecutor {
|
|
|
1571
1289
|
});
|
|
1572
1290
|
if (info.devOnly) {
|
|
1573
1291
|
devOnlyKeys.add(key);
|
|
1574
|
-
//* A
|
|
1575
|
-
//* pages behind would ship them stripped of the chrome they were written under.
|
|
1292
|
+
//* A dev-only layout takes its whole subtree, or its pages would ship without their chrome.
|
|
1576
1293
|
if (parsed.kind === "layout") devOnlyDirs.push(key.replace(/[^/]+$/, ""));
|
|
1577
1294
|
}
|
|
1578
1295
|
}
|
|
@@ -1590,12 +1307,7 @@ export class AppExecutor extends SysExecutor {
|
|
|
1590
1307
|
this.verbose(`[route] excluded ${dropped.length} dev-only route file(s) from the build: ${dropped.join(", ")}`);
|
|
1591
1308
|
return pageKeys.filter((key) => !isDevOnly(key));
|
|
1592
1309
|
}
|
|
1593
|
-
/**
|
|
1594
|
-
* Every directory that contributes route files, as `page`-relative key prefixes. Lib roots are the
|
|
1595
|
-
* symlinks `syncPages` created, so route keys stay app-relative while `realDir` is what a file watcher
|
|
1596
|
-
* reports. Enumeration never crosses a symlink (neither Bun's glob nor TypeScript's `include` does),
|
|
1597
|
-
* which is why linked page folders have to be listed here instead of found by walking `page`.
|
|
1598
|
-
*/
|
|
1310
|
+
/** Lib roots are the `syncPages` symlinks, listed because neither Bun's glob nor TS `include` crosses one. */
|
|
1599
1311
|
async getPageRoots(): Promise<PageRoot[]> {
|
|
1600
1312
|
const akanConfig = await this.getConfig();
|
|
1601
1313
|
const pageDir = `${this.cwdPath}/page`;
|
|
@@ -1616,18 +1328,16 @@ export class AppExecutor extends SysExecutor {
|
|
|
1616
1328
|
}
|
|
1617
1329
|
|
|
1618
1330
|
static readonly #pageLibsDir = "(libs)";
|
|
1619
|
-
/** Where `(libs)` may live: once at the page root, or once per basePath when the app declares subRoutes. */
|
|
1620
1331
|
static #pageLibParents(basePaths: Iterable<string>): string[] {
|
|
1621
1332
|
const parents = [...basePaths].map((basePath) => `${basePath}/`);
|
|
1622
1333
|
return parents.length ? parents : [""];
|
|
1623
1334
|
}
|
|
1624
|
-
/**
|
|
1335
|
+
/** Whether the linked page set changed, which makes the app's route keys stale. */
|
|
1625
1336
|
async syncPages(libDeps: string[]): Promise<boolean> {
|
|
1626
1337
|
const akanConfig = await this.getConfig();
|
|
1627
1338
|
const parents = AppExecutor.#pageLibParents(akanConfig.basePaths);
|
|
1628
1339
|
const libs = await this.#resolvePageLibs(akanConfig.syncPageLibs, libDeps);
|
|
1629
|
-
//*
|
|
1630
|
-
//* exactly the ones a sync has to clean up.
|
|
1340
|
+
//* Not `getPageRoots`: it drops links whose target is gone, exactly the ones a sync must clean up.
|
|
1631
1341
|
const linked = (
|
|
1632
1342
|
await Promise.all(
|
|
1633
1343
|
parents.map(async (parent) => {
|
|
@@ -1697,8 +1407,7 @@ export class AppExecutor extends SysExecutor {
|
|
|
1697
1407
|
);
|
|
1698
1408
|
}
|
|
1699
1409
|
static async #linkLibAsset(targetPath: string, linkPath: string) {
|
|
1700
|
-
//*
|
|
1701
|
-
//* Windows junctions are the exception and resolve their target as an absolute path.
|
|
1410
|
+
//* Relative, so it survives a remount (containers, CI); a Windows junction needs an absolute target.
|
|
1702
1411
|
const isWindows = process.platform === "win32";
|
|
1703
1412
|
try {
|
|
1704
1413
|
const target = isWindows ? targetPath : path.relative(path.dirname(linkPath), targetPath);
|
|
@@ -1722,8 +1431,6 @@ export class AppExecutor extends SysExecutor {
|
|
|
1722
1431
|
if (write) await this.#runPluginSyncAssets();
|
|
1723
1432
|
return scanInfo;
|
|
1724
1433
|
}
|
|
1725
|
-
//* build-time asset generation is delegated to plugins (e.g. the push plugin writes
|
|
1726
|
-
//* public/firebase-messaging-sw.js). The framework itself no longer knows about firebase.
|
|
1727
1434
|
async #runPluginSyncAssets() {
|
|
1728
1435
|
const plugins = await this.collectPlugins();
|
|
1729
1436
|
if (!plugins.some((plugin) => plugin.syncAssets)) return;
|
|
@@ -1755,8 +1462,6 @@ export class AppExecutor extends SysExecutor {
|
|
|
1755
1462
|
},
|
|
1756
1463
|
};
|
|
1757
1464
|
}
|
|
1758
|
-
//* Aggregate plugins declared by this app's akan.config plus each of its lib dependencies'
|
|
1759
|
-
//* akan.config. This is how a lib (e.g. libs/util) opts an app into a feature turnkey.
|
|
1760
1465
|
async collectPlugins(): Promise<AkanPlugin[]> {
|
|
1761
1466
|
const scanInfo = (await this.scan({ write: false })) as AppInfo;
|
|
1762
1467
|
const libDeps = scanInfo.getLibs();
|
|
@@ -1793,23 +1498,15 @@ export class AppExecutor extends SysExecutor {
|
|
|
1793
1498
|
await decreaseBuildNum(this);
|
|
1794
1499
|
}
|
|
1795
1500
|
}
|
|
1796
|
-
interface LibExecutorOptions {
|
|
1797
|
-
workspace?: WorkspaceExecutor;
|
|
1798
|
-
name: string;
|
|
1799
|
-
}
|
|
1800
1501
|
export class LibExecutor extends SysExecutor {
|
|
1801
1502
|
dist: Executor;
|
|
1802
1503
|
override emoji = execEmoji.lib;
|
|
1803
|
-
constructor({ workspace, name }:
|
|
1504
|
+
constructor({ workspace, name }: NamedExecutorOptions) {
|
|
1804
1505
|
super({ workspace, name, type: "lib" });
|
|
1805
1506
|
this.dist = new Executor(`dist/${name}`, `${this.workspace.workspaceRoot}/dist/libs/${name}`);
|
|
1806
1507
|
}
|
|
1807
|
-
static #execs = new Map<string, LibExecutor>();
|
|
1808
1508
|
static from(executor: SysExecutor | WorkspaceExecutor, name: string) {
|
|
1809
|
-
|
|
1810
|
-
if (exec) return exec;
|
|
1811
|
-
else if (executor instanceof WorkspaceExecutor) return new LibExecutor({ workspace: executor, name });
|
|
1812
|
-
else return new LibExecutor({ workspace: executor.workspace, name });
|
|
1509
|
+
return new LibExecutor({ workspace: executor instanceof WorkspaceExecutor ? executor : executor.workspace, name });
|
|
1813
1510
|
}
|
|
1814
1511
|
|
|
1815
1512
|
#akanConfig: AkanLibConfig | null = null;
|
|
@@ -1820,31 +1517,25 @@ export class LibExecutor extends SysExecutor {
|
|
|
1820
1517
|
}
|
|
1821
1518
|
}
|
|
1822
1519
|
|
|
1823
|
-
interface PkgExecutorOptions {
|
|
1824
|
-
workspace?: WorkspaceExecutor;
|
|
1825
|
-
name: string;
|
|
1826
|
-
}
|
|
1827
1520
|
export class PkgExecutor extends Executor {
|
|
1828
1521
|
workspace: WorkspaceExecutor;
|
|
1829
1522
|
override name: string;
|
|
1830
1523
|
dist: Executor;
|
|
1831
1524
|
override emoji = execEmoji.pkg;
|
|
1832
|
-
constructor({ workspace = WorkspaceExecutor.fromRoot(), name }:
|
|
1525
|
+
constructor({ workspace = WorkspaceExecutor.fromRoot(), name }: NamedExecutorOptions) {
|
|
1833
1526
|
super(name, `${workspace.workspaceRoot}/pkgs/${name}`);
|
|
1834
1527
|
this.workspace = workspace;
|
|
1835
1528
|
this.name = name;
|
|
1836
1529
|
this.dist = new Executor(`dist/${name}`, `${this.workspace.workspaceRoot}/dist/pkgs/${name}`);
|
|
1837
1530
|
}
|
|
1838
1531
|
static from(executor: SysExecutor | WorkspaceExecutor, name: string) {
|
|
1839
|
-
|
|
1840
|
-
return new PkgExecutor({ workspace: executor.workspace, name });
|
|
1532
|
+
return new PkgExecutor({ workspace: executor instanceof WorkspaceExecutor ? executor : executor.workspace, name });
|
|
1841
1533
|
}
|
|
1842
1534
|
|
|
1843
1535
|
#scanInfo: PkgInfo | null = null;
|
|
1844
1536
|
async scan({ refresh }: { refresh?: boolean } = {}): Promise<PkgInfo> {
|
|
1845
1537
|
if (this.#scanInfo && !refresh) return this.#scanInfo;
|
|
1846
1538
|
const scanInfo = await PkgInfo.fromExecutor(this, { refresh });
|
|
1847
|
-
// this.writeJson("akan.pkg.json", pkgScanResult);
|
|
1848
1539
|
this.#scanInfo = scanInfo;
|
|
1849
1540
|
return scanInfo;
|
|
1850
1541
|
}
|
|
@@ -1855,9 +1546,7 @@ export class PkgExecutor extends Executor {
|
|
|
1855
1546
|
};
|
|
1856
1547
|
const rootVersion = rootDeps[dep];
|
|
1857
1548
|
if (rootVersion) return rootVersion;
|
|
1858
|
-
//
|
|
1859
|
-
// that pin has to reach the published package: neither an override nor a dependency's own shrinkwrap is
|
|
1860
|
-
// applied to a consumer's install, so the version has to be a dependency of what we publish.
|
|
1549
|
+
// Overrides never reach a consumer's install, so a pinned transitive dep is published as a dependency.
|
|
1861
1550
|
const overrideVersion = rootPackageJson.overrides?.[dep];
|
|
1862
1551
|
if (typeof overrideVersion === "string") return overrideVersion;
|
|
1863
1552
|
|
|
@@ -1887,17 +1576,9 @@ export class PkgExecutor extends Executor {
|
|
|
1887
1576
|
if (missingDeps.length > 0)
|
|
1888
1577
|
throw new Error(`Missing dependency versions in root package.json: ${missingDeps.join(", ")}`);
|
|
1889
1578
|
|
|
1890
|
-
const
|
|
1891
|
-
names.map((dep) =>
|
|
1892
|
-
|
|
1893
|
-
if (!version) throw new Error(`Missing dependency versions in root package.json: ${dep}`);
|
|
1894
|
-
return [dep, version] as const;
|
|
1895
|
-
});
|
|
1896
|
-
|
|
1897
|
-
return {
|
|
1898
|
-
dependencies: Object.fromEntries(toDependencyEntries(dependencyNames)),
|
|
1899
|
-
devDependencies: Object.fromEntries(toDependencyEntries(devDependencyNames)),
|
|
1900
|
-
};
|
|
1579
|
+
const toDependencyMap = (names: string[]) =>
|
|
1580
|
+
Object.fromEntries(names.map((dep) => [dep, dependencyVersions.get(dep) as string]));
|
|
1581
|
+
return { dependencies: toDependencyMap(dependencyNames), devDependencies: toDependencyMap(devDependencyNames) };
|
|
1901
1582
|
}
|
|
1902
1583
|
async updatePackageJsonDependencies(
|
|
1903
1584
|
dependencies: string[] = [],
|