@akanjs/devkit 3.0.0-beta.2 → 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/README.md +1 -0
- package/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +6 -8
- 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 +82 -254
- package/akanApp/devHostPolicy.test.ts +10 -34
- package/akanApp/devHostPolicy.ts +12 -98
- package/akanConfig/akanConfig.test.ts +85 -69
- package/akanConfig/akanConfig.ts +69 -153
- package/akanConfig/types.ts +1 -0
- package/akanContext.ts +106 -159
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.test.ts +5 -3
- package/applicationBuildRunner.ts +54 -61
- package/applicationReleasePackager.ts +24 -44
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +9 -30
- package/artifact/implicitRootLayout.ts +34 -106
- package/artifact/routeSeedIndex.test.ts +7 -6
- package/artifact/routeSeedIndex.ts +11 -44
- package/biome.base.json +6 -1
- package/biomeBase.ts +2 -5
- package/capacitorApp.test.ts +32 -16
- package/capacitorApp.ts +110 -238
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +77 -39
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +26 -5
- package/cloud/globalConfig.ts +70 -32
- package/codeAgent/agent/AkanCodeServices.ts +97 -0
- package/codeAgent/agent/AkanContextFiles.ts +94 -0
- package/codeAgent/agent/CodeAgent.ts +551 -0
- package/codeAgent/agent/CodeAgentAsks.ts +66 -0
- package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
- package/codeAgent/agent/CodeAgentGate.ts +93 -0
- package/codeAgent/agent/CodeAgentProxy.ts +82 -0
- package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
- package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
- package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
- package/codeAgent/agent/CodeAgentUi.ts +47 -0
- package/codeAgent/agent/CodeMailbox.ts +148 -0
- package/codeAgent/agent/CodeSessionFork.ts +56 -0
- package/codeAgent/agent/CodeSessionIndex.ts +78 -0
- package/codeAgent/agent/SubagentPool.ts +214 -0
- package/codeAgent/agent/akanCodeModel.ts +47 -0
- package/codeAgent/agent/akanCodePaths.ts +31 -0
- package/codeAgent/agent/akanContextFiles.test.ts +114 -0
- package/codeAgent/agent/akanSystemPrompt.ts +46 -0
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +523 -0
- package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
- package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
- package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
- package/codeAgent/agent/codeMailbox.test.ts +76 -0
- package/codeAgent/agent/codeSessionFork.test.ts +58 -0
- package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
- package/codeAgent/agent/consoleToStderr.ts +8 -0
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +82 -0
- package/codeAgent/feedback/PreviewChrome.ts +82 -0
- package/codeAgent/feedback/PreviewFeedback.ts +46 -0
- package/codeAgent/feedback/PreviewView.ts +95 -0
- package/codeAgent/feedback/TurnFeedback.ts +98 -0
- package/codeAgent/feedback/VerifyFeedback.ts +40 -0
- package/codeAgent/feedback/previewChrome.test.ts +73 -0
- package/codeAgent/index.ts +34 -0
- package/codeAgent/skills/akan-code/SKILL.md +130 -0
- package/codeAgent/skills/akan-debug/SKILL.md +42 -0
- package/codeAgent/skills/akan-deps/SKILL.md +50 -0
- package/codeAgent/skills/akan-err/SKILL.md +39 -0
- package/codeAgent/skills/akan-module/SKILL.md +45 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
- package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
- package/codeAgent/skills/akan-service/SKILL.md +34 -0
- package/codeAgent/skills/akan-store/SKILL.md +67 -0
- package/codeAgent/skills/akan-ui/SKILL.md +63 -0
- package/codeAgent/skills/akan-validate/SKILL.md +44 -0
- package/codeAgent/tools/AkanCli.ts +50 -0
- package/codeAgent/tools/AkanCodePlugins.ts +136 -0
- package/codeAgent/tools/AkanEditScope.ts +48 -0
- package/codeAgent/tools/AkanEnvKeys.ts +50 -0
- package/codeAgent/tools/AkanToolPack.ts +92 -0
- package/codeAgent/tools/AkanVerifier.ts +48 -0
- package/codeAgent/tools/AskToolPack.ts +99 -0
- package/codeAgent/tools/MailToolPack.ts +76 -0
- package/codeAgent/tools/McpClient.ts +187 -0
- package/codeAgent/tools/McpOAuth.ts +233 -0
- package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
- package/codeAgent/tools/McpServerConfig.ts +129 -0
- package/codeAgent/tools/McpSignIn.ts +158 -0
- package/codeAgent/tools/McpTokenStore.ts +81 -0
- package/codeAgent/tools/McpToolPack.ts +199 -0
- package/codeAgent/tools/SessionToolPack.ts +91 -0
- package/codeAgent/tools/WebToolPack.ts +147 -0
- package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
- package/codeAgent/tools/askToolPack.test.ts +116 -0
- package/codeAgent/tools/mcpOAuth.test.ts +612 -0
- package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
- 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 +46 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +2 -10
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +68 -145
- 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 +31 -80
- package/devkitUtils.test.ts +48 -20
- package/executors.linkFallback.test.ts +103 -0
- package/executors.test.ts +132 -106
- package/executors.ts +194 -484
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +5 -3
- package/fileSys.ts +6 -8
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +27 -38
- package/frontendBuild/autoImportSync.ts +31 -65
- package/frontendBuild/buildRouteClient.test.ts +62 -37
- 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 +6 -19
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/fontOptimizer.test.ts +6 -14
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +46 -69
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +8 -32
- package/frontendBuild/hmrWatcher.ts +14 -71
- package/frontendBuild/pagesBundleBuilder.ts +4 -29
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/routeClientBuilder.ts +23 -46
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
- 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 +21 -24
- 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 +49 -161
- package/index.ts +5 -17
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +47 -194
- package/integration/devStabilityHarness.test.ts +13 -28
- 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 +8 -24
- package/lint/no-deep-internal-import.grit +2 -2
- package/lint/no-redeclare-predefined-endpoint.grit +2 -1
- package/linter.test.ts +1 -2
- package/linter.ts +26 -116
- package/mobile/mobileTarget.ts +14 -13
- package/package.json +10 -5
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +46 -0
- package/platformTest/BunTestReport.test.ts +60 -0
- package/platformTest/BunTestReport.ts +65 -0
- package/platformTest/IdleWatchedProcess.test.ts +56 -0
- package/platformTest/IdleWatchedProcess.ts +73 -0
- package/platformTest/LinuxDockerTarget.ts +105 -0
- package/platformTest/PlatformTestRun.test.ts +64 -0
- package/platformTest/PlatformTestRun.ts +141 -0
- package/platformTest/PlatformTestTarget.ts +142 -0
- package/platformTest/TestSnapshot.test.ts +72 -0
- package/platformTest/TestSnapshot.ts +103 -0
- package/platformTest/WindowsSshTarget.ts +151 -0
- package/prompter.ts +3 -13
- package/qualityScanner.test.ts +9 -26
- package/qualityScanner.ts +197 -285
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.test.ts +0 -1
- package/routeSourceValidator.ts +7 -42
- package/scanInfo.ts +87 -216
- 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/stripAnsi.ts +3 -0
- package/subspace.test.ts +30 -25
- package/subspace.ts +81 -115
- package/subspaceConfig.ts +11 -9
- package/testHelpers.ts +194 -0
- 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/types.ts +1 -0
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +101 -110
- 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/rolloutGate.test.ts +1 -1
- package/workflow/source.ts +52 -69
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.ts +4 -19
- package/abstractCompactor.ts +0 -112
- package/aiEditor.test.ts +0 -60
- package/aiEditor.ts +0 -387
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/guideline.ts +0 -19
- 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,10 +7,11 @@ 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,
|
|
14
|
+
lstat,
|
|
14
15
|
mkdir,
|
|
15
16
|
readdir as readDirEntries,
|
|
16
17
|
realpath,
|
|
@@ -42,14 +43,12 @@ import {
|
|
|
42
43
|
import { AkanAppConfig, AkanLibConfig, decreaseBuildNum, increaseBuildNum } from "./akanConfig";
|
|
43
44
|
import { getRootBoundarySegments, isRootBoundarySegments } from "./artifact/implicitRootLayout";
|
|
44
45
|
import { CodegenLock } from "./codegenLock";
|
|
45
|
-
import { FileSys } from "./fileSys";
|
|
46
|
-
import { getDirname } from "./getDirname";
|
|
46
|
+
import { FileSys, getDirname } from "./fileSys";
|
|
47
47
|
import { Linter } from "./linter";
|
|
48
48
|
import { resolveRepoName } from "./repoIdentity";
|
|
49
49
|
import { AppInfo, LibInfo, PkgInfo, WorkspaceInfo } from "./scanInfo";
|
|
50
50
|
import { Spinner } from "./spinner";
|
|
51
|
-
// Type-only:
|
|
52
|
-
// the resident module graph.
|
|
51
|
+
// Type-only: `getTypeChecker` loads it on demand to keep `typescript` out of the resident module graph.
|
|
53
52
|
import type { TypeChecker } from "./typeChecker";
|
|
54
53
|
import type { FileContent, PackageJson, TsConfigJson } from "./types";
|
|
55
54
|
|
|
@@ -62,41 +61,15 @@ export interface PageRoot {
|
|
|
62
61
|
keyPrefix: string;
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
const staticTemplateFileExtensions = new Set(
|
|
66
|
-
".avif"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
".eot",
|
|
71
|
-
".gif",
|
|
72
|
-
".html",
|
|
73
|
-
".ico",
|
|
74
|
-
".jpeg",
|
|
75
|
-
".jpg",
|
|
76
|
-
".js",
|
|
77
|
-
".json",
|
|
78
|
-
".map",
|
|
79
|
-
".md",
|
|
80
|
-
".mjs",
|
|
81
|
-
".mp3",
|
|
82
|
-
".mp4",
|
|
83
|
-
".ogg",
|
|
84
|
-
".otf",
|
|
85
|
-
".pdf",
|
|
86
|
-
".png",
|
|
87
|
-
".svg",
|
|
88
|
-
".ttf",
|
|
89
|
-
".txt",
|
|
90
|
-
".wasm",
|
|
91
|
-
".wav",
|
|
92
|
-
".webm",
|
|
93
|
-
".webp",
|
|
94
|
-
".woff",
|
|
95
|
-
".woff2",
|
|
96
|
-
".xml",
|
|
97
|
-
]);
|
|
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
|
+
);
|
|
98
69
|
|
|
99
|
-
|
|
70
|
+
//? A backslash is a path separator on Windows but an escape in a POSIX shell, so only there does it force quoting.
|
|
71
|
+
const plainCommandArgPattern = process.platform === "win32" ? /^[\w@%+=:,./\\-]+$/ : /^[\w@%+=:,./-]+$/;
|
|
72
|
+
const formatCommandArg = (value: string) => (plainCommandArgPattern.test(value) ? value : JSON.stringify(value));
|
|
100
73
|
|
|
101
74
|
const formatCommandForDisplay = (command: string, args: string[] = []) =>
|
|
102
75
|
[command, ...args].map(formatCommandArg).join(" ");
|
|
@@ -155,7 +128,7 @@ export const execEmoji = {
|
|
|
155
128
|
pkg: "📦",
|
|
156
129
|
dist: "💿",
|
|
157
130
|
module: "⚙️",
|
|
158
|
-
default: "✈️",
|
|
131
|
+
default: "✈️",
|
|
159
132
|
};
|
|
160
133
|
|
|
161
134
|
const parseEnvFile = (envPath: string): Record<string, string> => {
|
|
@@ -183,6 +156,11 @@ const parseEnvFile = (envPath: string): Record<string, string> => {
|
|
|
183
156
|
return env;
|
|
184
157
|
};
|
|
185
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
|
+
|
|
186
164
|
export class Executor {
|
|
187
165
|
static verbose = false;
|
|
188
166
|
static setVerbose(verbose: boolean) {
|
|
@@ -208,158 +186,53 @@ export class Executor {
|
|
|
208
186
|
#stderr(data: Buffer) {
|
|
209
187
|
Logger.raw(chalk.red(data.toString()));
|
|
210
188
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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 }> {
|
|
214
194
|
let stdout = "";
|
|
215
195
|
let stderr = "";
|
|
216
196
|
proc.stdout?.on("data", (data: Buffer) => {
|
|
217
197
|
stdout += data.toString();
|
|
198
|
+
if (keepLogs) this.logs.push(data.toString());
|
|
218
199
|
this.#stdout(data);
|
|
219
200
|
});
|
|
220
201
|
proc.stderr?.on("data", (data: Buffer) => {
|
|
221
202
|
stderr += data.toString();
|
|
222
|
-
this
|
|
203
|
+
if (keepLogs) this.logs.push(data.toString());
|
|
204
|
+
if (redStderr) this.#stderr(data);
|
|
205
|
+
else this.#stdout(data); // 정상로그도 stderr로 나옴
|
|
223
206
|
});
|
|
224
207
|
return new Promise((resolve, reject) => {
|
|
225
|
-
proc.on("error", (
|
|
226
|
-
reject(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
stdout,
|
|
233
|
-
stderr,
|
|
234
|
-
cause: error,
|
|
235
|
-
}),
|
|
236
|
-
);
|
|
237
|
-
});
|
|
238
|
-
proc.on("exit", (code, signal) => {
|
|
239
|
-
if (code || signal)
|
|
240
|
-
reject(
|
|
241
|
-
new CommandExecutionError({
|
|
242
|
-
command,
|
|
243
|
-
cwd,
|
|
244
|
-
code,
|
|
245
|
-
signal,
|
|
246
|
-
stdout,
|
|
247
|
-
stderr,
|
|
248
|
-
}),
|
|
249
|
-
);
|
|
250
|
-
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 });
|
|
251
215
|
});
|
|
252
216
|
});
|
|
253
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
|
+
}
|
|
254
223
|
|
|
255
224
|
spawn(command: string, args: string[] = [], options: SpawnOptions = {}): Promise<string> {
|
|
256
|
-
const
|
|
257
|
-
const proc = spawn(command, args, {
|
|
258
|
-
|
|
259
|
-
// stdio: "inherit",
|
|
260
|
-
...options,
|
|
261
|
-
});
|
|
262
|
-
let stdout = "";
|
|
263
|
-
let stderr = "";
|
|
264
|
-
|
|
265
|
-
proc.stdout?.on("data", (data: Buffer) => {
|
|
266
|
-
stdout += data.toString();
|
|
267
|
-
this.logs.push(data.toString());
|
|
268
|
-
this.#stdout(data);
|
|
269
|
-
});
|
|
270
|
-
proc.stderr?.on("data", (data: Buffer) => {
|
|
271
|
-
stderr += data.toString();
|
|
272
|
-
this.logs.push(data.toString());
|
|
273
|
-
this.#stdout(data); // 정상로그도 stderr로 나옴
|
|
274
|
-
});
|
|
275
|
-
return new Promise((resolve, reject) => {
|
|
276
|
-
proc.on("error", (error) => {
|
|
277
|
-
reject(
|
|
278
|
-
new CommandExecutionError({
|
|
279
|
-
command,
|
|
280
|
-
args,
|
|
281
|
-
cwd,
|
|
282
|
-
code: null,
|
|
283
|
-
signal: null,
|
|
284
|
-
stdout,
|
|
285
|
-
stderr,
|
|
286
|
-
cause: error,
|
|
287
|
-
}),
|
|
288
|
-
);
|
|
289
|
-
});
|
|
290
|
-
proc.on("close", (code, signal) => {
|
|
291
|
-
if (code !== 0 || signal)
|
|
292
|
-
reject(
|
|
293
|
-
new CommandExecutionError({
|
|
294
|
-
command,
|
|
295
|
-
args,
|
|
296
|
-
cwd,
|
|
297
|
-
code,
|
|
298
|
-
signal,
|
|
299
|
-
stdout,
|
|
300
|
-
stderr,
|
|
301
|
-
}),
|
|
302
|
-
);
|
|
303
|
-
else resolve(stdout);
|
|
304
|
-
});
|
|
305
|
-
});
|
|
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);
|
|
306
228
|
}
|
|
307
229
|
spawnSync(command: string, args: string[] = [], options: SpawnOptions = {}): ChildProcess {
|
|
308
|
-
|
|
309
|
-
cwd: this.cwdPath,
|
|
310
|
-
// stdio: "inherit",
|
|
311
|
-
...options,
|
|
312
|
-
});
|
|
313
|
-
return proc;
|
|
230
|
+
return spawn(command, args, { cwd: this.cwdPath, ...options });
|
|
314
231
|
}
|
|
315
|
-
fork(modulePath: string, args: string[] = [], options: ForkOptions = {}) {
|
|
316
|
-
const
|
|
317
|
-
const proc = fork(modulePath, args, {
|
|
318
|
-
|
|
319
|
-
// stdio: ["ignore", "inherit", "inherit", "ipc"],
|
|
320
|
-
...options,
|
|
321
|
-
});
|
|
322
|
-
let stdout = "";
|
|
323
|
-
let stderr = "";
|
|
324
|
-
proc.stdout?.on("data", (data: Buffer) => {
|
|
325
|
-
stdout += data.toString();
|
|
326
|
-
this.#stdout(data);
|
|
327
|
-
});
|
|
328
|
-
proc.stderr?.on("data", (data: Buffer) => {
|
|
329
|
-
stderr += data.toString();
|
|
330
|
-
this.#stderr(data);
|
|
331
|
-
});
|
|
332
|
-
return new Promise((resolve, reject) => {
|
|
333
|
-
proc.on("error", (error) => {
|
|
334
|
-
reject(
|
|
335
|
-
new CommandExecutionError({
|
|
336
|
-
command: modulePath,
|
|
337
|
-
args,
|
|
338
|
-
cwd,
|
|
339
|
-
code: null,
|
|
340
|
-
signal: null,
|
|
341
|
-
stdout,
|
|
342
|
-
stderr,
|
|
343
|
-
cause: error,
|
|
344
|
-
}),
|
|
345
|
-
);
|
|
346
|
-
});
|
|
347
|
-
proc.on("exit", (code, signal) => {
|
|
348
|
-
if (code || signal)
|
|
349
|
-
reject(
|
|
350
|
-
new CommandExecutionError({
|
|
351
|
-
command: modulePath,
|
|
352
|
-
args,
|
|
353
|
-
cwd,
|
|
354
|
-
code,
|
|
355
|
-
signal,
|
|
356
|
-
stdout,
|
|
357
|
-
stderr,
|
|
358
|
-
}),
|
|
359
|
-
);
|
|
360
|
-
else resolve({ code, signal });
|
|
361
|
-
});
|
|
362
|
-
});
|
|
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 }));
|
|
363
236
|
}
|
|
364
237
|
getPath(filePath: string) {
|
|
365
238
|
if (path.isAbsolute(filePath)) return filePath;
|
|
@@ -400,16 +273,14 @@ export class Executor {
|
|
|
400
273
|
}
|
|
401
274
|
async getFilesAndDirs(dirPath: string): Promise<{ files: string[]; dirs: string[] }> {
|
|
402
275
|
const fullDirPath = this.getPath(dirPath);
|
|
403
|
-
const
|
|
404
|
-
const files = Array.from(
|
|
405
|
-
const
|
|
406
|
-
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 }));
|
|
407
279
|
const dirs = allEntries.filter((entry) => !files.includes(entry));
|
|
408
280
|
return { files, dirs };
|
|
409
281
|
}
|
|
410
282
|
async exists(filePath: string) {
|
|
411
|
-
|
|
412
|
-
return await FileSys.exists(readPath);
|
|
283
|
+
return await FileSys.exists(this.getPath(filePath));
|
|
413
284
|
}
|
|
414
285
|
async remove(filePath: string) {
|
|
415
286
|
const readPath = this.getPath(filePath);
|
|
@@ -418,8 +289,7 @@ export class Executor {
|
|
|
418
289
|
return this;
|
|
419
290
|
}
|
|
420
291
|
async removeDir(dirPath: string) {
|
|
421
|
-
//* XXX: `path.join(…, ".")` drops a trailing
|
|
422
|
-
//* 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.
|
|
423
293
|
const readPath = path.join(this.getPath(dirPath), ".");
|
|
424
294
|
if (await FileSys.entryExists(readPath)) await rm(readPath, { recursive: true, force: true });
|
|
425
295
|
this.logger.verbose(`Remove directory ${readPath}`);
|
|
@@ -433,8 +303,7 @@ export class Executor {
|
|
|
433
303
|
const writePath = this.getPath(filePath);
|
|
434
304
|
const dir = path.dirname(writePath);
|
|
435
305
|
if (!(await FileSys.dirExists(dir))) await mkdir(dir, { recursive: true });
|
|
436
|
-
//? Biome
|
|
437
|
-
//? 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.
|
|
438
307
|
let contentStr = typeof content === "string" ? content : `${JSON.stringify(content, null, 2)}\n`;
|
|
439
308
|
|
|
440
309
|
if (await FileSys.fileExists(writePath)) {
|
|
@@ -461,12 +330,10 @@ export class Executor {
|
|
|
461
330
|
return { filePath, content };
|
|
462
331
|
}
|
|
463
332
|
async readFile(filePath: string) {
|
|
464
|
-
|
|
465
|
-
return await FileSys.readText(readPath);
|
|
333
|
+
return await FileSys.readText(this.getPath(filePath));
|
|
466
334
|
}
|
|
467
335
|
async readJson(filePath: string) {
|
|
468
|
-
|
|
469
|
-
return await FileSys.readJson<object>(readPath);
|
|
336
|
+
return await FileSys.readJson<object>(this.getPath(filePath));
|
|
470
337
|
}
|
|
471
338
|
async cp(srcPath: string, destPath: string, { dereference = false }: { dereference?: boolean } = {}) {
|
|
472
339
|
const src = this.getPath(srcPath);
|
|
@@ -474,9 +341,10 @@ export class Executor {
|
|
|
474
341
|
if (!(await FileSys.exists(src))) return;
|
|
475
342
|
const isDirectory = (await stat(src)).isDirectory();
|
|
476
343
|
if (!(await FileSys.exists(dest)) && isDirectory) await mkdir(dest, { recursive: true });
|
|
477
|
-
//* `cp -r` keeps symlinks on GNU
|
|
478
|
-
//*
|
|
479
|
-
if (dereference
|
|
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.
|
|
346
|
+
if (dereference || process.platform === "win32")
|
|
347
|
+
await cpEntry(src, dest, { recursive: isDirectory, dereference: true, force: true });
|
|
480
348
|
else await $`cp -r ${src}${isDirectory ? "/." : ""} ${dest}`;
|
|
481
349
|
}
|
|
482
350
|
log(msg: string) {
|
|
@@ -498,19 +366,10 @@ export class Executor {
|
|
|
498
366
|
#tsconfig: TsConfigJson | null = null;
|
|
499
367
|
async getTsConfig(pathname = "tsconfig.json", { refresh }: { refresh?: boolean } = {}): Promise<TsConfigJson> {
|
|
500
368
|
if (this.#tsconfig && !refresh) return this.#tsconfig;
|
|
501
|
-
|
|
369
|
+
let tsconfig = (await this.readJson(pathname)) as TsConfigJson;
|
|
502
370
|
if (tsconfig.extends) {
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
...extendsTsconfig,
|
|
506
|
-
...tsconfig,
|
|
507
|
-
compilerOptions: {
|
|
508
|
-
...extendsTsconfig.compilerOptions,
|
|
509
|
-
...tsconfig.compilerOptions,
|
|
510
|
-
},
|
|
511
|
-
} as TsConfigJson;
|
|
512
|
-
this.#tsconfig = result;
|
|
513
|
-
return result;
|
|
371
|
+
const base = await this.getTsConfig(tsconfig.extends, { refresh });
|
|
372
|
+
tsconfig = { ...base, ...tsconfig, compilerOptions: { ...base.compilerOptions, ...tsconfig.compilerOptions } };
|
|
514
373
|
}
|
|
515
374
|
this.#tsconfig = tsconfig;
|
|
516
375
|
return tsconfig;
|
|
@@ -558,6 +417,11 @@ export class Executor {
|
|
|
558
417
|
dict: { [key: string]: string } = {},
|
|
559
418
|
options: { [key: string]: unknown } = {},
|
|
560
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
|
+
);
|
|
561
425
|
if (targetPath.endsWith(".ts") || targetPath.endsWith(".tsx")) {
|
|
562
426
|
const getContent = (await import(templatePath)) as {
|
|
563
427
|
default: (
|
|
@@ -570,32 +434,20 @@ export class Executor {
|
|
|
570
434
|
if (result === null) return null;
|
|
571
435
|
const filename = typeof result === "object" ? result.filename : path.basename(targetPath).replace(".js", ".ts");
|
|
572
436
|
const content = typeof result === "object" ? result.content : result;
|
|
573
|
-
const
|
|
574
|
-
const convertedTargetPath = Object.entries(dict).reduce(
|
|
575
|
-
(path, [key, value]) => path.replace(new RegExp(`__${key}__`, "g"), value),
|
|
576
|
-
`${dirname}/${filename}`,
|
|
577
|
-
);
|
|
437
|
+
const convertedTargetPath = convertPath(`${path.dirname(targetPath)}/${filename}`);
|
|
578
438
|
this.logger.verbose(`Apply template ${templatePath} to ${convertedTargetPath}`);
|
|
579
439
|
return this.writeFile(convertedTargetPath, content, { overwrite });
|
|
580
440
|
} else if (targetPath.endsWith(".template")) {
|
|
581
441
|
const content = await FileSys.readText(templatePath);
|
|
582
|
-
const convertedTargetPath =
|
|
583
|
-
(path, [key, value]) => path.replace(new RegExp(`__${key}__`, "g"), value),
|
|
584
|
-
targetPath.slice(0, -9),
|
|
585
|
-
);
|
|
442
|
+
const convertedTargetPath = convertPath(targetPath.slice(0, -9));
|
|
586
443
|
const convertedContent = Object.entries(dict).reduce(
|
|
587
444
|
(data, [key, value]) => data.replace(new RegExp(`<%= ${key} %>`, "g"), value),
|
|
588
445
|
content,
|
|
589
446
|
);
|
|
590
447
|
this.logger.verbose(`Apply template ${templatePath} to ${convertedTargetPath}`);
|
|
591
|
-
return this.writeFile(convertedTargetPath, convertedContent, {
|
|
592
|
-
overwrite,
|
|
593
|
-
});
|
|
448
|
+
return this.writeFile(convertedTargetPath, convertedContent, { overwrite });
|
|
594
449
|
} else if (staticTemplateFileExtensions.has(path.extname(targetPath).toLowerCase())) {
|
|
595
|
-
const convertedTargetPath =
|
|
596
|
-
(path, [key, value]) => path.replace(new RegExp(`__${key}__`, "g"), value),
|
|
597
|
-
targetPath,
|
|
598
|
-
);
|
|
450
|
+
const convertedTargetPath = convertPath(targetPath);
|
|
599
451
|
const writePath = this.getPath(convertedTargetPath);
|
|
600
452
|
const dirname = path.dirname(writePath);
|
|
601
453
|
if (!(await FileSys.dirExists(dirname))) await mkdir(dirname, { recursive: true });
|
|
@@ -621,13 +473,11 @@ export class Executor {
|
|
|
621
473
|
}): Promise<FileContent[]> {
|
|
622
474
|
const templateRoot = await this.#resolveTemplateRoot();
|
|
623
475
|
const templatePath = `${templateRoot}${template ? `/${template}` : ""}`;
|
|
624
|
-
|
|
625
|
-
if ((await stat(prefixTemplatePath)).isFile()) {
|
|
626
|
-
const filename = path.basename(prefixTemplatePath);
|
|
476
|
+
if ((await stat(templatePath)).isFile()) {
|
|
627
477
|
const fileContent = await this.#applyTemplateFile(
|
|
628
478
|
{
|
|
629
|
-
templatePath
|
|
630
|
-
targetPath: path.join(basePath,
|
|
479
|
+
templatePath,
|
|
480
|
+
targetPath: path.join(basePath, path.basename(templatePath)),
|
|
631
481
|
scanInfo,
|
|
632
482
|
overwrite,
|
|
633
483
|
},
|
|
@@ -685,25 +535,13 @@ export class Executor {
|
|
|
685
535
|
options?: { [key: string]: unknown };
|
|
686
536
|
overwrite?: boolean;
|
|
687
537
|
}): Promise<FileContent[]> {
|
|
688
|
-
const
|
|
689
|
-
...(options.dict ?? {}),
|
|
690
|
-
...Object.fromEntries(
|
|
691
|
-
Object.entries(options.dict ?? {}).map(([key, value]) => [capitalize(key), capitalize(value)]),
|
|
692
|
-
),
|
|
693
|
-
};
|
|
694
|
-
const fileContents = await this._applyTemplate({ ...options, dict });
|
|
538
|
+
const fileContents = await this._applyTemplate({ ...options, dict: withCapitalizedDict(options.dict) });
|
|
695
539
|
await this.#formatAppliedTemplate(fileContents);
|
|
696
540
|
return fileContents;
|
|
697
541
|
}
|
|
698
542
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
* a model named Task and wrong for one named Zoo, and `organizeImports` fails `biome check` — so a
|
|
702
|
-
* scaffold that is not formatted on the way out is red for most model names, whatever the template says.
|
|
703
|
-
*
|
|
704
|
-
* Best-effort: `create-akan-workspace` scaffolds before `bun install`, so there is no local Biome binary
|
|
705
|
-
* and often no config above the target yet. An unformatted file is a lint fix; a failed scaffold is not.
|
|
706
|
-
*/
|
|
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.
|
|
707
545
|
async #formatAppliedTemplate(fileContents: FileContent[]) {
|
|
708
546
|
const filePaths = fileContents
|
|
709
547
|
.map((fileContent) => fileContent.filePath)
|
|
@@ -716,9 +554,7 @@ export class Executor {
|
|
|
716
554
|
this.logger.verbose(`Skipped formatting scaffolded files: ${err instanceof Error ? err.message : String(err)}`);
|
|
717
555
|
}
|
|
718
556
|
}
|
|
719
|
-
// Async so `typescript` (~65MB resident)
|
|
720
|
-
// not by every process that imports an executor. `typeCheckAsync` below runs in a subprocess and
|
|
721
|
-
// never touches this path.
|
|
557
|
+
// Async so `typescript` (~65MB resident) loads only in the commands that typecheck.
|
|
722
558
|
async getTypeChecker() {
|
|
723
559
|
const { TypeChecker } = await import("./typeChecker");
|
|
724
560
|
this.typeChecker ??= new TypeChecker(this);
|
|
@@ -784,10 +620,10 @@ export class Executor {
|
|
|
784
620
|
filePath: string,
|
|
785
621
|
{ fix = false, dryRun = false }: { fix?: boolean; dryRun?: boolean } = {},
|
|
786
622
|
): Promise<{
|
|
787
|
-
results: unknown[];
|
|
623
|
+
results: unknown[];
|
|
788
624
|
message: string;
|
|
789
|
-
errors: unknown[];
|
|
790
|
-
warnings: unknown[];
|
|
625
|
+
errors: unknown[];
|
|
626
|
+
warnings: unknown[];
|
|
791
627
|
}> {
|
|
792
628
|
const path = this.getPath(filePath);
|
|
793
629
|
const linter = this.getLinter();
|
|
@@ -814,7 +650,6 @@ export class WorkspaceExecutor extends Executor {
|
|
|
814
650
|
this.repoName = repoName;
|
|
815
651
|
}
|
|
816
652
|
|
|
817
|
-
static #execs = new Map<string, WorkspaceExecutor>();
|
|
818
653
|
static fromRoot({
|
|
819
654
|
workspaceRoot = process.cwd(),
|
|
820
655
|
repoName = resolveRepoName(workspaceRoot),
|
|
@@ -822,7 +657,7 @@ export class WorkspaceExecutor extends Executor {
|
|
|
822
657
|
workspaceRoot?: string;
|
|
823
658
|
repoName?: string;
|
|
824
659
|
} = {}) {
|
|
825
|
-
return
|
|
660
|
+
return new WorkspaceExecutor({ workspaceRoot, repoName });
|
|
826
661
|
}
|
|
827
662
|
static getBaseDevEnv(envPath?: string) {
|
|
828
663
|
// Bun auto-loads .env, so we use process.env directly
|
|
@@ -871,11 +706,9 @@ export class WorkspaceExecutor extends Executor {
|
|
|
871
706
|
return await WorkspaceInfo.fromExecutor(this);
|
|
872
707
|
}
|
|
873
708
|
async getApps() {
|
|
874
|
-
if (!(await FileSys.dirExists(`${this.workspaceRoot}/apps`))) return [];
|
|
875
709
|
return await this.#getDirHasFile(`${this.workspaceRoot}/apps`, "akan.config.ts");
|
|
876
710
|
}
|
|
877
711
|
async getLibs() {
|
|
878
|
-
if (!(await FileSys.dirExists(`${this.workspaceRoot}/libs`))) return [];
|
|
879
712
|
return await this.#getDirHasFile(`${this.workspaceRoot}/libs`, "akan.config.ts");
|
|
880
713
|
}
|
|
881
714
|
async getSyss() {
|
|
@@ -883,7 +716,6 @@ export class WorkspaceExecutor extends Executor {
|
|
|
883
716
|
return [appNames, libNames] as [string[], string[]];
|
|
884
717
|
}
|
|
885
718
|
async getPkgs() {
|
|
886
|
-
if (!(await FileSys.dirExists(`${this.workspaceRoot}/pkgs`))) return [];
|
|
887
719
|
return await this.#getDirHasFile(`${this.workspaceRoot}/pkgs`, "package.json");
|
|
888
720
|
}
|
|
889
721
|
async getExecs() {
|
|
@@ -946,13 +778,7 @@ export class WorkspaceExecutor extends Executor {
|
|
|
946
778
|
async hasChanges() {
|
|
947
779
|
return !!(await this.spawn("git", ["status", "--porcelain"])).trim();
|
|
948
780
|
}
|
|
949
|
-
/**
|
|
950
|
-
* Workspace-relative paths git knows about, sorted. `untracked` adds files that exist but are not
|
|
951
|
-
* committed yet, still honoring `.gitignore` — which is what a freshly copied library looks like.
|
|
952
|
-
*
|
|
953
|
-
* Reading the file set from git is what keeps generated barrels, the `page/**` and `public/libs`
|
|
954
|
-
* symlinks, env values and the lockfile out of it without any caller restating that list.
|
|
955
|
-
*/
|
|
781
|
+
/** Sorted workspace-relative paths; `untracked` adds uncommitted files, still honoring `.gitignore`. */
|
|
956
782
|
async listGitFiles(paths: string[], { untracked = false }: { untracked?: boolean } = {}) {
|
|
957
783
|
if (!paths.length) return [];
|
|
958
784
|
const mode = untracked ? ["--cached", "--others", "--exclude-standard"] : ["--cached"];
|
|
@@ -963,6 +789,7 @@ export class WorkspaceExecutor extends Executor {
|
|
|
963
789
|
.sort();
|
|
964
790
|
}
|
|
965
791
|
async #getDirHasFile(basePath: string, targetFilename: string) {
|
|
792
|
+
if (!(await FileSys.dirExists(basePath))) return [];
|
|
966
793
|
const AVOID_DIRS = ["node_modules", "dist", "public", "webkit"];
|
|
967
794
|
const getDirs = async (dirname: string, maxDepth = 3, results: string[] = [], prefix = "") => {
|
|
968
795
|
const dirs = await this.readdir(dirname);
|
|
@@ -970,8 +797,7 @@ export class WorkspaceExecutor extends Executor {
|
|
|
970
797
|
dirs.map(async (dir) => {
|
|
971
798
|
if (AVOID_DIRS.includes(dir)) return;
|
|
972
799
|
const dirPath = path.join(dirname, dir);
|
|
973
|
-
//*
|
|
974
|
-
//* 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.
|
|
975
801
|
if (await FileSys.dirExists(dirPath)) {
|
|
976
802
|
const hasTargetFile = await FileSys.fileExists(path.join(dirPath, targetFilename));
|
|
977
803
|
if (hasTargetFile) results.push(`${prefix}${dir}`);
|
|
@@ -981,50 +807,34 @@ export class WorkspaceExecutor extends Executor {
|
|
|
981
807
|
);
|
|
982
808
|
return results;
|
|
983
809
|
};
|
|
984
|
-
return await getDirs(basePath);
|
|
810
|
+
return (await getDirs(basePath)).sort((a, b) => a.localeCompare(b));
|
|
985
811
|
}
|
|
986
812
|
|
|
987
|
-
async
|
|
813
|
+
async #fromEverySys<T>(read: (sys: SysExecutor) => Promise<T[]>): Promise<T[]> {
|
|
988
814
|
const [appNames, libNames] = await this.getSyss();
|
|
989
|
-
const
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
).flat(),
|
|
996
|
-
];
|
|
997
|
-
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());
|
|
998
821
|
}
|
|
999
822
|
async getConstantFiles() {
|
|
1000
|
-
|
|
1001
|
-
const moduleConstantExampleFiles = [
|
|
1002
|
-
...(await Promise.all(appNames.map((appName) => AppExecutor.from(this, appName).getConstantFiles()))).flat(),
|
|
1003
|
-
...(await Promise.all(libNames.map((libName) => LibExecutor.from(this, libName).getConstantFiles()))).flat(),
|
|
1004
|
-
];
|
|
1005
|
-
return moduleConstantExampleFiles;
|
|
823
|
+
return await this.#fromEverySys((sys) => sys.getConstantFiles());
|
|
1006
824
|
}
|
|
1007
825
|
async getDictionaryFiles() {
|
|
1008
|
-
|
|
1009
|
-
const moduleDictionaryExampleFiles = [
|
|
1010
|
-
...(await Promise.all(appNames.map((appName) => AppExecutor.from(this, appName).getDictionaryFiles()))).flat(),
|
|
1011
|
-
...(await Promise.all(libNames.map((libName) => LibExecutor.from(this, libName).getDictionaryFiles()))).flat(),
|
|
1012
|
-
];
|
|
1013
|
-
return moduleDictionaryExampleFiles;
|
|
826
|
+
return await this.#fromEverySys((sys) => sys.getDictionaryFiles());
|
|
1014
827
|
}
|
|
1015
828
|
async getViewFiles() {
|
|
1016
|
-
|
|
1017
|
-
const viewExampleFiles = [
|
|
1018
|
-
...(await Promise.all(appNames.map((appName) => AppExecutor.from(this, appName).getViewsSourceCode()))).flat(),
|
|
1019
|
-
...(await Promise.all(libNames.map((libName) => LibExecutor.from(this, libName).getViewsSourceCode()))).flat(),
|
|
1020
|
-
];
|
|
1021
|
-
return viewExampleFiles;
|
|
829
|
+
return await this.#fromEverySys((sys) => sys.getViewsSourceCode());
|
|
1022
830
|
}
|
|
1023
831
|
}
|
|
1024
832
|
|
|
1025
|
-
interface
|
|
833
|
+
interface NamedExecutorOptions {
|
|
1026
834
|
workspace?: WorkspaceExecutor;
|
|
1027
835
|
name: string;
|
|
836
|
+
}
|
|
837
|
+
interface SysExecutorOptions extends NamedExecutorOptions {
|
|
1028
838
|
type: "app" | "lib";
|
|
1029
839
|
}
|
|
1030
840
|
|
|
@@ -1093,29 +903,12 @@ export class SysExecutor extends Executor {
|
|
|
1093
903
|
options: { exec: this, facet },
|
|
1094
904
|
}),
|
|
1095
905
|
),
|
|
1096
|
-
...
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
}),
|
|
1103
|
-
),
|
|
1104
|
-
...scanInfo.getServiceModules().map((model) =>
|
|
1105
|
-
this._applyTemplate({
|
|
1106
|
-
basePath: `lib/_${model}`,
|
|
1107
|
-
template: "moduleRoot",
|
|
1108
|
-
scanInfo,
|
|
1109
|
-
dict: { model, Model: capitalize(model) },
|
|
1110
|
-
}),
|
|
1111
|
-
),
|
|
1112
|
-
...scanInfo.getScalarModules().map((model) =>
|
|
1113
|
-
this._applyTemplate({
|
|
1114
|
-
basePath: `lib/__scalar/${model}`,
|
|
1115
|
-
template: "moduleRoot",
|
|
1116
|
-
scanInfo,
|
|
1117
|
-
dict: { model, Model: capitalize(model) },
|
|
1118
|
-
}),
|
|
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) } }),
|
|
1119
912
|
),
|
|
1120
913
|
];
|
|
1121
914
|
}
|
|
@@ -1137,9 +930,7 @@ export class SysExecutor extends Executor {
|
|
|
1137
930
|
: await LibInfo.fromExecutor(this as unknown as LibExecutor, {
|
|
1138
931
|
refresh,
|
|
1139
932
|
});
|
|
1140
|
-
//* `writeLib`
|
|
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.
|
|
933
|
+
//* Locked: `writeLib` rewrites dependency-lib barrels that other dev servers regenerate and watch too.
|
|
1143
934
|
if (write)
|
|
1144
935
|
await CodegenLock.run(this.workspace.workspaceRoot, `scan:${this.name}`, async () => {
|
|
1145
936
|
await Promise.all(this.#getScanTemplateTasks(scanInfo));
|
|
@@ -1156,11 +947,7 @@ export class SysExecutor extends Executor {
|
|
|
1156
947
|
this.#scanInfo = scanInfo;
|
|
1157
948
|
return scanInfo;
|
|
1158
949
|
}
|
|
1159
|
-
/**
|
|
1160
|
-
* 스코프 에이전트 색인(apps|libs/<name>/AGENTS.md) 재생성 — own + 의존 lib 레시피만 싣는다(프레임워크
|
|
1161
|
-
* 레시피는 루트 AGENTS.md 소관). scan(write) 경로에 물려 있어 sync/build/start 어디를 지나도 갱신되고,
|
|
1162
|
-
* `akan lint` 가 같은 렌더 결과와 비교해 신선도를 강제한다. 마커 밖 내용은 사용자 소유라 보존한다.
|
|
1163
|
-
*/
|
|
950
|
+
/** Rewrites only the marker block of the scoped AGENTS.md; content outside the markers belongs to the user. */
|
|
1164
951
|
async syncAgentsIndex(scanInfo?: AppInfo | LibInfo) {
|
|
1165
952
|
const info = scanInfo ?? (await this.scan({ write: false }));
|
|
1166
953
|
const scope = { type: this.type, name: this.name };
|
|
@@ -1215,69 +1002,49 @@ export class SysExecutor extends Executor {
|
|
|
1215
1002
|
}
|
|
1216
1003
|
|
|
1217
1004
|
async getDatabaseModules() {
|
|
1218
|
-
|
|
1219
|
-
.filter((name) => !name.startsWith("_") && !name.
|
|
1220
|
-
.filter((name) =>
|
|
1221
|
-
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`));
|
|
1222
1008
|
}
|
|
1223
1009
|
|
|
1224
1010
|
async getServiceModules() {
|
|
1225
|
-
|
|
1011
|
+
return (await this.readdir("lib"))
|
|
1226
1012
|
.filter((name) => name.startsWith("_") && !name.startsWith("__"))
|
|
1227
|
-
.filter((name) =>
|
|
1228
|
-
return serviceModules;
|
|
1013
|
+
.filter((name) => existsSync(`${this.cwdPath}/lib/${name}/${name.slice(1)}.service.ts`));
|
|
1229
1014
|
}
|
|
1230
1015
|
|
|
1231
1016
|
async getScalarModules() {
|
|
1232
|
-
|
|
1017
|
+
return (await this.readdir("lib/__scalar"))
|
|
1233
1018
|
.filter((name) => !name.startsWith("_"))
|
|
1234
|
-
.filter((name) =>
|
|
1235
|
-
return scalarModules;
|
|
1019
|
+
.filter((name) => existsSync(`${this.cwdPath}/lib/__scalar/${name}/${name}.constant.ts`));
|
|
1236
1020
|
}
|
|
1237
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
|
+
}
|
|
1238
1031
|
async getViewComponents() {
|
|
1239
|
-
|
|
1240
|
-
.filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts"))
|
|
1241
|
-
.filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${capitalize(name)}.View.tsx`).exists());
|
|
1242
|
-
return viewComponents;
|
|
1032
|
+
return await this.#getComponentModules("View");
|
|
1243
1033
|
}
|
|
1244
|
-
|
|
1245
1034
|
async getUnitComponents() {
|
|
1246
|
-
|
|
1247
|
-
.filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts"))
|
|
1248
|
-
.filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${capitalize(name)}.Unit.tsx`).exists());
|
|
1249
|
-
return unitComponents;
|
|
1035
|
+
return await this.#getComponentModules("Unit");
|
|
1250
1036
|
}
|
|
1251
1037
|
async getTemplateComponents() {
|
|
1252
|
-
|
|
1253
|
-
.filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts"))
|
|
1254
|
-
.filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${capitalize(name)}.Template.tsx`).exists());
|
|
1255
|
-
return templateComponents;
|
|
1038
|
+
return await this.#getComponentModules("Template");
|
|
1256
1039
|
}
|
|
1257
|
-
|
|
1258
1040
|
async getViewsSourceCode() {
|
|
1259
|
-
|
|
1260
|
-
return Promise.all(
|
|
1261
|
-
viewComponents.map((viewComponent) =>
|
|
1262
|
-
this.getLocalFile(`lib/${viewComponent}/${capitalize(viewComponent)}.View.tsx`),
|
|
1263
|
-
),
|
|
1264
|
-
);
|
|
1041
|
+
return await this.#getComponentSources("View");
|
|
1265
1042
|
}
|
|
1266
1043
|
async getUnitsSourceCode() {
|
|
1267
|
-
|
|
1268
|
-
return Promise.all(
|
|
1269
|
-
unitComponents.map((unitComponent) =>
|
|
1270
|
-
this.getLocalFile(`lib/${unitComponent}/${capitalize(unitComponent)}.Unit.tsx`),
|
|
1271
|
-
),
|
|
1272
|
-
);
|
|
1044
|
+
return await this.#getComponentSources("Unit");
|
|
1273
1045
|
}
|
|
1274
1046
|
async getTemplatesSourceCode() {
|
|
1275
|
-
|
|
1276
|
-
return Promise.all(
|
|
1277
|
-
templateComponents.map((templateComponent) =>
|
|
1278
|
-
this.getLocalFile(`lib/${templateComponent}/${capitalize(templateComponent)}.Template.tsx`),
|
|
1279
|
-
),
|
|
1280
|
-
);
|
|
1047
|
+
return await this.#getComponentSources("Template");
|
|
1281
1048
|
}
|
|
1282
1049
|
|
|
1283
1050
|
async getScalarConstantFiles() {
|
|
@@ -1292,7 +1059,9 @@ export class SysExecutor extends Executor {
|
|
|
1292
1059
|
async getScalarDictionaryFiles() {
|
|
1293
1060
|
const scalarModules = await this.getScalarModules();
|
|
1294
1061
|
return Promise.all(
|
|
1295
|
-
scalarModules.map((scalarModule) =>
|
|
1062
|
+
scalarModules.map((scalarModule) =>
|
|
1063
|
+
this.getLocalFile(`lib/__scalar/${scalarModule}/${scalarModule}.dictionary.ts`),
|
|
1064
|
+
),
|
|
1296
1065
|
);
|
|
1297
1066
|
}
|
|
1298
1067
|
|
|
@@ -1326,30 +1095,15 @@ export class SysExecutor extends Executor {
|
|
|
1326
1095
|
dict?: { [key: string]: string };
|
|
1327
1096
|
overwrite?: boolean;
|
|
1328
1097
|
}): Promise<FileContent[]> {
|
|
1329
|
-
const dict = {
|
|
1330
|
-
...(options.dict ?? {}),
|
|
1331
|
-
...Object.fromEntries(
|
|
1332
|
-
Object.entries(options.dict ?? {}).map(([key, value]) => [capitalize(key), capitalize(value)]),
|
|
1333
|
-
),
|
|
1334
|
-
};
|
|
1335
1098
|
const scanInfo = await this.scan();
|
|
1336
|
-
const fileContents = await this._applyTemplate({
|
|
1337
|
-
...options,
|
|
1338
|
-
scanInfo,
|
|
1339
|
-
dict,
|
|
1340
|
-
});
|
|
1099
|
+
const fileContents = await this._applyTemplate({ ...options, scanInfo, dict: withCapitalizedDict(options.dict) });
|
|
1341
1100
|
await this.scan();
|
|
1342
1101
|
return fileContents;
|
|
1343
1102
|
}
|
|
1344
1103
|
}
|
|
1345
1104
|
|
|
1346
|
-
interface AppExecutorOptions {
|
|
1347
|
-
workspace?: WorkspaceExecutor;
|
|
1348
|
-
name: string;
|
|
1349
|
-
}
|
|
1350
1105
|
export class AppExecutor extends SysExecutor {
|
|
1351
|
-
// `typescript`
|
|
1352
|
-
// ever imports an executor. Route validation is the sole consumer here and is already async.
|
|
1106
|
+
// Lazy: the validator pulls in `typescript` (~65MB resident).
|
|
1353
1107
|
static #routeSourceValidator: typeof import("./routeSourceValidator").RouteSourceValidator | null = null;
|
|
1354
1108
|
static async #getRouteSourceValidator() {
|
|
1355
1109
|
AppExecutor.#routeSourceValidator ??= (await import("./routeSourceValidator")).RouteSourceValidator;
|
|
@@ -1357,29 +1111,17 @@ export class AppExecutor extends SysExecutor {
|
|
|
1357
1111
|
}
|
|
1358
1112
|
dist: Executor;
|
|
1359
1113
|
override emoji = execEmoji.app;
|
|
1360
|
-
constructor({ workspace, name }:
|
|
1114
|
+
constructor({ workspace, name }: NamedExecutorOptions) {
|
|
1361
1115
|
super({ workspace, name, type: "app" });
|
|
1362
1116
|
this.dist = new Executor(`dist/${name}`, `${this.workspace.workspaceRoot}/dist/apps/${name}`);
|
|
1363
1117
|
}
|
|
1364
|
-
static #execs = new Map<string, AppExecutor>();
|
|
1365
1118
|
static from(executor: SysExecutor | WorkspaceExecutor, name: string) {
|
|
1366
|
-
|
|
1367
|
-
if (exec) return exec;
|
|
1368
|
-
else if (executor instanceof WorkspaceExecutor) return new AppExecutor({ workspace: executor, name });
|
|
1369
|
-
else return new AppExecutor({ workspace: executor.workspace, name });
|
|
1119
|
+
return new AppExecutor({ workspace: executor instanceof WorkspaceExecutor ? executor : executor.workspace, name });
|
|
1370
1120
|
}
|
|
1371
1121
|
getEnv() {
|
|
1372
1122
|
return WorkspaceExecutor.getBaseDevEnv().env;
|
|
1373
1123
|
}
|
|
1374
|
-
/**
|
|
1375
|
-
* This app's dev port, derived from its position in the sorted `apps/` listing so several apps can run
|
|
1376
|
-
* at once without colliding.
|
|
1377
|
-
*
|
|
1378
|
-
* `AKAN_DEV_PORT` pins it instead, because the derived value *moves*: the index shifts whenever any other
|
|
1379
|
-
* app directory appears or disappears, and a dev host recomputes this on every restart. So a session that
|
|
1380
|
-
* adds an app relands its dev server on a different port, and anything that reserved a port relative to
|
|
1381
|
-
* the old one is left pointing at nothing.
|
|
1382
|
-
*/
|
|
1124
|
+
/** `AKAN_DEV_PORT` pins it; the derived port moves whenever an app directory appears or disappears. */
|
|
1383
1125
|
async getDevPort() {
|
|
1384
1126
|
const pinned = Number(process.env.AKAN_DEV_PORT);
|
|
1385
1127
|
if (Number.isInteger(pinned) && pinned > 0 && pinned <= 65_535) return pinned;
|
|
@@ -1390,10 +1132,8 @@ export class AppExecutor extends SysExecutor {
|
|
|
1390
1132
|
return basePort + appIndex + portOffset;
|
|
1391
1133
|
}
|
|
1392
1134
|
getCommandEnv(env: Record<string, string> = {}): Record<string, string> {
|
|
1393
|
-
const
|
|
1394
|
-
const
|
|
1395
|
-
const PORT = (basePort + portOffset).toString();
|
|
1396
|
-
const AKAN_PUBLIC_SERVER_PORT = portOffset ? (8282 + portOffset).toString() : undefined;
|
|
1135
|
+
const { portOffset } = WorkspaceExecutor.getBaseDevEnv();
|
|
1136
|
+
const PORT = (8282 + portOffset).toString();
|
|
1397
1137
|
return {
|
|
1398
1138
|
...process.env,
|
|
1399
1139
|
AKAN_PUBLIC_APP_NAME: this.name,
|
|
@@ -1401,48 +1141,48 @@ export class AppExecutor extends SysExecutor {
|
|
|
1401
1141
|
NODE_NO_WARNINGS: "1",
|
|
1402
1142
|
PORT,
|
|
1403
1143
|
AKAN_PUBLIC_CLIENT_PORT: PORT,
|
|
1404
|
-
...(
|
|
1144
|
+
...(portOffset ? { AKAN_PUBLIC_SERVER_PORT: PORT } : {}),
|
|
1405
1145
|
...env,
|
|
1406
1146
|
};
|
|
1407
1147
|
}
|
|
1148
|
+
async getDatabaseModeEnv() {
|
|
1149
|
+
const akanConfig = await this.getConfig();
|
|
1150
|
+
return {
|
|
1151
|
+
AKAN_DATABASE_MODE: akanConfig.resolveDatabaseMode(),
|
|
1152
|
+
AKAN_DATABASE_MODES: akanConfig.database.modes.join(","),
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1408
1155
|
async prepareCommand(type: "build" | "start") {
|
|
1409
1156
|
const akanConfig = await this.getConfig();
|
|
1410
|
-
const databaseMode = process.env.AKAN_DATABASE_MODE ?? akanConfig.defaultDatabaseMode ?? "single";
|
|
1411
1157
|
const routeEnv = {
|
|
1412
1158
|
AKAN_PUBLIC_BASE_PATHS: [...akanConfig.basePaths].join(","),
|
|
1413
1159
|
AKAN_PUBLIC_API_PREFIX: akanConfig.api.prefix,
|
|
1414
1160
|
AKAN_PUBLIC_WS_PREFIX: akanConfig.api.websocketPrefix,
|
|
1415
|
-
|
|
1161
|
+
...(await this.getDatabaseModeEnv()),
|
|
1416
1162
|
};
|
|
1417
1163
|
Object.assign(process.env, routeEnv);
|
|
1418
1164
|
if (type === "build") {
|
|
1419
|
-
//* `scanSync`
|
|
1420
|
-
//* 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.
|
|
1421
1166
|
this.#excludeDevOnlyPages = true;
|
|
1422
1167
|
this.#pageKeys = null;
|
|
1423
1168
|
await this.dist.removeDir(this.dist.cwdPath);
|
|
1424
1169
|
await Promise.all([this.dist.mkdir("private"), this.dist.mkdir("public")]);
|
|
1425
|
-
//* Lib assets are symlinks
|
|
1426
|
-
//*
|
|
1427
|
-
//* `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.
|
|
1428
1172
|
await Promise.all([
|
|
1429
1173
|
this.cp("private", `${this.dist.cwdPath}/private`, { dereference: true }),
|
|
1430
1174
|
...(akanConfig.web.ssr ? [this.cp("public", `${this.dist.cwdPath}/public`, { dereference: true })] : []),
|
|
1431
1175
|
]);
|
|
1432
1176
|
} else {
|
|
1433
1177
|
await this.removeDir(".akan");
|
|
1434
|
-
//? `
|
|
1435
|
-
//? 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.
|
|
1436
1179
|
if (!akanConfig.web.ssr || !akanConfig.web.csr)
|
|
1437
1180
|
this.logger.verbose(
|
|
1438
1181
|
`akan.config.ts disables web.${!akanConfig.web.ssr ? "ssr" : "csr"}; \`akan start\` still serves it, \`akan build\` will not`,
|
|
1439
1182
|
);
|
|
1440
1183
|
}
|
|
1441
|
-
//?
|
|
1442
|
-
//?
|
|
1443
|
-
//? and a dev server that believes it is production serves from the production route cache, whose every
|
|
1444
|
-
//? route throws because a dev artifact carries no routes manifest. Written into `process.env` and not only
|
|
1445
|
-
//? 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.
|
|
1446
1186
|
if (type === "start") process.env.NODE_ENV = "development";
|
|
1447
1187
|
const devPort = type === "start" ? (await this.getDevPort()).toString() : undefined;
|
|
1448
1188
|
const env = this.getCommandEnv({
|
|
@@ -1450,18 +1190,14 @@ export class AppExecutor extends SysExecutor {
|
|
|
1450
1190
|
...routeEnv,
|
|
1451
1191
|
...(devPort ? { PORT: devPort, AKAN_PUBLIC_CLIENT_PORT: devPort, AKAN_PUBLIC_SERVER_PORT: devPort } : {}),
|
|
1452
1192
|
});
|
|
1453
|
-
// `
|
|
1454
|
-
//
|
|
1455
|
-
// which filters to AKAN_PUBLIC_*) sees AKAN_PUBLIC_APP_NAME — otherwise SSR throws
|
|
1456
|
-
// "environment variable AKAN_PUBLIC_APP_NAME is required". Only AKAN_PUBLIC_* is baked into bundles, so
|
|
1457
|
-
// this does not leak non-public env.
|
|
1458
|
-
// The port keys are this machine's dev allocation, and `define` turns an `AKAN_PUBLIC_*` into a literal the
|
|
1459
|
-
// artifact can never be run with a different value for — a baked port would outrank the `PORT` the container
|
|
1460
|
-
// is started with and send every SSR self-call to a port nothing bound.
|
|
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.
|
|
1461
1195
|
if (type === "build") {
|
|
1462
1196
|
const buildEnv = { ...env };
|
|
1463
1197
|
delete buildEnv.AKAN_PUBLIC_CLIENT_PORT;
|
|
1464
1198
|
delete buildEnv.AKAN_PUBLIC_SERVER_PORT;
|
|
1199
|
+
delete buildEnv.AKAN_PUBLIC_OPERATION_MODE;
|
|
1200
|
+
delete process.env.AKAN_PUBLIC_OPERATION_MODE;
|
|
1465
1201
|
Object.assign(process.env, buildEnv);
|
|
1466
1202
|
}
|
|
1467
1203
|
return { env };
|
|
@@ -1470,14 +1206,15 @@ export class AppExecutor extends SysExecutor {
|
|
|
1470
1206
|
getPublicEnv(...patterns: string[]) {
|
|
1471
1207
|
if (this.#publicEnv) return this.#publicEnv;
|
|
1472
1208
|
const searchPatterns = [...patterns, "AKAN_PUBLIC_*"];
|
|
1473
|
-
const regexes = searchPatterns.map(
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1209
|
+
const regexes = searchPatterns.map(
|
|
1210
|
+
(pattern) =>
|
|
1211
|
+
new RegExp(
|
|
1212
|
+
`^${pattern
|
|
1213
|
+
.split("*")
|
|
1214
|
+
.map((part) => part.replace(/[.+^${}()|[\]\\?]/g, "\\$&"))
|
|
1215
|
+
.join(".*")}$`,
|
|
1216
|
+
),
|
|
1217
|
+
);
|
|
1481
1218
|
const publicEnv: Record<string, string> = {};
|
|
1482
1219
|
for (const [k, v] of Object.entries(process.env)) {
|
|
1483
1220
|
if (typeof v !== "string") continue;
|
|
@@ -1490,14 +1227,11 @@ export class AppExecutor extends SysExecutor {
|
|
|
1490
1227
|
#akanConfig: AkanAppConfig | null = null;
|
|
1491
1228
|
override async getConfig({ refresh }: { refresh?: boolean } = {}) {
|
|
1492
1229
|
if (this.#akanConfig && !refresh) return this.#akanConfig;
|
|
1493
|
-
// A refresh means the config file may have been edited; bust the import cache so the fresh
|
|
1494
|
-
// module is evaluated instead of Bun's cached instance.
|
|
1495
1230
|
this.#akanConfig = await AkanAppConfig.from(this, { bustImportCache: refresh });
|
|
1496
1231
|
return this.#akanConfig;
|
|
1497
1232
|
}
|
|
1498
1233
|
|
|
1499
1234
|
#pageKeys: string[] | null = null;
|
|
1500
|
-
/** Set once `prepareCommand("build")` runs, so every later consumer of the route set agrees on it. */
|
|
1501
1235
|
#excludeDevOnlyPages = false;
|
|
1502
1236
|
async getPageKeys({ refresh }: { refresh?: boolean } = {}): Promise<string[]> {
|
|
1503
1237
|
if (this.#pageKeys && !refresh) return this.#pageKeys;
|
|
@@ -1507,7 +1241,9 @@ export class AppExecutor extends SysExecutor {
|
|
|
1507
1241
|
const owners = new Map<string, { absPath: string; fromLib: boolean }>();
|
|
1508
1242
|
const devOnlyKeys = new Set<string>();
|
|
1509
1243
|
const devOnlyDirs: string[] = [];
|
|
1510
|
-
|
|
1244
|
+
const pageRoots = await this.getPageRoots();
|
|
1245
|
+
const libKeyPrefixes = pageRoots.map((root) => root.keyPrefix).filter(Boolean);
|
|
1246
|
+
for (const root of pageRoots) {
|
|
1511
1247
|
if (!(await FileSys.dirExists(root.dir))) continue;
|
|
1512
1248
|
for await (const rel of glob.scan({
|
|
1513
1249
|
cwd: root.dir,
|
|
@@ -1517,6 +1253,8 @@ export class AppExecutor extends SysExecutor {
|
|
|
1517
1253
|
const segments = rel.split(path.sep);
|
|
1518
1254
|
if (segments.some((s) => s === "node_modules")) continue;
|
|
1519
1255
|
const posix = `${root.keyPrefix}${segments.join("/")}`;
|
|
1256
|
+
//* A lib folder that is a copy rather than a link (the Windows fallback) is walked into from `page` too.
|
|
1257
|
+
if (!root.keyPrefix && libKeyPrefixes.some((prefix) => posix.startsWith(prefix))) continue;
|
|
1520
1258
|
const absPath = path.join(root.dir, ...segments);
|
|
1521
1259
|
validatePageSourceFile(posix, { filePath: absPath });
|
|
1522
1260
|
if (!isRouteSourceFile(posix)) continue;
|
|
@@ -1532,8 +1270,7 @@ export class AppExecutor extends SysExecutor {
|
|
|
1532
1270
|
const fromLib = !!root.keyPrefix;
|
|
1533
1271
|
const routeId = `${parsed.kind}:${parsed.pattern}`;
|
|
1534
1272
|
const owner = owners.get(routeId);
|
|
1535
|
-
//* App-owned routes
|
|
1536
|
-
//* 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.
|
|
1537
1274
|
if (owner && (owner.fromLib || fromLib)) {
|
|
1538
1275
|
throw new Error(
|
|
1539
1276
|
`[route-convention] duplicate ${parsed.kind} route "${parsed.pattern}" in app "${this.name}":\n- ${owner.absPath}\n- ${absPath}`,
|
|
@@ -1552,8 +1289,7 @@ export class AppExecutor extends SysExecutor {
|
|
|
1552
1289
|
});
|
|
1553
1290
|
if (info.devOnly) {
|
|
1554
1291
|
devOnlyKeys.add(key);
|
|
1555
|
-
//* A
|
|
1556
|
-
//* 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.
|
|
1557
1293
|
if (parsed.kind === "layout") devOnlyDirs.push(key.replace(/[^/]+$/, ""));
|
|
1558
1294
|
}
|
|
1559
1295
|
}
|
|
@@ -1571,12 +1307,7 @@ export class AppExecutor extends SysExecutor {
|
|
|
1571
1307
|
this.verbose(`[route] excluded ${dropped.length} dev-only route file(s) from the build: ${dropped.join(", ")}`);
|
|
1572
1308
|
return pageKeys.filter((key) => !isDevOnly(key));
|
|
1573
1309
|
}
|
|
1574
|
-
/**
|
|
1575
|
-
* Every directory that contributes route files, as `page`-relative key prefixes. Lib roots are the
|
|
1576
|
-
* symlinks `syncPages` created, so route keys stay app-relative while `realDir` is what a file watcher
|
|
1577
|
-
* reports. Enumeration never crosses a symlink (neither Bun's glob nor TypeScript's `include` does),
|
|
1578
|
-
* which is why linked page folders have to be listed here instead of found by walking `page`.
|
|
1579
|
-
*/
|
|
1310
|
+
/** Lib roots are the `syncPages` symlinks, listed because neither Bun's glob nor TS `include` crosses one. */
|
|
1580
1311
|
async getPageRoots(): Promise<PageRoot[]> {
|
|
1581
1312
|
const akanConfig = await this.getConfig();
|
|
1582
1313
|
const pageDir = `${this.cwdPath}/page`;
|
|
@@ -1597,18 +1328,16 @@ export class AppExecutor extends SysExecutor {
|
|
|
1597
1328
|
}
|
|
1598
1329
|
|
|
1599
1330
|
static readonly #pageLibsDir = "(libs)";
|
|
1600
|
-
/** Where `(libs)` may live: once at the page root, or once per basePath when the app declares subRoutes. */
|
|
1601
1331
|
static #pageLibParents(basePaths: Iterable<string>): string[] {
|
|
1602
1332
|
const parents = [...basePaths].map((basePath) => `${basePath}/`);
|
|
1603
1333
|
return parents.length ? parents : [""];
|
|
1604
1334
|
}
|
|
1605
|
-
/**
|
|
1335
|
+
/** Whether the linked page set changed, which makes the app's route keys stale. */
|
|
1606
1336
|
async syncPages(libDeps: string[]): Promise<boolean> {
|
|
1607
1337
|
const akanConfig = await this.getConfig();
|
|
1608
1338
|
const parents = AppExecutor.#pageLibParents(akanConfig.basePaths);
|
|
1609
1339
|
const libs = await this.#resolvePageLibs(akanConfig.syncPageLibs, libDeps);
|
|
1610
|
-
//*
|
|
1611
|
-
//* 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.
|
|
1612
1341
|
const linked = (
|
|
1613
1342
|
await Promise.all(
|
|
1614
1343
|
parents.map(async (parent) => {
|
|
@@ -1619,7 +1348,11 @@ export class AppExecutor extends SysExecutor {
|
|
|
1619
1348
|
)
|
|
1620
1349
|
).flat();
|
|
1621
1350
|
const wanted = parents.flatMap((parent) => libs.map((lib) => `${parent}${AppExecutor.#pageLibsDir}/(${lib})/`));
|
|
1622
|
-
|
|
1351
|
+
//* A copy — the Windows fallback when a junction is refused — does not follow its lib, so only links are current.
|
|
1352
|
+
const isLink = async (key: string) =>
|
|
1353
|
+
(await lstat(`${this.cwdPath}/page/${key.slice(0, -1)}`).catch(() => null))?.isSymbolicLink() ?? false;
|
|
1354
|
+
if (linked.sort().join(",") === wanted.sort().join(",") && (await Promise.all(linked.map(isLink))).every(Boolean))
|
|
1355
|
+
return false;
|
|
1623
1356
|
await Promise.all(
|
|
1624
1357
|
parents.map((parent) => this.removeDir(`${this.cwdPath}/page/${parent}${AppExecutor.#pageLibsDir}`)),
|
|
1625
1358
|
);
|
|
@@ -1674,8 +1407,7 @@ export class AppExecutor extends SysExecutor {
|
|
|
1674
1407
|
);
|
|
1675
1408
|
}
|
|
1676
1409
|
static async #linkLibAsset(targetPath: string, linkPath: string) {
|
|
1677
|
-
//*
|
|
1678
|
-
//* 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.
|
|
1679
1411
|
const isWindows = process.platform === "win32";
|
|
1680
1412
|
try {
|
|
1681
1413
|
const target = isWindows ? targetPath : path.relative(path.dirname(linkPath), targetPath);
|
|
@@ -1699,8 +1431,6 @@ export class AppExecutor extends SysExecutor {
|
|
|
1699
1431
|
if (write) await this.#runPluginSyncAssets();
|
|
1700
1432
|
return scanInfo;
|
|
1701
1433
|
}
|
|
1702
|
-
//* build-time asset generation is delegated to plugins (e.g. the push plugin writes
|
|
1703
|
-
//* public/firebase-messaging-sw.js). The framework itself no longer knows about firebase.
|
|
1704
1434
|
async #runPluginSyncAssets() {
|
|
1705
1435
|
const plugins = await this.collectPlugins();
|
|
1706
1436
|
if (!plugins.some((plugin) => plugin.syncAssets)) return;
|
|
@@ -1714,6 +1444,7 @@ export class AppExecutor extends SysExecutor {
|
|
|
1714
1444
|
executor: this,
|
|
1715
1445
|
getPath: (rel) => this.getPath(rel),
|
|
1716
1446
|
fileExists: (rel) => FileSys.fileExists(this.getPath(rel)),
|
|
1447
|
+
readFile: (rel) => this.readFile(rel),
|
|
1717
1448
|
writeFile: async (rel, content, opts) => {
|
|
1718
1449
|
await this.writeFile(rel, content, opts);
|
|
1719
1450
|
},
|
|
@@ -1731,8 +1462,6 @@ export class AppExecutor extends SysExecutor {
|
|
|
1731
1462
|
},
|
|
1732
1463
|
};
|
|
1733
1464
|
}
|
|
1734
|
-
//* Aggregate plugins declared by this app's akan.config plus each of its lib dependencies'
|
|
1735
|
-
//* akan.config. This is how a lib (e.g. libs/util) opts an app into a feature turnkey.
|
|
1736
1465
|
async collectPlugins(): Promise<AkanPlugin[]> {
|
|
1737
1466
|
const scanInfo = (await this.scan({ write: false })) as AppInfo;
|
|
1738
1467
|
const libDeps = scanInfo.getLibs();
|
|
@@ -1769,23 +1498,15 @@ export class AppExecutor extends SysExecutor {
|
|
|
1769
1498
|
await decreaseBuildNum(this);
|
|
1770
1499
|
}
|
|
1771
1500
|
}
|
|
1772
|
-
interface LibExecutorOptions {
|
|
1773
|
-
workspace?: WorkspaceExecutor;
|
|
1774
|
-
name: string;
|
|
1775
|
-
}
|
|
1776
1501
|
export class LibExecutor extends SysExecutor {
|
|
1777
1502
|
dist: Executor;
|
|
1778
1503
|
override emoji = execEmoji.lib;
|
|
1779
|
-
constructor({ workspace, name }:
|
|
1504
|
+
constructor({ workspace, name }: NamedExecutorOptions) {
|
|
1780
1505
|
super({ workspace, name, type: "lib" });
|
|
1781
1506
|
this.dist = new Executor(`dist/${name}`, `${this.workspace.workspaceRoot}/dist/libs/${name}`);
|
|
1782
1507
|
}
|
|
1783
|
-
static #execs = new Map<string, LibExecutor>();
|
|
1784
1508
|
static from(executor: SysExecutor | WorkspaceExecutor, name: string) {
|
|
1785
|
-
|
|
1786
|
-
if (exec) return exec;
|
|
1787
|
-
else if (executor instanceof WorkspaceExecutor) return new LibExecutor({ workspace: executor, name });
|
|
1788
|
-
else return new LibExecutor({ workspace: executor.workspace, name });
|
|
1509
|
+
return new LibExecutor({ workspace: executor instanceof WorkspaceExecutor ? executor : executor.workspace, name });
|
|
1789
1510
|
}
|
|
1790
1511
|
|
|
1791
1512
|
#akanConfig: AkanLibConfig | null = null;
|
|
@@ -1796,31 +1517,25 @@ export class LibExecutor extends SysExecutor {
|
|
|
1796
1517
|
}
|
|
1797
1518
|
}
|
|
1798
1519
|
|
|
1799
|
-
interface PkgExecutorOptions {
|
|
1800
|
-
workspace?: WorkspaceExecutor;
|
|
1801
|
-
name: string;
|
|
1802
|
-
}
|
|
1803
1520
|
export class PkgExecutor extends Executor {
|
|
1804
1521
|
workspace: WorkspaceExecutor;
|
|
1805
1522
|
override name: string;
|
|
1806
1523
|
dist: Executor;
|
|
1807
1524
|
override emoji = execEmoji.pkg;
|
|
1808
|
-
constructor({ workspace = WorkspaceExecutor.fromRoot(), name }:
|
|
1525
|
+
constructor({ workspace = WorkspaceExecutor.fromRoot(), name }: NamedExecutorOptions) {
|
|
1809
1526
|
super(name, `${workspace.workspaceRoot}/pkgs/${name}`);
|
|
1810
1527
|
this.workspace = workspace;
|
|
1811
1528
|
this.name = name;
|
|
1812
1529
|
this.dist = new Executor(`dist/${name}`, `${this.workspace.workspaceRoot}/dist/pkgs/${name}`);
|
|
1813
1530
|
}
|
|
1814
1531
|
static from(executor: SysExecutor | WorkspaceExecutor, name: string) {
|
|
1815
|
-
|
|
1816
|
-
return new PkgExecutor({ workspace: executor.workspace, name });
|
|
1532
|
+
return new PkgExecutor({ workspace: executor instanceof WorkspaceExecutor ? executor : executor.workspace, name });
|
|
1817
1533
|
}
|
|
1818
1534
|
|
|
1819
1535
|
#scanInfo: PkgInfo | null = null;
|
|
1820
1536
|
async scan({ refresh }: { refresh?: boolean } = {}): Promise<PkgInfo> {
|
|
1821
1537
|
if (this.#scanInfo && !refresh) return this.#scanInfo;
|
|
1822
1538
|
const scanInfo = await PkgInfo.fromExecutor(this, { refresh });
|
|
1823
|
-
// this.writeJson("akan.pkg.json", pkgScanResult);
|
|
1824
1539
|
this.#scanInfo = scanInfo;
|
|
1825
1540
|
return scanInfo;
|
|
1826
1541
|
}
|
|
@@ -1831,6 +1546,9 @@ export class PkgExecutor extends Executor {
|
|
|
1831
1546
|
};
|
|
1832
1547
|
const rootVersion = rootDeps[dep];
|
|
1833
1548
|
if (rootVersion) return rootVersion;
|
|
1549
|
+
// Overrides never reach a consumer's install, so a pinned transitive dep is published as a dependency.
|
|
1550
|
+
const overrideVersion = rootPackageJson.overrides?.[dep];
|
|
1551
|
+
if (typeof overrideVersion === "string") return overrideVersion;
|
|
1834
1552
|
|
|
1835
1553
|
try {
|
|
1836
1554
|
const packageJsonPath = `pkgs/${dep}/package.json`;
|
|
@@ -1858,17 +1576,9 @@ export class PkgExecutor extends Executor {
|
|
|
1858
1576
|
if (missingDeps.length > 0)
|
|
1859
1577
|
throw new Error(`Missing dependency versions in root package.json: ${missingDeps.join(", ")}`);
|
|
1860
1578
|
|
|
1861
|
-
const
|
|
1862
|
-
names.map((dep) =>
|
|
1863
|
-
|
|
1864
|
-
if (!version) throw new Error(`Missing dependency versions in root package.json: ${dep}`);
|
|
1865
|
-
return [dep, version] as const;
|
|
1866
|
-
});
|
|
1867
|
-
|
|
1868
|
-
return {
|
|
1869
|
-
dependencies: Object.fromEntries(toDependencyEntries(dependencyNames)),
|
|
1870
|
-
devDependencies: Object.fromEntries(toDependencyEntries(devDependencyNames)),
|
|
1871
|
-
};
|
|
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) };
|
|
1872
1582
|
}
|
|
1873
1583
|
async updatePackageJsonDependencies(
|
|
1874
1584
|
dependencies: string[] = [],
|