@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,33 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
4
2
|
import path from "node:path";
|
|
3
|
+
import { tempDirs, writeText as write } from "../testHelpers";
|
|
5
4
|
import { createTsconfigPackageResolver } from "../transforms/barrelImportsPlugin";
|
|
6
5
|
import { CLIENT_BUNDLE_NAMING } from "./clientBuildTypes";
|
|
7
6
|
import { ClientEntriesBundler } from "./clientEntriesBundler";
|
|
8
7
|
import { GraphClientEntryDiscovery } from "./clientEntryDiscovery";
|
|
9
8
|
import { RouteClientBuilder } from "./routeClientBuilder";
|
|
10
9
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const makeTempRoot = async () => {
|
|
14
|
-
const root = await mkdtemp(path.join(os.tmpdir(), "akan-client-entry-"));
|
|
15
|
-
tempRoots.push(root);
|
|
16
|
-
return root;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const write = async (filePath: string, content: string) => {
|
|
20
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
21
|
-
await writeFile(filePath, content);
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
afterEach(async () => {
|
|
25
|
-
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
26
|
-
});
|
|
10
|
+
const makeTempRoot = tempDirs("akan-client-entry-");
|
|
27
11
|
|
|
28
12
|
describe("route client store bootstrap", () => {
|
|
29
13
|
test("wraps client entries with app client bootstrap before re-exporting components", () => {
|
|
30
|
-
const original = "/repo/pkgs/akanjs/ui/Model/NewWrapper_Client.tsx";
|
|
14
|
+
const original = path.resolve("/repo/pkgs/akanjs/ui/Model/NewWrapper_Client.tsx");
|
|
31
15
|
const source = RouteClientBuilder.createStoreBootstrapEntrySource({
|
|
32
16
|
appName: "akan",
|
|
33
17
|
originalEntry: original,
|
|
@@ -147,6 +131,57 @@ describe("route client store bootstrap", () => {
|
|
|
147
131
|
);
|
|
148
132
|
});
|
|
149
133
|
|
|
134
|
+
test("bundles every route's entries into one graph so a page never loads a module twice", async () => {
|
|
135
|
+
const root = await makeTempRoot();
|
|
136
|
+
const appDir = path.join(root, "apps/demo");
|
|
137
|
+
const pageA = path.join(appDir, "page/a.tsx");
|
|
138
|
+
const pageB = path.join(appDir, "page/b.tsx");
|
|
139
|
+
const entryA = path.join(appDir, "ui/A.tsx");
|
|
140
|
+
const entryB = path.join(appDir, "ui/B.tsx");
|
|
141
|
+
const shared = path.join(appDir, "common/shared.ts");
|
|
142
|
+
await write(pageA, 'import { A } from "../ui/A";\nexport default A;\n');
|
|
143
|
+
await write(pageB, 'import { B } from "../ui/B";\nexport default B;\n');
|
|
144
|
+
await write(entryA, '"use client";\nimport { mark } from "../common/shared";\nexport const A = () => mark;\n');
|
|
145
|
+
await write(entryB, '"use client";\nimport { mark } from "../common/shared";\nexport const B = () => mark;\n');
|
|
146
|
+
await write(shared, 'export const mark = "shared-module-marker";\n');
|
|
147
|
+
const app = {
|
|
148
|
+
name: "demo",
|
|
149
|
+
cwdPath: appDir,
|
|
150
|
+
dist: { cwdPath: path.join(root, "dist/apps/demo") },
|
|
151
|
+
workspace: { workspaceRoot: root },
|
|
152
|
+
getConfig: async () => ({ barrelImports: [], optimizeImports: [] }),
|
|
153
|
+
getTsConfig: async () => ({ compilerOptions: { paths: {} } }),
|
|
154
|
+
getPublicEnv: () => ({}),
|
|
155
|
+
} as never;
|
|
156
|
+
const discovery = new GraphClientEntryDiscovery({ barrelImports: [] }, async () => null);
|
|
157
|
+
const build = (seeds: string[], knownEntries: Set<string>) =>
|
|
158
|
+
new RouteClientBuilder({
|
|
159
|
+
app,
|
|
160
|
+
seeds,
|
|
161
|
+
graphSeeds: [pageA, pageB],
|
|
162
|
+
knownEntries,
|
|
163
|
+
discovery,
|
|
164
|
+
artifact: {} as never,
|
|
165
|
+
}).build();
|
|
166
|
+
|
|
167
|
+
const first = await build([pageA], new Set());
|
|
168
|
+
const second = await build([pageB], new Set(first.newEntries));
|
|
169
|
+
|
|
170
|
+
expect(first.newEntries).toEqual([entryA, entryB]);
|
|
171
|
+
expect(first.discoveredEntries).toEqual([entryA]);
|
|
172
|
+
expect(first.clientDeps).not.toContain(entryB);
|
|
173
|
+
expect(Object.keys(first.clientDepsByEntry ?? {}).sort()).toEqual([entryA, entryB]);
|
|
174
|
+
expect(second.newEntries).toEqual([]);
|
|
175
|
+
expect(second.discoveredEntries).toEqual([entryB]);
|
|
176
|
+
expect(Object.keys(second.manifestDelta)).toEqual([]);
|
|
177
|
+
const clientOut = path.join(appDir, ".akan/artifact/client");
|
|
178
|
+
const outputs = [...new Bun.Glob("**/*.js").scanSync(clientOut)];
|
|
179
|
+
const carriers = await Promise.all(
|
|
180
|
+
outputs.map(async (file) => (await Bun.file(path.join(clientOut, file)).text()).includes("shared-module-marker")),
|
|
181
|
+
);
|
|
182
|
+
expect(carriers.filter(Boolean)).toHaveLength(1);
|
|
183
|
+
});
|
|
184
|
+
|
|
150
185
|
test("discovers client entries from installed akanjs package sources", async () => {
|
|
151
186
|
const root = await makeTempRoot();
|
|
152
187
|
const seed = path.join(root, "apps/demo/page/_index.tsx");
|
|
@@ -159,24 +194,14 @@ describe("route client store bootstrap", () => {
|
|
|
159
194
|
await write(uiEntry, 'export { ClientPathWrapper } from "./System/Client";\n');
|
|
160
195
|
await write(clientEntry, '"use client";\nexport const ClientPathWrapper = () => null;\n');
|
|
161
196
|
|
|
197
|
+
const entryFiles = new Map([
|
|
198
|
+
["akanjs/ui", uiEntry],
|
|
199
|
+
["akanjs/ui/System/Client.tsx", clientEntry],
|
|
200
|
+
]);
|
|
162
201
|
const discovery = new GraphClientEntryDiscovery({ barrelImports: ["akanjs/ui"] }, async (specifier) => {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
entryFile: uiEntry,
|
|
167
|
-
pkgDir: path.dirname(uiEntry),
|
|
168
|
-
preserveFilePath: true,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
if (specifier === "akanjs/ui/System/Client.tsx") {
|
|
172
|
-
return {
|
|
173
|
-
pkgName: "akanjs/ui/System/Client.tsx",
|
|
174
|
-
entryFile: clientEntry,
|
|
175
|
-
pkgDir: path.dirname(clientEntry),
|
|
176
|
-
preserveFilePath: true,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
return null;
|
|
202
|
+
const entryFile = entryFiles.get(specifier);
|
|
203
|
+
if (!entryFile) return null;
|
|
204
|
+
return { pkgName: specifier, entryFile, pkgDir: path.dirname(entryFile), preserveFilePath: true };
|
|
180
205
|
});
|
|
181
206
|
|
|
182
207
|
expect(await discovery.discover([seed])).toEqual([clientEntry]);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { App } from "../commandDecorators";
|
|
2
|
+
|
|
3
|
+
export const bundleDefine = (app: App, command: "build" | "start", renderEnv: "ssr" | "csr") => {
|
|
4
|
+
const nodeEnv = command === "build" ? "production" : (process.env.NODE_ENV ?? "development");
|
|
5
|
+
return {
|
|
6
|
+
"process.env.NODE_ENV": JSON.stringify(nodeEnv),
|
|
7
|
+
"process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify(renderEnv),
|
|
8
|
+
...Object.fromEntries(
|
|
9
|
+
Object.entries(app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]),
|
|
10
|
+
),
|
|
11
|
+
};
|
|
12
|
+
};
|
|
@@ -10,39 +10,14 @@ export interface BuildClientOptions {
|
|
|
10
10
|
outputDir?: string;
|
|
11
11
|
servePrefix?: string;
|
|
12
12
|
mode?: "production" | "test";
|
|
13
|
-
/** Additional Bun plugins applied to the client bundle build. */
|
|
14
13
|
plugins?: BunPlugin[];
|
|
15
|
-
/**
|
|
16
|
-
* Legacy fallback: additional directories to filesystem-scan for
|
|
17
|
-
* `"use client"` entry points. Only used when `workspaceRoot` /
|
|
18
|
-
* `tsconfigPaths` are not provided (in which case we fall back to the old
|
|
19
|
-
* filesystem-scan strategy). Prefer the graph-based discovery.
|
|
20
|
-
*/
|
|
14
|
+
/** Legacy filesystem-scan roots, used only when `workspaceRoot` / `tsconfigPaths` are absent. */
|
|
21
15
|
extraClientScanRoots?: string[];
|
|
22
|
-
/**
|
|
23
|
-
* Environment variables to inline into the browser bundle via `define`.
|
|
24
|
-
* Keys are the original `process.env` names. Each key is emitted as
|
|
25
|
-
* `process.env.<KEY>` so existing server/client shared code that reads
|
|
26
|
-
* `process.env.AKAN_PUBLIC_*` keeps working unchanged.
|
|
27
|
-
*/
|
|
16
|
+
/** Inlined via `define` as `process.env.<KEY>`. */
|
|
28
17
|
publicEnv?: Record<string, string>;
|
|
29
|
-
/**
|
|
30
|
-
* Workspace root used by the graph-based client-entry discovery to resolve
|
|
31
|
-
* tsconfig-aliased specifiers (`@libs/shared/ui`, `@apps/<name>/client`,
|
|
32
|
-
* ...). When provided together with `tsconfigPaths`, discovery traverses
|
|
33
|
-
* the import graph starting from `appDir` instead of doing a filesystem
|
|
34
|
-
* scan, so `"use client"` files that no route transitively imports (e.g.
|
|
35
|
-
* unused editor components in a shared UI package) stay out of the client
|
|
36
|
-
* bundle.
|
|
37
|
-
*/
|
|
18
|
+
/** With `tsconfigPaths`, discovery walks the import graph from `appDir` instead of scanning the filesystem. */
|
|
38
19
|
workspaceRoot?: string;
|
|
39
|
-
/** Workspace tsconfig `compilerOptions.paths`. See `workspaceRoot`. */
|
|
40
20
|
tsconfigPaths?: Record<string, string[]>;
|
|
41
|
-
/**
|
|
42
|
-
* Barrel specifiers whose imports are flattened by `barrelImportsPlugin`.
|
|
43
|
-
* Discovery rewrites barrel imports with the same analyzer before scanning,
|
|
44
|
-
* so the traversal sees the same leaf files the bundler will see.
|
|
45
|
-
*/
|
|
46
21
|
barrelImports?: string[];
|
|
47
22
|
}
|
|
48
23
|
|
|
@@ -73,10 +48,7 @@ export interface BundleClientEntriesOptions {
|
|
|
73
48
|
entries: string[];
|
|
74
49
|
plugins?: BunPlugin[];
|
|
75
50
|
outputSubdir?: string;
|
|
76
|
-
/**
|
|
77
|
-
* Future Fast Refresh hook. Bun only injects the React Refresh transform;
|
|
78
|
-
* the browser runtime/update protocol still needs to be provided by Akan.
|
|
79
|
-
*/
|
|
51
|
+
/** Bun only injects the React Refresh transform; the browser runtime and update protocol are Akan's. */
|
|
80
52
|
reactFastRefresh?: boolean;
|
|
81
53
|
}
|
|
82
54
|
|
|
@@ -92,13 +64,10 @@ export interface BundleClientEntriesInternalOptions extends BundleClientEntriesO
|
|
|
92
64
|
export interface BundleClientEntriesResult {
|
|
93
65
|
manifest: ClientManifest;
|
|
94
66
|
ssrManifest: SsrManifest;
|
|
95
|
-
/** Absolute path → serve URL for every entry point we produced. */
|
|
96
67
|
entryUrlsByAbsPath: Map<string, string>;
|
|
97
|
-
/** Absolute source entry path → emitted entry file absolute path. */
|
|
98
68
|
entryOutputAbsByAbsPath: Map<string, string>;
|
|
99
|
-
/** Absolute source files that Bun included for each client entry bundle. */
|
|
100
69
|
entryDepsByAbsPath: Map<string, string[]>;
|
|
101
|
-
/**
|
|
70
|
+
/** Workspace-relative client reference id per absolute source entry. */
|
|
102
71
|
clientReferenceIdByAbsPath: Map<string, string>;
|
|
103
72
|
}
|
|
104
73
|
|
|
@@ -2,7 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { SsrManifest, SsrManifestEntry } from "akanjs/server";
|
|
4
4
|
import type { BunPlugin } from "bun";
|
|
5
|
-
import { toClientReferencePath } from "../transforms/rscUseClientTransform";
|
|
5
|
+
import { scanUseClientExports, toClientReferencePath } from "../transforms/rscUseClientTransform";
|
|
6
|
+
import { bundleDefine } from "./bundleDefine";
|
|
6
7
|
import {
|
|
7
8
|
type BundleClientEntriesInternalOptions,
|
|
8
9
|
type BundleClientEntriesResult,
|
|
@@ -13,11 +14,6 @@ import {
|
|
|
13
14
|
type OpaqueEntryAliases,
|
|
14
15
|
} from "./clientBuildTypes";
|
|
15
16
|
|
|
16
|
-
/**
|
|
17
|
-
* Low-level primitive shared by the eager base build and the lazy per-route
|
|
18
|
-
* builds. Takes a flat entrypoints list, runs `Bun.build`, and extracts a
|
|
19
|
-
* `ClientManifest` / `SsrManifest` covering only those entries.
|
|
20
|
-
*/
|
|
21
17
|
export class ClientEntriesBundler {
|
|
22
18
|
#app: BundleClientEntriesInternalOptions["app"];
|
|
23
19
|
#entries: string[];
|
|
@@ -29,7 +25,6 @@ export class ClientEntriesBundler {
|
|
|
29
25
|
#target: ClientBundleTarget;
|
|
30
26
|
#outputSubdir: string;
|
|
31
27
|
#reactFastRefresh: boolean;
|
|
32
|
-
#artifactDir: string;
|
|
33
28
|
#outdir: string;
|
|
34
29
|
#servePrefix: string;
|
|
35
30
|
#manifest: ClientManifest = {};
|
|
@@ -52,8 +47,8 @@ export class ClientEntriesBundler {
|
|
|
52
47
|
this.#target = options.target ?? "browser";
|
|
53
48
|
this.#outputSubdir = options.outputSubdir ?? "client";
|
|
54
49
|
this.#reactFastRefresh = options.reactFastRefresh ?? false;
|
|
55
|
-
|
|
56
|
-
this.#outdir = `${
|
|
50
|
+
const artifactDir = `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
|
|
51
|
+
this.#outdir = `${artifactDir}/${this.#outputSubdir}`;
|
|
57
52
|
this.#servePrefix = `/_akan/${this.#outputSubdir}`;
|
|
58
53
|
}
|
|
59
54
|
|
|
@@ -68,7 +63,7 @@ export class ClientEntriesBundler {
|
|
|
68
63
|
format: "esm",
|
|
69
64
|
naming: CLIENT_BUNDLE_NAMING,
|
|
70
65
|
metafile: true,
|
|
71
|
-
define: this.#
|
|
66
|
+
define: bundleDefine(this.#app, this.#command, "ssr"),
|
|
72
67
|
minify: this.#command === "build",
|
|
73
68
|
optimizeImports: akanConfig.optimizeImports,
|
|
74
69
|
reactFastRefresh: this.#command === "start" && this.#outputSubdir === "client" && this.#reactFastRefresh,
|
|
@@ -100,17 +95,6 @@ export class ClientEntriesBundler {
|
|
|
100
95
|
};
|
|
101
96
|
}
|
|
102
97
|
|
|
103
|
-
#getDefine(): Record<string, string> {
|
|
104
|
-
const nodeEnv = this.#command === "build" ? "production" : (process.env.NODE_ENV ?? "development");
|
|
105
|
-
return {
|
|
106
|
-
"process.env.NODE_ENV": JSON.stringify(nodeEnv),
|
|
107
|
-
"process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("ssr"),
|
|
108
|
-
...Object.fromEntries(
|
|
109
|
-
Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]),
|
|
110
|
-
),
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
98
|
async #createOpaqueEntryAliases(): Promise<OpaqueEntryAliases> {
|
|
115
99
|
const aliasDir = path.join(this.#app.cwdPath, ".akan", "generated", "client-entry-alias", this.#outputSubdir);
|
|
116
100
|
fs.mkdirSync(aliasDir, { recursive: true });
|
|
@@ -141,22 +125,10 @@ export class ClientEntriesBundler {
|
|
|
141
125
|
}
|
|
142
126
|
|
|
143
127
|
async #scanEntryExportNames(absEntry: string): Promise<string[]> {
|
|
144
|
-
|
|
145
|
-
const transpiler = new Bun.Transpiler({ loader: this.#loaderForEntry(absEntry) });
|
|
146
|
-
return transpiler.scan(source).exports;
|
|
128
|
+
return scanUseClientExports(await Bun.file(absEntry).text(), absEntry, this.#app.workspace.workspaceRoot);
|
|
147
129
|
}
|
|
148
130
|
|
|
149
|
-
|
|
150
|
-
if (absPath.endsWith(".tsx")) return "tsx";
|
|
151
|
-
if (absPath.endsWith(".jsx")) return "jsx";
|
|
152
|
-
if (absPath.endsWith(".ts")) return "ts";
|
|
153
|
-
return "js";
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Build a BunPlugin that marks a fixed set of bare specifiers as external via `onResolve`,
|
|
158
|
-
* as opposed to `Bun.build({ external })`, so macro-time imports still resolve normally.
|
|
159
|
-
*/
|
|
131
|
+
// `onResolve` rather than `Bun.build({ external })`, so macro-time imports still resolve normally.
|
|
160
132
|
#createExternalSpecifiersPlugin(): BunPlugin {
|
|
161
133
|
const set = new Set(this.#external);
|
|
162
134
|
const subpathSet = new Set(this.#externalSubpaths);
|
|
@@ -182,11 +154,8 @@ export class ClientEntriesBundler {
|
|
|
182
154
|
exactExternals: Set<string>,
|
|
183
155
|
subpathExternals: Set<string>,
|
|
184
156
|
): boolean {
|
|
185
|
-
if (exactExternals.has(specifier)) return true;
|
|
186
|
-
if (
|
|
187
|
-
for (const external of subpathExternals) {
|
|
188
|
-
if (specifier.startsWith(`${external}/`)) return true;
|
|
189
|
-
}
|
|
157
|
+
if (exactExternals.has(specifier) || subpathExternals.has(specifier)) return true;
|
|
158
|
+
for (const external of subpathExternals) if (specifier.startsWith(`${external}/`)) return true;
|
|
190
159
|
return false;
|
|
191
160
|
}
|
|
192
161
|
|
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { SOURCE_EXTS } from "../akanApp/devHostPolicy";
|
|
2
3
|
import type { App } from "../commandDecorators";
|
|
3
4
|
import { BarrelAnalyzer } from "../transforms/barrelAnalyzer";
|
|
4
5
|
import { createTsconfigPackageResolver, rewriteBarrelImports } from "../transforms/barrelImportsPlugin";
|
|
5
6
|
import type { AkanConfig, ClientEntryDiscovery, ScannedImport } from "./clientBuildTypes";
|
|
6
7
|
|
|
7
8
|
const USE_CLIENT_RE = /^\s*(?:\/\*[\s\S]*?\*\/\s*|\/\/[^\n]*\n\s*)*["']use client["']/;
|
|
8
|
-
const SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
9
9
|
const NODE_MODULES_RE = /[\\/]node_modules[\\/]/;
|
|
10
10
|
const AKANJS_NODE_MODULE_RE = /[\\/]node_modules[\\/]akanjs[\\/]/;
|
|
11
|
-
//
|
|
12
|
-
// the bundler handles via dedicated loaders. Skipping them avoids resolver
|
|
13
|
-
// noise (Bun's scanImports surfaces CSS / asset imports too).
|
|
11
|
+
// Bun's scanImports also surfaces CSS and asset imports, none of which can be a client component.
|
|
14
12
|
const NON_SOURCE_EXT_RE = /\.(css|scss|sass|less|json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav)$/i;
|
|
15
13
|
type PackageResolver = Awaited<ReturnType<typeof createTsconfigPackageResolver>>;
|
|
16
14
|
|
|
17
|
-
/**
|
|
18
|
-
* Everything the traversal ever asks of a file, which is why its text is not kept.
|
|
19
|
-
*
|
|
20
|
-
* `imports` is empty for a client entry: the walk stops there, so they are never scanned.
|
|
21
|
-
*/
|
|
22
15
|
interface FileFacts {
|
|
23
16
|
isClientEntry: boolean;
|
|
24
17
|
imports: ScannedImport[];
|
|
@@ -26,33 +19,18 @@ interface FileFacts {
|
|
|
26
19
|
|
|
27
20
|
const shouldSkipNodeModule = (absPath: string) => NODE_MODULES_RE.test(absPath) && !AKANJS_NODE_MODULE_RE.test(absPath);
|
|
28
21
|
|
|
29
|
-
/**
|
|
30
|
-
* Graph-based `"use client"` discovery, seeded from an explicit file list.
|
|
31
|
-
*
|
|
32
|
-
* Walks imports — including dynamic `import()` — while flattening barrel
|
|
33
|
-
* specifiers through the same `BarrelAnalyzer` the runtime plugin uses, so
|
|
34
|
-
* the traversal matches the module graph the bundler will actually see.
|
|
35
|
-
*/
|
|
22
|
+
/** `"use client"` discovery over the import graph (dynamic imports included), flattening barrels as the bundler does. */
|
|
36
23
|
export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
|
|
37
24
|
#akanConfig: Pick<AkanConfig, "barrelImports">;
|
|
38
25
|
#resolvePackage: PackageResolver;
|
|
39
26
|
#analyzer: BarrelAnalyzer;
|
|
40
27
|
#tsTranspiler = new Bun.Transpiler({ loader: "tsx" });
|
|
41
28
|
#fileExistsCache = new Map<string, Promise<boolean>>();
|
|
42
|
-
|
|
43
|
-
* Derived facts per file, never the source text.
|
|
44
|
-
*
|
|
45
|
-
* This instance lives as long as the builder process does — it is rebuilt only when the akan config
|
|
46
|
-
* changes — so caching whole source texts (and a second, barrel-rewritten copy of each) meant the
|
|
47
|
-
* watcher held every source file it had ever walked for the whole dev session. Nothing downstream
|
|
48
|
-
* wanted the text: the walk needs one boolean and one import list per file, and both were already
|
|
49
|
-
* cached separately under the same key.
|
|
50
|
-
*/
|
|
29
|
+
// Facts, never source text: this instance lives as long as the builder process, so texts would pin every file walked.
|
|
51
30
|
#factsCache = new Map<string, Promise<FileFacts | null>>();
|
|
52
31
|
#resolvedFileCache = new Map<string, Promise<string | null>>();
|
|
53
32
|
#resolvedSpecifierCache = new Map<string, Promise<string | null>>();
|
|
54
33
|
#reachableEntriesCache = new Map<string, Set<string>>();
|
|
55
|
-
/** Keys of the three caches above whose answer was "not there" — see `#forgetMissing`. */
|
|
56
34
|
#missingFiles = new Set<string>();
|
|
57
35
|
#unresolvedPaths = new Set<string>();
|
|
58
36
|
#unresolvedSpecifiers = new Set<string>();
|
|
@@ -83,20 +61,12 @@ export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
|
|
|
83
61
|
this.#reachableEntriesCache.delete(absPath);
|
|
84
62
|
}
|
|
85
63
|
if (files.length === 0) return;
|
|
86
|
-
//
|
|
87
|
-
// child can affect any reachable-entry cache above it.
|
|
64
|
+
// Reachable-entry results are transitive, so a changed child invalidates every ancestor.
|
|
88
65
|
this.#reachableEntriesCache.clear();
|
|
89
66
|
this.#forgetMissing();
|
|
90
67
|
}
|
|
91
68
|
|
|
92
|
-
|
|
93
|
-
* Drop every "there is no such file" answer, because a batch may be what created it.
|
|
94
|
-
*
|
|
95
|
-
* These caches are keyed by extension-less path and by `dir\0specifier`, neither of which maps back
|
|
96
|
-
* to the path that just appeared, so a negative recorded before a module existed is unreachable any
|
|
97
|
-
* other way — and this instance lives as long as the builder process. Positive answers are kept:
|
|
98
|
-
* they are keyed by a real path, which arrives in `files` when it changes or goes away.
|
|
99
|
-
*/
|
|
69
|
+
// Negatives are keyed by extension-less path or `dir\0specifier`, which a new file's path cannot reach, so all go.
|
|
100
70
|
#forgetMissing(): void {
|
|
101
71
|
for (const key of this.#missingFiles) this.#fileExistsCache.delete(key);
|
|
102
72
|
for (const key of this.#unresolvedPaths) this.#resolvedFileCache.delete(key);
|
|
@@ -121,10 +91,6 @@ export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
|
|
|
121
91
|
return cached;
|
|
122
92
|
}
|
|
123
93
|
|
|
124
|
-
/**
|
|
125
|
-
* Read a file once and keep only what the traversal asks of it. The text itself is dropped as soon
|
|
126
|
-
* as the boolean and the import list are out of it — see `#factsCache`.
|
|
127
|
-
*/
|
|
128
94
|
#facts(file: string): Promise<FileFacts | null> {
|
|
129
95
|
const absPath = path.resolve(file);
|
|
130
96
|
let cached = this.#factsCache.get(absPath);
|
|
@@ -134,7 +100,6 @@ export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
|
|
|
134
100
|
.text()
|
|
135
101
|
.catch(() => null);
|
|
136
102
|
if (content === null) return null;
|
|
137
|
-
// A client entry ends the walk, so its imports are never needed.
|
|
138
103
|
if (USE_CLIENT_RE.test(content)) return { isClientEntry: true, imports: [] };
|
|
139
104
|
return { isClientEntry: false, imports: this.#scanImports(await this.#rewrite(content)) };
|
|
140
105
|
})();
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import type { BaseBuildArtifact } from "akanjs/server";
|
|
4
4
|
import { type PageEntry, resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
|
|
5
5
|
import type { App } from "../commandDecorators";
|
|
6
|
+
import { bundleDefine } from "./bundleDefine";
|
|
6
7
|
import { getPageKeyBasePath } from "./cssCompiler";
|
|
7
8
|
import { PagesBundleBuilder } from "./pagesBundleBuilder";
|
|
8
9
|
import { PagesEntrySourceGenerator } from "./pagesEntrySourceGenerator";
|
|
@@ -61,10 +62,9 @@ export class CsrArtifactBuilder {
|
|
|
61
62
|
splitting: false,
|
|
62
63
|
minify: true,
|
|
63
64
|
env: "AKAN_PUBLIC_*",
|
|
64
|
-
define: this.#
|
|
65
|
+
define: bundleDefine(this.#app, this.#command, "csr"),
|
|
65
66
|
optimizeImports: akanConfig.optimizeImports,
|
|
66
|
-
//
|
|
67
|
-
// the route graph is Tailwind source, and every root layout stylesheet in the graph would land in every HTML.
|
|
67
|
+
// A raw `.css` in the route graph is Tailwind source; the base artifact's compiled sheet is the only stylesheet.
|
|
68
68
|
plugins: [PagesBundleBuilder.createCssStubPlugin()],
|
|
69
69
|
});
|
|
70
70
|
|
|
@@ -118,17 +118,6 @@ export class CsrArtifactBuilder {
|
|
|
118
118
|
return path.join(this.#generatedDir, filename);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
#define(): Record<string, string> {
|
|
122
|
-
const nodeEnv = this.#command === "build" ? "production" : (process.env.NODE_ENV ?? "development");
|
|
123
|
-
return {
|
|
124
|
-
"process.env.NODE_ENV": JSON.stringify(nodeEnv),
|
|
125
|
-
"process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("csr"),
|
|
126
|
-
...Object.fromEntries(
|
|
127
|
-
Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]),
|
|
128
|
-
),
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
121
|
async #createEntryFile(basePath: string, pageEntries: PageEntry[]): Promise<readonly [string, string]> {
|
|
133
122
|
return [
|
|
134
123
|
this.#generatedPath(CsrArtifactBuilder.entryFilename(basePath)),
|
|
@@ -168,7 +157,7 @@ void bootCsr(pages);
|
|
|
168
157
|
|
|
169
158
|
async #inlineCsrArtifacts(cssAssets: Record<string, CssAsset>): Promise<void> {
|
|
170
159
|
const jsFiles = new Set<string>();
|
|
171
|
-
for (const htmlPath of await this.#
|
|
160
|
+
for (const htmlPath of await this.#listOutputFiles((filePath) => filePath.endsWith(".html"))) {
|
|
172
161
|
const htmlFile = Bun.file(htmlPath);
|
|
173
162
|
if (!(await htmlFile.exists())) continue;
|
|
174
163
|
const basePath = CsrArtifactBuilder.basePathOfHtml(htmlPath);
|
|
@@ -210,10 +199,6 @@ void bootCsr(pages);
|
|
|
210
199
|
return { html: next, jsFiles };
|
|
211
200
|
}
|
|
212
201
|
|
|
213
|
-
async #htmlOutputPaths(): Promise<string[]> {
|
|
214
|
-
return await this.#listOutputFiles((filePath) => filePath.endsWith(".html"));
|
|
215
|
-
}
|
|
216
|
-
|
|
217
202
|
async #listOutputFiles(predicate: (filePath: string) => boolean): Promise<string[]> {
|
|
218
203
|
const glob = new Bun.Glob("**/*");
|
|
219
204
|
const files: string[] = [];
|
|
@@ -223,12 +208,8 @@ void bootCsr(pages);
|
|
|
223
208
|
return files.sort();
|
|
224
209
|
}
|
|
225
210
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
* of the text being searched — and a React bundle contains `<body` and `</head>` as strings. Positions are
|
|
229
|
-
* taken on a copy with script, style and comment bodies blanked, and the snippet always lands after whatever
|
|
230
|
-
* was injected before it: prepending would reverse the cascade order the caller chose.
|
|
231
|
-
*/
|
|
211
|
+
// Bun hoists the module script into <head> and a React bundle contains "</head>" as text, so positions come from a
|
|
212
|
+
// copy with script/style/comment bodies blanked. Never prepends: that would reverse the caller's cascade order.
|
|
232
213
|
static injectBeforeHeadEnd(html: string, snippet: string): string {
|
|
233
214
|
const scannable = CsrArtifactBuilder.blankEmbeddedContent(html);
|
|
234
215
|
const headEnd = scannable.search(/<\/head\s*>/i);
|
|
@@ -17,18 +17,12 @@ describe("CssCandidateCache token extraction", () => {
|
|
|
17
17
|
expect(candidates).toEqual(expect.arrayContaining(["w-full", "hover:bg-muted", "md:w-1/2", "text-[#fff]"]));
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
// A candidate that opens with `[` used to be torn into `_td` + `px-3`, so every descendant-variant
|
|
21
|
-
// rule in the framework compiled to nothing and its element rendered unstyled with no diagnostic.
|
|
22
20
|
test("keeps an arbitrary variant that starts with a bracket", async () => {
|
|
23
21
|
const candidates = await scan(`const c = "[&_td]:px-3 [&>*]:gap-2 [&_tbody_tr]:border-t";`);
|
|
24
22
|
expect(candidates).toEqual(expect.arrayContaining(["[&_td]:px-3", "[&>*]:gap-2", "[&_tbody_tr]:border-t"]));
|
|
25
23
|
expect(candidates).not.toContain("_td");
|
|
26
24
|
});
|
|
27
25
|
|
|
28
|
-
// A labelled tuple type looks like an arbitrary property, and compiles to a rule whose declaration is
|
|
29
|
-
// TypeScript. `optimize()` cannot parse that, and drops far more than the offending rule on its way
|
|
30
|
-
// out — every variant in the build went missing, so nothing hovered, focused or responded to a
|
|
31
|
-
// breakpoint. Tailwind writes a space in an arbitrary value as `_`, so whitespace rules the token out.
|
|
32
26
|
test("ignores a labelled tuple type that only looks like an arbitrary property", async () => {
|
|
33
27
|
const candidates = await scan(
|
|
34
28
|
`type A = [key: string, make: () => unknown][];\ntype B = [setQueryArgs: (a: Args) => Args, options?: Policy];`,
|
|
@@ -1,18 +1,7 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* matters: an arbitrary variant starts with `[` (`[&_td]:px-3`, `[&>*]:gap-2`), and a pattern anchored
|
|
6
|
-
* on a word character tears it into `_td` plus `px-3` instead — both meaningless, so the rule compiles
|
|
7
|
-
* to nothing and the element silently renders unstyled.
|
|
8
|
-
*
|
|
9
|
-
* Brackets must hold no whitespace. Tailwind spells a space in an arbitrary value `_`, so a bracket
|
|
10
|
-
* group containing one is never a class — but it is exactly the shape of a labelled tuple type
|
|
11
|
-
* (`[key: string, make: () => unknown]`). Scanned as a candidate, that compiles to an arbitrary
|
|
12
|
-
* *property* rule carrying TypeScript as its declaration, which the lightningcss pass in `optimize()`
|
|
13
|
-
* cannot parse; its error recovery then drops a whole span of the stylesheet — in practice every
|
|
14
|
-
* variant rule in the build, leaving hover, focus, responsive and dark styles silently absent.
|
|
15
|
-
*/
|
|
3
|
+
// The leading `[` alternation keeps arbitrary variants (`[&_td]:px-3`) whole. Brackets hold no whitespace: a labelled
|
|
4
|
+
// tuple type scanned as a class compiles to a rule lightningcss cannot parse, and it then drops every variant rule.
|
|
16
5
|
const CANDIDATE_RE = /-?(?:[\w@]|\[[^\]\s]+\])[\w:/.-]*(?:\[[^\]\s]+\][\w:/.-]*)*/g;
|
|
17
6
|
|
|
18
7
|
interface CachedFile {
|
|
@@ -26,19 +15,9 @@ interface CacheFile {
|
|
|
26
15
|
files: Record<string, CachedFile>;
|
|
27
16
|
}
|
|
28
17
|
|
|
29
|
-
/**
|
|
30
|
-
* Tailwind candidate tokens per source file, cached on disk across builds.
|
|
31
|
-
*
|
|
32
|
-
* The scan reads the **full text** of every source file on every CSS rebuild — measured at 385-508ms
|
|
33
|
-
* per save on `apps/akan`. Phase 2 moved css compilation into a per-generation batch worker, so an
|
|
34
|
-
* in-memory cache (what `03-phase3-topology-and-trim.md` §3.4 originally proposed) buys nothing: the
|
|
35
|
-
* process that would hold it exits before the next save. Disk is what survives the worker, a builder
|
|
36
|
-
* recycle and a dev-host restart alike, which is the same reasoning §3.3 used for the font cache.
|
|
37
|
-
*
|
|
38
|
-
* Keyed on (mtime, size) per file, so a save re-reads only the files in that batch.
|
|
39
|
-
*/
|
|
18
|
+
/** Tailwind candidates per source file, on disk: the batch worker that compiles CSS exits before the next save. */
|
|
40
19
|
export class CssCandidateCache {
|
|
41
|
-
|
|
20
|
+
// Bump when the token regex or the entry shape changes, so stale extractions are not reused.
|
|
42
21
|
static readonly #version = 3;
|
|
43
22
|
readonly #path: string;
|
|
44
23
|
readonly #entries = new Map<string, CachedFile>();
|
|
@@ -58,7 +37,6 @@ export class CssCandidateCache {
|
|
|
58
37
|
return this.#rescanned;
|
|
59
38
|
}
|
|
60
39
|
|
|
61
|
-
/** A cache that cannot be read or is a version behind simply starts empty — it is only an optimisation. */
|
|
62
40
|
async load(): Promise<this> {
|
|
63
41
|
const raw = (await Bun.file(this.#path)
|
|
64
42
|
.json()
|
|
@@ -71,12 +49,7 @@ export class CssCandidateCache {
|
|
|
71
49
|
return this;
|
|
72
50
|
}
|
|
73
51
|
|
|
74
|
-
|
|
75
|
-
* The file's candidate tokens, read from disk only when its (mtime, size) no longer matches.
|
|
76
|
-
*
|
|
77
|
-
* Read errors propagate, as they did before this cache existed: a source file the compiler cannot
|
|
78
|
-
* read is a broken build, not a cache miss to paper over.
|
|
79
|
-
*/
|
|
52
|
+
// A read error propagates: an unreadable source is a broken build, not a cache miss.
|
|
80
53
|
async candidatesFor(file: string): Promise<string[]> {
|
|
81
54
|
const stats = await stat(file).catch(() => null);
|
|
82
55
|
const cached = this.#entries.get(file);
|
|
@@ -94,13 +67,6 @@ export class CssCandidateCache {
|
|
|
94
67
|
return candidates;
|
|
95
68
|
}
|
|
96
69
|
|
|
97
|
-
/**
|
|
98
|
-
* Persist, dropping files the scan no longer reaches so a renamed or deleted module does not keep
|
|
99
|
-
* feeding its classes to the compiler forever.
|
|
100
|
-
*
|
|
101
|
-
* A rebuild that re-read nothing writes nothing: the boot double-build and any CSS rebuild triggered
|
|
102
|
-
* by something other than a source edit would otherwise rewrite the whole file for no change.
|
|
103
|
-
*/
|
|
104
70
|
async save(present: Set<string>): Promise<void> {
|
|
105
71
|
for (const file of [...this.#entries.keys()]) {
|
|
106
72
|
if (present.has(file)) continue;
|
|
@@ -111,8 +77,7 @@ export class CssCandidateCache {
|
|
|
111
77
|
this.#dirty = false;
|
|
112
78
|
const files: Record<string, CachedFile> = {};
|
|
113
79
|
for (const [file, entry] of this.#entries) files[file] = entry;
|
|
114
|
-
//
|
|
115
|
-
// still compile.
|
|
80
|
+
// An unwritable cache is a slow build, not a failed one: a read-only checkout must still compile.
|
|
116
81
|
await Bun.write(
|
|
117
82
|
this.#path,
|
|
118
83
|
JSON.stringify({ version: CssCandidateCache.#version, files } satisfies CacheFile),
|