@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,27 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
4
2
|
import path from "node:path";
|
|
5
|
-
import {
|
|
3
|
+
import { ApplicationBuildReporter } from "../applicationBuildReporter";
|
|
4
|
+
import { tempDirs, writeText as write } from "../testHelpers";
|
|
5
|
+
import { BarrelAnalyzer, type BarrelExportTarget } from "./barrelAnalyzer";
|
|
6
6
|
import { rewriteBarrelImports } from "./barrelImportsPlugin";
|
|
7
|
-
import { toClientReferencePath, transformUseClient } from "./rscUseClientTransform";
|
|
7
|
+
import { scanUseClientExports, toClientReferencePath, transformUseClient } from "./rscUseClientTransform";
|
|
8
|
+
import { createUseClientBundlePlugin } from "./useClientBundlePlugin";
|
|
8
9
|
|
|
9
|
-
const
|
|
10
|
+
const makeTempRoot = tempDirs("akan-devkit-transform-");
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
tempRoots.push(root);
|
|
14
|
-
return root;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const write = async (filePath: string, content: string) => {
|
|
18
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
19
|
-
await writeFile(filePath, content);
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
afterEach(async () => {
|
|
23
|
-
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
24
|
-
});
|
|
12
|
+
const fakeAnalyzer = (entries: [string, BarrelExportTarget][]) =>
|
|
13
|
+
({ analyze: async () => new Map(entries) }) as unknown as BarrelAnalyzer;
|
|
25
14
|
|
|
26
15
|
describe("transformUseClient", () => {
|
|
27
16
|
test("returns null for non-client modules and generated root layouts", () => {
|
|
@@ -38,6 +27,19 @@ describe("transformUseClient", () => {
|
|
|
38
27
|
).toBeNull();
|
|
39
28
|
});
|
|
40
29
|
|
|
30
|
+
test("a client module with no exports stays server code and is not reported as a client module", () => {
|
|
31
|
+
const reported: string[] = [];
|
|
32
|
+
const onClientModule = (file: string) => reported.push(file);
|
|
33
|
+
expect(
|
|
34
|
+
transformUseClient('"use client";\nimport "./side-effect";\n', { path: "/repo/ui/Fx.tsx", onClientModule }),
|
|
35
|
+
).toBeNull();
|
|
36
|
+
expect(reported).toEqual([]);
|
|
37
|
+
expect(
|
|
38
|
+
transformUseClient('"use client"; export const value = 1;', { path: "/repo/ui/V.tsx", onClientModule }),
|
|
39
|
+
).not.toBeNull();
|
|
40
|
+
expect(reported).toEqual(["/repo/ui/V.tsx"]);
|
|
41
|
+
});
|
|
42
|
+
|
|
41
43
|
test("stubs named and default exports as RSC client references", () => {
|
|
42
44
|
const source = [
|
|
43
45
|
"// comment before directive",
|
|
@@ -62,6 +64,65 @@ describe("transformUseClient", () => {
|
|
|
62
64
|
});
|
|
63
65
|
});
|
|
64
66
|
|
|
67
|
+
describe('export * in a "use client" module', () => {
|
|
68
|
+
test("is refused with the file, the specifier and the fix, since the server would see none of its names", () => {
|
|
69
|
+
const source = [
|
|
70
|
+
'"use client";',
|
|
71
|
+
'export * from "../../ui/UserLeave";',
|
|
72
|
+
"export const General = () => null;",
|
|
73
|
+
"",
|
|
74
|
+
].join("\n");
|
|
75
|
+
expect(() =>
|
|
76
|
+
transformUseClient(source, { path: "/repo/libs/shared/lib/user/User.Template.tsx", workspaceRoot: "/repo" }),
|
|
77
|
+
).toThrow(
|
|
78
|
+
'libs/shared/lib/user/User.Template.tsx is a "use client" module, so it cannot `export * from "../../ui/UserLeave"`',
|
|
79
|
+
);
|
|
80
|
+
expect(() => scanUseClientExports('"use client";\nexport * from "./a";\n', "/repo/ui/Barrel.ts")).toThrow(
|
|
81
|
+
"Re-export them by name",
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("leaves every other export shape alone, and a module that is not a client boundary", () => {
|
|
86
|
+
const file = "/repo/ui/Barrel.tsx";
|
|
87
|
+
const shapes = [
|
|
88
|
+
'"use client";',
|
|
89
|
+
'export type * from "./types";',
|
|
90
|
+
'export * as ns from "./namespace";',
|
|
91
|
+
'export { LeaveInfo, Voc } from "./leave";',
|
|
92
|
+
"// export * from './commented';",
|
|
93
|
+
"export const text = \"export * from './a string'\";",
|
|
94
|
+
"",
|
|
95
|
+
].join("\n");
|
|
96
|
+
expect(scanUseClientExports(shapes, file).sort()).toEqual(["LeaveInfo", "Voc", "ns", "text"]);
|
|
97
|
+
expect(scanUseClientExports('export * from "./a";\nexport const X = 1;\n', file)).toEqual(["X"]);
|
|
98
|
+
const stub = transformUseClient(shapes, { path: file, workspaceRoot: "/repo" });
|
|
99
|
+
expect(stub).toContain("export const LeaveInfo = registerClientReference");
|
|
100
|
+
expect(stub).toContain("export const Voc = registerClientReference");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("fails the server bundle, and the build report carries the reason out of the bundler's error", async () => {
|
|
104
|
+
const root = await makeTempRoot();
|
|
105
|
+
await write(path.join(root, "Leave.tsx"), '"use client";\nexport const LeaveInfo = () => null;\n');
|
|
106
|
+
await write(
|
|
107
|
+
path.join(root, "Template.tsx"),
|
|
108
|
+
'"use client";\nexport * from "./Leave";\nexport const General = () => null;\n',
|
|
109
|
+
);
|
|
110
|
+
await write(path.join(root, "page.tsx"), 'import * as Template from "./Template";\nexport const T = Template;\n');
|
|
111
|
+
const failure = await Bun.build({
|
|
112
|
+
entrypoints: [path.join(root, "page.tsx")],
|
|
113
|
+
target: "bun",
|
|
114
|
+
external: ["react-server-dom-webpack/server.node"],
|
|
115
|
+
plugins: [createUseClientBundlePlugin({ workspaceRoot: root })],
|
|
116
|
+
}).then(
|
|
117
|
+
() => null,
|
|
118
|
+
(error: unknown) => error,
|
|
119
|
+
);
|
|
120
|
+
expect(ApplicationBuildReporter.formatError(failure)).toContain(
|
|
121
|
+
'Template.tsx is a "use client" module, so it cannot `export * from "./Leave"`',
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
65
126
|
describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
66
127
|
test("analyzes local, named, aliased, and star re-exports while skipping type and namespace exports", async () => {
|
|
67
128
|
const root = await makeTempRoot();
|
|
@@ -97,14 +158,83 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
|
97
158
|
expect(map?.has("ns")).toBe(false);
|
|
98
159
|
});
|
|
99
160
|
|
|
161
|
+
test("a name a module exports itself wins over the same name its star re-exports bring, as ES decides", async () => {
|
|
162
|
+
const root = await makeTempRoot();
|
|
163
|
+
const pkgDir = path.join(root, "pkg");
|
|
164
|
+
await write(
|
|
165
|
+
path.join(pkgDir, "index.ts"),
|
|
166
|
+
[
|
|
167
|
+
'export * from "./a";',
|
|
168
|
+
'export { X } from "./c";',
|
|
169
|
+
'export * from "./nested";',
|
|
170
|
+
"export const Local = 1;",
|
|
171
|
+
"",
|
|
172
|
+
].join("\n"),
|
|
173
|
+
);
|
|
174
|
+
await write(path.join(pkgDir, "a.ts"), "export const X = 2;\nexport const Local = 2;\nexport const OnlyA = 2;\n");
|
|
175
|
+
await write(path.join(pkgDir, "c.ts"), "export const X = 1;\n");
|
|
176
|
+
await write(path.join(pkgDir, "nested", "index.ts"), 'export * from "./deep";\nexport { Y } from "./y";\n');
|
|
177
|
+
await write(path.join(pkgDir, "nested", "deep.ts"), "export const Y = 2;\n");
|
|
178
|
+
await write(path.join(pkgDir, "nested", "y.ts"), "export const Y = 1;\n");
|
|
179
|
+
const analyzer = new BarrelAnalyzer({
|
|
180
|
+
resolvePackage: async () => ({ pkgName: "@scope/pkg", entryFile: path.join(pkgDir, "index.ts"), pkgDir }),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const runtime = (await import(path.join(pkgDir, "index.ts"))) as Record<string, number>;
|
|
184
|
+
expect([runtime.X, runtime.Local, runtime.Y, runtime.OnlyA]).toEqual([1, 1, 1, 2]);
|
|
185
|
+
const map = await analyzer.analyze("@scope/pkg");
|
|
186
|
+
expect(map?.get("X")).toEqual({ subpath: "@scope/pkg/c", originalName: "X" });
|
|
187
|
+
expect(map?.get("Local")).toEqual({ subpath: "@scope/pkg", originalName: "Local" });
|
|
188
|
+
expect(map?.get("Y")).toEqual({ subpath: "@scope/pkg/nested/y", originalName: "Y" });
|
|
189
|
+
expect(map?.get("OnlyA")).toEqual({ subpath: "@scope/pkg/a", originalName: "OnlyA" });
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("two analyses that interleave each keep their own place in their barrel", async () => {
|
|
193
|
+
const root = await makeTempRoot();
|
|
194
|
+
const barrels = { first: ["a1", "a2", "a3"], second: ["b1", "b2"] } as const;
|
|
195
|
+
for (const [pkgName, names] of Object.entries(barrels)) {
|
|
196
|
+
const lines = names.map((name) => `export { ${name} } from "./${name}";`);
|
|
197
|
+
// Parks the second walk past the end of the first barrel, so a shared regex position would end the first early.
|
|
198
|
+
const padding = pkgName === "second" ? [`// ${"-".repeat(200)}`] : [];
|
|
199
|
+
await write(path.join(root, pkgName, "index.ts"), [...padding, ...lines, ""].join("\n"));
|
|
200
|
+
for (const name of names) await write(path.join(root, pkgName, `${name}.ts`), `export const ${name} = 1;\n`);
|
|
201
|
+
}
|
|
202
|
+
const parked = { first: Promise.withResolvers<void>(), second: Promise.withResolvers<void>() };
|
|
203
|
+
const released = { first: Promise.withResolvers<void>(), second: Promise.withResolvers<void>() };
|
|
204
|
+
const analyzer = new BarrelAnalyzer({
|
|
205
|
+
resolvePackage: async (pkgName) => ({
|
|
206
|
+
pkgName,
|
|
207
|
+
entryFile: path.join(root, pkgName, "index.ts"),
|
|
208
|
+
pkgDir: path.join(root, pkgName),
|
|
209
|
+
}),
|
|
210
|
+
resolveRelative: async (fromFile, relSpec) => {
|
|
211
|
+
const pkgName = path.basename(path.dirname(fromFile)) as keyof typeof barrels;
|
|
212
|
+
if (relSpec === `./${barrels[pkgName][0]}`) {
|
|
213
|
+
parked[pkgName].resolve();
|
|
214
|
+
await released[pkgName].promise;
|
|
215
|
+
}
|
|
216
|
+
return path.join(path.dirname(fromFile), `${relSpec.slice(2)}.ts`);
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const first = analyzer.analyze("first");
|
|
221
|
+
await parked.first.promise;
|
|
222
|
+
const second = analyzer.analyze("second");
|
|
223
|
+
await parked.second.promise;
|
|
224
|
+
released.first.resolve();
|
|
225
|
+
const firstMap = await first;
|
|
226
|
+
released.second.resolve();
|
|
227
|
+
const secondMap = await second;
|
|
228
|
+
|
|
229
|
+
expect(firstMap?.get("a3")).toEqual({ subpath: "first/a3", originalName: "a3" });
|
|
230
|
+
expect(secondMap?.get("b2")).toEqual({ subpath: "second/b2", originalName: "b2" });
|
|
231
|
+
});
|
|
232
|
+
|
|
100
233
|
test("rewrites flattenable named imports and preserves default, type, and unknown imports", async () => {
|
|
101
|
-
const analyzer =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
["Bee", { subpath: "@scope/pkg/leaf", originalName: "B" }],
|
|
106
|
-
]),
|
|
107
|
-
} as unknown as BarrelAnalyzer;
|
|
234
|
+
const analyzer = fakeAnalyzer([
|
|
235
|
+
["A", { subpath: "@scope/pkg/leaf", originalName: "A" }],
|
|
236
|
+
["Bee", { subpath: "@scope/pkg/leaf", originalName: "B" }],
|
|
237
|
+
]);
|
|
108
238
|
|
|
109
239
|
const rewritten = await rewriteBarrelImports(
|
|
110
240
|
'import DefaultExport, { A, Bee as LocalBee, type Shape, Missing } from "@scope/pkg";\nconsole.log(A);',
|
|
@@ -126,9 +256,7 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
|
126
256
|
});
|
|
127
257
|
|
|
128
258
|
test("preserves generated client barrel side effects when flattening app client imports", async () => {
|
|
129
|
-
const analyzer = {
|
|
130
|
-
analyze: async () => new Map([["st", { subpath: "@apps/demo/lib/st", originalName: "st" }]]),
|
|
131
|
-
} as unknown as BarrelAnalyzer;
|
|
259
|
+
const analyzer = fakeAnalyzer([["st", { subpath: "@apps/demo/lib/st", originalName: "st" }]]);
|
|
132
260
|
|
|
133
261
|
const rewritten = await rewriteBarrelImports(
|
|
134
262
|
'import { st } from "@apps/demo/client";\nvoid st;\n',
|
|
@@ -142,9 +270,7 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
|
142
270
|
});
|
|
143
271
|
|
|
144
272
|
test("does not rewrite import-looking code inside template literals", async () => {
|
|
145
|
-
const analyzer = {
|
|
146
|
-
analyze: async () => new Map([["AkanApp", { subpath: "akanjs/server/akanApp", originalName: "AkanApp" }]]),
|
|
147
|
-
} as unknown as BarrelAnalyzer;
|
|
273
|
+
const analyzer = fakeAnalyzer([["AkanApp", { subpath: "akanjs/server/akanApp", originalName: "AkanApp" }]]);
|
|
148
274
|
|
|
149
275
|
const source = [
|
|
150
276
|
'import { Code } from "@apps/docs/ui";',
|
|
@@ -164,13 +290,10 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
|
164
290
|
});
|
|
165
291
|
|
|
166
292
|
test("rewrites akanjs/server value imports to leaf subpaths", async () => {
|
|
167
|
-
const analyzer =
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
["Try", { subpath: "akanjs/server/decorators", originalName: "Try" }],
|
|
172
|
-
]),
|
|
173
|
-
} as unknown as BarrelAnalyzer;
|
|
293
|
+
const analyzer = fakeAnalyzer([
|
|
294
|
+
["AkanOption", { subpath: "akanjs/server/akanOption", originalName: "AkanOption" }],
|
|
295
|
+
["Try", { subpath: "akanjs/server/decorators", originalName: "Try" }],
|
|
296
|
+
]);
|
|
174
297
|
|
|
175
298
|
const rewritten = await rewriteBarrelImports(
|
|
176
299
|
'import { AkanOption, Try } from "akanjs/server";\nexport const option = new AkanOption();\n',
|
|
@@ -188,10 +311,9 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
|
188
311
|
});
|
|
189
312
|
|
|
190
313
|
test("rewrites single-package Akan facet barrels to leaf subpaths", async () => {
|
|
191
|
-
const analyzer =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
} as unknown as BarrelAnalyzer;
|
|
314
|
+
const analyzer = fakeAnalyzer([
|
|
315
|
+
["BottomInset", { subpath: "akanjs/ui/Layout/BottomInset", originalName: "BottomInset" }],
|
|
316
|
+
]);
|
|
195
317
|
|
|
196
318
|
const rewritten = await rewriteBarrelImports('import { BottomInset } from "akanjs/ui";\n', ["akanjs/ui"], analyzer);
|
|
197
319
|
|
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir,
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
4
|
+
import { tempDirs } from "../testHelpers";
|
|
5
5
|
import { createTsconfigPackageResolver } from "./barrelImportsPlugin";
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
const makeTempRoot = async () => {
|
|
10
|
-
const root = await mkdtemp(path.join(os.tmpdir(), "akan-devkit-resolver-"));
|
|
11
|
-
tempRoots.push(root);
|
|
12
|
-
return root;
|
|
13
|
-
};
|
|
7
|
+
const makeTempRoot = tempDirs("akan-devkit-resolver-");
|
|
14
8
|
|
|
15
9
|
const write = async (root: string, relPath: string, content = "export const value = 1;\n") => {
|
|
16
10
|
const filePath = path.join(root, relPath);
|
|
@@ -25,10 +19,6 @@ const resolverFor = async (root: string, paths: Record<string, string[]>) =>
|
|
|
25
19
|
getTsConfig: async () => ({ compilerOptions: { paths } }),
|
|
26
20
|
} as never);
|
|
27
21
|
|
|
28
|
-
afterEach(async () => {
|
|
29
|
-
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
30
|
-
});
|
|
31
|
-
|
|
32
22
|
describe("createTsconfigPackageResolver — exact tsconfig mapping", () => {
|
|
33
23
|
test("resolves a package barrel and keeps the specifier", async () => {
|
|
34
24
|
const root = await makeTempRoot();
|
|
@@ -43,9 +33,6 @@ describe("createTsconfigPackageResolver — exact tsconfig mapping", () => {
|
|
|
43
33
|
});
|
|
44
34
|
|
|
45
35
|
test("collapses a facet specifier to its parent package", async () => {
|
|
46
|
-
// `@libs/util/server` maps to the sibling file `libs/util/server.ts`, not to a package directory, so a
|
|
47
|
-
// leaf rewritten under the raw specifier would become `@libs/util/server/lib/sig` — a path that does
|
|
48
|
-
// not exist. The parent specifier is what the `@libs/*` wildcard can actually resolve.
|
|
49
36
|
const root = await makeTempRoot();
|
|
50
37
|
const entryFile = await write(root, "libs/util/server.ts");
|
|
51
38
|
const resolve = await resolverFor(root, { "@libs/util/server": ["./libs/util/server.ts"] });
|
|
@@ -134,8 +121,6 @@ describe("createTsconfigPackageResolver — wildcard tsconfig mapping", () => {
|
|
|
134
121
|
});
|
|
135
122
|
|
|
136
123
|
test("stops at the matched prefix instead of falling through to a shorter one", async () => {
|
|
137
|
-
// Falling through would resolve `@libs/util/ui` against `./libs/*`, silently loading an unrelated
|
|
138
|
-
// directory that happens to sit at the same subpath under a different root.
|
|
139
124
|
const root = await makeTempRoot();
|
|
140
125
|
await write(root, "libs/util/ui/index.ts");
|
|
141
126
|
const resolve = await resolverFor(root, {
|
|
@@ -183,9 +168,7 @@ describe("createTsconfigPackageResolver — node_modules fallback", () => {
|
|
|
183
168
|
});
|
|
184
169
|
|
|
185
170
|
test("resolves a scoped package subpath", async () => {
|
|
186
|
-
//
|
|
187
|
-
// first slash. Kept vendor-neutral on purpose: `packageExports.test.ts` reads every
|
|
188
|
-
// `"@akanjs/devkit/<x>"` literal under this package as a subpath the monorepo imports.
|
|
171
|
+
// Vendor-neutral on purpose: `packageExports.test.ts` reads every `"@akanjs/devkit/<x>"` literal here as an import.
|
|
189
172
|
const root = await makeTempRoot();
|
|
190
173
|
await writePackageJson(root, "@vendor/kit", { exports: { "./lint": "./lint/index.ts" } });
|
|
191
174
|
const entryFile = await write(root, "node_modules/@vendor/kit/lint/index.ts");
|
|
@@ -195,8 +178,7 @@ describe("createTsconfigPackageResolver — node_modules fallback", () => {
|
|
|
195
178
|
});
|
|
196
179
|
|
|
197
180
|
test("falls back to module/main when exports resolution declines the specifier", async () => {
|
|
198
|
-
// `main
|
|
199
|
-
// resolves it against the package directory, and it is the one path that does not preserve the file.
|
|
181
|
+
// `main` without a leading `./` is declined by the exports resolver, so only the last fallback resolves it.
|
|
200
182
|
const root = await makeTempRoot();
|
|
201
183
|
await writePackageJson(root, "plainpkg", { main: "index.js" });
|
|
202
184
|
const entryFile = await write(root, "node_modules/plainpkg/index.js");
|
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
import type { BunPlugin } from "bun";
|
|
2
|
+
import { loaderFor } from "./moduleSyntax";
|
|
2
3
|
import { transformUseClient } from "./rscUseClientTransform";
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
* The bundle produced by `PagesBundleBuilder` is loaded in the RSC worker
|
|
10
|
-
* with `--conditions react-server`; client components are never supposed
|
|
11
|
-
* to execute there. The resulting stub references keep the client
|
|
12
|
-
* component manifest lookups working at render time while ensuring the
|
|
13
|
-
* server bundle never drags the real client source graph in.
|
|
14
|
-
*
|
|
15
|
-
* Important: the second argument to `registerClientReference` must match the
|
|
16
|
-
* key the client manifest uses. When `workspaceRoot` is provided, both sides
|
|
17
|
-
* use workspace-relative keys so production artifacts are portable.
|
|
18
|
-
*/
|
|
19
|
-
export function createUseClientBundlePlugin(options: { workspaceRoot?: string } = {}): BunPlugin {
|
|
5
|
+
/** `workspaceRoot` makes reference keys workspace-relative; they must match the client manifest's keys. */
|
|
6
|
+
export function createUseClientBundlePlugin(
|
|
7
|
+
options: { workspaceRoot?: string; onClientModule?: (path: string, exports: string[]) => void } = {},
|
|
8
|
+
): BunPlugin {
|
|
20
9
|
return {
|
|
21
10
|
name: "akan-use-client-bundle",
|
|
22
11
|
setup(build) {
|
|
@@ -31,6 +20,7 @@ export function createUseClientBundlePlugin(options: { workspaceRoot?: string }
|
|
|
31
20
|
const stubbed = transformUseClient(source, {
|
|
32
21
|
path: args.path,
|
|
33
22
|
workspaceRoot: options.workspaceRoot,
|
|
23
|
+
onClientModule: options.onClientModule,
|
|
34
24
|
});
|
|
35
25
|
if (stubbed === null) return undefined;
|
|
36
26
|
return { contents: stubbed, loader: loaderFor(args.path) };
|
|
@@ -38,10 +28,3 @@ export function createUseClientBundlePlugin(options: { workspaceRoot?: string }
|
|
|
38
28
|
},
|
|
39
29
|
};
|
|
40
30
|
}
|
|
41
|
-
|
|
42
|
-
function loaderFor(absPath: string): "ts" | "tsx" | "js" | "jsx" {
|
|
43
|
-
if (absPath.endsWith(".tsx")) return "tsx";
|
|
44
|
-
if (absPath.endsWith(".jsx")) return "jsx";
|
|
45
|
-
if (absPath.endsWith(".ts")) return "ts";
|
|
46
|
-
return "js";
|
|
47
|
-
}
|
package/typeChecker.ts
CHANGED
|
@@ -38,31 +38,15 @@ export class TypeChecker {
|
|
|
38
38
|
}
|
|
39
39
|
this.config = parsedConfig;
|
|
40
40
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Find tsconfig.json by walking up the directory tree
|
|
43
|
-
*/
|
|
44
41
|
#findConfigFile(searchPath: string): string | undefined {
|
|
45
42
|
return ts.findConfigFile(searchPath, (fileName) => ts.sys.fileExists(fileName), "tsconfig.json");
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
|
|
49
|
-
* Type-check a single TypeScript file
|
|
50
|
-
* @param filePath - Path to the TypeScript file to check
|
|
51
|
-
* @returns Array of diagnostic messages
|
|
52
|
-
*/
|
|
53
|
-
check(filePath: string): {
|
|
54
|
-
diagnostics: ts.Diagnostic[];
|
|
55
|
-
errors: ts.Diagnostic[];
|
|
56
|
-
warnings: ts.Diagnostic[];
|
|
57
|
-
fileDiagnostics: ts.Diagnostic[];
|
|
58
|
-
fileErrors: ts.Diagnostic[];
|
|
59
|
-
fileWarnings: ts.Diagnostic[];
|
|
60
|
-
} {
|
|
45
|
+
check(filePath: string) {
|
|
61
46
|
const program = ts.createProgram([filePath], this.config.options);
|
|
62
47
|
const diagnostics = [
|
|
63
48
|
...program.getSemanticDiagnostics(),
|
|
64
49
|
...program.getSyntacticDiagnostics(),
|
|
65
|
-
// Only check declaration diagnostics when declaration emit is enabled
|
|
66
50
|
...(this.config.options.declaration ? program.getDeclarationDiagnostics() : []),
|
|
67
51
|
];
|
|
68
52
|
const errors = diagnostics.filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
|
|
@@ -73,11 +57,6 @@ export class TypeChecker {
|
|
|
73
57
|
return { diagnostics, errors, warnings, fileDiagnostics, fileErrors, fileWarnings };
|
|
74
58
|
}
|
|
75
59
|
|
|
76
|
-
/**
|
|
77
|
-
* Format diagnostics for console output
|
|
78
|
-
* @param diagnostics - Array of TypeScript diagnostics
|
|
79
|
-
* @returns Formatted string
|
|
80
|
-
*/
|
|
81
60
|
formatDiagnostics(diagnostics: ts.Diagnostic[]): string {
|
|
82
61
|
if (diagnostics.length === 0) return chalk.bold("✅ No type errors found");
|
|
83
62
|
|
|
@@ -86,48 +65,24 @@ export class TypeChecker {
|
|
|
86
65
|
let warningCount = 0;
|
|
87
66
|
let suggestionCount = 0;
|
|
88
67
|
|
|
89
|
-
// Group diagnostics by file
|
|
90
68
|
const diagnosticsByFile = new Map<string, ts.Diagnostic[]>();
|
|
91
69
|
diagnostics.forEach((diagnostic) => {
|
|
92
70
|
if (diagnostic.category === ts.DiagnosticCategory.Error) errorCount++;
|
|
93
71
|
else if (diagnostic.category === ts.DiagnosticCategory.Warning) warningCount++;
|
|
94
72
|
else if (diagnostic.category === ts.DiagnosticCategory.Suggestion) suggestionCount++;
|
|
95
|
-
|
|
96
|
-
if (diagnostic.file) {
|
|
97
|
-
const fileName = diagnostic.file.fileName;
|
|
98
|
-
if (!diagnosticsByFile.has(fileName)) diagnosticsByFile.set(fileName, []);
|
|
99
|
-
const fileDiagnostics = diagnosticsByFile.get(fileName);
|
|
100
|
-
if (fileDiagnostics) fileDiagnostics.push(diagnostic);
|
|
101
|
-
} else {
|
|
102
|
-
if (!diagnosticsByFile.has("")) diagnosticsByFile.set("", []);
|
|
103
|
-
const fileDiagnostics = diagnosticsByFile.get("");
|
|
104
|
-
if (fileDiagnostics) fileDiagnostics.push(diagnostic);
|
|
105
|
-
}
|
|
73
|
+
diagnosticsByFile.getOrInsert(diagnostic.file?.fileName ?? "", []).push(diagnostic);
|
|
106
74
|
});
|
|
107
75
|
|
|
108
|
-
// Format diagnostics by file
|
|
109
76
|
diagnosticsByFile.forEach((fileDiagnostics, fileName) => {
|
|
110
77
|
if (fileName) output.push(`\n${chalk.cyan(fileName)}`);
|
|
111
78
|
|
|
112
79
|
fileDiagnostics.forEach((diagnostic) => {
|
|
113
|
-
const categoryText =
|
|
114
|
-
diagnostic.category === ts.DiagnosticCategory.Error
|
|
115
|
-
? "error"
|
|
116
|
-
: diagnostic.category === ts.DiagnosticCategory.Warning
|
|
117
|
-
? "warning"
|
|
118
|
-
: "suggestion";
|
|
119
|
-
const categoryColor =
|
|
120
|
-
diagnostic.category === ts.DiagnosticCategory.Error
|
|
121
|
-
? chalk.red
|
|
122
|
-
: diagnostic.category === ts.DiagnosticCategory.Warning
|
|
123
|
-
? chalk.yellow
|
|
124
|
-
: chalk.blue;
|
|
125
|
-
const icon =
|
|
80
|
+
const [categoryText, categoryColor, icon] =
|
|
126
81
|
diagnostic.category === ts.DiagnosticCategory.Error
|
|
127
|
-
? "❌"
|
|
82
|
+
? (["error", chalk.red, "❌"] as const)
|
|
128
83
|
: diagnostic.category === ts.DiagnosticCategory.Warning
|
|
129
|
-
? "⚠️"
|
|
130
|
-
: "💡";
|
|
84
|
+
? (["warning", chalk.yellow, "⚠️"] as const)
|
|
85
|
+
: (["suggestion", chalk.blue, "💡"] as const);
|
|
131
86
|
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
|
|
132
87
|
const tsCode = chalk.dim(`(TS${diagnostic.code})`);
|
|
133
88
|
|
|
@@ -137,7 +92,6 @@ export class TypeChecker {
|
|
|
137
92
|
output.push(`\n ${icon} ${categoryColor(categoryText)}: ${message} ${tsCode}`);
|
|
138
93
|
output.push(` ${chalk.gray("at")} ${fileName}:${chalk.bold(`${line + 1}:${character + 1}`)}`);
|
|
139
94
|
|
|
140
|
-
// Show source line with underline
|
|
141
95
|
const sourceLines = diagnostic.file.text.split("\n");
|
|
142
96
|
if (line < sourceLines.length) {
|
|
143
97
|
const sourceLine = sourceLines[line];
|
|
@@ -145,7 +99,6 @@ export class TypeChecker {
|
|
|
145
99
|
|
|
146
100
|
output.push(`\n${chalk.dim(`${lineNumber} |`)} ${sourceLine}`);
|
|
147
101
|
|
|
148
|
-
// Create underline with squiggly for TypeScript
|
|
149
102
|
const underlinePrefix = " ".repeat(character);
|
|
150
103
|
const length = diagnostic.length ?? 1;
|
|
151
104
|
const underline = "~".repeat(Math.max(1, length));
|
|
@@ -166,11 +119,6 @@ export class TypeChecker {
|
|
|
166
119
|
return `\n${summary.join(", ")} found${output.join("\n")}`;
|
|
167
120
|
}
|
|
168
121
|
|
|
169
|
-
/**
|
|
170
|
-
* Get detailed diagnostic information with code snippet
|
|
171
|
-
* @param filePath - Path to the TypeScript file to check
|
|
172
|
-
* @returns Object containing diagnostics and detailed information
|
|
173
|
-
*/
|
|
174
122
|
getDetailedDiagnostics(filePath: string): {
|
|
175
123
|
diagnostics: ts.Diagnostic[];
|
|
176
124
|
details: { line: number; column: number; message: string; code: number; codeSnippet?: string }[];
|
|
@@ -199,11 +147,7 @@ export class TypeChecker {
|
|
|
199
147
|
return { diagnostics, details };
|
|
200
148
|
}
|
|
201
149
|
|
|
202
|
-
/**
|
|
203
|
-
* Check if a file has type errors
|
|
204
|
-
* @param filePath - Path to the TypeScript file to check
|
|
205
|
-
* @returns true if there are no type errors, false otherwise
|
|
206
|
-
*/
|
|
150
|
+
/** Also false when the check itself throws. */
|
|
207
151
|
hasNoTypeErrors(filePath: string): boolean {
|
|
208
152
|
try {
|
|
209
153
|
const { diagnostics } = this.check(filePath);
|
package/types.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface PackageJson {
|
|
|
10
10
|
peerDependencies?: Record<string, string>;
|
|
11
11
|
peerDependenciesMeta?: Record<string, { optional?: boolean }>;
|
|
12
12
|
optionalDependencies?: Record<string, string>;
|
|
13
|
+
trustedDependencies?: string[];
|
|
14
|
+
overrides?: Record<string, string>;
|
|
13
15
|
engines?: Record<string, string>;
|
|
14
16
|
exports?: Record<string, string | Record<string, string>>;
|
|
15
17
|
bun?: {
|
package/uploadRelease.ts
CHANGED
|
@@ -2,10 +2,8 @@ import { Logger, RestClient } from "akanjs/common";
|
|
|
2
2
|
|
|
3
3
|
import { Spinner } from "./spinner";
|
|
4
4
|
|
|
5
|
-
const spinning = (message: string) => {
|
|
6
|
-
|
|
7
|
-
return spinner;
|
|
8
|
-
};
|
|
5
|
+
const spinning = (message: string) => new Spinner(message, { prefix: message, enableSpin: true }).start();
|
|
6
|
+
|
|
9
7
|
export const uploadRelease = async (
|
|
10
8
|
appName: string,
|
|
11
9
|
{
|
|
@@ -27,33 +25,20 @@ export const uploadRelease = async (
|
|
|
27
25
|
const logger = new Logger("uploadRelease");
|
|
28
26
|
const basePath = local ? "http://localhost:8282/backend" : "https://cloud.akanjs.com/backend";
|
|
29
27
|
const httpClient = new RestClient(basePath);
|
|
30
|
-
const buildPath = `${workspaceRoot}/releases/builds/${appName}-release.tar.gz`;
|
|
31
|
-
const appBuildPath = `${workspaceRoot}/releases/builds/${appName}-appBuild.zip`;
|
|
32
|
-
const sourcePath = `${workspaceRoot}/releases/sources/${appName}-source.tar.gz`;
|
|
33
|
-
|
|
34
28
|
const readingFilesSpinner = spinning("Reading files...");
|
|
35
29
|
try {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
30
|
+
const uploads = [
|
|
31
|
+
[`builds/${appName}-release.tar.gz`, `${appName}-release.tar.gz`],
|
|
32
|
+
[`sources/${appName}-source.tar.gz`, `${appName}-source.tar.gz`],
|
|
33
|
+
[`builds/${appName}-appBuild.zip`, `${appName}-appBuild.zip`],
|
|
34
|
+
].map(([relativePath, name]) => ({ file: Bun.file(`${workspaceRoot}/releases/${relativePath}`), name }));
|
|
35
|
+
const metas = uploads.map(({ file }) => ({ lastModifiedAt: new Date(file.lastModified), size: file.size }));
|
|
42
36
|
readingFilesSpinner.succeed("Reading files... done");
|
|
43
37
|
|
|
44
38
|
const preparingFormSpinner = spinning("Preparing form data...");
|
|
45
39
|
const formData = new FormData();
|
|
46
|
-
formData.append("files",
|
|
47
|
-
formData.append("
|
|
48
|
-
formData.append("files", appBuildFile, `${appName}-appBuild.zip`);
|
|
49
|
-
formData.append(
|
|
50
|
-
"metas",
|
|
51
|
-
JSON.stringify([
|
|
52
|
-
{ lastModifiedAt: buildStat.mtime, size: buildStat.size },
|
|
53
|
-
{ lastModifiedAt: sourceStat.mtime, size: sourceStat.size },
|
|
54
|
-
{ lastModifiedAt: appBuildStat.mtime, size: appBuildStat.size },
|
|
55
|
-
]),
|
|
56
|
-
);
|
|
40
|
+
for (const { file, name } of uploads) formData.append("files", file, name);
|
|
41
|
+
formData.append("metas", JSON.stringify(metas));
|
|
57
42
|
formData.append("type", "release");
|
|
58
43
|
preparingFormSpinner.succeed("Preparing form data... done");
|
|
59
44
|
|
package/workflow/artifacts.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { capitalize } from "akanjs/common";
|
|
1
2
|
import type { Workspace } from "../commandDecorators";
|
|
2
3
|
import type {
|
|
3
4
|
GeneratedSyncState,
|
|
@@ -146,6 +147,23 @@ export const workflowCommandsForPlan = (plan: WorkflowPlan) =>
|
|
|
146
147
|
kind: validation.kind,
|
|
147
148
|
})) satisfies WorkflowApplyCommand[];
|
|
148
149
|
|
|
150
|
+
const planInputString = (plan: WorkflowPlan, key: string) => {
|
|
151
|
+
const value = plan.inputs[key];
|
|
152
|
+
return typeof value === "string" ? value : "";
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export const workflowPathsForPlan = (plan: WorkflowPlan) => {
|
|
156
|
+
const app = planInputString(plan, "app");
|
|
157
|
+
const module = planInputString(plan, "module");
|
|
158
|
+
const moduleClass = module ? capitalize(module) : "<Module>";
|
|
159
|
+
return plan.predictedChanges.map((change) =>
|
|
160
|
+
change.target
|
|
161
|
+
.replace(/^\*\//, app ? `apps/${app}/` : "")
|
|
162
|
+
.replaceAll("<module>", module || "<module>")
|
|
163
|
+
.replaceAll("<Module>", moduleClass),
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
|
|
149
167
|
export const workflowRunsDir = ".akan/workflows/runs";
|
|
150
168
|
|
|
151
169
|
export const createWorkflowRunId = (prefix = "run") =>
|
|
@@ -9,11 +9,6 @@ import type {
|
|
|
9
9
|
WorkflowStepResult,
|
|
10
10
|
} from "./types";
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* `WorkflowExecutor` takes its registry by constructor injection and its plan as data, so `apply` runs with
|
|
14
|
-
* no filesystem and no workspace — the `workspace` argument is what gates every post-apply check. These
|
|
15
|
-
* cover the ordering rules that `cli/workflow/workflow.test.ts` can only reach through a real scaffold.
|
|
16
|
-
*/
|
|
17
12
|
const step = (id: string, tool = `tool:${id}`): WorkflowStep => ({
|
|
18
13
|
id,
|
|
19
14
|
tool,
|
|
@@ -21,8 +16,7 @@ const step = (id: string, tool = `tool:${id}`): WorkflowStep => ({
|
|
|
21
16
|
description: id,
|
|
22
17
|
});
|
|
23
18
|
|
|
24
|
-
//
|
|
25
|
-
// step runs, so a partial literal fails at runtime instead of at the type level.
|
|
19
|
+
// Not a cast partial: `apply` reads `validation` before the first step, so a missing field fails only at runtime.
|
|
26
20
|
const planOf = (steps: WorkflowStep[], diagnostics: WorkflowDiagnostic[] = []): WorkflowPlan => ({
|
|
27
21
|
schemaVersion: 1,
|
|
28
22
|
workflow: "add-field",
|
|
@@ -37,10 +31,6 @@ const planOf = (steps: WorkflowStep[], diagnostics: WorkflowDiagnostic[] = []):
|
|
|
37
31
|
requiresApproval: true,
|
|
38
32
|
});
|
|
39
33
|
|
|
40
|
-
/**
|
|
41
|
-
* A registry that records **which key** each call resolved through, not just that a step ran — the
|
|
42
|
-
* resolution order is the thing under test, and every candidate key would otherwise record the same step id.
|
|
43
|
-
*/
|
|
44
34
|
const changed = (path: string): PrimitiveChangedFile => ({ path, action: "modify", reason: "test" });
|
|
45
35
|
|
|
46
36
|
const recordingRegistry = (results: Record<string, WorkflowStepResult | undefined> = {}) => {
|
|
@@ -79,7 +69,6 @@ describe("WorkflowExecutor.apply", () => {
|
|
|
79
69
|
|
|
80
70
|
test("resolves a runner by workflow-scoped key first, then tool, then bare step id", async () => {
|
|
81
71
|
const { calls, registry, on } = recordingRegistry();
|
|
82
|
-
// All three keys are registered for the same step, so only the winner records a call.
|
|
83
72
|
on(workflowStepKey("add-field", "scoped"));
|
|
84
73
|
on("tool:scoped");
|
|
85
74
|
on("scoped");
|