@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,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { optimize } from "@tailwindcss/node";
|
|
3
|
+
import type { AkanNativeAppId, AkanWebConfig } from "akanjs";
|
|
3
4
|
import type { BaseBuildArtifact } from "akanjs/server";
|
|
4
5
|
import { resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
|
|
5
6
|
import { computeRouteSeedIndex, type RouteSeedIndex, saveRouteSeedIndex } from "../artifact/routeSeedIndex";
|
|
@@ -36,6 +37,12 @@ export class SsrBaseArtifactBuilder {
|
|
|
36
37
|
this.#absArtifactDir = path.resolve(this.#artifactDir);
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
//? The web router reads this, so `akan start` writes the whole surface: a native shell asks the dev server for the
|
|
41
|
+
//? CSR page even when the app ships none, and a narrowed dev artifact hands it the SSR page, which cannot run there.
|
|
42
|
+
static servedWeb(command: "build" | "start", web: AkanWebConfig): AkanWebConfig {
|
|
43
|
+
return command === "start" ? { ssr: true, csr: true } : web;
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
async build(): Promise<BuildSsrBaseArtifactResult> {
|
|
40
47
|
const akanConfig = await this.#app.getConfig();
|
|
41
48
|
const { rscClientUrl, rscRuntimeClientManifest, rscRuntimeSsrManifest, vendorMap } =
|
|
@@ -77,15 +84,16 @@ export class SsrBaseArtifactBuilder {
|
|
|
77
84
|
branches: [...akanConfig.branches],
|
|
78
85
|
i18n: akanConfig.i18n,
|
|
79
86
|
imageConfig: akanConfig.images,
|
|
80
|
-
web: akanConfig.web,
|
|
81
|
-
deepLinkAssociations: Object.values(akanConfig.
|
|
87
|
+
web: SsrBaseArtifactBuilder.servedWeb(this.#command, akanConfig.web),
|
|
88
|
+
deepLinkAssociations: Object.values(akanConfig.native.targets)
|
|
82
89
|
.filter((target) => (target.deepLinks?.domains?.length ?? 0) > 0)
|
|
83
90
|
.map((target) => ({
|
|
84
91
|
targetName: target.name,
|
|
85
|
-
|
|
92
|
+
iosAppId: SsrBaseArtifactBuilder.#appIdOn(target.appId, "ios"),
|
|
93
|
+
androidAppId: SsrBaseArtifactBuilder.#appIdOn(target.appId, "android"),
|
|
86
94
|
domains: target.deepLinks?.domains ?? [],
|
|
87
|
-
iosTeamId: target.
|
|
88
|
-
androidSha256CertFingerprints: target.
|
|
95
|
+
iosTeamId: target.ios?.teamId,
|
|
96
|
+
androidSha256CertFingerprints: target.android?.sha256CertFingerprints,
|
|
89
97
|
})),
|
|
90
98
|
};
|
|
91
99
|
await Bun.write(path.join(this.#absArtifactDir, "base-artifact.json"), `${JSON.stringify(artifact, null, 2)}\n`);
|
|
@@ -185,8 +193,7 @@ export class SsrBaseArtifactBuilder {
|
|
|
185
193
|
}> {
|
|
186
194
|
const cssCompiler = new CssCompiler(this.#app);
|
|
187
195
|
const cssByBasePath = await cssCompiler.getCssByBasePath();
|
|
188
|
-
//
|
|
189
|
-
// (devkit/lint/no-raw-palette-class.grit 외 3종), 콘트라스트는 `akan lint` 의 themeValidator.
|
|
196
|
+
// The style contract (vocabulary closure, WCAG contrast) is enforced by `akan lint`, not by the build.
|
|
190
197
|
const optimizedFonts = await new FontOptimizer(this.#app, this.#command).optimize();
|
|
191
198
|
const cssAssets = Object.fromEntries(
|
|
192
199
|
await Promise.all(
|
|
@@ -216,12 +223,11 @@ export class SsrBaseArtifactBuilder {
|
|
|
216
223
|
return [basePath, { cssUrl, cssRelPath }] as const;
|
|
217
224
|
}
|
|
218
225
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
*/
|
|
226
|
+
// Checked against the written asset, not the compiled text: it is the only stylesheet an SSR render serves.
|
|
227
|
+
static #appIdOn(appId: AkanNativeAppId, platform: "ios" | "android"): string | null {
|
|
228
|
+
return typeof appId === "string" ? appId : (appId[platform] ?? appId.default ?? null);
|
|
229
|
+
}
|
|
230
|
+
|
|
225
231
|
static #warnDroppedImports(app: App, cssRelPath: string, css: string, imported: ImportedStylesheet[]) {
|
|
226
232
|
for (const { cssPath, declaredNames } of imported) {
|
|
227
233
|
if (declaredNames.length === 0 || declaredNames.some((name) => css.includes(`${name}:`))) continue;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { SSR_DEV_DIRNAME, SSR_DEV_ROUTE_PREFIX } from "akanjs/server/hmr/csrDevManifest";
|
|
4
|
+
import { resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
|
|
5
|
+
import { computeRouteSeedIndex } from "../artifact/routeSeedIndex";
|
|
6
|
+
import type { App } from "../commandDecorators";
|
|
7
|
+
import { bundleDefine } from "./bundleDefine";
|
|
8
|
+
import type { ClientEntryDiscovery } from "./clientBuildTypes";
|
|
9
|
+
import { GraphClientEntryDiscovery } from "./clientEntryDiscovery";
|
|
10
|
+
import { CsrDevPaths } from "./csrDevPaths";
|
|
11
|
+
import type { CsrDevContext } from "./csrDevTypes";
|
|
12
|
+
import { DevRegistryBundler } from "./devRegistryBundler";
|
|
13
|
+
import { VENDOR_SPECIFIERS } from "./vendorSpecifiers";
|
|
14
|
+
|
|
15
|
+
//* Dev-only client code of SSR pages as a module registry, under `.akan/artifact/ssr-dev`. Its roots are a bootstrap
|
|
16
|
+
//* (the app's client runtime) and every `"use client"` entry; React and the akanjs vendor facets stay the import map's,
|
|
17
|
+
//* so the page keeps one React and one store registry.
|
|
18
|
+
export class SsrDevBundler extends DevRegistryBundler {
|
|
19
|
+
static readonly #formatVersion = 2;
|
|
20
|
+
static readonly bootstrapKey = "";
|
|
21
|
+
readonly reloadsOnEntryChange = false;
|
|
22
|
+
#discovery: ClientEntryDiscovery | null = null;
|
|
23
|
+
readonly #sharedDiscovery: (() => ClientEntryDiscovery) | null;
|
|
24
|
+
|
|
25
|
+
/** `discovery` is the resident builder's, which it keeps current with every save; a worker discovers afresh. */
|
|
26
|
+
constructor(app: App, { discovery }: { discovery?: () => ClientEntryDiscovery } = {}) {
|
|
27
|
+
super(app, { dirName: SSR_DEV_DIRNAME, routePrefix: SSR_DEV_ROUTE_PREFIX, library: true });
|
|
28
|
+
this.#sharedDiscovery = discovery ?? null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async context(): Promise<CsrDevContext> {
|
|
32
|
+
const akanConfig = await this.app.getConfig();
|
|
33
|
+
const define = bundleDefine(this.app, "start", "ssr");
|
|
34
|
+
const optimizeImports = [...akanConfig.optimizeImports];
|
|
35
|
+
const configKey = Bun.hash(
|
|
36
|
+
JSON.stringify(["ssr", SsrDevBundler.#formatVersion, Bun.version, define, optimizeImports, VENDOR_SPECIFIERS]),
|
|
37
|
+
).toString(36);
|
|
38
|
+
return {
|
|
39
|
+
pageEntries: [],
|
|
40
|
+
basePaths: [...akanConfig.basePaths],
|
|
41
|
+
htmlBasePaths: [],
|
|
42
|
+
define,
|
|
43
|
+
optimizeImports,
|
|
44
|
+
configKey,
|
|
45
|
+
refreshFile: null,
|
|
46
|
+
externals: VENDOR_SPECIFIERS,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
//? The client runtime of an SSR page inlines no dictionary (`useClient.ts` reads one only when rendering csr), so
|
|
51
|
+
//? a dictionary save is none of this registry's business.
|
|
52
|
+
override isMetadataFile(file: string): boolean {
|
|
53
|
+
return super.isMetadataFile(file) && !file.endsWith(".dictionary.ts") && path.basename(file) !== "dict.ts";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Run first in the tab, before any client module the payload names: the store registry must exist by then.
|
|
57
|
+
async writeEntries(): Promise<{ files: Record<string, string>; changed: string[] }> {
|
|
58
|
+
await mkdir(this.entryDir, { recursive: true });
|
|
59
|
+
const file = path.join(this.entryDir, "bootstrap.ts");
|
|
60
|
+
const hasStore = await Bun.file(path.join(this.app.cwdPath, "lib", "st.ts")).exists();
|
|
61
|
+
const source = hasStore ? `import ${JSON.stringify(`@apps/${this.app.name}/client`)};\n` : "export {};\n";
|
|
62
|
+
const unchanged = (await Bun.file(file).exists()) && (await Bun.file(file).text()) === source;
|
|
63
|
+
if (!unchanged) await Bun.write(file, source);
|
|
64
|
+
const entryFile = CsrDevPaths.realpath(file);
|
|
65
|
+
return { files: { [SsrDevBundler.bootstrapKey]: entryFile }, changed: unchanged ? [] : [entryFile] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Every `"use client"` entry the app's routes reach, the ones a route build names in its manifest. */
|
|
69
|
+
async clientEntries(): Promise<string[]> {
|
|
70
|
+
const pageEntries = await resolveSsrPageEntriesForApp(this.app, await this.app.getPageKeys());
|
|
71
|
+
const seedIndex = computeRouteSeedIndex(pageEntries);
|
|
72
|
+
const seeds = [...new Set([...seedIndex.globalLayoutFiles, ...seedIndex.entries.flatMap((entry) => entry.seeds)])];
|
|
73
|
+
const discovery = this.#sharedDiscovery?.() ?? (await this.#ownDiscovery());
|
|
74
|
+
return (await discovery.discover(seeds)).map((file) => CsrDevPaths.realpath(file));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async #ownDiscovery(): Promise<ClientEntryDiscovery> {
|
|
78
|
+
this.#discovery ??= await GraphClientEntryDiscovery.create(this.app);
|
|
79
|
+
return this.#discovery;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
protected async rootFiles(entryFiles: string[]): Promise<string[]> {
|
|
83
|
+
return [...entryFiles, ...(await this.clientEntries())];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Reports the style contract that `akan lint` enforces, in the shape build/dev/lint share.
|
|
3
|
-
*
|
|
4
|
-
* Only contrast lives here. The vocabulary closure — raw palette classes, arbitrary colors, dropped
|
|
5
|
-
* daisyUI slots, inline color literals, interpolated arbitrary values — is enforced by the grit plugins
|
|
6
|
-
* in `lint/*.grit` during the biome run, so it is not re-scanned. Contrast cannot be a lint rule at all:
|
|
7
|
-
* it is arithmetic over resolved token *values*, which no syntactic pattern can reach.
|
|
8
|
-
*/
|
|
1
|
+
// Only contrast: `lint/*.grit` enforces the vocabulary closure, and contrast is arithmetic no syntactic rule can do.
|
|
9
2
|
import type { ThemeContrastViolation } from "./themeValidator";
|
|
10
3
|
|
|
11
4
|
export interface StyleContractViolations {
|
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* WCAG contrast checking over the semantic token pairs. No dependencies, pure functions, and no akanjs
|
|
3
|
-
* runtime import — it has to run inside the lint path.
|
|
4
|
-
*
|
|
5
|
-
* The `--x` / `--x-foreground` pairing is what makes the check possible at all: it says which two values
|
|
6
|
-
* are going to end up on top of each other. That is the guard against a generated palette shipping a site
|
|
7
|
-
* nobody can read.
|
|
8
|
-
*
|
|
9
|
-
* Thresholds (WCAG 2.1):
|
|
10
|
-
* - body and primary surfaces (background, primary, secondary, accent, neutral, card, popover): 4.5:1,
|
|
11
|
-
* the AA floor for normal text
|
|
12
|
-
* - status and secondary pairs (info, success, warning, destructive, open, muted): 3:1, the AA floor for
|
|
13
|
-
* UI components and large text
|
|
14
|
-
* The shipped light/dark palette in styles.css clears all of them.
|
|
15
|
-
*/
|
|
1
|
+
// No akanjs runtime import: this runs inside the lint path.
|
|
16
2
|
|
|
17
3
|
export interface ThemeContrastViolation {
|
|
18
4
|
scope: string;
|
|
@@ -30,6 +16,7 @@ interface PairDef {
|
|
|
30
16
|
threshold: number;
|
|
31
17
|
}
|
|
32
18
|
|
|
19
|
+
// WCAG 2.1 AA: 4.5:1 is the floor for normal text, 3:1 for UI components and large text.
|
|
33
20
|
const PAIRS: PairDef[] = [
|
|
34
21
|
{ base: "background", fg: "foreground", threshold: 4.5 },
|
|
35
22
|
{ base: "primary", fg: "primary-foreground", threshold: 4.5 },
|
|
@@ -46,13 +33,11 @@ const PAIRS: PairDef[] = [
|
|
|
46
33
|
{ base: "open", fg: "open-foreground", threshold: 3 },
|
|
47
34
|
];
|
|
48
35
|
|
|
49
|
-
// Only these scopes are paired up; tokens scoped to something else (`.campaign-x`, …) are left alone.
|
|
50
36
|
const THEME_SCOPES = new Set([":root", '[data-theme="dark"]', '[data-theme="light"]']);
|
|
51
37
|
|
|
52
38
|
export type ThemeTokensByScope = Record<string, Record<string, string>>;
|
|
53
39
|
|
|
54
40
|
export class ThemeValidator {
|
|
55
|
-
/** Extracts tokens from the css text and checks every scope it recognizes. */
|
|
56
41
|
validate(css: string): ThemeContrastViolation[] {
|
|
57
42
|
const tokensByScope = ThemeValidator.parseThemeTokens(css);
|
|
58
43
|
const violations: ThemeContrastViolation[] = [];
|
|
@@ -71,7 +56,7 @@ export class ThemeValidator {
|
|
|
71
56
|
if (!bg || !front) continue;
|
|
72
57
|
const bgRgb = ThemeValidator.parseHex(bg);
|
|
73
58
|
const fgRgb = ThemeValidator.parseHex(front);
|
|
74
|
-
if (!bgRgb || !fgRgb) continue;
|
|
59
|
+
if (!bgRgb || !fgRgb) continue;
|
|
75
60
|
const ratio = ThemeValidator.contrastRatio(bgRgb, fgRgb);
|
|
76
61
|
if (ratio >= threshold) continue;
|
|
77
62
|
violations.push({
|
|
@@ -87,15 +72,9 @@ export class ThemeValidator {
|
|
|
87
72
|
return violations;
|
|
88
73
|
}
|
|
89
74
|
|
|
90
|
-
/**
|
|
91
|
-
* Reads `--token: value` out of `:root` / `[data-theme="…"]` blocks. A grouped selector
|
|
92
|
-
* (`:root, [data-theme="dark"] { … }`) distributes the same tokens to each selector, and a scope that
|
|
93
|
-
* appears twice keeps the later value — so passing framework css first and the app's second reflects the
|
|
94
|
-
* app's overrides.
|
|
95
|
-
*/
|
|
75
|
+
/** A scope declared twice keeps the later value, so pass the framework css before the app's. */
|
|
96
76
|
static parseThemeTokens(css: string): ThemeTokensByScope {
|
|
97
77
|
const result: ThemeTokensByScope = {};
|
|
98
|
-
// Flat rule blocks only; an at-rule (@theme, @keyframes) carries `@` in the selector and is skipped.
|
|
99
78
|
const blockRe = /(?:^|})\s*([^{}@]+?)\s*\{([^{}]*)\}/g;
|
|
100
79
|
for (const block of css.matchAll(blockRe)) {
|
|
101
80
|
const selectors = block[1].split(",").map((s) => s.trim());
|
|
@@ -114,7 +93,6 @@ export class ThemeValidator {
|
|
|
114
93
|
}
|
|
115
94
|
|
|
116
95
|
static #normalizeScope(selector: string): string {
|
|
117
|
-
// Quote normalization: [data-theme=dark] / [data-theme='dark'] -> [data-theme="dark"]
|
|
118
96
|
return selector.replace(/\[data-theme=['"]?([\w-]+)['"]?\]/g, '[data-theme="$1"]').trim();
|
|
119
97
|
}
|
|
120
98
|
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir,
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import type { App } from "../commandDecorators";
|
|
5
|
+
import { tempDirs } from "../testHelpers";
|
|
6
6
|
import { WatchRootResolver } from "./watchRootResolver";
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const tempRoot = tempDirs("akan-watch-roots-");
|
|
9
9
|
|
|
10
10
|
const makeWorkspace = async (dirs: string[]) => {
|
|
11
|
-
const root = await
|
|
12
|
-
roots.push(root);
|
|
11
|
+
const root = await tempRoot();
|
|
13
12
|
await Promise.all(dirs.map((dir) => mkdir(path.join(root, dir), { recursive: true })));
|
|
14
13
|
return root;
|
|
15
14
|
};
|
|
@@ -25,24 +24,18 @@ interface AppStub {
|
|
|
25
24
|
libDeps?: string[] | null;
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
const
|
|
29
|
-
({
|
|
30
|
-
cwdPath: path.join(workspaceRoot, "apps",
|
|
27
|
+
const resolveRoots = (workspaceRoot: string, { paths = {}, libDeps = null }: AppStub = {}) =>
|
|
28
|
+
new WatchRootResolver({
|
|
29
|
+
cwdPath: path.join(workspaceRoot, "apps", "app1"),
|
|
31
30
|
workspace: { workspaceRoot },
|
|
32
31
|
getTsConfig: async () => ({ compilerOptions: { paths } }),
|
|
33
32
|
getScanInfo: () => (libDeps ? { type: "app", libDeps } : null),
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
afterEach(async () => {
|
|
37
|
-
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
38
|
-
});
|
|
33
|
+
} as unknown as App).resolve();
|
|
39
34
|
|
|
40
35
|
describe("WatchRootResolver", () => {
|
|
41
36
|
test("narrows the apps container to the app being served", async () => {
|
|
42
37
|
const workspaceRoot = await makeWorkspace(["apps/app1/page", "apps/app2/page", "libs/util"]);
|
|
43
|
-
const resolved = await
|
|
44
|
-
makeApp(workspaceRoot, "app1", { paths: { "@apps/*": ["./apps/*"], "@libs/*": ["./libs/*"] } }),
|
|
45
|
-
).resolve();
|
|
38
|
+
const resolved = await resolveRoots(workspaceRoot, { paths: { "@apps/*": ["./apps/*"], "@libs/*": ["./libs/*"] } });
|
|
46
39
|
|
|
47
40
|
expect(resolved).toContain(path.join(workspaceRoot, "apps/app1"));
|
|
48
41
|
expect(resolved).not.toContain(path.join(workspaceRoot, "apps"));
|
|
@@ -51,9 +44,7 @@ describe("WatchRootResolver", () => {
|
|
|
51
44
|
|
|
52
45
|
test("narrows the libs container to the app's own dependencies", async () => {
|
|
53
46
|
const workspaceRoot = await makeWorkspace(["apps/app1/page", "libs/util", "libs/shared"]);
|
|
54
|
-
const resolved = await
|
|
55
|
-
makeApp(workspaceRoot, "app1", { paths: { "@libs/*": ["./libs/*"] }, libDeps: ["util"] }),
|
|
56
|
-
).resolve();
|
|
47
|
+
const resolved = await resolveRoots(workspaceRoot, { paths: { "@libs/*": ["./libs/*"] }, libDeps: ["util"] });
|
|
57
48
|
|
|
58
49
|
expect(resolved).toContain(path.join(workspaceRoot, "libs/util"));
|
|
59
50
|
expect(resolved).not.toContain(path.join(workspaceRoot, "libs"));
|
|
@@ -63,9 +54,7 @@ describe("WatchRootResolver", () => {
|
|
|
63
54
|
test("drops the libs container entirely when the app depends on no lib", async () => {
|
|
64
55
|
const workspaceRoot = await makeWorkspace(["apps/app1/page", "libs/util"]);
|
|
65
56
|
await writeManifest(workspaceRoot, "apps/app1", []);
|
|
66
|
-
const resolved = await
|
|
67
|
-
makeApp(workspaceRoot, "app1", { paths: { "@libs/*": ["./libs/*"] } }),
|
|
68
|
-
).resolve();
|
|
57
|
+
const resolved = await resolveRoots(workspaceRoot, { paths: { "@libs/*": ["./libs/*"] } });
|
|
69
58
|
|
|
70
59
|
expect(resolved).toEqual([path.join(workspaceRoot, "apps/app1/page")]);
|
|
71
60
|
});
|
|
@@ -76,9 +65,7 @@ describe("WatchRootResolver", () => {
|
|
|
76
65
|
await writeManifest(workspaceRoot, "libs/shared", ["util"]);
|
|
77
66
|
await writeManifest(workspaceRoot, "libs/util", []);
|
|
78
67
|
await writeManifest(workspaceRoot, "libs/unused", []);
|
|
79
|
-
const resolved = await
|
|
80
|
-
makeApp(workspaceRoot, "app1", { paths: { "@libs/*": ["./libs/*"] } }),
|
|
81
|
-
).resolve();
|
|
68
|
+
const resolved = await resolveRoots(workspaceRoot, { paths: { "@libs/*": ["./libs/*"] } });
|
|
82
69
|
|
|
83
70
|
expect(resolved).toContain(path.join(workspaceRoot, "libs/shared"));
|
|
84
71
|
expect(resolved).toContain(path.join(workspaceRoot, "libs/util"));
|
|
@@ -87,9 +74,7 @@ describe("WatchRootResolver", () => {
|
|
|
87
74
|
|
|
88
75
|
test("keeps the libs container whole when the app manifest is missing", async () => {
|
|
89
76
|
const workspaceRoot = await makeWorkspace(["apps/app1/page", "libs/util", "libs/shared"]);
|
|
90
|
-
const resolved = await
|
|
91
|
-
makeApp(workspaceRoot, "app1", { paths: { "@libs/*": ["./libs/*"] } }),
|
|
92
|
-
).resolve();
|
|
77
|
+
const resolved = await resolveRoots(workspaceRoot, { paths: { "@libs/*": ["./libs/*"] } });
|
|
93
78
|
|
|
94
79
|
expect(resolved).toContain(path.join(workspaceRoot, "libs"));
|
|
95
80
|
});
|
|
@@ -97,24 +82,20 @@ describe("WatchRootResolver", () => {
|
|
|
97
82
|
test("keeps the libs container whole when a dependency lib is unsynced", async () => {
|
|
98
83
|
const workspaceRoot = await makeWorkspace(["apps/app1/page", "libs/util", "libs/shared"]);
|
|
99
84
|
await writeManifest(workspaceRoot, "apps/app1", ["shared"]);
|
|
100
|
-
const resolved = await
|
|
101
|
-
makeApp(workspaceRoot, "app1", { paths: { "@libs/*": ["./libs/*"] } }),
|
|
102
|
-
).resolve();
|
|
85
|
+
const resolved = await resolveRoots(workspaceRoot, { paths: { "@libs/*": ["./libs/*"] } });
|
|
103
86
|
|
|
104
87
|
expect(resolved).toContain(path.join(workspaceRoot, "libs"));
|
|
105
88
|
});
|
|
106
89
|
|
|
107
90
|
test("resolves a package alias to its own directory, filename and glob stripped", async () => {
|
|
108
91
|
const workspaceRoot = await makeWorkspace(["apps/app1/page", "pkgs/akanjs/base"]);
|
|
109
|
-
const resolved = await
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}),
|
|
117
|
-
).resolve();
|
|
92
|
+
const resolved = await resolveRoots(workspaceRoot, {
|
|
93
|
+
paths: {
|
|
94
|
+
akanjs: ["./pkgs/akanjs/index.ts"],
|
|
95
|
+
"akanjs/*": ["./pkgs/akanjs/*"],
|
|
96
|
+
missing: ["./pkgs/nothing/index.ts"],
|
|
97
|
+
},
|
|
98
|
+
});
|
|
118
99
|
|
|
119
100
|
expect(resolved).toContain(path.join(workspaceRoot, "pkgs/akanjs"));
|
|
120
101
|
expect(resolved).not.toContain(path.join(workspaceRoot, "pkgs/nothing"));
|
|
@@ -122,7 +103,7 @@ describe("WatchRootResolver", () => {
|
|
|
122
103
|
|
|
123
104
|
test("always watches the app's page tree", async () => {
|
|
124
105
|
const workspaceRoot = await makeWorkspace(["apps/app1/page"]);
|
|
125
|
-
const resolved = await
|
|
106
|
+
const resolved = await resolveRoots(workspaceRoot);
|
|
126
107
|
|
|
127
108
|
expect(resolved).toEqual([path.join(workspaceRoot, "apps/app1/page")]);
|
|
128
109
|
});
|
|
@@ -3,23 +3,7 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import type { App } from "../commandDecorators";
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* The directories one app's dev watcher follows, resolved from its tsconfig `paths`.
|
|
8
|
-
*
|
|
9
|
-
* Two aliases strip down to a workspace container rather than a package, and taking either verbatim puts
|
|
10
|
-
* code the app never imports under the watcher — where a save rebuilds, restarts and reloads it for
|
|
11
|
-
* nothing, and both builders rewrite the same generated barrels:
|
|
12
|
-
*
|
|
13
|
-
* - `@apps/*` becomes the `apps/` container. Apps are leaves of the workspace graph — never one another's
|
|
14
|
-
* dependencies — so it is replaced by this app's own directory.
|
|
15
|
-
* - `@libs/*` becomes the `libs/` container. A lib is a dependency only if the app reaches it, so it is
|
|
16
|
-
* replaced by the app's own lib dependencies, transitively. The set is resolved once, at watcher
|
|
17
|
-
* install: a lib that becomes a dependency mid-session needs a fresh `akan start` anyway, because the
|
|
18
|
-
* import that made it one also needs a `sync`.
|
|
19
|
-
*
|
|
20
|
-
* Failing to resolve that set keeps the container whole, because a wrong narrowing is a dev server that
|
|
21
|
-
* silently ignores edits.
|
|
22
|
-
*/
|
|
6
|
+
/** Watch roots from tsconfig paths: `@apps/*` narrows to this app, `@libs/*` to its lib deps (whole when unknown). */
|
|
23
7
|
export class WatchRootResolver {
|
|
24
8
|
#app: App;
|
|
25
9
|
|
|
@@ -32,14 +16,15 @@ export class WatchRootResolver {
|
|
|
32
16
|
const appDir = path.resolve(this.#app.cwdPath);
|
|
33
17
|
const appsContainer = path.dirname(appDir);
|
|
34
18
|
const libsContainer = path.resolve(this.#app.workspace.workspaceRoot, "libs");
|
|
35
|
-
const libRoots = await this.#
|
|
19
|
+
const libRoots = (await this.#resolveLibDeps(libsContainer))
|
|
20
|
+
?.map((name) => path.join(libsContainer, name))
|
|
21
|
+
.filter((dir) => fs.existsSync(dir));
|
|
36
22
|
const set = new Set<string>();
|
|
37
23
|
set.add(path.resolve(`${this.#app.cwdPath}/page`));
|
|
38
24
|
for (const targets of Object.values(tsconfig.compilerOptions.paths ?? {})) {
|
|
39
25
|
for (const target of targets) {
|
|
40
26
|
if (!target) continue;
|
|
41
27
|
if (path.isAbsolute(target)) continue;
|
|
42
|
-
// Strip the trailing filename and glob so we watch the package root dir.
|
|
43
28
|
const cleaned = target.replace(/\/?\*+.*$/, "").replace(/\/[^/]+\.[^/]+$/, "");
|
|
44
29
|
const resolved = path.resolve(this.#app.workspace.workspaceRoot, cleaned);
|
|
45
30
|
if (resolved === libsContainer && libRoots) {
|
|
@@ -53,16 +38,9 @@ export class WatchRootResolver {
|
|
|
53
38
|
return [...set];
|
|
54
39
|
}
|
|
55
40
|
|
|
56
|
-
async #resolveLibRoots(libsContainer: string): Promise<string[] | null> {
|
|
57
|
-
const libDeps = await this.#resolveLibDeps(libsContainer);
|
|
58
|
-
if (!libDeps) return null;
|
|
59
|
-
return libDeps.map((name) => path.join(libsContainer, name)).filter((dir) => fs.existsSync(dir));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
41
|
async #resolveLibDeps(libsContainer: string): Promise<string[] | null> {
|
|
63
42
|
const scanInfo = this.#app.getScanInfo({ allowEmpty: true });
|
|
64
|
-
//
|
|
65
|
-
// idle watcher run in processes that did not, so they take the manifests `scan` wrote instead.
|
|
43
|
+
// Transitive, but only in a process that scanned; the builder and idle watcher read the synced manifests.
|
|
66
44
|
if (scanInfo?.type === "app") return scanInfo.libDeps;
|
|
67
45
|
const direct = await WatchRootResolver.#readManifestLibDeps(path.join(this.#app.cwdPath, "akan.app.json"));
|
|
68
46
|
if (!direct) return null;
|
package/getRelatedCnsts.ts
CHANGED
|
@@ -40,9 +40,6 @@ const scanModuleSpecifiers = (source: string, filePath: string, includeExports:
|
|
|
40
40
|
return importSpecifiers;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
/**
|
|
44
|
-
* TypeScript 설정 파일을 읽고 파싱하는 함수
|
|
45
|
-
*/
|
|
46
43
|
export const parseTsConfig = (tsConfigPath: string = "./tsconfig.json") => {
|
|
47
44
|
const configFile = ts.readConfigFile(tsConfigPath, (path) => {
|
|
48
45
|
return ts.sys.readFile(path);
|
|
@@ -51,85 +48,36 @@ export const parseTsConfig = (tsConfigPath: string = "./tsconfig.json") => {
|
|
|
51
48
|
return ts.parseJsonConfigFileContent(configFile.config, ts.sys, realpathSync(tsConfigPath).replace(/[^/\\]+$/, ""));
|
|
52
49
|
};
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
* 파일 내의 임포트를 기반으로 관련 파일을 수집하는 함수
|
|
56
|
-
*/
|
|
57
|
-
export const collectImportedFiles = (constantFilePath: string, parsedConfig: ts.ParsedCommandLine) => {
|
|
51
|
+
const collectFiles = (constantFilePath: string, parsedConfig: ts.ParsedCommandLine, includeExports: boolean) => {
|
|
58
52
|
const allFilesToAnalyze = new Set<string>([constantFilePath]);
|
|
59
53
|
const analyzedFiles = new Set<string>();
|
|
60
|
-
const spinner = ora("Collecting related files...");
|
|
54
|
+
const spinner = ora(includeExports ? "Collecting files from exports..." : "Collecting related files...");
|
|
61
55
|
spinner.start();
|
|
62
|
-
|
|
63
|
-
function collectImported(filePath: string) {
|
|
56
|
+
const collect = (filePath: string) => {
|
|
64
57
|
if (analyzedFiles.has(filePath)) return;
|
|
65
58
|
analyzedFiles.add(filePath);
|
|
66
|
-
|
|
67
59
|
const source = readFileSync(filePath, "utf-8");
|
|
68
|
-
for (const importPath of scanModuleSpecifiers(source, filePath,
|
|
60
|
+
for (const importPath of scanModuleSpecifiers(source, filePath, includeExports)) {
|
|
69
61
|
if (!importPath.startsWith(".")) continue;
|
|
70
|
-
|
|
71
62
|
const resolved = ts.resolveModuleName(importPath, filePath, parsedConfig.options, ts.sys).resolvedModule
|
|
72
63
|
?.resolvedFileName;
|
|
73
|
-
|
|
74
64
|
if (resolved && !allFilesToAnalyze.has(resolved)) {
|
|
75
65
|
allFilesToAnalyze.add(resolved);
|
|
76
|
-
|
|
77
|
-
collectImported(resolved);
|
|
66
|
+
collect(resolved);
|
|
78
67
|
}
|
|
79
68
|
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// 임포트 기반 관련 파일 수집 실행
|
|
83
|
-
collectImported(constantFilePath);
|
|
84
|
-
spinner.succeed(`Found ${allFilesToAnalyze.size} related files.`);
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
allFilesToAnalyze,
|
|
88
|
-
analyzedFiles,
|
|
89
69
|
};
|
|
70
|
+
collect(constantFilePath);
|
|
71
|
+
spinner.succeed(`Found ${allFilesToAnalyze.size} related files${includeExports ? " from exports" : ""}.`);
|
|
72
|
+
return { allFilesToAnalyze, analyzedFiles };
|
|
90
73
|
};
|
|
91
74
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
*/
|
|
95
|
-
export const collectExportedFiles = (constantFilePath: string, parsedConfig: ts.ParsedCommandLine) => {
|
|
96
|
-
const allFilesToAnalyze = new Set<string>([constantFilePath]);
|
|
97
|
-
const analyzedFiles = new Set<string>();
|
|
98
|
-
const spinner = ora("Collecting files from exports...");
|
|
99
|
-
spinner.start();
|
|
100
|
-
|
|
101
|
-
function collectExported(filePath: string) {
|
|
102
|
-
if (analyzedFiles.has(filePath)) return;
|
|
103
|
-
analyzedFiles.add(filePath);
|
|
75
|
+
export const collectImportedFiles = (constantFilePath: string, parsedConfig: ts.ParsedCommandLine) =>
|
|
76
|
+
collectFiles(constantFilePath, parsedConfig, false);
|
|
104
77
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (!importPath.startsWith(".")) continue;
|
|
78
|
+
export const collectExportedFiles = (constantFilePath: string, parsedConfig: ts.ParsedCommandLine) =>
|
|
79
|
+
collectFiles(constantFilePath, parsedConfig, true);
|
|
108
80
|
|
|
109
|
-
const resolved = ts.resolveModuleName(importPath, filePath, parsedConfig.options, ts.sys).resolvedModule
|
|
110
|
-
?.resolvedFileName;
|
|
111
|
-
|
|
112
|
-
if (resolved && !allFilesToAnalyze.has(resolved)) {
|
|
113
|
-
allFilesToAnalyze.add(resolved);
|
|
114
|
-
// 재귀적으로 export/import 연결 파일도 확인
|
|
115
|
-
collectExported(resolved);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// export 기반 관련 파일 수집 실행
|
|
121
|
-
collectExported(constantFilePath);
|
|
122
|
-
spinner.succeed(`Found ${allFilesToAnalyze.size} related files from exports.`);
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
allFilesToAnalyze,
|
|
126
|
-
analyzedFiles,
|
|
127
|
-
};
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* TypeScript 프로그램 생성 함수
|
|
132
|
-
*/
|
|
133
81
|
export const createTsProgram = (filePaths: Set<string>, options: ts.CompilerOptions) => {
|
|
134
82
|
const spinner = ora("Creating TypeScript program for all files...");
|
|
135
83
|
spinner.start();
|
|
@@ -145,9 +93,6 @@ export const createTsProgram = (filePaths: Set<string>, options: ts.CompilerOpti
|
|
|
145
93
|
};
|
|
146
94
|
};
|
|
147
95
|
|
|
148
|
-
/**
|
|
149
|
-
* 심볼 캐싱 및 조회 함수 생성기
|
|
150
|
-
*/
|
|
151
96
|
export const createSymbolCache = (checker: ts.TypeChecker) => {
|
|
152
97
|
const symbolCache = new Map<string, ts.Symbol | undefined>();
|
|
153
98
|
|
|
@@ -162,9 +107,6 @@ export const createSymbolCache = (checker: ts.TypeChecker) => {
|
|
|
162
107
|
};
|
|
163
108
|
};
|
|
164
109
|
|
|
165
|
-
/**
|
|
166
|
-
* 파일 속성 분석 함수
|
|
167
|
-
*/
|
|
168
110
|
export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Program, checker: ts.TypeChecker) => {
|
|
169
111
|
const propertyMap = new Map<
|
|
170
112
|
string,
|
|
@@ -185,7 +127,6 @@ export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Progr
|
|
|
185
127
|
const spinner = ora("Analyzing property relationships...");
|
|
186
128
|
spinner.start();
|
|
187
129
|
|
|
188
|
-
// 파일 내 속성 분석 함수
|
|
189
130
|
function analyzeFileProperties(filePath: string) {
|
|
190
131
|
if (analyzedFiles.has(filePath)) return;
|
|
191
132
|
analyzedFiles.add(filePath);
|
|
@@ -193,23 +134,19 @@ export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Progr
|
|
|
193
134
|
const source = program.getSourceFile(filePath);
|
|
194
135
|
if (!source) return;
|
|
195
136
|
|
|
196
|
-
// 소스 라인 캐싱 (한 번만 분할)
|
|
197
137
|
if (!sourceLineCache.has(filePath)) {
|
|
198
138
|
sourceLineCache.set(filePath, source.getFullText().split("\n"));
|
|
199
139
|
}
|
|
200
140
|
const sourceLines = sourceLineCache.get(filePath);
|
|
201
141
|
|
|
202
|
-
// 속성 분석 함수
|
|
203
142
|
function visit(node: ts.Node) {
|
|
204
143
|
if (!source) return;
|
|
205
144
|
|
|
206
|
-
// 프로퍼티 접근 표현식 처리
|
|
207
145
|
if (ts.isPropertyAccessExpression(node)) {
|
|
208
146
|
const left = node.expression;
|
|
209
147
|
const right = node.name;
|
|
210
148
|
const { line } = ts.getLineAndCharacterOfPosition(source, node.getStart());
|
|
211
149
|
|
|
212
|
-
// 패턴 검색 로직 - 기존 문자열 기반 검색 유지
|
|
213
150
|
if (
|
|
214
151
|
ts.isIdentifier(left) &&
|
|
215
152
|
sourceLines &&
|
|
@@ -219,14 +156,12 @@ export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Progr
|
|
|
219
156
|
(sourceLines[line]?.includes(`@Field.Prop(() => ${left.text}.${right.text}`) ||
|
|
220
157
|
sourceLines[line].includes(`base.Filter(${left.text}.${right.text},`))
|
|
221
158
|
) {
|
|
222
|
-
// 캐싱된 심볼 사용
|
|
223
159
|
const symbol = getCachedSymbol(right);
|
|
224
160
|
|
|
225
161
|
if (symbol?.declarations && symbol.declarations.length > 0) {
|
|
226
162
|
const key = symbol.declarations[0]?.getSourceFile().fileName.split("/").pop()?.split(".")[0] ?? "";
|
|
227
163
|
const property = propertyMap.get(key);
|
|
228
164
|
const isScalar = symbol.declarations[0]?.getSourceFile().fileName.includes("_") ?? false;
|
|
229
|
-
// 현재 작업 중인 디렉토리 이전은 제거
|
|
230
165
|
const symbolFilePath = symbol.declarations[0]
|
|
231
166
|
?.getSourceFile()
|
|
232
167
|
.fileName.replace(`${ts.sys.getCurrentDirectory()}/`, "");
|
|
@@ -254,7 +189,6 @@ export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Progr
|
|
|
254
189
|
}
|
|
255
190
|
}
|
|
256
191
|
} else if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
257
|
-
// 모듈 선언 표현식 처리
|
|
258
192
|
const importPath = node.moduleSpecifier.text;
|
|
259
193
|
|
|
260
194
|
if (importPath.startsWith(".")) {
|
|
@@ -276,15 +210,12 @@ export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Progr
|
|
|
276
210
|
}
|
|
277
211
|
}
|
|
278
212
|
|
|
279
|
-
// 모든 자식 노드 처리
|
|
280
213
|
ts.forEachChild(node, visit);
|
|
281
214
|
}
|
|
282
215
|
|
|
283
|
-
// 파일 분석 시작
|
|
284
216
|
visit(source);
|
|
285
217
|
}
|
|
286
218
|
|
|
287
|
-
// 모든 파일의 속성 관계 분석
|
|
288
219
|
for (const filePath of filesToAnalyze) {
|
|
289
220
|
analyzeFileProperties(filePath);
|
|
290
221
|
}
|
|
@@ -294,21 +225,10 @@ export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Progr
|
|
|
294
225
|
return propertyMap;
|
|
295
226
|
};
|
|
296
227
|
|
|
297
|
-
/**
|
|
298
|
-
* 메인 함수: 상수 파일 관련 요소 분석
|
|
299
|
-
*/
|
|
300
228
|
export const getRelatedCnsts = (constantFilePath: string) => {
|
|
301
|
-
// 1. TypeScript 설정 파일 읽기 및 파싱
|
|
302
229
|
const parsedConfig = parseTsConfig();
|
|
303
|
-
|
|
304
|
-
// 2. 상수 파일 관련 파일 수집 (임포트 기반)
|
|
305
230
|
const { allFilesToAnalyze } = collectImportedFiles(constantFilePath, parsedConfig);
|
|
306
|
-
// 3. TypeScript 프로그램 생성 및 타입 체커 설정
|
|
307
231
|
const { program, checker } = createTsProgram(allFilesToAnalyze, parsedConfig.options);
|
|
308
|
-
|
|
309
|
-
// 4. 파일 속성 분석
|
|
310
232
|
const propertyMap = analyzeProperties(allFilesToAnalyze, program, checker);
|
|
311
|
-
|
|
312
|
-
// 5. 결과 반환
|
|
313
233
|
return Array.from(propertyMap.entries()).map(([key, value]) => ({ key, ...value }));
|
|
314
234
|
};
|