@akanjs/devkit 3.0.0-beta.9 → 3.0.0
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 +21 -7
- package/README.md +17 -8
- 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 +439 -141
- package/akanConfig/akanConfig.ts +359 -275
- package/akanConfig/index.ts +1 -0
- package/akanConfig/types.ts +22 -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 +270 -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 +182 -0
- package/mobile/desktopBin.ts +144 -0
- package/mobile/desktopServerStage.test.ts +252 -0
- package/mobile/desktopServerStage.ts +178 -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 +184 -0
- package/mobile/nativeApp.ts +454 -0
- package/mobile/nativeAppLine.test.ts +69 -0
- package/mobile/nativeAppLine.ts +71 -0
- package/mobile/nativeConfig.test.ts +438 -0
- package/mobile/nativeConfig.ts +294 -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 +9 -25
- package/subspace.ts +48 -115
- package/subspaceConfig.ts +3 -11
- 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
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { Logger } from "akanjs/common";
|
|
3
3
|
import { compile } from "tailwindcss";
|
|
4
|
+
import { SOURCE_EXTS } from "../akanApp/devHostPolicy";
|
|
4
5
|
import type { App } from "../commandDecorators";
|
|
5
6
|
import { BarrelAnalyzer } from "../transforms/barrelAnalyzer";
|
|
6
7
|
import { createTsconfigPackageResolver, rewriteBarrelImports } from "../transforms/barrelImportsPlugin";
|
|
8
|
+
import { CsrEntryFiles } from "./csrEntryFiles";
|
|
7
9
|
import { CssCandidateCache } from "./cssCandidateCache";
|
|
8
10
|
import { CssImportResolver } from "./cssImportResolver";
|
|
9
11
|
|
|
10
|
-
const SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"] as const;
|
|
11
12
|
const NON_SOURCE_EXT_RE = /\.(json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav)$/i;
|
|
12
13
|
const NODE_MODULES_RE = /[\\/]node_modules[\\/]/;
|
|
13
14
|
const AKANJS_NODE_MODULE_RE = /[\\/]node_modules[\\/]akanjs[\\/]/;
|
|
@@ -17,7 +18,6 @@ interface CssDiscovery {
|
|
|
17
18
|
sourcePaths: string[];
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
/** One `@import` target and the custom properties it declares, which is what proves it arrived downstream. */
|
|
21
21
|
export interface ImportedStylesheet {
|
|
22
22
|
cssPath: string;
|
|
23
23
|
declaredNames: string[];
|
|
@@ -32,32 +32,18 @@ export class CssCompiler {
|
|
|
32
32
|
this.#app = app;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
* Beside the sources rather than in `dist`, because the cache is keyed on source mtimes: a build and
|
|
37
|
-
* the dev server describe the same files, so they should share it rather than each pay the first scan.
|
|
38
|
-
*/
|
|
35
|
+
// Beside the sources, not in `dist`: the build and the dev server scan the same files, so they share one cache.
|
|
39
36
|
get #candidateCachePath() {
|
|
40
37
|
return path.join(this.#app.cwdPath, ".akan/cache/cssCandidates.json");
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
#cssText: string | null = null;
|
|
44
41
|
#cssTextByBasePath: Record<string, string> | null = null;
|
|
45
|
-
|
|
46
|
-
* Import resolution memoised for the life of one compiler, which is one rebuild.
|
|
47
|
-
*
|
|
48
|
-
* The discovery BFS resolves the same specifier from every directory that imports it, and each miss
|
|
49
|
-
* costs up to 13 sequential `exists()` calls — the bare path, six extensions, six `index.*`. Nothing
|
|
50
|
-
* here outlives the rebuild, so there is no invalidation to get wrong.
|
|
51
|
-
*/
|
|
42
|
+
// Memoised for one compiler, i.e. one rebuild: nothing outlives it, so there is nothing to invalidate.
|
|
52
43
|
#fileExistsCache = new Map<string, Promise<boolean>>();
|
|
53
44
|
#resolvedFileCache = new Map<string, Promise<string | null>>();
|
|
54
45
|
#resolvedSpecifierCache = new Map<string, Promise<string | null>>();
|
|
55
|
-
/** Every stylesheet this compile reached, entry points and `@import` targets alike. */
|
|
56
46
|
#discoveredCssPaths = new Set<string>();
|
|
57
|
-
/**
|
|
58
|
-
* `@import` targets per base path, so whoever writes the asset can check the file it actually wrote — the
|
|
59
|
-
* compiled text and the written artifact are two different places a declaration can go missing.
|
|
60
|
-
*/
|
|
61
47
|
importedStylesheetsByBasePath: Record<string, ImportedStylesheet[]> = {};
|
|
62
48
|
|
|
63
49
|
#fileExists(absPath: string): Promise<boolean> {
|
|
@@ -107,41 +93,31 @@ export class CssCompiler {
|
|
|
107
93
|
const pageKeys = await this.#app.getPageKeys({ refresh });
|
|
108
94
|
const basePaths = [...akanConfig.basePaths];
|
|
109
95
|
const rootPageKeys = pageKeys.filter((pageKey) => getPageKeyBasePath(pageKey, basePaths) === null);
|
|
96
|
+
const compileBasePath = async (basePath: string, basePathPageKeys: string[], label = basePath) => {
|
|
97
|
+
if (basePathPageKeys.length === 0) return [basePath, ""] as const;
|
|
98
|
+
const started = Date.now();
|
|
99
|
+
const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: basePathPageKeys });
|
|
100
|
+
const { css, imported } = await this.#compileWithImports(cssPaths, sourcePaths);
|
|
101
|
+
this.importedStylesheetsByBasePath[basePath] = imported;
|
|
102
|
+
this.#logger.verbose(
|
|
103
|
+
`css base=${label} paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`,
|
|
104
|
+
);
|
|
105
|
+
return [basePath, css] as const;
|
|
106
|
+
};
|
|
110
107
|
const cssEntries = await Promise.all([
|
|
111
|
-
(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
`css base=root paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`,
|
|
119
|
-
);
|
|
120
|
-
return ["", css] as const;
|
|
121
|
-
})(),
|
|
122
|
-
...basePaths.map(async (basePath) => {
|
|
123
|
-
const basePathPageKeys = pageKeys.filter((pageKey) => getPageKeyBasePath(pageKey, basePaths) === basePath);
|
|
124
|
-
if (basePathPageKeys.length === 0) return [basePath, ""] as const;
|
|
125
|
-
const started = Date.now();
|
|
126
|
-
const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: basePathPageKeys });
|
|
127
|
-
const { css, imported } = await this.#compileWithImports(cssPaths, sourcePaths);
|
|
128
|
-
this.importedStylesheetsByBasePath[basePath] = imported;
|
|
129
|
-
this.#logger.verbose(
|
|
130
|
-
`css base=${basePath} paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`,
|
|
131
|
-
);
|
|
132
|
-
return [basePath, css] as const;
|
|
133
|
-
}),
|
|
108
|
+
compileBasePath("", rootPageKeys, "root"),
|
|
109
|
+
...basePaths.map((basePath) =>
|
|
110
|
+
compileBasePath(
|
|
111
|
+
basePath,
|
|
112
|
+
pageKeys.filter((pageKey) => getPageKeyBasePath(pageKey, basePaths) === basePath),
|
|
113
|
+
),
|
|
114
|
+
),
|
|
134
115
|
]);
|
|
135
116
|
this.#cssTextByBasePath = Object.fromEntries(cssEntries);
|
|
136
117
|
await this.#warnUnreachableStylesheets();
|
|
137
118
|
return this.#cssTextByBasePath;
|
|
138
119
|
}
|
|
139
120
|
|
|
140
|
-
/**
|
|
141
|
-
* A stylesheet under `page/` reaches the build only by being imported from a route source. One that nothing
|
|
142
|
-
* imports compiles to nothing and reports success, which is indistinguishable from an empty theme — so say it
|
|
143
|
-
* out loud once per compile rather than leaving it to be noticed as unstyled elements in the browser.
|
|
144
|
-
*/
|
|
145
121
|
async #warnUnreachableStylesheets() {
|
|
146
122
|
const pageDir = path.join(this.#app.cwdPath, "page");
|
|
147
123
|
const glob = new Bun.Glob("**/*.css");
|
|
@@ -225,11 +201,7 @@ export class CssCompiler {
|
|
|
225
201
|
return { cssPaths, sourcePaths: [...sourceFiles] };
|
|
226
202
|
}
|
|
227
203
|
|
|
228
|
-
|
|
229
|
-
* `libs/<lib>/ui/tokens.css` of every lib the page graph reached, so a lib can own the fixed colours its own
|
|
230
|
-
* components need instead of each consuming app re-declaring them. Ordered ahead of the app's stylesheets:
|
|
231
|
-
* the app is the last word on any variable both declare.
|
|
232
|
-
*/
|
|
204
|
+
// Lib tokens go ahead of the app's stylesheets, so the app has the last word on a variable both declare.
|
|
233
205
|
async #libTokenStylesheets(sourceFiles: Set<string>): Promise<string[]> {
|
|
234
206
|
const libsRoot = path.join(this.#app.workspace.workspaceRoot, "libs");
|
|
235
207
|
const libNames = new Set<string>();
|
|
@@ -252,10 +224,7 @@ export class CssCompiler {
|
|
|
252
224
|
return css;
|
|
253
225
|
}
|
|
254
226
|
|
|
255
|
-
|
|
256
|
-
* The collector is per compile rather than per compiler instance: `getCssByBasePath` compiles every base path
|
|
257
|
-
* concurrently, so instance state would mix one base path's imports into another's check.
|
|
258
|
-
*/
|
|
227
|
+
// A per-compile collector: getCssByBasePath compiles base paths concurrently, so instance state would mix them.
|
|
259
228
|
async #compileWithImports(
|
|
260
229
|
cssPaths: string[],
|
|
261
230
|
sourcePaths: string[],
|
|
@@ -279,7 +248,6 @@ export class CssCompiler {
|
|
|
279
248
|
|
|
280
249
|
const sourceDirs = new Set<string>();
|
|
281
250
|
for (const entry of compilers) {
|
|
282
|
-
if (!entry) continue;
|
|
283
251
|
for (const s of entry.compiler.sources as { base: string }[]) sourceDirs.add(s.base);
|
|
284
252
|
}
|
|
285
253
|
const scanStarted = Date.now();
|
|
@@ -290,7 +258,6 @@ export class CssCompiler {
|
|
|
290
258
|
const parts: string[] = [];
|
|
291
259
|
const imported: ImportedStylesheet[] = [];
|
|
292
260
|
for (const entry of compilers) {
|
|
293
|
-
if (!entry) continue;
|
|
294
261
|
const part = entry.compiler.build(candidates);
|
|
295
262
|
parts.push(part);
|
|
296
263
|
for (const [cssPath, content] of entry.imported) {
|
|
@@ -317,28 +284,19 @@ export class CssCompiler {
|
|
|
317
284
|
return { path: p, base: path.dirname(p), content };
|
|
318
285
|
}
|
|
319
286
|
|
|
320
|
-
|
|
321
|
-
* Every specifier is verified here, path-shaped ones included. An `@import` the pipeline cannot resolve is
|
|
322
|
-
* a build error and never a no-op: the vocabulary closure means a component whose token declaration failed
|
|
323
|
-
* to load renders unstyled, which nothing downstream can distinguish from a design choice.
|
|
324
|
-
*/
|
|
287
|
+
// An unresolvable @import throws: under vocabulary closure a missing token file just renders unstyled.
|
|
325
288
|
async #resolveCssImport(id: string, fromBase: string): Promise<string> {
|
|
326
289
|
if (id.startsWith(".") || id.startsWith("/")) {
|
|
327
290
|
const filePath = path.resolve(fromBase, id);
|
|
328
291
|
if (await this.#fileExists(filePath)) return filePath;
|
|
329
292
|
throw new Error(`[css] failed to resolve stylesheet import "${id}" from ${fromBase} (no file at ${filePath})`);
|
|
330
293
|
}
|
|
331
|
-
|
|
332
|
-
const resolved = await
|
|
294
|
+
this.#cssImportResolver ??= await CssImportResolver.create(this.#app);
|
|
295
|
+
const resolved = await this.#cssImportResolver.resolve(id, fromBase);
|
|
333
296
|
if (resolved) return resolved;
|
|
334
297
|
throw new Error(`[css] failed to resolve stylesheet import "${id}" from ${fromBase}`);
|
|
335
298
|
}
|
|
336
299
|
|
|
337
|
-
async #getCssImportResolver() {
|
|
338
|
-
if (this.#cssImportResolver) return this.#cssImportResolver;
|
|
339
|
-
this.#cssImportResolver = await CssImportResolver.create(this.#app);
|
|
340
|
-
return this.#cssImportResolver;
|
|
341
|
-
}
|
|
342
300
|
async #loadModule(id: string, fromBase: string) {
|
|
343
301
|
const p = require.resolve(id, { paths: [fromBase] });
|
|
344
302
|
const mod = await import(p);
|
|
@@ -349,10 +307,7 @@ export class CssCompiler {
|
|
|
349
307
|
fromBase: string,
|
|
350
308
|
resolvePackage: Awaited<ReturnType<typeof createTsconfigPackageResolver>>,
|
|
351
309
|
): Promise<string | null> {
|
|
352
|
-
// Keyed by importer directory even for bare specifiers: the
|
|
353
|
-
// directory-independent, but the `Bun.resolveSync` / `require.resolve` fallbacks below are not.
|
|
354
|
-
// The expensive part — the `exists()` probes — is deduplicated by absolute path instead, which is
|
|
355
|
-
// shared across every importer.
|
|
310
|
+
// Keyed by importer directory even for bare specifiers: the Bun/require fallbacks are directory-dependent.
|
|
356
311
|
const cacheKey = `${fromBase}\0${id}`;
|
|
357
312
|
let cached = this.#resolvedSpecifierCache.get(cacheKey);
|
|
358
313
|
if (cached) return cached;
|
|
@@ -423,17 +378,14 @@ function resolveSourceWithRequire(id: string, fromBase: string): string | null {
|
|
|
423
378
|
}
|
|
424
379
|
|
|
425
380
|
function isSourceFile(filePath: string) {
|
|
426
|
-
return SOURCE_EXTS.
|
|
381
|
+
return SOURCE_EXTS.has(path.extname(filePath));
|
|
427
382
|
}
|
|
428
383
|
|
|
429
384
|
export function isIgnoredNodeModuleSource(filePath: string): boolean {
|
|
430
385
|
return NODE_MODULES_RE.test(filePath) && !AKANJS_NODE_MODULE_RE.test(filePath);
|
|
431
386
|
}
|
|
432
387
|
|
|
433
|
-
/**
|
|
434
|
-
* `@theme` blocks are stripped first: those variables are emitted only when a utility uses one, so their
|
|
435
|
-
* absence from a build says nothing about whether the stylesheet arrived.
|
|
436
|
-
*/
|
|
388
|
+
/** Skips `@theme` blocks: Tailwind emits those variables only when a utility uses one. */
|
|
437
389
|
export function declaredCustomProperties(css: string): string[] {
|
|
438
390
|
const withoutThemeBlocks = css.replace(/@theme[^{]*\{[^}]*\}/g, "");
|
|
439
391
|
return [...new Set([...withoutThemeBlocks.matchAll(/(?:^|[\s;{])(--[\w-]+)\s*:/g)].map(([, name]) => name))].filter(
|
|
@@ -442,8 +394,5 @@ export function declaredCustomProperties(css: string): string[] {
|
|
|
442
394
|
}
|
|
443
395
|
|
|
444
396
|
export function getPageKeyBasePath(pageKey: string, basePaths: string[]): string | null {
|
|
445
|
-
|
|
446
|
-
const segments = normalized.split("/");
|
|
447
|
-
const firstPublicSegment = segments.find((segment) => segment !== "[lang]" && !/^\(.+\)$/.test(segment));
|
|
448
|
-
return firstPublicSegment && basePaths.includes(firstPublicSegment) ? firstPublicSegment : null;
|
|
397
|
+
return CsrEntryFiles.basePathOfPageKey(pageKey, basePaths);
|
|
449
398
|
}
|
|
@@ -25,8 +25,8 @@ export class CssImportResolver {
|
|
|
25
25
|
async resolve(id: string, fromBase: string): Promise<string | null> {
|
|
26
26
|
for (const resolve of [
|
|
27
27
|
() => this.#resolveWithTsconfig(id),
|
|
28
|
-
() => this.#
|
|
29
|
-
() => this.#
|
|
28
|
+
() => this.#resolveCssWith(fromBase, (base) => Bun.resolveSync(id, base)),
|
|
29
|
+
() => this.#resolveCssWith(fromBase, (base) => require.resolve(id, { paths: [base] })),
|
|
30
30
|
() => this.#resolvePackageStyle(id, fromBase),
|
|
31
31
|
]) {
|
|
32
32
|
const resolved = await resolve();
|
|
@@ -35,22 +35,10 @@ export class CssImportResolver {
|
|
|
35
35
|
return null;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
#
|
|
38
|
+
#resolveCssWith(fromBase: string, resolveFrom: (base: string) => string): string | null {
|
|
39
39
|
for (const base of this.#resolutionBases(fromBase)) {
|
|
40
40
|
try {
|
|
41
|
-
const resolved =
|
|
42
|
-
if (CssImportResolver.isCssFile(resolved)) return resolved;
|
|
43
|
-
} catch {
|
|
44
|
-
// Try the next known package resolution root.
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
#resolveWithRequire(id: string, fromBase: string): string | null {
|
|
51
|
-
for (const base of this.#resolutionBases(fromBase)) {
|
|
52
|
-
try {
|
|
53
|
-
const resolved = require.resolve(id, { paths: [base] });
|
|
41
|
+
const resolved = resolveFrom(base);
|
|
54
42
|
if (CssImportResolver.isCssFile(resolved)) return resolved;
|
|
55
43
|
} catch {
|
|
56
44
|
// Try the next known package resolution root.
|
|
@@ -111,8 +99,7 @@ export class CssImportResolver {
|
|
|
111
99
|
? exportValue
|
|
112
100
|
: exportValue?.style || exportValue?.import || exportValue?.default;
|
|
113
101
|
if (exportedEntry) return await this.#firstExisting(path.resolve(pkgDir, exportedEntry));
|
|
114
|
-
//* A subpath
|
|
115
|
-
//* style entry here would load a different stylesheet than the author asked for and report success.
|
|
102
|
+
//* A subpath resolves literally: the package's own style entry would silently load a different stylesheet.
|
|
116
103
|
if (subpath !== ".") return await this.#firstExisting(path.resolve(pkgDir, subpath));
|
|
117
104
|
return await this.#firstExisting(path.resolve(pkgDir, pkg.exports?.["."]?.style || pkg.style || "index.css"));
|
|
118
105
|
} catch {
|
|
@@ -135,7 +122,7 @@ export class CssImportResolver {
|
|
|
135
122
|
|
|
136
123
|
async #firstExisting(basePath: string): Promise<string | null> {
|
|
137
124
|
for (const suffix of CSS_IMPORT_EXTS) {
|
|
138
|
-
const candidate = `${basePath}${suffix}
|
|
125
|
+
const candidate = path.normalize(`${basePath}${suffix}`);
|
|
139
126
|
if (await Bun.file(candidate).exists()) return candidate;
|
|
140
127
|
}
|
|
141
128
|
return null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import type { ChangeKind, DevChangeAction, DevChangePlan, DevChangeRole } from "akanjs/server";
|
|
3
|
+
import { SOURCE_EXTS } from "../akanApp/devHostPolicy";
|
|
3
4
|
|
|
4
|
-
const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
5
5
|
const CONFIG_BASENAMES = new Set(["akan.config.ts", "bunfig.toml", "tsconfig.json", "package.json"]);
|
|
6
6
|
const BARREL_FACETS = new Set(["common", "srvkit", "ui", "webkit", "plugin"]);
|
|
7
7
|
const CLIENT_SUFFIXES = [".Template.tsx", ".Unit.tsx", ".Util.tsx", ".View.tsx", ".Zone.tsx", ".store.ts"];
|
|
@@ -93,15 +93,15 @@ export class DevChangePlanner {
|
|
|
93
93
|
roles.add("barrel");
|
|
94
94
|
reasons.add(isGenerated ? "generated-index" : "barrel-facet-child");
|
|
95
95
|
}
|
|
96
|
-
if (isSource && this.#isServerBiased(
|
|
96
|
+
if (isSource && this.#isServerBiased(base, parts)) {
|
|
97
97
|
roles.add("server");
|
|
98
98
|
reasons.add("server-path");
|
|
99
99
|
}
|
|
100
|
-
if (isSource && this.#isClientBiased(
|
|
100
|
+
if (isSource && this.#isClientBiased(base, parts)) {
|
|
101
101
|
roles.add("client");
|
|
102
102
|
reasons.add("client-path");
|
|
103
103
|
}
|
|
104
|
-
if (isSource && this.#isSharedBiased(
|
|
104
|
+
if (isSource && this.#isSharedBiased(base, parts)) {
|
|
105
105
|
roles.add("shared");
|
|
106
106
|
reasons.add("shared-path");
|
|
107
107
|
}
|
|
@@ -123,8 +123,7 @@ export class DevChangePlanner {
|
|
|
123
123
|
return roles;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
#isServerBiased(
|
|
127
|
-
const base = path.basename(abs);
|
|
126
|
+
#isServerBiased(base: string, parts: string[]): boolean {
|
|
128
127
|
return (
|
|
129
128
|
parts.includes("srvkit") ||
|
|
130
129
|
SERVER_SUFFIXES.some((suffix) => base.endsWith(suffix)) ||
|
|
@@ -133,8 +132,7 @@ export class DevChangePlanner {
|
|
|
133
132
|
);
|
|
134
133
|
}
|
|
135
134
|
|
|
136
|
-
#isClientBiased(
|
|
137
|
-
const base = path.basename(abs);
|
|
135
|
+
#isClientBiased(base: string, parts: string[]): boolean {
|
|
138
136
|
return (
|
|
139
137
|
parts.includes("ui") ||
|
|
140
138
|
parts.includes("webkit") ||
|
|
@@ -143,8 +141,7 @@ export class DevChangePlanner {
|
|
|
143
141
|
);
|
|
144
142
|
}
|
|
145
143
|
|
|
146
|
-
#isSharedBiased(
|
|
147
|
-
const base = path.basename(abs);
|
|
144
|
+
#isSharedBiased(base: string, parts: string[]): boolean {
|
|
148
145
|
return (
|
|
149
146
|
parts.includes("common") ||
|
|
150
147
|
SHARED_SUFFIXES.some((suffix) => base.endsWith(suffix)) ||
|
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir,
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
4
|
+
import { tempDirs } from "../testHelpers";
|
|
5
5
|
import { DevGeneratedIndexSync } from "./devGeneratedIndexSync";
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
const makeTempRoot = async () => {
|
|
10
|
-
const root = await mkdtemp(path.join(os.tmpdir(), "akan-generated-index-"));
|
|
11
|
-
tempRoots.push(root);
|
|
12
|
-
return root;
|
|
13
|
-
};
|
|
7
|
+
const makeTempRoot = tempDirs("akan-generated-index-");
|
|
14
8
|
|
|
15
9
|
const seedFacet = async (root: string, facet: string, { files, dirs }: { files: string[]; dirs: string[] }) => {
|
|
16
10
|
const dir = path.join(root, "libs", "util", facet);
|
|
@@ -28,23 +22,19 @@ const barrelFor = async (root: string, facet: string, seed: { files: string[]; d
|
|
|
28
22
|
return readFile(path.join(dir, "index.ts"), "utf8");
|
|
29
23
|
};
|
|
30
24
|
|
|
31
|
-
afterEach(async () => {
|
|
32
|
-
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
33
|
-
});
|
|
34
|
-
|
|
35
25
|
describe("DevGeneratedIndexSync facet barrels", () => {
|
|
36
26
|
test("camelCase facets export only clean camelCase names", async () => {
|
|
37
27
|
const content = await barrelFor(await makeTempRoot(), "srvkit", {
|
|
38
28
|
files: [
|
|
39
29
|
"aes.ts",
|
|
40
30
|
"cloudflareApi.ts",
|
|
41
|
-
"cloudflareApi.helper.ts",
|
|
42
|
-
"pushNotificationServer.type.ts",
|
|
43
|
-
"PushNotificationServer.ts",
|
|
44
|
-
"my_snake.ts",
|
|
45
|
-
"kebab-case.ts",
|
|
31
|
+
"cloudflareApi.helper.ts",
|
|
32
|
+
"pushNotificationServer.type.ts",
|
|
33
|
+
"PushNotificationServer.ts",
|
|
34
|
+
"my_snake.ts",
|
|
35
|
+
"kebab-case.ts",
|
|
46
36
|
],
|
|
47
|
-
dirs: ["storageApi", "BadDir"],
|
|
37
|
+
dirs: ["storageApi", "BadDir"],
|
|
48
38
|
trigger: "aes.ts",
|
|
49
39
|
});
|
|
50
40
|
expect(content).toBe(`export * from "./aes";\nexport * from "./cloudflareApi";\nexport * from "./storageApi";\n`);
|
|
@@ -52,13 +42,8 @@ describe("DevGeneratedIndexSync facet barrels", () => {
|
|
|
52
42
|
|
|
53
43
|
test("ui facet exports only clean PascalCase names", async () => {
|
|
54
44
|
const content = await barrelFor(await makeTempRoot(), "ui", {
|
|
55
|
-
files: [
|
|
56
|
-
|
|
57
|
-
"AkanLogo.tsx",
|
|
58
|
-
"Globe_Dynamic.tsx", // underscore → skipped
|
|
59
|
-
"lowerStart.tsx", // camelCase in ui facet → skipped
|
|
60
|
-
],
|
|
61
|
-
dirs: ["Code", "badDir"], // camelCase dir → skipped
|
|
45
|
+
files: ["Globe.tsx", "AkanLogo.tsx", "Globe_Dynamic.tsx", "lowerStart.tsx"],
|
|
46
|
+
dirs: ["Code", "badDir"],
|
|
62
47
|
trigger: "Globe.tsx",
|
|
63
48
|
});
|
|
64
49
|
expect(content).toBe(`export * from "./AkanLogo";\nexport * from "./Code";\nexport * from "./Globe";\n`);
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, rm, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { capitalize } from "akanjs/common";
|
|
3
4
|
import { FileSys } from "../fileSys";
|
|
4
5
|
|
|
5
6
|
const BARREL_FACETS = new Set(["common", "srvkit", "ui", "webkit", "plugin"]);
|
|
6
7
|
const FACET_SOURCE_FILE_RE = /\.(ts|tsx)$/;
|
|
7
8
|
const FACET_EXCLUDED_FILE_RE = /(^index\.tsx?$|\.d\.ts$|\.(test|spec)\.(ts|tsx)$|\.css$|\.scss$|\.sass$)/;
|
|
8
|
-
// `ui` exports PascalCase names only; `common`/`srvkit`/`webkit` export camelCase names only. Names with
|
|
9
|
-
// dots, underscores, or hyphens (e.g. `foo.helper`, `Globe_Dynamic`, `kebab-case`) match neither and are skipped.
|
|
10
9
|
const FACET_PASCAL_CASE_RE = /^[A-Z][A-Za-z0-9]*$/;
|
|
11
10
|
const FACET_CAMEL_CASE_RE = /^[a-z][A-Za-z0-9]*$/;
|
|
12
11
|
const MODULE_UI_TYPES = ["Template", "Unit", "Util", "View", "Zone"] as const;
|
|
@@ -159,6 +158,4 @@ const exists = async (file: string) =>
|
|
|
159
158
|
.then(() => true)
|
|
160
159
|
.catch(() => false);
|
|
161
160
|
|
|
162
|
-
const capitalize = (value: string) => `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
163
|
-
|
|
164
161
|
const formatError = (err: unknown) => (err instanceof Error ? err.message : String(err));
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isAkanRuntimeMetadataFile } from "akanjs/server/hmr/runtimeMetadataFile";
|
|
4
|
+
import type { App } from "../commandDecorators";
|
|
5
|
+
import { CsrDevArtifactWriter } from "./csrDevArtifactWriter";
|
|
6
|
+
import { CsrDevModuleCompiler } from "./csrDevModuleCompiler";
|
|
7
|
+
import { CsrDevPatcher } from "./csrDevPatcher";
|
|
8
|
+
import { CsrDevPaths } from "./csrDevPaths";
|
|
9
|
+
import { CsrDevResolver } from "./csrDevResolver";
|
|
10
|
+
import type { CsrDevCompiledModule, CsrDevContext, CsrDevGraph } from "./csrDevTypes";
|
|
11
|
+
import { RegistryMetadataFingerprint } from "./registryMetadataFingerprint";
|
|
12
|
+
import { WatchRootResolver } from "./watchRootResolver";
|
|
13
|
+
|
|
14
|
+
export interface CsrDevUpdate {
|
|
15
|
+
generation: number;
|
|
16
|
+
reload: boolean;
|
|
17
|
+
reason?: string;
|
|
18
|
+
patchUrl?: string;
|
|
19
|
+
changedIds: string[];
|
|
20
|
+
moduleCount: number;
|
|
21
|
+
epoch?: number;
|
|
22
|
+
/** The registry did not exist before this build, so no tab holds a module of it. */
|
|
23
|
+
first?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CsrDevUpdateOptions {
|
|
27
|
+
/** Called once an update exists: for a patch before app.js is rewritten, for a reload after everything is on disk. */
|
|
28
|
+
announce?: (update: CsrDevUpdate) => void;
|
|
29
|
+
/** Files the registry must hold once this update is done; the ones it lacks are compiled into it. */
|
|
30
|
+
roots?: string[];
|
|
31
|
+
/**
|
|
32
|
+
* Compile only the roots the registry lacks: a route build asks for its entries, and a file saved meanwhile is the
|
|
33
|
+
* save's own patch to make, with the hold only that patch knows.
|
|
34
|
+
*/
|
|
35
|
+
onlyRoots?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DevRegistryTarget {
|
|
39
|
+
dirName: string;
|
|
40
|
+
routePrefix: string;
|
|
41
|
+
library: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
//* A dev module registry under `.akan/artifact/<dirName>`: this owns the context, the entries and the full build;
|
|
45
|
+
//* `CsrDevPatcher` owns the incremental path, which the resident builder keeps in memory between saves.
|
|
46
|
+
export abstract class DevRegistryBundler {
|
|
47
|
+
readonly outDir: string;
|
|
48
|
+
readonly routePrefix: string;
|
|
49
|
+
readonly paths: CsrDevPaths;
|
|
50
|
+
readonly writer: CsrDevArtifactWriter;
|
|
51
|
+
protected readonly app: App;
|
|
52
|
+
protected readonly entryDir: string;
|
|
53
|
+
/** A CSR entry holds the route table, so moving one reloads the tab; an SSR root is one more module to hold. */
|
|
54
|
+
abstract readonly reloadsOnEntryChange: boolean;
|
|
55
|
+
|
|
56
|
+
constructor(app: App, { dirName, routePrefix, library }: DevRegistryTarget) {
|
|
57
|
+
this.app = app;
|
|
58
|
+
this.routePrefix = routePrefix;
|
|
59
|
+
this.paths = new CsrDevPaths(app.workspace.workspaceRoot);
|
|
60
|
+
this.outDir = path.join(app.cwdPath, ".akan/artifact", dirName);
|
|
61
|
+
this.entryDir = path.join(app.cwdPath, ".akan/generated", dirName);
|
|
62
|
+
this.writer = new CsrDevArtifactWriter(this.outDir, { routePrefix, library });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Null when there is nothing to build. */
|
|
66
|
+
abstract context(): Promise<CsrDevContext | null>;
|
|
67
|
+
|
|
68
|
+
abstract writeEntries(context: CsrDevContext): Promise<{ files: Record<string, string>; changed: string[] }>;
|
|
69
|
+
|
|
70
|
+
/** Every file a full build compiles from, the entries included. */
|
|
71
|
+
protected abstract rootFiles(entryFiles: string[]): Promise<string[]>;
|
|
72
|
+
|
|
73
|
+
// A build worker: a throwaway patcher that starts from the disk, and builds everything itself when it cannot patch.
|
|
74
|
+
async update(changedFiles: string[] = [], options: CsrDevUpdateOptions = {}): Promise<CsrDevUpdate | null> {
|
|
75
|
+
const result = await new CsrDevPatcher(this).update(changedFiles, { ...options, allowWholeAppBuilds: true });
|
|
76
|
+
if (result.kind === "unchanged") return null;
|
|
77
|
+
if (result.kind === "update") return result.update;
|
|
78
|
+
const built = await this.fullBuild(result.context, result.generation, result.reason);
|
|
79
|
+
const update = result.first ? { ...built, first: true } : built;
|
|
80
|
+
options.announce?.(update);
|
|
81
|
+
return update;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Of these files, the ones the registry still reaches from its roots through imports: those it keeps building. */
|
|
85
|
+
async wantedRoots(files: string[], graph: CsrDevGraph): Promise<Set<string>> {
|
|
86
|
+
const reached = await this.reachable(graph);
|
|
87
|
+
return new Set(files.filter((file) => reached.has(this.paths.idOf(file))));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The ids its roots reach through imports, the refresh runtime included. */
|
|
91
|
+
async reachable(graph: CsrDevGraph): Promise<Set<string>> {
|
|
92
|
+
const roots = await this.rootFiles(Object.values(graph.entries).map((id) => this.paths.fileOf(id)));
|
|
93
|
+
const reached = new Set<string>();
|
|
94
|
+
const stack = [graph.refresh, ...roots.map((file) => this.paths.idOf(file))];
|
|
95
|
+
for (let id = stack.pop(); id !== undefined; id = stack.pop()) {
|
|
96
|
+
if (reached.has(id)) continue;
|
|
97
|
+
reached.add(id);
|
|
98
|
+
stack.push(...(graph.modules[id]?.deps ?? []));
|
|
99
|
+
}
|
|
100
|
+
return reached;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async holds(files: string[]): Promise<boolean> {
|
|
104
|
+
const graph = await this.writer.readJson<CsrDevGraph>("graph.json");
|
|
105
|
+
return graph !== null && files.every((file) => graph.modules[this.paths.idOf(file)] !== undefined);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A save of one goes to a whole build: `lib/useClient.ts` inlines what they declare through macros. */
|
|
109
|
+
isMetadataFile(file: string): boolean {
|
|
110
|
+
return isAkanRuntimeMetadataFile(file);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async metadataFingerprint(): Promise<string> {
|
|
114
|
+
const libsContainer = path.resolve(this.app.workspace.workspaceRoot, "libs");
|
|
115
|
+
const roots = await new WatchRootResolver(this.app).resolve();
|
|
116
|
+
// The whole container comes back when the lib dependencies are unknown (before the first sync).
|
|
117
|
+
const libRoots = roots.includes(libsContainer)
|
|
118
|
+
? fs.readdirSync(libsContainer).map((name) => path.join(libsContainer, name))
|
|
119
|
+
: roots.filter((root) => path.dirname(root) === libsContainer);
|
|
120
|
+
return await RegistryMetadataFingerprint.of([path.resolve(this.app.cwdPath), ...libRoots], (file) =>
|
|
121
|
+
this.isMetadataFile(file),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
//? Compiled before anything is written: a build that fails (a module the user broke) leaves the registry the tabs
|
|
126
|
+
//? and the next patch read as it was, where clearing the directory first left no registry at all.
|
|
127
|
+
async fullBuild(context: CsrDevContext, generation: number, reason: string): Promise<CsrDevUpdate> {
|
|
128
|
+
const metadata = await this.metadataFingerprint();
|
|
129
|
+
const entries = await this.writeEntries(context);
|
|
130
|
+
const roots = await this.rootFiles(Object.values(entries.files));
|
|
131
|
+
const resolver = new CsrDevResolver({ paths: this.paths, context, entryFiles: roots });
|
|
132
|
+
await resolver.prepass();
|
|
133
|
+
const compiler = this.compiler(context, resolver);
|
|
134
|
+
const { modules: compiled } = await compiler.compile(
|
|
135
|
+
[...roots, ...(context.refreshFile ? [context.refreshFile] : [])],
|
|
136
|
+
new Set(),
|
|
137
|
+
);
|
|
138
|
+
const graph: CsrDevGraph = {
|
|
139
|
+
version: 1,
|
|
140
|
+
configKey: context.configKey,
|
|
141
|
+
refresh: context.refreshFile
|
|
142
|
+
? this.paths.idOf(context.refreshFile)
|
|
143
|
+
: `${CsrDevPaths.vendorPrefix}react-refresh/runtime`,
|
|
144
|
+
entries: this.entryIds(entries.files),
|
|
145
|
+
modules: {},
|
|
146
|
+
resolution: {},
|
|
147
|
+
pending: [],
|
|
148
|
+
metadata,
|
|
149
|
+
};
|
|
150
|
+
this.merge(graph, resolver, compiled);
|
|
151
|
+
await this.writer.markBuilding();
|
|
152
|
+
await this.writer.writeModules(compiled);
|
|
153
|
+
const vendorFile = await this.writer.writeVendor(graph);
|
|
154
|
+
const epoch = Date.now();
|
|
155
|
+
await this.writer.writeApp(graph, { generation, vendorFile, epoch });
|
|
156
|
+
await this.writer.writeState(graph, { version: 1, generation, vendorFile, entries: graph.entries, epoch });
|
|
157
|
+
await this.writer.pruneAfterFullBuild(graph, vendorFile, { generation, assets: compiler.assets });
|
|
158
|
+
await this.writer.clearBuilding();
|
|
159
|
+
return { generation, reload: true, reason, changedIds: [], moduleCount: Object.keys(graph.modules).length, epoch };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
compiler(context: CsrDevContext, resolver: CsrDevResolver): CsrDevModuleCompiler {
|
|
163
|
+
return new CsrDevModuleCompiler({
|
|
164
|
+
app: this.app,
|
|
165
|
+
paths: this.paths,
|
|
166
|
+
resolver,
|
|
167
|
+
context,
|
|
168
|
+
outDir: this.outDir,
|
|
169
|
+
routePrefix: this.routePrefix,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
merge(graph: CsrDevGraph, resolver: CsrDevResolver, compiled: CsrDevCompiledModule[]): void {
|
|
174
|
+
for (const module of compiled)
|
|
175
|
+
graph.modules[module.id] = {
|
|
176
|
+
vendor: module.vendor,
|
|
177
|
+
mtimeMs: module.mtimeMs,
|
|
178
|
+
hash: module.hash,
|
|
179
|
+
deps: module.deps.map((dep) => (CsrDevPaths.isStub(dep) ? dep : this.paths.idOf(dep))),
|
|
180
|
+
...(module.helpers ? { helpers: module.helpers.hash } : {}),
|
|
181
|
+
};
|
|
182
|
+
//? Only what this round compiled leaves pending: a root another route's check failed on stays for its retry.
|
|
183
|
+
const compiledIds = new Set(compiled.map((module) => module.id));
|
|
184
|
+
graph.pending = graph.pending.filter((id) => !compiledIds.has(id));
|
|
185
|
+
graph.resolution = resolver.serialize();
|
|
186
|
+
graph.runtimeResolved = resolver.serializeRuntimeResolved();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
entryIds(files: Record<string, string>): Record<string, string> {
|
|
190
|
+
return Object.fromEntries(Object.entries(files).map(([key, file]) => [key, this.paths.idOf(file)]));
|
|
191
|
+
}
|
|
192
|
+
}
|