@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
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* WCAG contrast checking over the semantic token pairs. No dependencies, pure functions, and no akanjs
|
|
3
|
-
* runtime import — it has to run inside the lint path.
|
|
4
|
-
*
|
|
5
|
-
* The `--x` / `--x-foreground` pairing is what makes the check possible at all: it says which two values
|
|
6
|
-
* are going to end up on top of each other. That is the guard against a generated palette shipping a site
|
|
7
|
-
* nobody can read.
|
|
8
|
-
*
|
|
9
|
-
* Thresholds (WCAG 2.1):
|
|
10
|
-
* - body and primary surfaces (background, primary, secondary, accent, neutral, card, popover): 4.5:1,
|
|
11
|
-
* the AA floor for normal text
|
|
12
|
-
* - status and secondary pairs (info, success, warning, destructive, open, muted): 3:1, the AA floor for
|
|
13
|
-
* UI components and large text
|
|
14
|
-
* The shipped light/dark palette in styles.css clears all of them.
|
|
15
|
-
*/
|
|
1
|
+
// No akanjs runtime import: this runs inside the lint path.
|
|
16
2
|
|
|
17
3
|
export interface ThemeContrastViolation {
|
|
18
4
|
scope: string;
|
|
@@ -30,6 +16,7 @@ interface PairDef {
|
|
|
30
16
|
threshold: number;
|
|
31
17
|
}
|
|
32
18
|
|
|
19
|
+
// WCAG 2.1 AA: 4.5:1 is the floor for normal text, 3:1 for UI components and large text.
|
|
33
20
|
const PAIRS: PairDef[] = [
|
|
34
21
|
{ base: "background", fg: "foreground", threshold: 4.5 },
|
|
35
22
|
{ base: "primary", fg: "primary-foreground", threshold: 4.5 },
|
|
@@ -46,13 +33,11 @@ const PAIRS: PairDef[] = [
|
|
|
46
33
|
{ base: "open", fg: "open-foreground", threshold: 3 },
|
|
47
34
|
];
|
|
48
35
|
|
|
49
|
-
// Only these scopes are paired up; tokens scoped to something else (`.campaign-x`, …) are left alone.
|
|
50
36
|
const THEME_SCOPES = new Set([":root", '[data-theme="dark"]', '[data-theme="light"]']);
|
|
51
37
|
|
|
52
38
|
export type ThemeTokensByScope = Record<string, Record<string, string>>;
|
|
53
39
|
|
|
54
40
|
export class ThemeValidator {
|
|
55
|
-
/** Extracts tokens from the css text and checks every scope it recognizes. */
|
|
56
41
|
validate(css: string): ThemeContrastViolation[] {
|
|
57
42
|
const tokensByScope = ThemeValidator.parseThemeTokens(css);
|
|
58
43
|
const violations: ThemeContrastViolation[] = [];
|
|
@@ -71,7 +56,7 @@ export class ThemeValidator {
|
|
|
71
56
|
if (!bg || !front) continue;
|
|
72
57
|
const bgRgb = ThemeValidator.parseHex(bg);
|
|
73
58
|
const fgRgb = ThemeValidator.parseHex(front);
|
|
74
|
-
if (!bgRgb || !fgRgb) continue;
|
|
59
|
+
if (!bgRgb || !fgRgb) continue;
|
|
75
60
|
const ratio = ThemeValidator.contrastRatio(bgRgb, fgRgb);
|
|
76
61
|
if (ratio >= threshold) continue;
|
|
77
62
|
violations.push({
|
|
@@ -87,15 +72,9 @@ export class ThemeValidator {
|
|
|
87
72
|
return violations;
|
|
88
73
|
}
|
|
89
74
|
|
|
90
|
-
/**
|
|
91
|
-
* Reads `--token: value` out of `:root` / `[data-theme="…"]` blocks. A grouped selector
|
|
92
|
-
* (`:root, [data-theme="dark"] { … }`) distributes the same tokens to each selector, and a scope that
|
|
93
|
-
* appears twice keeps the later value — so passing framework css first and the app's second reflects the
|
|
94
|
-
* app's overrides.
|
|
95
|
-
*/
|
|
75
|
+
/** A scope declared twice keeps the later value, so pass the framework css before the app's. */
|
|
96
76
|
static parseThemeTokens(css: string): ThemeTokensByScope {
|
|
97
77
|
const result: ThemeTokensByScope = {};
|
|
98
|
-
// Flat rule blocks only; an at-rule (@theme, @keyframes) carries `@` in the selector and is skipped.
|
|
99
78
|
const blockRe = /(?:^|})\s*([^{}@]+?)\s*\{([^{}]*)\}/g;
|
|
100
79
|
for (const block of css.matchAll(blockRe)) {
|
|
101
80
|
const selectors = block[1].split(",").map((s) => s.trim());
|
|
@@ -114,7 +93,6 @@ export class ThemeValidator {
|
|
|
114
93
|
}
|
|
115
94
|
|
|
116
95
|
static #normalizeScope(selector: string): string {
|
|
117
|
-
// Quote normalization: [data-theme=dark] / [data-theme='dark'] -> [data-theme="dark"]
|
|
118
96
|
return selector.replace(/\[data-theme=['"]?([\w-]+)['"]?\]/g, '[data-theme="$1"]').trim();
|
|
119
97
|
}
|
|
120
98
|
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir,
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import type { App } from "../commandDecorators";
|
|
5
|
+
import { tempDirs } from "../testHelpers";
|
|
6
6
|
import { WatchRootResolver } from "./watchRootResolver";
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const tempRoot = tempDirs("akan-watch-roots-");
|
|
9
9
|
|
|
10
10
|
const makeWorkspace = async (dirs: string[]) => {
|
|
11
|
-
const root = await
|
|
12
|
-
roots.push(root);
|
|
11
|
+
const root = await tempRoot();
|
|
13
12
|
await Promise.all(dirs.map((dir) => mkdir(path.join(root, dir), { recursive: true })));
|
|
14
13
|
return root;
|
|
15
14
|
};
|
|
@@ -25,24 +24,18 @@ interface AppStub {
|
|
|
25
24
|
libDeps?: string[] | null;
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
const
|
|
29
|
-
({
|
|
30
|
-
cwdPath: path.join(workspaceRoot, "apps",
|
|
27
|
+
const resolveRoots = (workspaceRoot: string, { paths = {}, libDeps = null }: AppStub = {}) =>
|
|
28
|
+
new WatchRootResolver({
|
|
29
|
+
cwdPath: path.join(workspaceRoot, "apps", "app1"),
|
|
31
30
|
workspace: { workspaceRoot },
|
|
32
31
|
getTsConfig: async () => ({ compilerOptions: { paths } }),
|
|
33
32
|
getScanInfo: () => (libDeps ? { type: "app", libDeps } : null),
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
afterEach(async () => {
|
|
37
|
-
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
38
|
-
});
|
|
33
|
+
} as unknown as App).resolve();
|
|
39
34
|
|
|
40
35
|
describe("WatchRootResolver", () => {
|
|
41
36
|
test("narrows the apps container to the app being served", async () => {
|
|
42
37
|
const workspaceRoot = await makeWorkspace(["apps/app1/page", "apps/app2/page", "libs/util"]);
|
|
43
|
-
const resolved = await
|
|
44
|
-
makeApp(workspaceRoot, "app1", { paths: { "@apps/*": ["./apps/*"], "@libs/*": ["./libs/*"] } }),
|
|
45
|
-
).resolve();
|
|
38
|
+
const resolved = await resolveRoots(workspaceRoot, { paths: { "@apps/*": ["./apps/*"], "@libs/*": ["./libs/*"] } });
|
|
46
39
|
|
|
47
40
|
expect(resolved).toContain(path.join(workspaceRoot, "apps/app1"));
|
|
48
41
|
expect(resolved).not.toContain(path.join(workspaceRoot, "apps"));
|
|
@@ -51,9 +44,7 @@ describe("WatchRootResolver", () => {
|
|
|
51
44
|
|
|
52
45
|
test("narrows the libs container to the app's own dependencies", async () => {
|
|
53
46
|
const workspaceRoot = await makeWorkspace(["apps/app1/page", "libs/util", "libs/shared"]);
|
|
54
|
-
const resolved = await
|
|
55
|
-
makeApp(workspaceRoot, "app1", { paths: { "@libs/*": ["./libs/*"] }, libDeps: ["util"] }),
|
|
56
|
-
).resolve();
|
|
47
|
+
const resolved = await resolveRoots(workspaceRoot, { paths: { "@libs/*": ["./libs/*"] }, libDeps: ["util"] });
|
|
57
48
|
|
|
58
49
|
expect(resolved).toContain(path.join(workspaceRoot, "libs/util"));
|
|
59
50
|
expect(resolved).not.toContain(path.join(workspaceRoot, "libs"));
|
|
@@ -63,9 +54,7 @@ describe("WatchRootResolver", () => {
|
|
|
63
54
|
test("drops the libs container entirely when the app depends on no lib", async () => {
|
|
64
55
|
const workspaceRoot = await makeWorkspace(["apps/app1/page", "libs/util"]);
|
|
65
56
|
await writeManifest(workspaceRoot, "apps/app1", []);
|
|
66
|
-
const resolved = await
|
|
67
|
-
makeApp(workspaceRoot, "app1", { paths: { "@libs/*": ["./libs/*"] } }),
|
|
68
|
-
).resolve();
|
|
57
|
+
const resolved = await resolveRoots(workspaceRoot, { paths: { "@libs/*": ["./libs/*"] } });
|
|
69
58
|
|
|
70
59
|
expect(resolved).toEqual([path.join(workspaceRoot, "apps/app1/page")]);
|
|
71
60
|
});
|
|
@@ -76,9 +65,7 @@ describe("WatchRootResolver", () => {
|
|
|
76
65
|
await writeManifest(workspaceRoot, "libs/shared", ["util"]);
|
|
77
66
|
await writeManifest(workspaceRoot, "libs/util", []);
|
|
78
67
|
await writeManifest(workspaceRoot, "libs/unused", []);
|
|
79
|
-
const resolved = await
|
|
80
|
-
makeApp(workspaceRoot, "app1", { paths: { "@libs/*": ["./libs/*"] } }),
|
|
81
|
-
).resolve();
|
|
68
|
+
const resolved = await resolveRoots(workspaceRoot, { paths: { "@libs/*": ["./libs/*"] } });
|
|
82
69
|
|
|
83
70
|
expect(resolved).toContain(path.join(workspaceRoot, "libs/shared"));
|
|
84
71
|
expect(resolved).toContain(path.join(workspaceRoot, "libs/util"));
|
|
@@ -87,9 +74,7 @@ describe("WatchRootResolver", () => {
|
|
|
87
74
|
|
|
88
75
|
test("keeps the libs container whole when the app manifest is missing", async () => {
|
|
89
76
|
const workspaceRoot = await makeWorkspace(["apps/app1/page", "libs/util", "libs/shared"]);
|
|
90
|
-
const resolved = await
|
|
91
|
-
makeApp(workspaceRoot, "app1", { paths: { "@libs/*": ["./libs/*"] } }),
|
|
92
|
-
).resolve();
|
|
77
|
+
const resolved = await resolveRoots(workspaceRoot, { paths: { "@libs/*": ["./libs/*"] } });
|
|
93
78
|
|
|
94
79
|
expect(resolved).toContain(path.join(workspaceRoot, "libs"));
|
|
95
80
|
});
|
|
@@ -97,24 +82,20 @@ describe("WatchRootResolver", () => {
|
|
|
97
82
|
test("keeps the libs container whole when a dependency lib is unsynced", async () => {
|
|
98
83
|
const workspaceRoot = await makeWorkspace(["apps/app1/page", "libs/util", "libs/shared"]);
|
|
99
84
|
await writeManifest(workspaceRoot, "apps/app1", ["shared"]);
|
|
100
|
-
const resolved = await
|
|
101
|
-
makeApp(workspaceRoot, "app1", { paths: { "@libs/*": ["./libs/*"] } }),
|
|
102
|
-
).resolve();
|
|
85
|
+
const resolved = await resolveRoots(workspaceRoot, { paths: { "@libs/*": ["./libs/*"] } });
|
|
103
86
|
|
|
104
87
|
expect(resolved).toContain(path.join(workspaceRoot, "libs"));
|
|
105
88
|
});
|
|
106
89
|
|
|
107
90
|
test("resolves a package alias to its own directory, filename and glob stripped", async () => {
|
|
108
91
|
const workspaceRoot = await makeWorkspace(["apps/app1/page", "pkgs/akanjs/base"]);
|
|
109
|
-
const resolved = await
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}),
|
|
117
|
-
).resolve();
|
|
92
|
+
const resolved = await resolveRoots(workspaceRoot, {
|
|
93
|
+
paths: {
|
|
94
|
+
akanjs: ["./pkgs/akanjs/index.ts"],
|
|
95
|
+
"akanjs/*": ["./pkgs/akanjs/*"],
|
|
96
|
+
missing: ["./pkgs/nothing/index.ts"],
|
|
97
|
+
},
|
|
98
|
+
});
|
|
118
99
|
|
|
119
100
|
expect(resolved).toContain(path.join(workspaceRoot, "pkgs/akanjs"));
|
|
120
101
|
expect(resolved).not.toContain(path.join(workspaceRoot, "pkgs/nothing"));
|
|
@@ -122,7 +103,7 @@ describe("WatchRootResolver", () => {
|
|
|
122
103
|
|
|
123
104
|
test("always watches the app's page tree", async () => {
|
|
124
105
|
const workspaceRoot = await makeWorkspace(["apps/app1/page"]);
|
|
125
|
-
const resolved = await
|
|
106
|
+
const resolved = await resolveRoots(workspaceRoot);
|
|
126
107
|
|
|
127
108
|
expect(resolved).toEqual([path.join(workspaceRoot, "apps/app1/page")]);
|
|
128
109
|
});
|
|
@@ -3,23 +3,7 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import type { App } from "../commandDecorators";
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* The directories one app's dev watcher follows, resolved from its tsconfig `paths`.
|
|
8
|
-
*
|
|
9
|
-
* Two aliases strip down to a workspace container rather than a package, and taking either verbatim puts
|
|
10
|
-
* code the app never imports under the watcher — where a save rebuilds, restarts and reloads it for
|
|
11
|
-
* nothing, and both builders rewrite the same generated barrels:
|
|
12
|
-
*
|
|
13
|
-
* - `@apps/*` becomes the `apps/` container. Apps are leaves of the workspace graph — never one another's
|
|
14
|
-
* dependencies — so it is replaced by this app's own directory.
|
|
15
|
-
* - `@libs/*` becomes the `libs/` container. A lib is a dependency only if the app reaches it, so it is
|
|
16
|
-
* replaced by the app's own lib dependencies, transitively. The set is resolved once, at watcher
|
|
17
|
-
* install: a lib that becomes a dependency mid-session needs a fresh `akan start` anyway, because the
|
|
18
|
-
* import that made it one also needs a `sync`.
|
|
19
|
-
*
|
|
20
|
-
* Failing to resolve that set keeps the container whole, because a wrong narrowing is a dev server that
|
|
21
|
-
* silently ignores edits.
|
|
22
|
-
*/
|
|
6
|
+
/** Watch roots from tsconfig paths: `@apps/*` narrows to this app, `@libs/*` to its lib deps (whole when unknown). */
|
|
23
7
|
export class WatchRootResolver {
|
|
24
8
|
#app: App;
|
|
25
9
|
|
|
@@ -32,14 +16,15 @@ export class WatchRootResolver {
|
|
|
32
16
|
const appDir = path.resolve(this.#app.cwdPath);
|
|
33
17
|
const appsContainer = path.dirname(appDir);
|
|
34
18
|
const libsContainer = path.resolve(this.#app.workspace.workspaceRoot, "libs");
|
|
35
|
-
const libRoots = await this.#
|
|
19
|
+
const libRoots = (await this.#resolveLibDeps(libsContainer))
|
|
20
|
+
?.map((name) => path.join(libsContainer, name))
|
|
21
|
+
.filter((dir) => fs.existsSync(dir));
|
|
36
22
|
const set = new Set<string>();
|
|
37
23
|
set.add(path.resolve(`${this.#app.cwdPath}/page`));
|
|
38
24
|
for (const targets of Object.values(tsconfig.compilerOptions.paths ?? {})) {
|
|
39
25
|
for (const target of targets) {
|
|
40
26
|
if (!target) continue;
|
|
41
27
|
if (path.isAbsolute(target)) continue;
|
|
42
|
-
// Strip the trailing filename and glob so we watch the package root dir.
|
|
43
28
|
const cleaned = target.replace(/\/?\*+.*$/, "").replace(/\/[^/]+\.[^/]+$/, "");
|
|
44
29
|
const resolved = path.resolve(this.#app.workspace.workspaceRoot, cleaned);
|
|
45
30
|
if (resolved === libsContainer && libRoots) {
|
|
@@ -53,16 +38,9 @@ export class WatchRootResolver {
|
|
|
53
38
|
return [...set];
|
|
54
39
|
}
|
|
55
40
|
|
|
56
|
-
async #resolveLibRoots(libsContainer: string): Promise<string[] | null> {
|
|
57
|
-
const libDeps = await this.#resolveLibDeps(libsContainer);
|
|
58
|
-
if (!libDeps) return null;
|
|
59
|
-
return libDeps.map((name) => path.join(libsContainer, name)).filter((dir) => fs.existsSync(dir));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
41
|
async #resolveLibDeps(libsContainer: string): Promise<string[] | null> {
|
|
63
42
|
const scanInfo = this.#app.getScanInfo({ allowEmpty: true });
|
|
64
|
-
//
|
|
65
|
-
// idle watcher run in processes that did not, so they take the manifests `scan` wrote instead.
|
|
43
|
+
// Transitive, but only in a process that scanned; the builder and idle watcher read the synced manifests.
|
|
66
44
|
if (scanInfo?.type === "app") return scanInfo.libDeps;
|
|
67
45
|
const direct = await WatchRootResolver.#readManifestLibDeps(path.join(this.#app.cwdPath, "akan.app.json"));
|
|
68
46
|
if (!direct) return null;
|
package/getRelatedCnsts.ts
CHANGED
|
@@ -40,9 +40,6 @@ const scanModuleSpecifiers = (source: string, filePath: string, includeExports:
|
|
|
40
40
|
return importSpecifiers;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
/**
|
|
44
|
-
* TypeScript 설정 파일을 읽고 파싱하는 함수
|
|
45
|
-
*/
|
|
46
43
|
export const parseTsConfig = (tsConfigPath: string = "./tsconfig.json") => {
|
|
47
44
|
const configFile = ts.readConfigFile(tsConfigPath, (path) => {
|
|
48
45
|
return ts.sys.readFile(path);
|
|
@@ -51,85 +48,36 @@ export const parseTsConfig = (tsConfigPath: string = "./tsconfig.json") => {
|
|
|
51
48
|
return ts.parseJsonConfigFileContent(configFile.config, ts.sys, realpathSync(tsConfigPath).replace(/[^/\\]+$/, ""));
|
|
52
49
|
};
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
* 파일 내의 임포트를 기반으로 관련 파일을 수집하는 함수
|
|
56
|
-
*/
|
|
57
|
-
export const collectImportedFiles = (constantFilePath: string, parsedConfig: ts.ParsedCommandLine) => {
|
|
51
|
+
const collectFiles = (constantFilePath: string, parsedConfig: ts.ParsedCommandLine, includeExports: boolean) => {
|
|
58
52
|
const allFilesToAnalyze = new Set<string>([constantFilePath]);
|
|
59
53
|
const analyzedFiles = new Set<string>();
|
|
60
|
-
const spinner = ora("Collecting related files...");
|
|
54
|
+
const spinner = ora(includeExports ? "Collecting files from exports..." : "Collecting related files...");
|
|
61
55
|
spinner.start();
|
|
62
|
-
|
|
63
|
-
function collectImported(filePath: string) {
|
|
56
|
+
const collect = (filePath: string) => {
|
|
64
57
|
if (analyzedFiles.has(filePath)) return;
|
|
65
58
|
analyzedFiles.add(filePath);
|
|
66
|
-
|
|
67
59
|
const source = readFileSync(filePath, "utf-8");
|
|
68
|
-
for (const importPath of scanModuleSpecifiers(source, filePath,
|
|
60
|
+
for (const importPath of scanModuleSpecifiers(source, filePath, includeExports)) {
|
|
69
61
|
if (!importPath.startsWith(".")) continue;
|
|
70
|
-
|
|
71
62
|
const resolved = ts.resolveModuleName(importPath, filePath, parsedConfig.options, ts.sys).resolvedModule
|
|
72
63
|
?.resolvedFileName;
|
|
73
|
-
|
|
74
64
|
if (resolved && !allFilesToAnalyze.has(resolved)) {
|
|
75
65
|
allFilesToAnalyze.add(resolved);
|
|
76
|
-
|
|
77
|
-
collectImported(resolved);
|
|
66
|
+
collect(resolved);
|
|
78
67
|
}
|
|
79
68
|
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// 임포트 기반 관련 파일 수집 실행
|
|
83
|
-
collectImported(constantFilePath);
|
|
84
|
-
spinner.succeed(`Found ${allFilesToAnalyze.size} related files.`);
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
allFilesToAnalyze,
|
|
88
|
-
analyzedFiles,
|
|
89
69
|
};
|
|
70
|
+
collect(constantFilePath);
|
|
71
|
+
spinner.succeed(`Found ${allFilesToAnalyze.size} related files${includeExports ? " from exports" : ""}.`);
|
|
72
|
+
return { allFilesToAnalyze, analyzedFiles };
|
|
90
73
|
};
|
|
91
74
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
*/
|
|
95
|
-
export const collectExportedFiles = (constantFilePath: string, parsedConfig: ts.ParsedCommandLine) => {
|
|
96
|
-
const allFilesToAnalyze = new Set<string>([constantFilePath]);
|
|
97
|
-
const analyzedFiles = new Set<string>();
|
|
98
|
-
const spinner = ora("Collecting files from exports...");
|
|
99
|
-
spinner.start();
|
|
100
|
-
|
|
101
|
-
function collectExported(filePath: string) {
|
|
102
|
-
if (analyzedFiles.has(filePath)) return;
|
|
103
|
-
analyzedFiles.add(filePath);
|
|
75
|
+
export const collectImportedFiles = (constantFilePath: string, parsedConfig: ts.ParsedCommandLine) =>
|
|
76
|
+
collectFiles(constantFilePath, parsedConfig, false);
|
|
104
77
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (!importPath.startsWith(".")) continue;
|
|
78
|
+
export const collectExportedFiles = (constantFilePath: string, parsedConfig: ts.ParsedCommandLine) =>
|
|
79
|
+
collectFiles(constantFilePath, parsedConfig, true);
|
|
108
80
|
|
|
109
|
-
const resolved = ts.resolveModuleName(importPath, filePath, parsedConfig.options, ts.sys).resolvedModule
|
|
110
|
-
?.resolvedFileName;
|
|
111
|
-
|
|
112
|
-
if (resolved && !allFilesToAnalyze.has(resolved)) {
|
|
113
|
-
allFilesToAnalyze.add(resolved);
|
|
114
|
-
// 재귀적으로 export/import 연결 파일도 확인
|
|
115
|
-
collectExported(resolved);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// export 기반 관련 파일 수집 실행
|
|
121
|
-
collectExported(constantFilePath);
|
|
122
|
-
spinner.succeed(`Found ${allFilesToAnalyze.size} related files from exports.`);
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
allFilesToAnalyze,
|
|
126
|
-
analyzedFiles,
|
|
127
|
-
};
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* TypeScript 프로그램 생성 함수
|
|
132
|
-
*/
|
|
133
81
|
export const createTsProgram = (filePaths: Set<string>, options: ts.CompilerOptions) => {
|
|
134
82
|
const spinner = ora("Creating TypeScript program for all files...");
|
|
135
83
|
spinner.start();
|
|
@@ -145,9 +93,6 @@ export const createTsProgram = (filePaths: Set<string>, options: ts.CompilerOpti
|
|
|
145
93
|
};
|
|
146
94
|
};
|
|
147
95
|
|
|
148
|
-
/**
|
|
149
|
-
* 심볼 캐싱 및 조회 함수 생성기
|
|
150
|
-
*/
|
|
151
96
|
export const createSymbolCache = (checker: ts.TypeChecker) => {
|
|
152
97
|
const symbolCache = new Map<string, ts.Symbol | undefined>();
|
|
153
98
|
|
|
@@ -162,9 +107,6 @@ export const createSymbolCache = (checker: ts.TypeChecker) => {
|
|
|
162
107
|
};
|
|
163
108
|
};
|
|
164
109
|
|
|
165
|
-
/**
|
|
166
|
-
* 파일 속성 분석 함수
|
|
167
|
-
*/
|
|
168
110
|
export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Program, checker: ts.TypeChecker) => {
|
|
169
111
|
const propertyMap = new Map<
|
|
170
112
|
string,
|
|
@@ -185,7 +127,6 @@ export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Progr
|
|
|
185
127
|
const spinner = ora("Analyzing property relationships...");
|
|
186
128
|
spinner.start();
|
|
187
129
|
|
|
188
|
-
// 파일 내 속성 분석 함수
|
|
189
130
|
function analyzeFileProperties(filePath: string) {
|
|
190
131
|
if (analyzedFiles.has(filePath)) return;
|
|
191
132
|
analyzedFiles.add(filePath);
|
|
@@ -193,23 +134,19 @@ export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Progr
|
|
|
193
134
|
const source = program.getSourceFile(filePath);
|
|
194
135
|
if (!source) return;
|
|
195
136
|
|
|
196
|
-
// 소스 라인 캐싱 (한 번만 분할)
|
|
197
137
|
if (!sourceLineCache.has(filePath)) {
|
|
198
138
|
sourceLineCache.set(filePath, source.getFullText().split("\n"));
|
|
199
139
|
}
|
|
200
140
|
const sourceLines = sourceLineCache.get(filePath);
|
|
201
141
|
|
|
202
|
-
// 속성 분석 함수
|
|
203
142
|
function visit(node: ts.Node) {
|
|
204
143
|
if (!source) return;
|
|
205
144
|
|
|
206
|
-
// 프로퍼티 접근 표현식 처리
|
|
207
145
|
if (ts.isPropertyAccessExpression(node)) {
|
|
208
146
|
const left = node.expression;
|
|
209
147
|
const right = node.name;
|
|
210
148
|
const { line } = ts.getLineAndCharacterOfPosition(source, node.getStart());
|
|
211
149
|
|
|
212
|
-
// 패턴 검색 로직 - 기존 문자열 기반 검색 유지
|
|
213
150
|
if (
|
|
214
151
|
ts.isIdentifier(left) &&
|
|
215
152
|
sourceLines &&
|
|
@@ -219,14 +156,12 @@ export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Progr
|
|
|
219
156
|
(sourceLines[line]?.includes(`@Field.Prop(() => ${left.text}.${right.text}`) ||
|
|
220
157
|
sourceLines[line].includes(`base.Filter(${left.text}.${right.text},`))
|
|
221
158
|
) {
|
|
222
|
-
// 캐싱된 심볼 사용
|
|
223
159
|
const symbol = getCachedSymbol(right);
|
|
224
160
|
|
|
225
161
|
if (symbol?.declarations && symbol.declarations.length > 0) {
|
|
226
162
|
const key = symbol.declarations[0]?.getSourceFile().fileName.split("/").pop()?.split(".")[0] ?? "";
|
|
227
163
|
const property = propertyMap.get(key);
|
|
228
164
|
const isScalar = symbol.declarations[0]?.getSourceFile().fileName.includes("_") ?? false;
|
|
229
|
-
// 현재 작업 중인 디렉토리 이전은 제거
|
|
230
165
|
const symbolFilePath = symbol.declarations[0]
|
|
231
166
|
?.getSourceFile()
|
|
232
167
|
.fileName.replace(`${ts.sys.getCurrentDirectory()}/`, "");
|
|
@@ -254,7 +189,6 @@ export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Progr
|
|
|
254
189
|
}
|
|
255
190
|
}
|
|
256
191
|
} else if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
257
|
-
// 모듈 선언 표현식 처리
|
|
258
192
|
const importPath = node.moduleSpecifier.text;
|
|
259
193
|
|
|
260
194
|
if (importPath.startsWith(".")) {
|
|
@@ -276,15 +210,12 @@ export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Progr
|
|
|
276
210
|
}
|
|
277
211
|
}
|
|
278
212
|
|
|
279
|
-
// 모든 자식 노드 처리
|
|
280
213
|
ts.forEachChild(node, visit);
|
|
281
214
|
}
|
|
282
215
|
|
|
283
|
-
// 파일 분석 시작
|
|
284
216
|
visit(source);
|
|
285
217
|
}
|
|
286
218
|
|
|
287
|
-
// 모든 파일의 속성 관계 분석
|
|
288
219
|
for (const filePath of filesToAnalyze) {
|
|
289
220
|
analyzeFileProperties(filePath);
|
|
290
221
|
}
|
|
@@ -294,21 +225,10 @@ export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Progr
|
|
|
294
225
|
return propertyMap;
|
|
295
226
|
};
|
|
296
227
|
|
|
297
|
-
/**
|
|
298
|
-
* 메인 함수: 상수 파일 관련 요소 분석
|
|
299
|
-
*/
|
|
300
228
|
export const getRelatedCnsts = (constantFilePath: string) => {
|
|
301
|
-
// 1. TypeScript 설정 파일 읽기 및 파싱
|
|
302
229
|
const parsedConfig = parseTsConfig();
|
|
303
|
-
|
|
304
|
-
// 2. 상수 파일 관련 파일 수집 (임포트 기반)
|
|
305
230
|
const { allFilesToAnalyze } = collectImportedFiles(constantFilePath, parsedConfig);
|
|
306
|
-
// 3. TypeScript 프로그램 생성 및 타입 체커 설정
|
|
307
231
|
const { program, checker } = createTsProgram(allFilesToAnalyze, parsedConfig.options);
|
|
308
|
-
|
|
309
|
-
// 4. 파일 속성 분석
|
|
310
232
|
const propertyMap = analyzeProperties(allFilesToAnalyze, program, checker);
|
|
311
|
-
|
|
312
|
-
// 5. 결과 반환
|
|
313
233
|
return Array.from(propertyMap.entries()).map(([key, value]) => ({ key, ...value }));
|
|
314
234
|
};
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { ApplicationBuildReporter } from "@akanjs/devkit/applicationBuildReporter";
|
|
2
3
|
import type { App } from "@akanjs/devkit/commandDecorators";
|
|
3
|
-
// Subpath imports only,
|
|
4
|
-
// eager import is paid on every save. Measured on a 177-route app: `executors` 24ms, `frontendBuild`
|
|
5
|
-
// ~110ms, app config 5ms.
|
|
4
|
+
// Subpath imports only, as few as possible: spawned per generation, this process pays every import on every save.
|
|
6
5
|
import { AppExecutor, WorkspaceExecutor } from "@akanjs/devkit/executors";
|
|
7
6
|
import {
|
|
8
7
|
CsrArtifactBuilder,
|
|
@@ -15,19 +14,7 @@ import { Logger } from "akanjs/common";
|
|
|
15
14
|
import type { BuilderMessage, BuildPhase } from "akanjs/server";
|
|
16
15
|
import type { BuildBatchRequest, BuildBatchResult, OptimizedFonts, PagesBatchCssAssets } from "./buildBatchProtocol";
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
* One generation of frontend build work, in a process that exits when it is done.
|
|
20
|
-
*
|
|
21
|
-
* This exists for one reason: `Bun.build` retains native bundler arenas that the process never returns
|
|
22
|
-
* to the OS — `Bun.gc(true)` reclaims nothing and the JS heap stays flat while RSS climbs ~250MB per
|
|
23
|
-
* save. Exit is the only mechanism that gives that memory back, so the work that scales per save lives
|
|
24
|
-
* here rather than in the long-lived watcher.
|
|
25
|
-
*
|
|
26
|
-
* Nothing is cached here, by design. That costs less than it appears to: `CssCompiler` rebuilds its
|
|
27
|
-
* tailwind compilers on every `compileCss` call, and the watcher always asked for `refresh: true`, so
|
|
28
|
-
* there was no warm state to lose. What genuinely had to be preserved travels in the request — the
|
|
29
|
-
* validated page keys and the previous font optimization.
|
|
30
|
-
*/
|
|
17
|
+
// `Bun.build` keeps native bundler arenas that `Bun.gc(true)` cannot reclaim; only exiting returns them.
|
|
31
18
|
class BuildBatch {
|
|
32
19
|
#logger = new Logger("BuildBatch");
|
|
33
20
|
#request: BuildBatchRequest;
|
|
@@ -40,29 +27,16 @@ class BuildBatch {
|
|
|
40
27
|
}
|
|
41
28
|
|
|
42
29
|
async run(): Promise<BuildBatchResult> {
|
|
43
|
-
// `base` arrives alone, from a builder that cannot serve anything until it finishes.
|
|
44
30
|
if (this.#request.needs.includes("base")) await this.#buildBase();
|
|
45
|
-
//
|
|
46
|
-
// pages bundle the browser is waiting on, and css last because it depends on the rebuilt client.
|
|
31
|
+
// Order kept from the in-process build: csr before pages, css last because it depends on the rebuilt client.
|
|
47
32
|
if (this.#request.needs.includes("csr")) await this.#buildCsr();
|
|
48
33
|
if (this.#request.needs.includes("pages")) await this.#buildPages();
|
|
49
34
|
if (this.#request.needs.includes("css")) await this.#buildCss();
|
|
50
35
|
return this.#result;
|
|
51
36
|
}
|
|
52
37
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
* pages bundle; making it wait for the css compile behind it would add latency the in-process version
|
|
56
|
-
* never had, and it would move every artifact write into the window right before the watcher reports
|
|
57
|
-
* the generation complete — which is where a save issued immediately afterwards gets dropped by Bun's
|
|
58
|
-
* recursive `fs.watch` (`local/optimize-resource/06-watcher-dropped-event.md`).
|
|
59
|
-
*
|
|
60
|
-
* A bare `process.send` is safe here, unlike in the watcher, for one reason: this process ends by
|
|
61
|
-
* returning from `main`, and a natural exit flushes a pending ipc write (measured: 1MB delivered
|
|
62
|
-
* 20/20). It is `process.exit` that discards one — so adding an explicit exit to this file, at the end
|
|
63
|
-
* of `main` or anywhere after an emit, would silently start dropping `css-updated` payloads. Route
|
|
64
|
-
* sends through `BuilderChannel` if that ever becomes necessary.
|
|
65
|
-
*/
|
|
38
|
+
// A bare `process.send` is safe only because this process exits by returning from `main`, which flushes ipc;
|
|
39
|
+
// an explicit `process.exit` after an emit would silently drop payloads (route through BuilderChannel then).
|
|
66
40
|
#emit(message: BuilderMessage): void {
|
|
67
41
|
process.send?.(message);
|
|
68
42
|
}
|
|
@@ -80,11 +54,15 @@ class BuildBatch {
|
|
|
80
54
|
});
|
|
81
55
|
}
|
|
82
56
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
57
|
+
// `Bun.build` rejects with an AggregateError whose own message is only "Bundle failed"; the reasons are nested.
|
|
58
|
+
#fail(need: keyof BuildBatchResult["errors"], label: string, err: unknown): void {
|
|
59
|
+
const message = ApplicationBuildReporter.formatError(err, this.#request.workspaceRoot);
|
|
60
|
+
this.#logger.error(`${label} failed: ${message}`);
|
|
61
|
+
this.#result.errors[need] = message;
|
|
62
|
+
if (need !== "base") this.#emitStatus(need, message);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Streams nothing: the builder is not serving yet, so the watcher reads the outcome from the batch result.
|
|
88
66
|
async #buildBase(): Promise<void> {
|
|
89
67
|
const started = Date.now();
|
|
90
68
|
try {
|
|
@@ -93,9 +71,7 @@ class BuildBatch {
|
|
|
93
71
|
this.#result.optimizedFonts = optimizedFonts;
|
|
94
72
|
this.#logger.verbose(`base-artifact ok buildId=${artifact.pagesBundleBuildId} (${Date.now() - started}ms)`);
|
|
95
73
|
} catch (err) {
|
|
96
|
-
|
|
97
|
-
this.#logger.error(`base-artifact failed: ${message}`);
|
|
98
|
-
this.#result.errors.base = message;
|
|
74
|
+
this.#fail("base", "base-artifact", err);
|
|
99
75
|
}
|
|
100
76
|
}
|
|
101
77
|
|
|
@@ -106,10 +82,7 @@ class BuildBatch {
|
|
|
106
82
|
this.#logger.verbose(`csr-rebundle ok (${Date.now() - started}ms)`);
|
|
107
83
|
this.#emitStatus("csr");
|
|
108
84
|
} catch (err) {
|
|
109
|
-
|
|
110
|
-
this.#logger.error(`csr-rebundle failed: ${message}`);
|
|
111
|
-
this.#result.errors.csr = message;
|
|
112
|
-
this.#emitStatus("csr", message);
|
|
85
|
+
this.#fail("csr", "csr-rebundle", err);
|
|
113
86
|
}
|
|
114
87
|
}
|
|
115
88
|
|
|
@@ -129,10 +102,7 @@ class BuildBatch {
|
|
|
129
102
|
this.#emitStatus("pages");
|
|
130
103
|
this.#logger.verbose(`pages-rebundle ok buildId=${next.buildId} (${Date.now() - started}ms)`);
|
|
131
104
|
} catch (err) {
|
|
132
|
-
|
|
133
|
-
this.#logger.error(`pages-rebundle failed: ${message}`);
|
|
134
|
-
this.#result.errors.pages = message;
|
|
135
|
-
this.#emitStatus("pages", message);
|
|
105
|
+
this.#fail("pages", "pages-rebundle", err);
|
|
136
106
|
}
|
|
137
107
|
}
|
|
138
108
|
|
|
@@ -174,14 +144,10 @@ class BuildBatch {
|
|
|
174
144
|
});
|
|
175
145
|
this.#logger.verbose(`css-compile ok assets=${Object.keys(cssAssets).length} (${Date.now() - started}ms)`);
|
|
176
146
|
} catch (err) {
|
|
177
|
-
|
|
178
|
-
this.#logger.error(`css-rebuild failed: ${message}`);
|
|
179
|
-
this.#result.errors.css = message;
|
|
180
|
-
this.#emitStatus("css", message);
|
|
147
|
+
this.#fail("css", "css-rebuild", err);
|
|
181
148
|
}
|
|
182
149
|
}
|
|
183
150
|
|
|
184
|
-
/** Fonts are expensive and rarely change, so the previous result is reused unless this batch touched it. */
|
|
185
151
|
async #optimizeFonts(): Promise<OptimizedFonts> {
|
|
186
152
|
const previous = this.#request.optimizedFonts;
|
|
187
153
|
if (previous && !BuildBatch.#shouldReoptimizeFonts(previous, this.#request.changedFiles)) {
|
|
@@ -196,7 +162,6 @@ class BuildBatch {
|
|
|
196
162
|
}
|
|
197
163
|
|
|
198
164
|
static #shouldReoptimizeFonts(previous: OptimizedFonts, changedFiles: string[]): boolean {
|
|
199
|
-
if (changedFiles.length === 0) return false;
|
|
200
165
|
return changedFiles.some((file) => {
|
|
201
166
|
const normalized = path.resolve(file);
|
|
202
167
|
if (/\.(woff2?|ttf|otf)$/i.test(normalized)) return true;
|
|
@@ -213,15 +178,15 @@ class BuildBatch {
|
|
|
213
178
|
repoName: request.repoName,
|
|
214
179
|
});
|
|
215
180
|
const app = AppExecutor.from(workspace, request.appName);
|
|
216
|
-
// Seeded
|
|
217
|
-
// and repeating that here would be the single largest cost of spawning this process.
|
|
181
|
+
// Seeded, not rediscovered: route discovery would be the largest cost of spawning this process.
|
|
218
182
|
if (request.pageKeys) app.setPageKeys(request.pageKeys);
|
|
219
183
|
const result = await new BuildBatch(request, app).run();
|
|
220
184
|
process.send?.({ type: "build-batch-result", data: result });
|
|
221
185
|
}
|
|
222
186
|
}
|
|
223
187
|
|
|
224
|
-
|
|
188
|
+
// Awaited, not voided: on Windows Bun exits during a missing file's `Bun.file` read unless an entry await is pending.
|
|
189
|
+
await BuildBatch.main().catch((err) => {
|
|
225
190
|
console.error(err);
|
|
226
191
|
process.exit(1);
|
|
227
192
|
});
|