@akanjs/devkit 3.0.0-beta.3 → 3.0.0-beta.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEV_RUNTIME_KNOBS.md +44 -26
- package/README.ko.md +2 -2
- package/README.md +3 -2
- 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 +56 -3
- package/akanApp/akanApp.host.ts +218 -278
- package/akanApp/devBootLatch.test.ts +39 -0
- package/akanApp/devBootLatch.ts +39 -0
- package/akanApp/devHostPolicy.test.ts +81 -35
- package/akanApp/devHostPolicy.ts +41 -102
- package/akanConfig/akanBin.test.ts +62 -0
- package/akanConfig/akanBin.ts +78 -0
- package/akanConfig/akanConfig.test.ts +404 -141
- package/akanConfig/akanConfig.ts +317 -274
- package/akanConfig/index.ts +1 -0
- package/akanConfig/types.ts +21 -8
- package/akanContext.ts +110 -160
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.test.ts +41 -5
- package/applicationBuildRunner.ts +71 -63
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +32 -30
- package/artifact/implicitRootLayout.ts +58 -106
- package/artifact/routeSeedIndex.test.ts +7 -6
- package/artifact/routeSeedIndex.ts +11 -44
- package/biome.base.json +35 -2
- package/biomeBase.ts +2 -5
- 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 +71 -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/csrE2e/csrE2eHarness.fixture.ts +505 -0
- package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
- package/csrE2e/csrPageStack.e2e.test.ts +252 -0
- package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
- package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
- package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
- package/csrE2e/registryVendor.e2e.test.ts +221 -0
- package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
- 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 +176 -106
- package/executors.ts +212 -498
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +5 -3
- package/fileSys.ts +22 -9
- 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 +123 -37
- package/frontendBuild/bundleDefine.test.ts +12 -0
- package/frontendBuild/bundleDefine.ts +16 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
- package/frontendBuild/clientEntryDiscovery.ts +122 -110
- package/frontendBuild/csrArtifactBuilder.ts +9 -31
- package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
- package/frontendBuild/csrDevArtifactWriter.ts +307 -0
- package/frontendBuild/csrDevBundler.test.ts +66 -0
- package/frontendBuild/csrDevBundler.ts +95 -0
- package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
- package/frontendBuild/csrDevFactoryCheck.ts +39 -0
- package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
- package/frontendBuild/csrDevModuleCompiler.ts +422 -0
- package/frontendBuild/csrDevPatcher.test.ts +572 -0
- package/frontendBuild/csrDevPatcher.ts +548 -0
- package/frontendBuild/csrDevPaths.ts +145 -0
- package/frontendBuild/csrDevResolver.test.ts +239 -0
- package/frontendBuild/csrDevResolver.ts +363 -0
- package/frontendBuild/csrDevTypes.ts +61 -0
- package/frontendBuild/csrEntryFiles.ts +24 -0
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +31 -82
- 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/devRegistryBundler.ts +192 -0
- 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 +57 -70
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +59 -32
- package/frontendBuild/hmrWatcher.ts +31 -74
- package/frontendBuild/index.ts +4 -0
- package/frontendBuild/pagesBundleBuilder.ts +28 -30
- package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
- package/frontendBuild/registryMetadataFingerprint.ts +28 -0
- package/frontendBuild/routeClientBuilder.ts +100 -62
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/serverGraphFile.test.ts +77 -0
- package/frontendBuild/serverGraphFile.ts +93 -0
- package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
- package/frontendBuild/ssrDevBundler.ts +85 -0
- 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 +147 -63
- package/incrementalBuilder/buildBatchProtocol.ts +13 -38
- package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
- package/incrementalBuilder/buildBatchRunner.ts +26 -38
- 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/builderWorkQueue.test.ts +94 -0
- package/incrementalBuilder/builderWorkQueue.ts +126 -0
- package/incrementalBuilder/devWatchBatch.test.ts +21 -24
- package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
- package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +269 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
- package/index.ts +5 -19
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +105 -199
- 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/__fixtures__/no-document-cookie/bad.tsx +4 -0
- package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
- package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
- package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
- package/lint/gritRules.test.ts +8 -24
- package/lint/no-deep-internal-import.grit +2 -2
- package/lint/no-document-cookie.grit +29 -0
- package/lint/no-redeclare-predefined-endpoint.grit +2 -1
- package/lint/no-web-only-api-outside-webkit.grit +44 -0
- package/lint/no-web-storage.grit +30 -0
- package/linter.test.ts +1 -2
- package/linter.ts +26 -116
- package/mobile/desktopBin.test.ts +177 -0
- package/mobile/desktopBin.ts +143 -0
- package/mobile/desktopServerStage.test.ts +204 -0
- package/mobile/desktopServerStage.ts +138 -0
- package/mobile/index.ts +9 -0
- package/mobile/mobileTarget.test.ts +57 -8
- package/mobile/mobileTarget.ts +45 -26
- package/mobile/nativeApi.test.ts +30 -0
- package/mobile/nativeApi.ts +40 -0
- package/mobile/nativeApp.test.ts +151 -0
- package/mobile/nativeApp.ts +405 -0
- package/mobile/nativeAppLine.test.ts +69 -0
- package/mobile/nativeAppLine.ts +71 -0
- package/mobile/nativeConfig.test.ts +414 -0
- package/mobile/nativeConfig.ts +296 -0
- package/mobile/nativePluginFolders.test.ts +81 -0
- package/mobile/nativePluginFolders.ts +60 -0
- package/mobile/nativeWebDir.test.ts +80 -0
- package/mobile/nativeWebDir.ts +66 -0
- package/mobile/placeholderAppId.test.ts +13 -0
- package/mobile/placeholderAppId.ts +16 -0
- package/mobile/usageDescriptions.test.ts +27 -0
- package/mobile/usageDescriptions.ts +24 -0
- package/package.json +11 -11
- 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 +200 -286
- 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 +91 -225
- 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 +42 -20
- package/transforms/serverEnvPlugin.test.ts +103 -0
- package/transforms/serverEnvPlugin.ts +41 -0
- package/transforms/transforms.test.ts +166 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +6 -23
- package/typeChecker.ts +7 -63
- package/types.ts +2 -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.test.ts +18 -3
- package/workspaceLayout.ts +22 -23
- package/abstractCompactor.ts +0 -112
- package/aiEditor.test.ts +0 -60
- package/aiEditor.ts +0 -387
- package/applicationReleasePackager.ts +0 -209
- package/capacitorApp.test.ts +0 -372
- package/capacitorApp.ts +0 -1648
- 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
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { CsrDevPaths } from "./csrDevPaths";
|
|
4
|
+
import type { CsrDevContext } from "./csrDevTypes";
|
|
5
|
+
import { PagesBundleBuilder } from "./pagesBundleBuilder";
|
|
6
|
+
|
|
7
|
+
interface MetafileImport {
|
|
8
|
+
path: string;
|
|
9
|
+
kind: string;
|
|
10
|
+
original?: string;
|
|
11
|
+
external?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface PrepassMetafile {
|
|
15
|
+
inputs: Record<string, { imports: MetafileImport[] }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface BrowserMap {
|
|
19
|
+
/** What the `browser` object maps to `false`: an empty module in a browser build. */
|
|
20
|
+
offFiles: Set<string>;
|
|
21
|
+
offNames: Set<string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CsrDevResolverOptions {
|
|
25
|
+
paths: CsrDevPaths;
|
|
26
|
+
context: CsrDevContext;
|
|
27
|
+
entryFiles: string[];
|
|
28
|
+
resolution?: Record<string, Record<string, string>>;
|
|
29
|
+
/** Specifiers per importer id whose recorded resolution came from Bun's runtime resolver, not the browser build. */
|
|
30
|
+
runtimeResolved?: Record<string, string[]>;
|
|
31
|
+
/** When this process began: a package installed after it is one Bun's resolver here has not seen. */
|
|
32
|
+
startedAt?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
//* Resolution comes from a plain browser build: Bun's runtime resolver ignores the `browser` condition and picks Node
|
|
36
|
+
//* builds (`@firebase/util` resolves to its node-esm entry), which the registry build would then ship.
|
|
37
|
+
export class CsrDevResolver {
|
|
38
|
+
static readonly inline = "inline";
|
|
39
|
+
readonly #paths: CsrDevPaths;
|
|
40
|
+
readonly #context: CsrDevContext;
|
|
41
|
+
readonly #entryFiles: string[];
|
|
42
|
+
readonly #resolution = new Map<string, Map<string, string>>();
|
|
43
|
+
readonly #fallbacks = new Set<string>();
|
|
44
|
+
readonly #runtimeResolved = new Map<string, Set<string>>();
|
|
45
|
+
readonly #browserMaps = new Map<string, BrowserMap | null>();
|
|
46
|
+
/** Files a browser build loaded, so every import they make has its browser resolution recorded. */
|
|
47
|
+
readonly #loaded = new Set<string>();
|
|
48
|
+
readonly #startedAt: number;
|
|
49
|
+
#prepassDone = false;
|
|
50
|
+
|
|
51
|
+
constructor({
|
|
52
|
+
paths,
|
|
53
|
+
context,
|
|
54
|
+
entryFiles,
|
|
55
|
+
resolution = {},
|
|
56
|
+
runtimeResolved = {},
|
|
57
|
+
startedAt = Date.now() - process.uptime() * 1000,
|
|
58
|
+
}: CsrDevResolverOptions) {
|
|
59
|
+
this.#paths = paths;
|
|
60
|
+
this.#startedAt = startedAt;
|
|
61
|
+
this.#context = context;
|
|
62
|
+
this.#entryFiles = entryFiles;
|
|
63
|
+
for (const [importer, specifiers] of Object.entries(runtimeResolved))
|
|
64
|
+
this.#runtimeResolved.set(paths.fileOf(importer), new Set(specifiers));
|
|
65
|
+
for (const [importer, bySpecifier] of Object.entries(resolution))
|
|
66
|
+
this.#resolution.set(
|
|
67
|
+
paths.fileOf(importer),
|
|
68
|
+
new Map(
|
|
69
|
+
Object.entries(bySpecifier).map(([specifier, target]) => [
|
|
70
|
+
specifier,
|
|
71
|
+
CsrDevPaths.isStub(target) ? target : paths.fileOf(target),
|
|
72
|
+
]),
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get prepassDone(): boolean {
|
|
78
|
+
return this.#prepassDone;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get fallbacks(): ReadonlySet<string> {
|
|
82
|
+
return this.#fallbacks;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async prepass(): Promise<void> {
|
|
86
|
+
const result = await Bun.build({
|
|
87
|
+
entrypoints: [...this.#entryFiles, ...(this.#context.refreshFile ? [this.#context.refreshFile] : [])],
|
|
88
|
+
target: "browser",
|
|
89
|
+
metafile: true,
|
|
90
|
+
external: [...(this.#context.externals ?? [])],
|
|
91
|
+
env: "AKAN_PUBLIC_*",
|
|
92
|
+
define: this.#context.define,
|
|
93
|
+
optimizeImports: this.#context.optimizeImports,
|
|
94
|
+
plugins: [PagesBundleBuilder.createCssStubPlugin()],
|
|
95
|
+
});
|
|
96
|
+
this.#record((result.metafile as unknown as PrepassMetafile).inputs);
|
|
97
|
+
this.#prepassDone = true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
//? The browser build drops a side-effect-free barrel's unused re-exports without loading them, yet the registry
|
|
101
|
+
//? compiles every file an import names. Such a file's own imports are resolved by a browser build of its own, one
|
|
102
|
+
//? level deep, not by the runtime resolver's Node conditions (vfile's `#minpath` would reach `node:path`).
|
|
103
|
+
async cover(files: string[]): Promise<void> {
|
|
104
|
+
if (!this.#prepassDone) return;
|
|
105
|
+
const pending = files.filter((file) => CsrDevPaths.isScript(file) && !this.#loaded.has(file));
|
|
106
|
+
if (pending.length === 0) return;
|
|
107
|
+
for (const file of pending) this.#loaded.add(file);
|
|
108
|
+
const entries = new Set(pending);
|
|
109
|
+
const result = await Bun.build({
|
|
110
|
+
entrypoints: pending,
|
|
111
|
+
target: "browser",
|
|
112
|
+
metafile: true,
|
|
113
|
+
throw: false,
|
|
114
|
+
external: [...(this.#context.externals ?? [])],
|
|
115
|
+
env: "AKAN_PUBLIC_*",
|
|
116
|
+
define: this.#context.define,
|
|
117
|
+
plugins: [
|
|
118
|
+
{
|
|
119
|
+
name: "akan-csr-cover",
|
|
120
|
+
//? What an entry imports is resolved, never parsed: a CommonJS stub, since an empty ESM module fails a named
|
|
121
|
+
//? import ("No matching export") and a failed build hands back no metafile at all.
|
|
122
|
+
setup: (build) => {
|
|
123
|
+
build.onLoad({ filter: /.*/ }, (args) =>
|
|
124
|
+
entries.has(CsrDevPaths.realpath(args.path))
|
|
125
|
+
? undefined
|
|
126
|
+
: { contents: "module.exports = {};", loader: "js" },
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
//? Left unrecorded when the build fails: those imports keep the runtime resolver's answer, as before.
|
|
133
|
+
if (result.metafile) this.#record((result.metafile as unknown as PrepassMetafile).inputs, entries);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#record(inputs: PrepassMetafile["inputs"], only?: Set<string>): void {
|
|
137
|
+
for (const [input, { imports }] of Object.entries(inputs)) {
|
|
138
|
+
const importer = CsrDevPaths.realpath(path.resolve(input));
|
|
139
|
+
if (only && !only.has(importer)) continue;
|
|
140
|
+
//? A re-export the build tree-shook keeps no specifier, so its file is resolved again as an entry of its own.
|
|
141
|
+
if (imports.every((record) => record.original)) this.#loaded.add(importer);
|
|
142
|
+
for (const record of imports) {
|
|
143
|
+
if (!record.original) continue;
|
|
144
|
+
if (record.external) {
|
|
145
|
+
if (record.original.startsWith("node:"))
|
|
146
|
+
this.#remember(importer, record.original, `${CsrDevPaths.stubPrefix}${record.original}`);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const target = path.resolve(record.path);
|
|
150
|
+
if (!fs.existsSync(target)) continue;
|
|
151
|
+
this.#remember(importer, record.original, CsrDevPaths.realpath(target));
|
|
152
|
+
this.#runtimeResolved.get(importer)?.delete(record.original);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
resolve(importer: string, specifier: string, kind: "import" | "require" = "import"): string | null {
|
|
158
|
+
if (this.#isInlineSpecifier(specifier)) return CsrDevResolver.inline;
|
|
159
|
+
const disabled = this.#disabledInBrowser(importer, specifier, kind);
|
|
160
|
+
if (disabled) return disabled;
|
|
161
|
+
const known = this.#resolution.get(importer)?.get(specifier);
|
|
162
|
+
const usable = known && this.#stillAnswers(importer, specifier, known, kind) ? known : null;
|
|
163
|
+
const target = usable ?? this.#resolveUnknown(importer, specifier, kind);
|
|
164
|
+
if (target === null || CsrDevPaths.isStub(target)) return target;
|
|
165
|
+
if (!CsrDevPaths.isScript(target)) return CsrDevResolver.inline;
|
|
166
|
+
if (!usable) this.#remember(importer, specifier, target);
|
|
167
|
+
return target;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
//? A relative record in app code stands while the disk still resolves it to the same file, as Bun would: a sibling
|
|
171
|
+
//? with a stronger extension (`x.tsx` beside `x.ts`), a file beside the folder it went into, or a record a CommonJS
|
|
172
|
+
//? re-export folded onto another file (`./a` → `x.js`) resolves anew. A package's files, and app code under a
|
|
173
|
+
//? package.json that maps files with a `browser` object, keep the prepass's answer, which a disk lookup would only
|
|
174
|
+
//? approximate.
|
|
175
|
+
#stillAnswers(importer: string, specifier: string, known: string, kind: "import" | "require"): boolean {
|
|
176
|
+
if (CsrDevPaths.isStub(known)) return true;
|
|
177
|
+
if (!fs.existsSync(known)) return false;
|
|
178
|
+
if (!specifier.startsWith(".") || CsrDevPaths.isVendorFile(importer)) return true;
|
|
179
|
+
if (this.#hasBrowserMap(path.dirname(importer))) return true;
|
|
180
|
+
const onDisk = CsrDevPaths.resolveRelative(path.dirname(importer), specifier, kind);
|
|
181
|
+
return onDisk !== null && CsrDevPaths.realpath(onDisk) === known;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
isRegistryModule(file: string): boolean {
|
|
185
|
+
if (!CsrDevPaths.isScript(file)) return false;
|
|
186
|
+
const packageName = CsrDevResolver.#packageNameOf(file);
|
|
187
|
+
if (packageName === null) return true;
|
|
188
|
+
return !this.#context.optimizeImports.some((pattern) => {
|
|
189
|
+
const base = CsrDevResolver.#patternBase(pattern);
|
|
190
|
+
return base === packageName || base.startsWith(`${packageName}/`);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
//? `false` in a package's `browser` object is an empty module to a browser build (object-inspect's
|
|
195
|
+
//? `./util.inspect.js`, which requires `util`). Its record carries no specifier, so the field is read here.
|
|
196
|
+
#disabledInBrowser(importer: string, specifier: string, kind: "import" | "require"): string | null {
|
|
197
|
+
const map = this.#browserMapOf(path.dirname(importer));
|
|
198
|
+
if (!map || (map.offFiles.size === 0 && map.offNames.size === 0)) return null;
|
|
199
|
+
if (!specifier.startsWith(".") && !path.isAbsolute(specifier))
|
|
200
|
+
return map.offNames.has(specifier) ? `${CsrDevPaths.emptyStubPrefix}${specifier}` : null;
|
|
201
|
+
const target = CsrDevPaths.resolveRelative(path.dirname(importer), specifier, kind);
|
|
202
|
+
return target && map.offFiles.has(CsrDevPaths.realpath(target))
|
|
203
|
+
? `${CsrDevPaths.emptyStubPrefix}${this.#paths.idOf(target)}`
|
|
204
|
+
: null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#hasBrowserMap(dir: string): boolean {
|
|
208
|
+
return this.#browserMapOf(dir) !== null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#browserMapOf(dir: string): BrowserMap | null {
|
|
212
|
+
const cached = this.#browserMaps.get(dir);
|
|
213
|
+
if (cached !== undefined) return cached;
|
|
214
|
+
const pkgFile = path.join(dir, "package.json");
|
|
215
|
+
const parent = path.dirname(dir);
|
|
216
|
+
let map: BrowserMap | null = null;
|
|
217
|
+
if (fs.existsSync(pkgFile)) {
|
|
218
|
+
try {
|
|
219
|
+
const browser = (JSON.parse(fs.readFileSync(pkgFile, "utf8")) as { browser?: unknown }).browser;
|
|
220
|
+
if (typeof browser === "object" && browser !== null) {
|
|
221
|
+
const off = Object.entries(browser)
|
|
222
|
+
.filter(([, target]) => target === false)
|
|
223
|
+
.map(([key]) => key);
|
|
224
|
+
const files = off
|
|
225
|
+
.filter((key) => key.startsWith("."))
|
|
226
|
+
.map((key) => CsrDevPaths.resolveRelative(dir, key))
|
|
227
|
+
.filter((file): file is string => !!file);
|
|
228
|
+
map = {
|
|
229
|
+
offFiles: new Set(files.map((file) => CsrDevPaths.realpath(file))),
|
|
230
|
+
offNames: new Set(off.filter((key) => !key.startsWith("."))),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
} catch {
|
|
234
|
+
// An unreadable package.json maps nothing; Bun's build fails on it anyway.
|
|
235
|
+
}
|
|
236
|
+
} else if (parent !== dir) map = this.#browserMapOf(parent);
|
|
237
|
+
this.#browserMaps.set(dir, map);
|
|
238
|
+
return map;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** A module that left the graph takes its records with it. */
|
|
242
|
+
forget(importer: string): void {
|
|
243
|
+
this.#resolution.delete(importer);
|
|
244
|
+
this.#runtimeResolved.delete(importer);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
serializeRuntimeResolved(): Record<string, string[]> {
|
|
248
|
+
return Object.fromEntries(
|
|
249
|
+
[...this.#runtimeResolved]
|
|
250
|
+
.filter(([, specifiers]) => specifiers.size > 0)
|
|
251
|
+
.map(([importer, specifiers]) => [this.#paths.idOf(importer), [...specifiers].sort()]),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// A target moved away is left out, so the graph does not carry it past a builder restart.
|
|
256
|
+
serialize(): Record<string, Record<string, string>> {
|
|
257
|
+
return Object.fromEntries(
|
|
258
|
+
[...this.#resolution].map(([importer, bySpecifier]) => [
|
|
259
|
+
this.#paths.idOf(importer),
|
|
260
|
+
Object.fromEntries(
|
|
261
|
+
[...bySpecifier]
|
|
262
|
+
.filter(([, target]) => CsrDevPaths.isStub(target) || fs.existsSync(target))
|
|
263
|
+
.map(([specifier, target]) => [specifier, CsrDevPaths.isStub(target) ? target : this.#paths.idOf(target)]),
|
|
264
|
+
),
|
|
265
|
+
]),
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
//? Bun.resolveSync keeps what it found per directory until a build walks that directory again, so in the resident
|
|
270
|
+
//? builder it may hand back a file moved away since (`Foo.tsx` → `Foo/index.tsx`, `.ts` → `.tsx`). A relative import
|
|
271
|
+
//? is therefore looked up on the disk as it is now, and a bare one it names a missing file for goes to a fresh worker.
|
|
272
|
+
#resolveUnknown(importer: string, specifier: string, kind: "import" | "require"): string | null {
|
|
273
|
+
if (specifier.startsWith("node:")) return `${CsrDevPaths.stubPrefix}${specifier}`;
|
|
274
|
+
const relative = specifier.startsWith(".") || path.isAbsolute(specifier);
|
|
275
|
+
const cannotResolve = () => new Error(`[csr-dev] cannot resolve "${specifier}" from ${this.#paths.idOf(importer)}`);
|
|
276
|
+
if (relative) {
|
|
277
|
+
const onDisk =
|
|
278
|
+
CsrDevPaths.resolveRelative(path.dirname(importer), specifier, kind) ??
|
|
279
|
+
CsrDevResolver.#onDisk(CsrDevPaths.tryResolve(specifier, path.dirname(importer)));
|
|
280
|
+
if (!onDisk) throw cannotResolve();
|
|
281
|
+
return CsrDevPaths.realpath(onDisk);
|
|
282
|
+
}
|
|
283
|
+
//? Ahead of the prepass too: a new file's bare imports (`akanjs/ui`, `@apps/<app>/client`) are ones its package
|
|
284
|
+
//? already resolved, and asking for the prepass would hand every such save to a build worker.
|
|
285
|
+
const sibling = this.#resolvedBySibling(importer, specifier);
|
|
286
|
+
if (sibling) return sibling;
|
|
287
|
+
const resolved = CsrDevPaths.tryResolve(specifier, path.dirname(importer));
|
|
288
|
+
const moved = resolved !== null && path.isAbsolute(resolved) && !fs.existsSync(resolved);
|
|
289
|
+
//? Bun keeps a process's node_modules listings: a package installed while the builder runs (`bun add`) resolves only
|
|
290
|
+
//? in a fresh process, so one installed since this process began goes to a worker. Any other miss, a subpath typo
|
|
291
|
+
//? of a package installed long ago included, fails here as the worker would.
|
|
292
|
+
const installed =
|
|
293
|
+
resolved === null && CsrDevPaths.installedSince(specifier, path.dirname(importer), this.#startedAt);
|
|
294
|
+
if (!this.#prepassDone || moved) {
|
|
295
|
+
//? A specifier no resolver finds in the user's own code is a typo, failed here like a missing relative file: the
|
|
296
|
+
//? resolution build a worker would run for it fails the same way. A package's `browser` field may map one away.
|
|
297
|
+
if (resolved === null && !installed && !CsrDevPaths.isVendorFile(importer)) throw cannotResolve();
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
if (!resolved) {
|
|
301
|
+
if (installed) return null;
|
|
302
|
+
throw cannotResolve();
|
|
303
|
+
}
|
|
304
|
+
if (!path.isAbsolute(resolved)) return `${CsrDevPaths.stubPrefix}${specifier}`;
|
|
305
|
+
this.#fallbacks.add(specifier);
|
|
306
|
+
this.#runtimeResolved.set(importer, (this.#runtimeResolved.get(importer) ?? new Set<string>()).add(specifier));
|
|
307
|
+
return CsrDevPaths.realpath(resolved);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
static #onDisk(resolved: string | null): string | null {
|
|
311
|
+
return resolved && path.isAbsolute(resolved) && fs.existsSync(resolved) ? resolved : null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
//? The browser build tree-shakes the unused re-exports of a side-effect-free barrel, so their imports have no
|
|
315
|
+
//? recorded resolution; a file of the same package resolves a bare specifier to the same target, conditions included.
|
|
316
|
+
//? Not one the runtime resolver found (it ignores the `browser` condition the prepass exists to honour), not the
|
|
317
|
+
//? importer's own record, and not a target moved away since.
|
|
318
|
+
#resolvedBySibling(importer: string, specifier: string): string | null {
|
|
319
|
+
const scope = CsrDevResolver.#packageScopeOf(importer);
|
|
320
|
+
for (const [other, bySpecifier] of this.#resolution) {
|
|
321
|
+
const target = bySpecifier.get(specifier);
|
|
322
|
+
if (!target || other === importer || this.#runtimeResolved.get(other)?.has(specifier)) continue;
|
|
323
|
+
if (!CsrDevPaths.isStub(target) && !fs.existsSync(target)) continue;
|
|
324
|
+
if (CsrDevResolver.#packageScopeOf(other) === scope) return target;
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
#remember(importer: string, specifier: string, target: string): void {
|
|
330
|
+
const byImporter = this.#resolution.get(importer) ?? new Map<string, string>();
|
|
331
|
+
this.#resolution.set(importer, byImporter.set(specifier, target));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
//? Only the barrel a bare import names is inlined, so its icons tree-shake per importer; what the barrel itself
|
|
335
|
+
//? imports stays a registry module, keeping shared state such as react-icons' IconContext a single instance.
|
|
336
|
+
#isInlineSpecifier(specifier: string): boolean {
|
|
337
|
+
if (specifier.startsWith(".") || path.isAbsolute(specifier) || specifier.startsWith("node:")) return false;
|
|
338
|
+
return this.#context.optimizeImports.some((pattern) => {
|
|
339
|
+
const base = CsrDevResolver.#patternBase(pattern);
|
|
340
|
+
return specifier === base || specifier.startsWith(`${base}/`);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
static #patternBase(pattern: string): string {
|
|
345
|
+
return pattern.endsWith("/*") ? pattern.slice(0, -2) : pattern;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
static #packageNameOf(file: string): string | null {
|
|
349
|
+
const marker = `${path.sep}node_modules${path.sep}`;
|
|
350
|
+
const index = file.lastIndexOf(marker);
|
|
351
|
+
if (index < 0) return null;
|
|
352
|
+
const [scope, name] = file.slice(index + marker.length).split(path.sep);
|
|
353
|
+
return (scope?.startsWith("@") ? `${scope}/${name}` : scope) || null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
static #packageScopeOf(file: string): string {
|
|
357
|
+
const marker = `${path.sep}node_modules${path.sep}`;
|
|
358
|
+
const index = file.lastIndexOf(marker);
|
|
359
|
+
if (index < 0) return "";
|
|
360
|
+
const [scope, name] = file.slice(index + marker.length).split(path.sep);
|
|
361
|
+
return file.slice(0, index + marker.length) + (scope?.startsWith("@") ? `${scope}${path.sep}${name}` : scope);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { PageEntry } from "../artifact/implicitRootLayout";
|
|
2
|
+
|
|
3
|
+
export interface CsrDevGraphModule {
|
|
4
|
+
vendor: boolean;
|
|
5
|
+
mtimeMs: number;
|
|
6
|
+
hash: string;
|
|
7
|
+
deps: string[];
|
|
8
|
+
helpers?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CsrDevGraph {
|
|
12
|
+
version: 1;
|
|
13
|
+
configKey: string;
|
|
14
|
+
refresh: string;
|
|
15
|
+
entries: Record<string, string>;
|
|
16
|
+
modules: Record<string, CsrDevGraphModule>;
|
|
17
|
+
resolution: Record<string, Record<string, string>>;
|
|
18
|
+
/** Specifiers per importer id `resolution` holds from Bun's runtime resolver rather than the browser build. */
|
|
19
|
+
runtimeResolved?: Record<string, string[]>;
|
|
20
|
+
pending: string[];
|
|
21
|
+
/** A build worker's failed round: the ids it built, and the files it failed in or named, by hash. */
|
|
22
|
+
failed?: { roots: string[]; files: Record<string, string> };
|
|
23
|
+
/** The stamp of the signal and dictionary sources the last whole build inlined; see `RegistryMetadataFingerprint`. */
|
|
24
|
+
metadata?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CsrDevContext {
|
|
28
|
+
pageEntries: PageEntry[];
|
|
29
|
+
basePaths: string[];
|
|
30
|
+
htmlBasePaths: string[];
|
|
31
|
+
define: Record<string, string>;
|
|
32
|
+
optimizeImports: string[];
|
|
33
|
+
configKey: string;
|
|
34
|
+
/** Null when the page already loads the React Refresh runtime (an SSR page's import map). */
|
|
35
|
+
refreshFile: string | null;
|
|
36
|
+
/** Bare specifiers the page already loads (an SSR page's import map): required as `vendor:<specifier>`, not compiled. */
|
|
37
|
+
externals?: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CsrDevSharedHelpers {
|
|
41
|
+
hash: string;
|
|
42
|
+
definition: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CsrDevCompiledModule {
|
|
46
|
+
id: string;
|
|
47
|
+
file: string;
|
|
48
|
+
vendor: boolean;
|
|
49
|
+
factory: string;
|
|
50
|
+
helpers: CsrDevSharedHelpers | null;
|
|
51
|
+
sourceMap?: string;
|
|
52
|
+
deps: string[];
|
|
53
|
+
mtimeMs: number;
|
|
54
|
+
hash: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Module factories by id and helper definitions by hash, kept in memory so app.js is written without a pass over the disk. */
|
|
58
|
+
export interface CsrDevCode {
|
|
59
|
+
modules: Map<string, string>;
|
|
60
|
+
helpers: Map<string, string>;
|
|
61
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { PageEntry } from "../artifact/implicitRootLayout";
|
|
3
|
+
|
|
4
|
+
//* The CSR entry file of each basePath's HTML, shared by the CSR artifact and the dev registry. Apart from both
|
|
5
|
+
//* builders so the resident dev builder names entries without loading the artifact builder's CSS compiler (tailwindcss).
|
|
6
|
+
export class CsrEntryFiles {
|
|
7
|
+
static entryFilename(basePath: string): string {
|
|
8
|
+
return `${basePath || "index"}.csr.tsx`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static pageEntriesForBasePath(pageEntries: PageEntry[], basePath: string, basePaths: string[]): PageEntry[] {
|
|
12
|
+
return pageEntries.filter((entry) => {
|
|
13
|
+
const entryBasePath = CsrEntryFiles.basePathOfPageKey(entry.key, basePaths);
|
|
14
|
+
return entryBasePath === null || entryBasePath === basePath;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static basePathOfPageKey(pageKey: string, basePaths: string[]): string | null {
|
|
19
|
+
const normalized = pageKey.split(path.sep).join("/").replace(/^\.\//, "");
|
|
20
|
+
const segments = normalized.split("/");
|
|
21
|
+
const firstPublicSegment = segments.find((segment) => segment !== "[lang]" && !/^\(.+\)$/.test(segment));
|
|
22
|
+
return firstPublicSegment && basePaths.includes(firstPublicSegment) ? firstPublicSegment : null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -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),
|