@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
package/fileEditor.ts
CHANGED
|
@@ -17,57 +17,29 @@ export class FileEditor {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
find(pattern: string | RegExp): number {
|
|
20
|
-
const lines = this.#content.split("\n");
|
|
21
20
|
const regex = typeof pattern === "string" ? new RegExp(pattern) : pattern;
|
|
22
|
-
|
|
23
|
-
for (let i = 0; i < lines.length; i++) {
|
|
24
|
-
const line = lines[i];
|
|
25
|
-
if (!line) continue;
|
|
26
|
-
if (regex.test(line)) return i;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return -1;
|
|
21
|
+
return this.#content.split("\n").findIndex((line) => !!line && regex.test(line));
|
|
30
22
|
}
|
|
31
23
|
|
|
32
24
|
findAll(pattern: string | RegExp): number[] {
|
|
33
|
-
const lines = this.#content.split("\n");
|
|
34
25
|
const regex = typeof pattern === "string" ? new RegExp(pattern) : pattern;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
for (let i = 0; i < lines.length; i++) {
|
|
38
|
-
const line = lines[i];
|
|
39
|
-
if (!line) continue;
|
|
40
|
-
if (regex.test(line)) matches.push(i);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return matches;
|
|
26
|
+
return this.#content.split("\n").flatMap((line, index) => (line && regex.test(line) ? [index] : []));
|
|
44
27
|
}
|
|
45
28
|
|
|
46
29
|
insertAfter(pattern: string | RegExp, data: string): this {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (lineIndex === -1) {
|
|
50
|
-
throw new Error(`Pattern not found: ${pattern}`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const lines = this.#content.split("\n");
|
|
54
|
-
lines.splice(lineIndex + 1, 0, data);
|
|
55
|
-
this.#content = lines.join("\n");
|
|
56
|
-
|
|
57
|
-
return this;
|
|
30
|
+
return this.#insertAt(pattern, data, 1);
|
|
58
31
|
}
|
|
59
32
|
|
|
60
33
|
insertBefore(pattern: string | RegExp, data: string): this {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (lineIndex === -1) {
|
|
64
|
-
throw new Error(`Pattern not found: ${pattern}`);
|
|
65
|
-
}
|
|
34
|
+
return this.#insertAt(pattern, data, 0);
|
|
35
|
+
}
|
|
66
36
|
|
|
37
|
+
#insertAt(pattern: string | RegExp, data: string, offset: number): this {
|
|
38
|
+
const lineIndex = this.find(pattern);
|
|
39
|
+
if (lineIndex === -1) throw new Error(`Pattern not found: ${pattern}`);
|
|
67
40
|
const lines = this.#content.split("\n");
|
|
68
|
-
lines.splice(lineIndex, 0, data);
|
|
41
|
+
lines.splice(lineIndex + offset, 0, data);
|
|
69
42
|
this.#content = lines.join("\n");
|
|
70
|
-
|
|
71
43
|
return this;
|
|
72
44
|
}
|
|
73
45
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { getDirname } from "./fileSys";
|
|
4
5
|
|
|
5
6
|
describe("getDirname", () => {
|
|
6
7
|
test("converts file URLs to filesystem paths", () => {
|
|
7
|
-
const
|
|
8
|
+
const filePath = path.resolve("/tmp/akan workspace/index.ts");
|
|
9
|
+
const dirname = getDirname(pathToFileURL(filePath).href);
|
|
8
10
|
|
|
9
|
-
expect(dirname).toBe(path.
|
|
11
|
+
expect(dirname).toBe(path.dirname(filePath));
|
|
10
12
|
});
|
|
11
13
|
|
|
12
14
|
test("keeps Windows drive paths valid when running on Windows", () => {
|
package/fileSys.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { lstat, rename, rm, stat } from "node:fs/promises";
|
|
2
|
+
import nodePath from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
2
4
|
import { Logger } from "akanjs/common";
|
|
3
5
|
|
|
6
|
+
export const getDirname = (url: string) => nodePath.dirname(fileURLToPath(url));
|
|
7
|
+
|
|
4
8
|
export class FileSys {
|
|
5
9
|
static logger = new Logger("FileSys");
|
|
6
10
|
static async fileExists(path: string) {
|
|
@@ -39,24 +43,33 @@ export class FileSys {
|
|
|
39
43
|
static async writeText(path: string, content: string) {
|
|
40
44
|
return await Bun.file(path).write(content);
|
|
41
45
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
* Generated barrels are written by every dev server watching the same `libs/` tree while those same
|
|
45
|
-
* servers' watchers are reading them, and a half-written barrel reads back as a user edit.
|
|
46
|
-
*
|
|
47
|
-
* The temp file is a sibling because `rename` is only atomic within a filesystem, and it lands in a
|
|
48
|
-
* watched directory — the `.tmp` suffix is what `HmrChangeClassifier` ignores it by.
|
|
49
|
-
*/
|
|
46
|
+
// One `rename`, so a watcher never reads a half-written barrel as a user edit. The temp is a sibling (rename is atomic
|
|
47
|
+
// only within a filesystem), and its `.tmp` suffix is what `HmrChangeClassifier` ignores it by.
|
|
50
48
|
static async writeTextAtomic(filePath: string, content: string) {
|
|
51
49
|
const temp = `${filePath}.${process.pid}.${Date.now().toString(36)}.tmp`;
|
|
52
50
|
try {
|
|
53
51
|
await Bun.write(temp, content);
|
|
54
|
-
await
|
|
52
|
+
await FileSys.replace(temp, filePath);
|
|
55
53
|
} catch (error) {
|
|
56
54
|
await rm(temp, { force: true }).catch(() => undefined);
|
|
57
55
|
throw error;
|
|
58
56
|
}
|
|
59
57
|
}
|
|
58
|
+
static readonly #replaceAttempts = 100;
|
|
59
|
+
// Windows refuses to rename over a file another process has open (EPERM, or EACCES/EBUSY). A reader holds it for one
|
|
60
|
+
// read, so the rename is retried for a few seconds instead of failing.
|
|
61
|
+
static async replace(temp: string, target: string) {
|
|
62
|
+
for (let attempt = 1; ; attempt++) {
|
|
63
|
+
try {
|
|
64
|
+
return await rename(temp, target);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
67
|
+
const busy = process.platform === "win32" && (code === "EPERM" || code === "EACCES" || code === "EBUSY");
|
|
68
|
+
if (!busy || attempt >= FileSys.#replaceAttempts) throw error;
|
|
69
|
+
await Bun.sleep(Math.min(10 * attempt, 50));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
60
73
|
static async writeJson(path: string, content: object) {
|
|
61
74
|
return await Bun.file(path).write(`${JSON.stringify(content, null, 2)}\n`);
|
|
62
75
|
}
|
package/formSetterScanner.ts
CHANGED
|
@@ -1,22 +1,9 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import type { QualityWarning, SourceFileInfo } from "./qualityScanner";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* A control handed `onChange={st.do.setTitleOnTask}` **by reference** names the field it writes, so it emits
|
|
8
|
-
* `data-akan-action` and `useFieldTool` publishes the field to the in-page agent. Any wrapper around that setter is
|
|
9
|
-
* an anonymous closure carrying neither, and the field goes quiet — for the agent, for an E2E selector, and for the
|
|
10
|
-
* accessibility tree.
|
|
11
|
-
*
|
|
12
|
-
* `no-unpublished-form-setter.grit` is the per-line enforcement, and it fires only on a pure forwarding wrapper,
|
|
13
|
-
* because every other shape has a legitimate reading and a lint error would be wrong. This is the other half: the
|
|
14
|
-
* per-file count of fields that ended up unreachable whatever the reason, which is the number worth watching. A
|
|
15
|
-
* warning rather than an error for the same reason — the remedy depends on why the wrapper is there.
|
|
16
|
-
*
|
|
17
|
-
* Only a handler that takes a parameter is counted. A zero-parameter handler (`onClick={() => st.do.setStatusOnUser
|
|
18
|
-
* ("active")}`) is a button setting a constant, not a form control, and its remedy is an `st.tool` beside it.
|
|
19
|
-
*/
|
|
4
|
+
// Only a setter passed by reference publishes its field (`data-akan-action`, agent tool). The grit rule flags only pure
|
|
5
|
+
// forwarding wrappers; this counts every field left unpublished whatever the reason. A zero-parameter handler is a
|
|
6
|
+
// button setting a constant, not a form control, so it is not counted.
|
|
20
7
|
export class FormSetterScanner {
|
|
21
8
|
static #fieldSetter = /^set[A-Za-z0-9_$]*On[A-Za-z0-9_$]*$/;
|
|
22
9
|
|
|
@@ -14,11 +14,6 @@ export interface BuildAllRoutesResult {
|
|
|
14
14
|
seedIndex: RouteSeedIndex;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* Walk every route in `pages` and produce its client bundle up-front.
|
|
19
|
-
* Intended to run under `akan build` (production) so the serve path
|
|
20
|
-
* never needs to compile.
|
|
21
|
-
*/
|
|
22
17
|
export class AllRoutesBuilder {
|
|
23
18
|
#app: App;
|
|
24
19
|
#artifact: BaseBuildArtifact;
|
|
@@ -47,23 +42,16 @@ export class AllRoutesBuilder {
|
|
|
47
42
|
this.#app.verbose(`[build-all] discovered ${seedIndex.entries.length} routes`);
|
|
48
43
|
this.#discovery = await GraphClientEntryDiscovery.create(this.#app);
|
|
49
44
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
// Discovery is cached and does no bundling, so collecting every entry up front costs almost nothing.
|
|
53
|
-
const allEntries: string[] = [];
|
|
54
|
-
const seen = new Set<string>();
|
|
45
|
+
// Every route's entries go into one Bun.build: chunk splitting only dedupes within a single build.
|
|
46
|
+
const allEntries = new Set<string>();
|
|
55
47
|
for (const entry of seedIndex.entries) {
|
|
56
48
|
const seeds = Array.from(new Set([...seedIndex.globalLayoutFiles, ...entry.seeds]));
|
|
57
|
-
for (const discovered of await this.#discovery.discover(seeds))
|
|
58
|
-
if (seen.has(discovered)) continue;
|
|
59
|
-
seen.add(discovered);
|
|
60
|
-
allEntries.push(discovered);
|
|
61
|
-
}
|
|
49
|
+
for (const discovered of await this.#discovery.discover(seeds)) allEntries.add(discovered);
|
|
62
50
|
this.#routeIds.push(entry.routeId);
|
|
63
51
|
}
|
|
64
|
-
this.#app.verbose(`[build-all] ${allEntries.
|
|
52
|
+
this.#app.verbose(`[build-all] ${allEntries.size} client entries across ${this.#routeIds.length} routes`);
|
|
65
53
|
|
|
66
|
-
const delta = await this.#buildEntries(allEntries);
|
|
54
|
+
const delta = await this.#buildEntries([...allEntries]);
|
|
67
55
|
this.#mergeDelta(delta);
|
|
68
56
|
this.#merged.knownEntries = Array.from(this.#knownSet);
|
|
69
57
|
|
|
@@ -113,10 +101,8 @@ export class AllRoutesBuilder {
|
|
|
113
101
|
}
|
|
114
102
|
|
|
115
103
|
#mergeDelta(delta: BuildRouteClientResult): void {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
this.#merged.ssrManifest.moduleMap[url] = byName;
|
|
119
|
-
}
|
|
104
|
+
Object.assign(this.#merged.clientManifest, delta.manifestDelta);
|
|
105
|
+
Object.assign(this.#merged.ssrManifest.moduleMap, delta.ssrManifestDelta.moduleMap);
|
|
120
106
|
for (const abs of delta.newEntries) this.#knownSet.add(abs);
|
|
121
107
|
}
|
|
122
108
|
}
|
|
@@ -1,21 +1,18 @@
|
|
|
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 { AutoImportSync, transformSource } from "./autoImportSync";
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const makeTempRoot = tempDirs("akan-auto-import-");
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
const seed = async (root: string, rel: string, content: string) => {
|
|
10
|
+
const abs = path.join(root, rel);
|
|
11
|
+
await mkdir(path.dirname(abs), { recursive: true });
|
|
12
|
+
await writeFile(abs, content);
|
|
13
|
+
return abs;
|
|
13
14
|
};
|
|
14
15
|
|
|
15
|
-
afterEach(async () => {
|
|
16
|
-
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
17
|
-
});
|
|
18
|
-
|
|
19
16
|
const constantCtx = { role: "constant", scope: "libs", project: "shared" } as const;
|
|
20
17
|
const documentCtx = { role: "document", scope: "libs", project: "shared" } as const;
|
|
21
18
|
const serviceCtx = { role: "service", scope: "libs", project: "shared" } as const;
|
|
@@ -161,6 +158,19 @@ describe("transformSource — client role (package client entry)", () => {
|
|
|
161
158
|
);
|
|
162
159
|
});
|
|
163
160
|
|
|
161
|
+
test("never claims fetch from a file that calls the platform fetch", () => {
|
|
162
|
+
const globalOnly = `"use client";\n\nexport const toBlob = async (url: string) => await (await fetch(url)).blob();\n`;
|
|
163
|
+
const mixed = `"use client";\n\nexport const C = async (url: string) => (await fetch(url)).ok && fetch.me() && st.foo;\n`;
|
|
164
|
+
const passed = `"use client";\n\nexport const load = (get = fetch) => get("/a");\n`;
|
|
165
|
+
|
|
166
|
+
expect(transformSource(globalOnly, "CropImage.tsx", clientLibCtx)).toBeNull();
|
|
167
|
+
expect(transformSource(mixed, "C.Zone.tsx", clientLibCtx)).toBe(
|
|
168
|
+
`"use client";\nimport { st } from "@libs/shared/client";\n\nexport const C = async (url: string) => (await fetch(url)).ok && fetch.me() && st.foo;\n`,
|
|
169
|
+
);
|
|
170
|
+
expect(transformSource(passed, "load.ts", clientLibCtx)).toBeNull();
|
|
171
|
+
expect(transformSource(globalOnly, "x.store.ts", storeCtx)).toBeNull();
|
|
172
|
+
});
|
|
173
|
+
|
|
164
174
|
test("handles webkit .ts files (no jsx) the same as tsx", () => {
|
|
165
175
|
const source = `import { getEnv } from "akanjs/base";\n\nexport const load = () => fetch.me();\n`;
|
|
166
176
|
const out = transformSource(source, "cookie.ts", clientLibCtx);
|
|
@@ -171,13 +181,6 @@ describe("transformSource — client role (package client entry)", () => {
|
|
|
171
181
|
});
|
|
172
182
|
|
|
173
183
|
describe("AutoImportSync.syncForBatch", () => {
|
|
174
|
-
const seed = async (root: string, rel: string, content: string) => {
|
|
175
|
-
const abs = path.join(root, rel);
|
|
176
|
-
await mkdir(path.dirname(abs), { recursive: true });
|
|
177
|
-
await writeFile(abs, content);
|
|
178
|
-
return abs;
|
|
179
|
-
};
|
|
180
|
-
|
|
181
184
|
test("writes the fix once, then is idempotent", async () => {
|
|
182
185
|
const root = await makeTempRoot();
|
|
183
186
|
const abs = await seed(
|
|
@@ -296,9 +299,7 @@ describe("AutoImportSync.syncForBatch", () => {
|
|
|
296
299
|
|
|
297
300
|
test("leaves JS globals and ambiguous domain names alone", async () => {
|
|
298
301
|
const root = await makeTempRoot();
|
|
299
|
-
// `Map` is a real model here but must not shadow the JS global.
|
|
300
302
|
await seed(root, "libs/x/lib/map/map.constant.ts", `export class Map extends via {}\n`);
|
|
301
|
-
// `Dup` is exported by two files → ambiguous → skipped.
|
|
302
303
|
await seed(root, "libs/x/lib/a/a.constant.ts", `export class Dup {}\n`);
|
|
303
304
|
await seed(root, "libs/x/lib/b/b.constant.ts", `export class Dup {}\n`);
|
|
304
305
|
const consumer = await seed(
|
|
@@ -335,7 +336,7 @@ describe("AutoImportSync.syncForBatch", () => {
|
|
|
335
336
|
|
|
336
337
|
test("srvkit: does not import a barrel whose file is absent", async () => {
|
|
337
338
|
const root = await makeTempRoot();
|
|
338
|
-
await seed(root, "libs/x/lib/dict.ts", `export const x = 1;\n`);
|
|
339
|
+
await seed(root, "libs/x/lib/dict.ts", `export const x = 1;\n`);
|
|
339
340
|
const file = await seed(root, "libs/x/srvkit/api.ts", `export const f = () => Err.of(srv.user);\n`);
|
|
340
341
|
const sync = new AutoImportSync({ workspaceRoot: root });
|
|
341
342
|
|
|
@@ -357,20 +358,11 @@ describe("AutoImportSync.syncForBatch", () => {
|
|
|
357
358
|
});
|
|
358
359
|
|
|
359
360
|
describe("AutoImportSync — scope", () => {
|
|
360
|
-
const writeAt = async (root: string, rel: string, source: string) => {
|
|
361
|
-
const abs = path.join(root, rel);
|
|
362
|
-
await mkdir(path.dirname(abs), { recursive: true });
|
|
363
|
-
await writeFile(abs, source);
|
|
364
|
-
return abs;
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
// `lib/__lib/` is generated and gitignored: `akan sync` rewrites it, which is also what makes the
|
|
368
|
-
// watcher hand it to the syncer, so an edit there is churn that the next sync discards.
|
|
369
361
|
test("leaves the generated lib/__lib stubs alone and still syncs lib/__scalar", async () => {
|
|
370
362
|
const root = await makeTempRoot();
|
|
371
363
|
const needsImport = "export class A {\n count = field(Int);\n}\n";
|
|
372
|
-
const generated = await
|
|
373
|
-
const scalar = await
|
|
364
|
+
const generated = await seed(root, "libs/shared/lib/__lib/lib.constant.ts", needsImport);
|
|
365
|
+
const scalar = await seed(root, "libs/shared/lib/__scalar/money/money.constant.ts", needsImport);
|
|
374
366
|
|
|
375
367
|
const result = await new AutoImportSync({ workspaceRoot: root }).syncForBatch([generated, scalar]);
|
|
376
368
|
|
|
@@ -380,11 +372,9 @@ describe("AutoImportSync — scope", () => {
|
|
|
380
372
|
expect(await readFile(scalar, "utf8")).toContain('import { Int } from "akanjs/base";');
|
|
381
373
|
});
|
|
382
374
|
|
|
383
|
-
// The transform is pure and the write is conditional on a difference, so a second pass over its own
|
|
384
|
-
// output writes nothing — without that the watcher would re-fire on the syncer's own edit.
|
|
385
375
|
test("a second pass over its own output changes nothing", async () => {
|
|
386
376
|
const root = await makeTempRoot();
|
|
387
|
-
const file = await
|
|
377
|
+
const file = await seed(
|
|
388
378
|
root,
|
|
389
379
|
"libs/shared/lib/task/task.constant.ts",
|
|
390
380
|
"export class A {\n count = field(Int);\n}\n",
|
|
@@ -397,11 +387,10 @@ describe("AutoImportSync — scope", () => {
|
|
|
397
387
|
expect(await readFile(file, "utf8")).toBe(afterFirst);
|
|
398
388
|
});
|
|
399
389
|
|
|
400
|
-
// One try/catch per file: a file that cannot be read is reported and the rest of the batch still runs.
|
|
401
390
|
test("a failing file is reported without abandoning the batch", async () => {
|
|
402
391
|
const root = await makeTempRoot();
|
|
403
392
|
const missing = path.join(root, "libs/shared/lib/task/gone.constant.ts");
|
|
404
|
-
const good = await
|
|
393
|
+
const good = await seed(
|
|
405
394
|
root,
|
|
406
395
|
"libs/shared/lib/task/task.constant.ts",
|
|
407
396
|
"export class A {\n count = field(Int);\n}\n",
|
|
@@ -3,11 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import ts from "typescript";
|
|
4
4
|
import { FileSys } from "../fileSys";
|
|
5
5
|
|
|
6
|
-
//*
|
|
7
|
-
//* framework symbols that are used but not imported (e.g. `Int` in a *.constant.ts, `fetch` in a *.store.ts)
|
|
8
|
-
//* are inserted automatically. Detection is a closed-registry scan: we only look for a fixed, framework-owned
|
|
9
|
-
//* set of identifiers per file role, so no type information is required. Writes only happen on a real diff,
|
|
10
|
-
//* which keeps the watcher from looping on its own edits.
|
|
6
|
+
//* Closed-registry scan (no type information); writes only on a real diff, or the watcher loops on its own edits.
|
|
11
7
|
|
|
12
8
|
export interface AutoImportSyncResult {
|
|
13
9
|
changedFiles: string[];
|
|
@@ -39,13 +35,9 @@ interface FileContext {
|
|
|
39
35
|
interface ImportTarget {
|
|
40
36
|
specifier: string;
|
|
41
37
|
kind: ImportKind;
|
|
42
|
-
//* When true, emitted as a type-only import (`import type { X }`, or inline `type X` when mixed with
|
|
43
|
-
//* value names from the same specifier). Only meaningful for `kind: "named"`.
|
|
44
38
|
typeOnly?: boolean;
|
|
45
39
|
}
|
|
46
40
|
|
|
47
|
-
//* A registry rule: the identifiers `names` should be imported from `specifier` with the given `kind`.
|
|
48
|
-
//* `specifier` may depend on the file's package (e.g. the client entrypoint).
|
|
49
41
|
interface ImportRule {
|
|
50
42
|
names: string[];
|
|
51
43
|
specifier: string | ((ctx: FileContext) => string);
|
|
@@ -55,23 +47,19 @@ interface ImportRule {
|
|
|
55
47
|
|
|
56
48
|
const TEST_FILE_RE = /\.(test|spec)\.(ts|tsx)$/;
|
|
57
49
|
|
|
58
|
-
//*
|
|
59
|
-
//* relative barrels `../cnst`, `../db`, `../srv`, `../dict` are always correct for them.
|
|
50
|
+
//* Domain files sit at a fixed depth (`lib/<model>/`), so the relative barrels below are always correct for them.
|
|
60
51
|
const AKAN_BASE: ImportRule = {
|
|
61
52
|
names: ["Int", "Float", "ID", "Any", "Upload", "enumOf", "dayjs"],
|
|
62
53
|
specifier: "akanjs/base",
|
|
63
54
|
kind: "named",
|
|
64
55
|
};
|
|
65
|
-
//* Type-only exports of akanjs/base — emitted as `type` imports (e.g. `import { type Dayjs, dayjs }`).
|
|
66
56
|
const AKAN_BASE_TYPES: ImportRule = { names: ["Dayjs"], specifier: "akanjs/base", kind: "named", typeOnly: true };
|
|
67
57
|
const CNST_NS: ImportRule = { names: ["cnst"], specifier: "../cnst", kind: "namespace" };
|
|
68
58
|
const DB_NS: ImportRule = { names: ["db"], specifier: "../db", kind: "namespace" };
|
|
69
59
|
const SRV_NS: ImportRule = { names: ["srv"], specifier: "../srv", kind: "namespace" };
|
|
70
60
|
const ERR_DICT: ImportRule = { names: ["Err"], specifier: "../dict", kind: "named" };
|
|
71
61
|
|
|
72
|
-
//*
|
|
73
|
-
//* only high-confidence, framework-owned identifiers are listed (domain model/scalar refs are resolved
|
|
74
|
-
//* separately). Order matters only when the same name could appear twice — keep names unique per role.
|
|
62
|
+
//* Keep names unique per role: targetFor takes the first rule that lists a name.
|
|
75
63
|
const RULES: Record<FileRole, ImportRule[]> = {
|
|
76
64
|
constant: [AKAN_BASE, AKAN_BASE_TYPES, { names: ["via"], specifier: "akanjs/constant", kind: "named" }],
|
|
77
65
|
document: [
|
|
@@ -143,8 +131,6 @@ const RULES: Record<FileRole, ImportRule[]> = {
|
|
|
143
131
|
],
|
|
144
132
|
};
|
|
145
133
|
|
|
146
|
-
//* Domain imports resolve open-ended model/scalar class refs against a per-package index, rather than
|
|
147
|
-
//* a fixed registry. Only these roles opt in, and each may pull from the listed sibling file kinds.
|
|
148
134
|
type DomainKind = "constant" | "document" | "signal";
|
|
149
135
|
type DomainIndex = Map<string, { file: string; kind: DomainKind }[]>;
|
|
150
136
|
const PASCAL_CASE_RE = /^[A-Z][A-Za-z0-9]*$/;
|
|
@@ -153,17 +139,17 @@ const DOMAIN_ROLE_KINDS: Partial<Record<FileRole, DomainKind[]>> = {
|
|
|
153
139
|
dictionary: ["constant", "document", "signal"],
|
|
154
140
|
common: ["constant"],
|
|
155
141
|
};
|
|
156
|
-
//*
|
|
157
|
-
//* Their specifier depth is per-file (see `#libBarrelResolver`), so they can't live in the RULES table.
|
|
142
|
+
//* srvkit/common import the lib barrels from a per-file depth, so they cannot live in the RULES table.
|
|
158
143
|
const LIB_BARRELS: Record<string, { barrel: string; kind: ImportKind }> = {
|
|
159
144
|
Err: { barrel: "dict", kind: "named" },
|
|
160
145
|
db: { barrel: "db", kind: "namespace" },
|
|
161
146
|
cnst: { barrel: "cnst", kind: "namespace" },
|
|
162
147
|
srv: { barrel: "srv", kind: "namespace" },
|
|
163
148
|
};
|
|
164
|
-
//*
|
|
165
|
-
//*
|
|
166
|
-
|
|
149
|
+
//* Importing Akan's `fetch` over the platform global silently rebinds `fetch(url)`, so such a name is claimed
|
|
150
|
+
//* only when every reference is member-shaped (`fetch.viewX()`).
|
|
151
|
+
const GLOBAL_NAMES = new Set(["fetch"]);
|
|
152
|
+
//* Structural globals a package model must not shadow (`new Map()`); `File` is left out on purpose — it is a real model.
|
|
167
153
|
const DOMAIN_DENYLIST = new Set([
|
|
168
154
|
"Map",
|
|
169
155
|
"Set",
|
|
@@ -204,7 +190,6 @@ const DOMAIN_DENYLIST = new Set([
|
|
|
204
190
|
|
|
205
191
|
export class AutoImportSync {
|
|
206
192
|
readonly #workspaceRoot: string;
|
|
207
|
-
//* Per-package domain index (symbol -> source file), invalidated when a package's model file changes.
|
|
208
193
|
readonly #domainCache = new Map<string, DomainIndex>();
|
|
209
194
|
|
|
210
195
|
constructor({ workspaceRoot }: AutoImportSyncOptions) {
|
|
@@ -216,8 +201,6 @@ export class AutoImportSync {
|
|
|
216
201
|
const errors: string[] = [];
|
|
217
202
|
const seen = new Set<string>();
|
|
218
203
|
|
|
219
|
-
// Drop cached domain indexes for any package whose model files changed in this batch, so a newly
|
|
220
|
-
// added/renamed model is visible to references resolved later in the same batch.
|
|
221
204
|
for (const file of files) {
|
|
222
205
|
const pkgRoot = this.#domainPkgRootOf(file);
|
|
223
206
|
if (pkgRoot) this.#domainCache.delete(pkgRoot);
|
|
@@ -240,8 +223,6 @@ export class AutoImportSync {
|
|
|
240
223
|
return { changedFiles, errors };
|
|
241
224
|
}
|
|
242
225
|
|
|
243
|
-
//* Resolve the role + package location for a file, or null when the file is out of scope
|
|
244
|
-
//* (outside apps/libs, a generated/declaration/test file, or a facet+extension we do not handle).
|
|
245
226
|
#contextFor(abs: string): FileContext | null {
|
|
246
227
|
const rel = path.relative(this.#workspaceRoot, abs);
|
|
247
228
|
if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
|
|
@@ -251,9 +232,7 @@ export class AutoImportSync {
|
|
|
251
232
|
const parts = rel.split(path.sep).filter(Boolean);
|
|
252
233
|
const [scope, project, facet] = parts;
|
|
253
234
|
if ((scope !== "apps" && scope !== "libs") || !project || !facet) return null;
|
|
254
|
-
//* `lib/__lib/`
|
|
255
|
-
//* `akan sync`, which is also what makes the watcher report them. An edit there is churn at best and
|
|
256
|
-
//* silently discarded at worst. `lib/__scalar/` is real source and stays in scope.
|
|
235
|
+
//* `lib/__lib/` is rewritten by every `akan sync`, so an edit there is discarded; `lib/__scalar/` is real source.
|
|
257
236
|
if (facet === "lib" && parts[3] === "__lib") return null;
|
|
258
237
|
const role = roleFor(facet, base);
|
|
259
238
|
if (!role) return null;
|
|
@@ -272,10 +251,6 @@ export class AutoImportSync {
|
|
|
272
251
|
return true;
|
|
273
252
|
}
|
|
274
253
|
|
|
275
|
-
//* Build the dynamic resolver for identifiers the static registry does not cover, or undefined when
|
|
276
|
-
//* the role has none. `srvkit`/`common` resolve the package's generated lib barrels (variable depth);
|
|
277
|
-
//* the domain roles resolve open-ended model/scalar refs against the per-package index. `common`
|
|
278
|
-
//* uses both — barrels first, then domain (the two symbol sets are disjoint).
|
|
279
254
|
async #extraResolverFor(abs: string, ctx: FileContext) {
|
|
280
255
|
const resolvers: ((symbol: string) => ImportTarget | null)[] = [];
|
|
281
256
|
if (ctx.role === "srvkit" || ctx.role === "common") resolvers.push(await this.#libBarrelResolver(abs, ctx));
|
|
@@ -291,22 +266,17 @@ export class AutoImportSync {
|
|
|
291
266
|
};
|
|
292
267
|
}
|
|
293
268
|
|
|
294
|
-
//* Resolve an unbound PascalCase identifier to a relative import of the package model/scalar that
|
|
295
|
-
//* exports it (only when exactly one package file of an allowed kind exports that name).
|
|
296
269
|
async #domainResolver(abs: string, ctx: FileContext, kinds: DomainKind[]) {
|
|
297
270
|
const index = await this.#domainIndex(path.join(this.#workspaceRoot, ctx.scope, ctx.project));
|
|
298
271
|
const fileDir = path.dirname(abs);
|
|
299
272
|
return (symbol: string): ImportTarget | null => {
|
|
300
273
|
if (!PASCAL_CASE_RE.test(symbol) || DOMAIN_DENYLIST.has(symbol)) return null;
|
|
301
274
|
const entries = (index.get(symbol) ?? []).filter((entry) => kinds.includes(entry.kind));
|
|
302
|
-
if (entries.length !== 1) return null;
|
|
275
|
+
if (entries.length !== 1) return null;
|
|
303
276
|
return { specifier: relativeSpecifier(fileDir, entries[0].file), kind: "named" };
|
|
304
277
|
};
|
|
305
278
|
}
|
|
306
279
|
|
|
307
|
-
//* srvkit/common files import the package's generated lib barrels from a variable depth
|
|
308
|
-
//* (`../lib/dict`, `../../lib/dict`, …). Resolve each barrel name to a path relative to this file,
|
|
309
|
-
//* but only for barrels that actually exist (so an absent `lib/srv.ts` never yields a broken import).
|
|
310
280
|
async #libBarrelResolver(abs: string, ctx: FileContext) {
|
|
311
281
|
const fileDir = path.dirname(abs);
|
|
312
282
|
const libDir = path.join(this.#workspaceRoot, ctx.scope, ctx.project, "lib");
|
|
@@ -326,7 +296,6 @@ export class AutoImportSync {
|
|
|
326
296
|
return index;
|
|
327
297
|
}
|
|
328
298
|
|
|
329
|
-
//* The apps/libs package root for a model file (`*.constant/document/signal.ts` under `lib/`), else null.
|
|
330
299
|
#domainPkgRootOf(file: string): string | null {
|
|
331
300
|
const rel = path.relative(this.#workspaceRoot, path.resolve(file));
|
|
332
301
|
if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
|
|
@@ -337,9 +306,6 @@ export class AutoImportSync {
|
|
|
337
306
|
}
|
|
338
307
|
}
|
|
339
308
|
|
|
340
|
-
//* Which facet+extension combinations get auto-imports, and the role that drives their registry.
|
|
341
|
-
//* `client` covers every file that pulls domain symbols from the package client entrypoint:
|
|
342
|
-
//* lib UI (.tsx), the `ui`/`page` facets (.tsx), and the `webkit` facet (.ts/.tsx).
|
|
343
309
|
const roleFor = (facet: string, base: string): FileRole | null => {
|
|
344
310
|
if (facet === "lib") {
|
|
345
311
|
if (base.endsWith(".constant.ts")) return "constant";
|
|
@@ -367,8 +333,7 @@ const targetFor = (symbol: string, ctx: FileContext): ImportTarget | null => {
|
|
|
367
333
|
return null;
|
|
368
334
|
};
|
|
369
335
|
|
|
370
|
-
|
|
371
|
-
//* handles identifiers not matched by the framework registry — domain model refs and srvkit barrels.
|
|
336
|
+
/** The rewritten source, or null when nothing changes. */
|
|
372
337
|
export const transformSource = (
|
|
373
338
|
source: string,
|
|
374
339
|
fileName: string,
|
|
@@ -378,13 +343,12 @@ export const transformSource = (
|
|
|
378
343
|
const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, scriptKindFor(fileName));
|
|
379
344
|
const bound = collectBoundNames(sf);
|
|
380
345
|
const used = collectUsedReferences(sf);
|
|
346
|
+
const bareGlobals = collectBareGlobalReferences(sf);
|
|
381
347
|
|
|
382
|
-
// Group the needed symbols by their import target: framework registry first, then dynamic resolver.
|
|
383
|
-
// Named groups map each name to whether it is type-only, so we can emit `import type`/inline `type`.
|
|
384
348
|
const namedBySpecifier = new Map<string, Map<string, boolean>>();
|
|
385
349
|
const namespaceImports: { name: string; specifier: string }[] = [];
|
|
386
350
|
for (const name of used) {
|
|
387
|
-
if (bound.has(name)) continue;
|
|
351
|
+
if (bound.has(name) || bareGlobals.has(name)) continue;
|
|
388
352
|
const target = targetFor(name, ctx) ?? resolveExtra?.(name) ?? null;
|
|
389
353
|
if (!target) continue;
|
|
390
354
|
if (target.kind === "namespace") namespaceImports.push({ name, specifier: target.specifier });
|
|
@@ -418,8 +382,7 @@ export const transformSource = (
|
|
|
418
382
|
for (const ns of namespaceImports) newStatements.push(`import * as ${ns.name} from "${ns.specifier}";`);
|
|
419
383
|
|
|
420
384
|
if (newStatements.length > 0) {
|
|
421
|
-
//
|
|
422
|
-
// zero-width insertion sharing a boundary with the merge replacement.
|
|
385
|
+
// A zero-width insertion at the anchor's boundary would collide with the anchor's own merge edit.
|
|
423
386
|
const anchorEdit = anchor ? edits.find((edit) => edit.start === anchor.getStart(sf)) : undefined;
|
|
424
387
|
if (anchorEdit) anchorEdit.text = `${anchorEdit.text}\n${newStatements.join("\n")}`;
|
|
425
388
|
else if (anchor)
|
|
@@ -438,8 +401,7 @@ export const transformSource = (
|
|
|
438
401
|
return out === source ? null : out;
|
|
439
402
|
};
|
|
440
403
|
|
|
441
|
-
//*
|
|
442
|
-
//* suppresses an insertion (safe); under-collecting would risk a duplicate import (unsafe).
|
|
404
|
+
//* Over-collecting only suppresses an insertion; under-collecting would add a duplicate import.
|
|
443
405
|
const collectBoundNames = (sf: ts.SourceFile): Set<string> => {
|
|
444
406
|
const names = new Set<string>();
|
|
445
407
|
const visit = (node: ts.Node) => {
|
|
@@ -478,8 +440,19 @@ const collectUsedReferences = (sf: ts.SourceFile): Set<string> => {
|
|
|
478
440
|
return used;
|
|
479
441
|
};
|
|
480
442
|
|
|
481
|
-
|
|
482
|
-
|
|
443
|
+
const collectBareGlobalReferences = (sf: ts.SourceFile): Set<string> => {
|
|
444
|
+
const bare = new Set<string>();
|
|
445
|
+
const visit = (node: ts.Node) => {
|
|
446
|
+
if (ts.isIdentifier(node) && GLOBAL_NAMES.has(node.text) && isReferencePosition(node)) {
|
|
447
|
+
const parent = node.parent;
|
|
448
|
+
if (!(parent && ts.isPropertyAccessExpression(parent) && parent.expression === node)) bare.add(node.text);
|
|
449
|
+
}
|
|
450
|
+
ts.forEachChild(node, visit);
|
|
451
|
+
};
|
|
452
|
+
ts.forEachChild(sf, visit);
|
|
453
|
+
return bare;
|
|
454
|
+
};
|
|
455
|
+
|
|
483
456
|
const isReferencePosition = (node: ts.Identifier): boolean => {
|
|
484
457
|
const parent = node.parent;
|
|
485
458
|
if (!parent) return true;
|
|
@@ -494,7 +467,7 @@ const isReferencePosition = (node: ts.Identifier): boolean => {
|
|
|
494
467
|
|
|
495
468
|
interface ExistingNamedImport {
|
|
496
469
|
decl: ts.ImportDeclaration;
|
|
497
|
-
names: Map<string, boolean>;
|
|
470
|
+
names: Map<string, boolean>;
|
|
498
471
|
}
|
|
499
472
|
|
|
500
473
|
const collectExistingNamedImports = (importDecls: ts.ImportDeclaration[]): Map<string, ExistingNamedImport> => {
|
|
@@ -505,7 +478,7 @@ const collectExistingNamedImports = (importDecls: ts.ImportDeclaration[]): Map<s
|
|
|
505
478
|
if (!clause || !bindings || !ts.isNamedImports(bindings)) continue;
|
|
506
479
|
if (!ts.isStringLiteral(decl.moduleSpecifier)) continue;
|
|
507
480
|
const specifier = decl.moduleSpecifier.text;
|
|
508
|
-
if (map.has(specifier)) continue;
|
|
481
|
+
if (map.has(specifier)) continue;
|
|
509
482
|
const names = new Map<string, boolean>();
|
|
510
483
|
for (const el of bindings.elements) names.set(el.name.text, clause.isTypeOnly || el.isTypeOnly);
|
|
511
484
|
map.set(specifier, { decl, names });
|
|
@@ -513,8 +486,6 @@ const collectExistingNamedImports = (importDecls: ts.ImportDeclaration[]): Map<s
|
|
|
513
486
|
return map;
|
|
514
487
|
};
|
|
515
488
|
|
|
516
|
-
//* Render a named import, hoisting to `import type { … }` when every name is type-only and otherwise
|
|
517
|
-
//* prefixing each type-only name with inline `type`. Names sorted case-insensitively (Biome order).
|
|
518
489
|
const formatNamedImport = (specifier: string, names: Map<string, boolean>): string => {
|
|
519
490
|
const entries = [...names.entries()].sort((a, b) => compareNames(a[0], b[0]));
|
|
520
491
|
if (entries.every(([, isType]) => isType))
|
|
@@ -534,8 +505,7 @@ const directivePrologueEnd = (sf: ts.SourceFile): number => {
|
|
|
534
505
|
|
|
535
506
|
const scriptKindFor = (fileName: string) => (fileName.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
536
507
|
|
|
537
|
-
|
|
538
|
-
//* import specifier sort — so e.g. `Dayjs` precedes `dayjs`.
|
|
508
|
+
// Matches Biome's import-specifier sort: case-insensitive, uppercase first on a tie (`Dayjs` before `dayjs`).
|
|
539
509
|
const compareNames = (a: string, b: string): number => {
|
|
540
510
|
const [la, lb] = [a.toLowerCase(), b.toLowerCase()];
|
|
541
511
|
if (la !== lb) return la < lb ? -1 : 1;
|
|
@@ -549,8 +519,6 @@ const fileExists = async (file: string) =>
|
|
|
549
519
|
.then((s) => s.isFile())
|
|
550
520
|
.catch(() => false);
|
|
551
521
|
|
|
552
|
-
// ---- Domain index ---------------------------------------------------------------------------------
|
|
553
|
-
|
|
554
522
|
const domainKindOf = (base: string): DomainKind | null => {
|
|
555
523
|
if (base.endsWith(".constant.ts")) return "constant";
|
|
556
524
|
if (base.endsWith(".document.ts")) return "document";
|
|
@@ -558,7 +526,6 @@ const domainKindOf = (base: string): DomainKind | null => {
|
|
|
558
526
|
return null;
|
|
559
527
|
};
|
|
560
528
|
|
|
561
|
-
//* Scan a package `lib/` for model files and index their exported PascalCase declarations.
|
|
562
529
|
const buildDomainIndex = async (libDir: string): Promise<DomainIndex> => {
|
|
563
530
|
const index: DomainIndex = new Map();
|
|
564
531
|
for (const file of await collectDomainFiles(libDir)) {
|
|
@@ -604,7 +571,6 @@ const exportedPascalNames = (source: string, fileName: string): string[] => {
|
|
|
604
571
|
return names.filter((name) => PASCAL_CASE_RE.test(name));
|
|
605
572
|
};
|
|
606
573
|
|
|
607
|
-
//* Relative module specifier from a file's directory to a target file, extension stripped, `./`-anchored.
|
|
608
574
|
const relativeSpecifier = (fromDir: string, toFile: string): string => {
|
|
609
575
|
const rel = path
|
|
610
576
|
.relative(fromDir, toFile)
|