@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,33 +1,17 @@
|
|
|
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";
|
|
3
|
+
import { tempDirs, writeText as write } from "../testHelpers";
|
|
5
4
|
import { createTsconfigPackageResolver } from "../transforms/barrelImportsPlugin";
|
|
6
5
|
import { CLIENT_BUNDLE_NAMING } from "./clientBuildTypes";
|
|
7
6
|
import { ClientEntriesBundler } from "./clientEntriesBundler";
|
|
8
7
|
import { GraphClientEntryDiscovery } from "./clientEntryDiscovery";
|
|
9
8
|
import { RouteClientBuilder } from "./routeClientBuilder";
|
|
10
9
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const makeTempRoot = async () => {
|
|
14
|
-
const root = await mkdtemp(path.join(os.tmpdir(), "akan-client-entry-"));
|
|
15
|
-
tempRoots.push(root);
|
|
16
|
-
return root;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const write = async (filePath: string, content: string) => {
|
|
20
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
21
|
-
await writeFile(filePath, content);
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
afterEach(async () => {
|
|
25
|
-
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
26
|
-
});
|
|
10
|
+
const makeTempRoot = tempDirs("akan-client-entry-");
|
|
27
11
|
|
|
28
12
|
describe("route client store bootstrap", () => {
|
|
29
13
|
test("wraps client entries with app client bootstrap before re-exporting components", () => {
|
|
30
|
-
const original = "/repo/pkgs/akanjs/ui/Model/NewWrapper_Client.tsx";
|
|
14
|
+
const original = path.resolve("/repo/pkgs/akanjs/ui/Model/NewWrapper_Client.tsx");
|
|
31
15
|
const source = RouteClientBuilder.createStoreBootstrapEntrySource({
|
|
32
16
|
appName: "akan",
|
|
33
17
|
originalEntry: original,
|
|
@@ -147,6 +131,58 @@ describe("route client store bootstrap", () => {
|
|
|
147
131
|
);
|
|
148
132
|
});
|
|
149
133
|
|
|
134
|
+
test("bundles every route's entries into one graph so a page never loads a module twice", async () => {
|
|
135
|
+
const root = await makeTempRoot();
|
|
136
|
+
const appDir = path.join(root, "apps/demo");
|
|
137
|
+
const pageA = path.join(appDir, "page/a.tsx");
|
|
138
|
+
const pageB = path.join(appDir, "page/b.tsx");
|
|
139
|
+
const entryA = path.join(appDir, "ui/A.tsx");
|
|
140
|
+
const entryB = path.join(appDir, "ui/B.tsx");
|
|
141
|
+
const shared = path.join(appDir, "common/shared.ts");
|
|
142
|
+
await write(pageA, 'import { A } from "../ui/A";\nexport default A;\n');
|
|
143
|
+
await write(pageB, 'import { B } from "../ui/B";\nexport default B;\n');
|
|
144
|
+
await write(entryA, '"use client";\nimport { mark } from "../common/shared";\nexport const A = () => mark;\n');
|
|
145
|
+
await write(entryB, '"use client";\nimport { mark } from "../common/shared";\nexport const B = () => mark;\n');
|
|
146
|
+
await write(shared, 'export const mark = "shared-module-marker";\n');
|
|
147
|
+
const app = {
|
|
148
|
+
name: "demo",
|
|
149
|
+
cwdPath: appDir,
|
|
150
|
+
dist: { cwdPath: path.join(root, "dist/apps/demo") },
|
|
151
|
+
workspace: { workspaceRoot: root },
|
|
152
|
+
getConfig: async () => ({ barrelImports: [], optimizeImports: [] }),
|
|
153
|
+
getTsConfig: async () => ({ compilerOptions: { paths: {} } }),
|
|
154
|
+
getPublicEnv: () => ({}),
|
|
155
|
+
} as never;
|
|
156
|
+
const discovery = new GraphClientEntryDiscovery({ barrelImports: [] }, async () => null);
|
|
157
|
+
const build = (seeds: string[], knownEntries: Set<string>) =>
|
|
158
|
+
new RouteClientBuilder({
|
|
159
|
+
app,
|
|
160
|
+
seeds,
|
|
161
|
+
graphSeeds: [pageA, pageB],
|
|
162
|
+
knownEntries,
|
|
163
|
+
discovery,
|
|
164
|
+
artifact: {} as never,
|
|
165
|
+
browser: "chunks",
|
|
166
|
+
}).build();
|
|
167
|
+
|
|
168
|
+
const first = await build([pageA], new Set());
|
|
169
|
+
const second = await build([pageB], new Set(first.newEntries));
|
|
170
|
+
|
|
171
|
+
expect(first.newEntries).toEqual([entryA, entryB]);
|
|
172
|
+
expect(first.discoveredEntries).toEqual([entryA]);
|
|
173
|
+
expect(first.clientDeps).not.toContain(entryB);
|
|
174
|
+
expect(Object.keys(first.clientDepsByEntry ?? {}).sort()).toEqual([entryA, entryB]);
|
|
175
|
+
expect(second.newEntries).toEqual([]);
|
|
176
|
+
expect(second.discoveredEntries).toEqual([entryB]);
|
|
177
|
+
expect(Object.keys(second.manifestDelta)).toEqual([]);
|
|
178
|
+
const clientOut = path.join(appDir, ".akan/artifact/client");
|
|
179
|
+
const outputs = [...new Bun.Glob("**/*.js").scanSync(clientOut)];
|
|
180
|
+
const carriers = await Promise.all(
|
|
181
|
+
outputs.map(async (file) => (await Bun.file(path.join(clientOut, file)).text()).includes("shared-module-marker")),
|
|
182
|
+
);
|
|
183
|
+
expect(carriers.filter(Boolean)).toHaveLength(1);
|
|
184
|
+
});
|
|
185
|
+
|
|
150
186
|
test("discovers client entries from installed akanjs package sources", async () => {
|
|
151
187
|
const root = await makeTempRoot();
|
|
152
188
|
const seed = path.join(root, "apps/demo/page/_index.tsx");
|
|
@@ -159,24 +195,14 @@ describe("route client store bootstrap", () => {
|
|
|
159
195
|
await write(uiEntry, 'export { ClientPathWrapper } from "./System/Client";\n');
|
|
160
196
|
await write(clientEntry, '"use client";\nexport const ClientPathWrapper = () => null;\n');
|
|
161
197
|
|
|
198
|
+
const entryFiles = new Map([
|
|
199
|
+
["akanjs/ui", uiEntry],
|
|
200
|
+
["akanjs/ui/System/Client.tsx", clientEntry],
|
|
201
|
+
]);
|
|
162
202
|
const discovery = new GraphClientEntryDiscovery({ barrelImports: ["akanjs/ui"] }, async (specifier) => {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
entryFile: uiEntry,
|
|
167
|
-
pkgDir: path.dirname(uiEntry),
|
|
168
|
-
preserveFilePath: true,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
if (specifier === "akanjs/ui/System/Client.tsx") {
|
|
172
|
-
return {
|
|
173
|
-
pkgName: "akanjs/ui/System/Client.tsx",
|
|
174
|
-
entryFile: clientEntry,
|
|
175
|
-
pkgDir: path.dirname(clientEntry),
|
|
176
|
-
preserveFilePath: true,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
return null;
|
|
203
|
+
const entryFile = entryFiles.get(specifier);
|
|
204
|
+
if (!entryFile) return null;
|
|
205
|
+
return { pkgName: specifier, entryFile, pkgDir: path.dirname(entryFile), preserveFilePath: true };
|
|
180
206
|
});
|
|
181
207
|
|
|
182
208
|
expect(await discovery.discover([seed])).toEqual([clientEntry]);
|
|
@@ -209,3 +235,63 @@ describe("route client store bootstrap", () => {
|
|
|
209
235
|
});
|
|
210
236
|
});
|
|
211
237
|
});
|
|
238
|
+
|
|
239
|
+
describe("fast refresh named default function", () => {
|
|
240
|
+
test("hoists a named default function into a declaration plus a trailing default export", () => {
|
|
241
|
+
const source = `export default async function Page<T>(props: T) {\n return null;\n}\n`;
|
|
242
|
+
const next = RouteClientBuilder.normalizeNamedDefaultFunctionForFastRefresh(source, { path: "/app/page.tsx" });
|
|
243
|
+
expect(next).toBe(`async function Page<T>(props: T) {\n return null;\n}\n\nexport default Page;\n`);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("leaves code quoted in a template literal or a comment alone", () => {
|
|
247
|
+
const source = [
|
|
248
|
+
`import { page } from "akanjs/client";`,
|
|
249
|
+
`const snippet = \``,
|
|
250
|
+
`export default function Page() {`,
|
|
251
|
+
` return <div />;`,
|
|
252
|
+
`}\`;`,
|
|
253
|
+
`/*`,
|
|
254
|
+
`export default function Commented() {}`,
|
|
255
|
+
`*/`,
|
|
256
|
+
`export default page().render(() => <pre>{snippet}</pre>);`,
|
|
257
|
+
"",
|
|
258
|
+
].join("\n");
|
|
259
|
+
expect(
|
|
260
|
+
RouteClientBuilder.normalizeNamedDefaultFunctionForFastRefresh(source, { path: "/app/page.tsx" }),
|
|
261
|
+
).toBeNull();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("rewrites only the real declaration when the same name is also quoted", () => {
|
|
265
|
+
const source = [
|
|
266
|
+
"const snippet = `",
|
|
267
|
+
"export default function Page() {}",
|
|
268
|
+
"`;",
|
|
269
|
+
"export default function Page() {",
|
|
270
|
+
" return snippet;",
|
|
271
|
+
"}",
|
|
272
|
+
"",
|
|
273
|
+
].join("\n");
|
|
274
|
+
const next = RouteClientBuilder.normalizeNamedDefaultFunctionForFastRefresh(source, { path: "/app/page.tsx" });
|
|
275
|
+
expect(next).toBe(
|
|
276
|
+
[
|
|
277
|
+
"const snippet = `",
|
|
278
|
+
"export default function Page() {}",
|
|
279
|
+
"`;",
|
|
280
|
+
"function Page() {",
|
|
281
|
+
" return snippet;",
|
|
282
|
+
"}",
|
|
283
|
+
"",
|
|
284
|
+
"export default Page;",
|
|
285
|
+
"",
|
|
286
|
+
].join("\n"),
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("leaves a file it cannot parse to the bundler", () => {
|
|
291
|
+
expect(
|
|
292
|
+
RouteClientBuilder.normalizeNamedDefaultFunctionForFastRefresh("export default function Page( {", {
|
|
293
|
+
path: "/app/page.tsx",
|
|
294
|
+
}),
|
|
295
|
+
).toBeNull();
|
|
296
|
+
});
|
|
297
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { App } from "../commandDecorators";
|
|
3
|
+
import { bundleDefine } from "./bundleDefine";
|
|
4
|
+
|
|
5
|
+
describe("bundleDefine", () => {
|
|
6
|
+
test("spells the public env in one order whatever order the process holds it in", () => {
|
|
7
|
+
const appWith = (env: Record<string, string>) => ({ getPublicEnv: () => env }) as unknown as App;
|
|
8
|
+
const builder = bundleDefine(appWith({ AKAN_PUBLIC_Z: "1", AKAN_PUBLIC_A: "2" }), "start", "ssr");
|
|
9
|
+
const worker = bundleDefine(appWith({ AKAN_PUBLIC_A: "2", AKAN_PUBLIC_Z: "1" }), "start", "ssr");
|
|
10
|
+
expect(JSON.stringify(builder)).toBe(JSON.stringify(worker));
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { App } from "../commandDecorators";
|
|
2
|
+
|
|
3
|
+
export const bundleDefine = (app: App, command: "build" | "start", renderEnv: "ssr" | "csr") => {
|
|
4
|
+
const nodeEnv = command === "build" ? "production" : (process.env.NODE_ENV ?? "development");
|
|
5
|
+
return {
|
|
6
|
+
"process.env.NODE_ENV": JSON.stringify(nodeEnv),
|
|
7
|
+
"process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify(renderEnv),
|
|
8
|
+
//? Sorted: Windows hands a child its environment sorted, while keys the config sets at runtime trail the parent's,
|
|
9
|
+
//? and the dev registries hash this object to tell a worker's build from the builder's.
|
|
10
|
+
...Object.fromEntries(
|
|
11
|
+
Object.entries(app.getPublicEnv())
|
|
12
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
13
|
+
.map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]),
|
|
14
|
+
),
|
|
15
|
+
};
|
|
16
|
+
};
|
|
@@ -10,39 +10,14 @@ export interface BuildClientOptions {
|
|
|
10
10
|
outputDir?: string;
|
|
11
11
|
servePrefix?: string;
|
|
12
12
|
mode?: "production" | "test";
|
|
13
|
-
/** Additional Bun plugins applied to the client bundle build. */
|
|
14
13
|
plugins?: BunPlugin[];
|
|
15
|
-
/**
|
|
16
|
-
* Legacy fallback: additional directories to filesystem-scan for
|
|
17
|
-
* `"use client"` entry points. Only used when `workspaceRoot` /
|
|
18
|
-
* `tsconfigPaths` are not provided (in which case we fall back to the old
|
|
19
|
-
* filesystem-scan strategy). Prefer the graph-based discovery.
|
|
20
|
-
*/
|
|
14
|
+
/** Legacy filesystem-scan roots, used only when `workspaceRoot` / `tsconfigPaths` are absent. */
|
|
21
15
|
extraClientScanRoots?: string[];
|
|
22
|
-
/**
|
|
23
|
-
* Environment variables to inline into the browser bundle via `define`.
|
|
24
|
-
* Keys are the original `process.env` names. Each key is emitted as
|
|
25
|
-
* `process.env.<KEY>` so existing server/client shared code that reads
|
|
26
|
-
* `process.env.AKAN_PUBLIC_*` keeps working unchanged.
|
|
27
|
-
*/
|
|
16
|
+
/** Inlined via `define` as `process.env.<KEY>`. */
|
|
28
17
|
publicEnv?: Record<string, string>;
|
|
29
|
-
/**
|
|
30
|
-
* Workspace root used by the graph-based client-entry discovery to resolve
|
|
31
|
-
* tsconfig-aliased specifiers (`@libs/shared/ui`, `@apps/<name>/client`,
|
|
32
|
-
* ...). When provided together with `tsconfigPaths`, discovery traverses
|
|
33
|
-
* the import graph starting from `appDir` instead of doing a filesystem
|
|
34
|
-
* scan, so `"use client"` files that no route transitively imports (e.g.
|
|
35
|
-
* unused editor components in a shared UI package) stay out of the client
|
|
36
|
-
* bundle.
|
|
37
|
-
*/
|
|
18
|
+
/** With `tsconfigPaths`, discovery walks the import graph from `appDir` instead of scanning the filesystem. */
|
|
38
19
|
workspaceRoot?: string;
|
|
39
|
-
/** Workspace tsconfig `compilerOptions.paths`. See `workspaceRoot`. */
|
|
40
20
|
tsconfigPaths?: Record<string, string[]>;
|
|
41
|
-
/**
|
|
42
|
-
* Barrel specifiers whose imports are flattened by `barrelImportsPlugin`.
|
|
43
|
-
* Discovery rewrites barrel imports with the same analyzer before scanning,
|
|
44
|
-
* so the traversal sees the same leaf files the bundler will see.
|
|
45
|
-
*/
|
|
46
21
|
barrelImports?: string[];
|
|
47
22
|
}
|
|
48
23
|
|
|
@@ -73,10 +48,7 @@ export interface BundleClientEntriesOptions {
|
|
|
73
48
|
entries: string[];
|
|
74
49
|
plugins?: BunPlugin[];
|
|
75
50
|
outputSubdir?: string;
|
|
76
|
-
/**
|
|
77
|
-
* Future Fast Refresh hook. Bun only injects the React Refresh transform;
|
|
78
|
-
* the browser runtime/update protocol still needs to be provided by Akan.
|
|
79
|
-
*/
|
|
51
|
+
/** Bun only injects the React Refresh transform; the browser runtime and update protocol are Akan's. */
|
|
80
52
|
reactFastRefresh?: boolean;
|
|
81
53
|
}
|
|
82
54
|
|
|
@@ -92,13 +64,10 @@ export interface BundleClientEntriesInternalOptions extends BundleClientEntriesO
|
|
|
92
64
|
export interface BundleClientEntriesResult {
|
|
93
65
|
manifest: ClientManifest;
|
|
94
66
|
ssrManifest: SsrManifest;
|
|
95
|
-
/** Absolute path → serve URL for every entry point we produced. */
|
|
96
67
|
entryUrlsByAbsPath: Map<string, string>;
|
|
97
|
-
/** Absolute source entry path → emitted entry file absolute path. */
|
|
98
68
|
entryOutputAbsByAbsPath: Map<string, string>;
|
|
99
|
-
/** Absolute source files that Bun included for each client entry bundle. */
|
|
100
69
|
entryDepsByAbsPath: Map<string, string[]>;
|
|
101
|
-
/**
|
|
70
|
+
/** Workspace-relative client reference id per absolute source entry. */
|
|
102
71
|
clientReferenceIdByAbsPath: Map<string, string>;
|
|
103
72
|
}
|
|
104
73
|
|
|
@@ -2,7 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { SsrManifest, SsrManifestEntry } from "akanjs/server";
|
|
4
4
|
import type { BunPlugin } from "bun";
|
|
5
|
-
import { toClientReferencePath } from "../transforms/rscUseClientTransform";
|
|
5
|
+
import { scanUseClientExports, toClientReferencePath } from "../transforms/rscUseClientTransform";
|
|
6
|
+
import { bundleDefine } from "./bundleDefine";
|
|
6
7
|
import {
|
|
7
8
|
type BundleClientEntriesInternalOptions,
|
|
8
9
|
type BundleClientEntriesResult,
|
|
@@ -13,11 +14,6 @@ import {
|
|
|
13
14
|
type OpaqueEntryAliases,
|
|
14
15
|
} from "./clientBuildTypes";
|
|
15
16
|
|
|
16
|
-
/**
|
|
17
|
-
* Low-level primitive shared by the eager base build and the lazy per-route
|
|
18
|
-
* builds. Takes a flat entrypoints list, runs `Bun.build`, and extracts a
|
|
19
|
-
* `ClientManifest` / `SsrManifest` covering only those entries.
|
|
20
|
-
*/
|
|
21
17
|
export class ClientEntriesBundler {
|
|
22
18
|
#app: BundleClientEntriesInternalOptions["app"];
|
|
23
19
|
#entries: string[];
|
|
@@ -29,7 +25,6 @@ export class ClientEntriesBundler {
|
|
|
29
25
|
#target: ClientBundleTarget;
|
|
30
26
|
#outputSubdir: string;
|
|
31
27
|
#reactFastRefresh: boolean;
|
|
32
|
-
#artifactDir: string;
|
|
33
28
|
#outdir: string;
|
|
34
29
|
#servePrefix: string;
|
|
35
30
|
#manifest: ClientManifest = {};
|
|
@@ -52,8 +47,8 @@ export class ClientEntriesBundler {
|
|
|
52
47
|
this.#target = options.target ?? "browser";
|
|
53
48
|
this.#outputSubdir = options.outputSubdir ?? "client";
|
|
54
49
|
this.#reactFastRefresh = options.reactFastRefresh ?? false;
|
|
55
|
-
|
|
56
|
-
this.#outdir = `${
|
|
50
|
+
const artifactDir = `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
|
|
51
|
+
this.#outdir = `${artifactDir}/${this.#outputSubdir}`;
|
|
57
52
|
this.#servePrefix = `/_akan/${this.#outputSubdir}`;
|
|
58
53
|
}
|
|
59
54
|
|
|
@@ -68,7 +63,7 @@ export class ClientEntriesBundler {
|
|
|
68
63
|
format: "esm",
|
|
69
64
|
naming: CLIENT_BUNDLE_NAMING,
|
|
70
65
|
metafile: true,
|
|
71
|
-
define: this.#
|
|
66
|
+
define: bundleDefine(this.#app, this.#command, "ssr"),
|
|
72
67
|
minify: this.#command === "build",
|
|
73
68
|
optimizeImports: akanConfig.optimizeImports,
|
|
74
69
|
reactFastRefresh: this.#command === "start" && this.#outputSubdir === "client" && this.#reactFastRefresh,
|
|
@@ -100,17 +95,6 @@ export class ClientEntriesBundler {
|
|
|
100
95
|
};
|
|
101
96
|
}
|
|
102
97
|
|
|
103
|
-
#getDefine(): Record<string, string> {
|
|
104
|
-
const nodeEnv = this.#command === "build" ? "production" : (process.env.NODE_ENV ?? "development");
|
|
105
|
-
return {
|
|
106
|
-
"process.env.NODE_ENV": JSON.stringify(nodeEnv),
|
|
107
|
-
"process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("ssr"),
|
|
108
|
-
...Object.fromEntries(
|
|
109
|
-
Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]),
|
|
110
|
-
),
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
98
|
async #createOpaqueEntryAliases(): Promise<OpaqueEntryAliases> {
|
|
115
99
|
const aliasDir = path.join(this.#app.cwdPath, ".akan", "generated", "client-entry-alias", this.#outputSubdir);
|
|
116
100
|
fs.mkdirSync(aliasDir, { recursive: true });
|
|
@@ -141,22 +125,10 @@ export class ClientEntriesBundler {
|
|
|
141
125
|
}
|
|
142
126
|
|
|
143
127
|
async #scanEntryExportNames(absEntry: string): Promise<string[]> {
|
|
144
|
-
|
|
145
|
-
const transpiler = new Bun.Transpiler({ loader: this.#loaderForEntry(absEntry) });
|
|
146
|
-
return transpiler.scan(source).exports;
|
|
128
|
+
return scanUseClientExports(await Bun.file(absEntry).text(), absEntry, this.#app.workspace.workspaceRoot);
|
|
147
129
|
}
|
|
148
130
|
|
|
149
|
-
|
|
150
|
-
if (absPath.endsWith(".tsx")) return "tsx";
|
|
151
|
-
if (absPath.endsWith(".jsx")) return "jsx";
|
|
152
|
-
if (absPath.endsWith(".ts")) return "ts";
|
|
153
|
-
return "js";
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Build a BunPlugin that marks a fixed set of bare specifiers as external via `onResolve`,
|
|
158
|
-
* as opposed to `Bun.build({ external })`, so macro-time imports still resolve normally.
|
|
159
|
-
*/
|
|
131
|
+
// `onResolve` rather than `Bun.build({ external })`, so macro-time imports still resolve normally.
|
|
160
132
|
#createExternalSpecifiersPlugin(): BunPlugin {
|
|
161
133
|
const set = new Set(this.#external);
|
|
162
134
|
const subpathSet = new Set(this.#externalSubpaths);
|
|
@@ -182,11 +154,8 @@ export class ClientEntriesBundler {
|
|
|
182
154
|
exactExternals: Set<string>,
|
|
183
155
|
subpathExternals: Set<string>,
|
|
184
156
|
): boolean {
|
|
185
|
-
if (exactExternals.has(specifier)) return true;
|
|
186
|
-
if (
|
|
187
|
-
for (const external of subpathExternals) {
|
|
188
|
-
if (specifier.startsWith(`${external}/`)) return true;
|
|
189
|
-
}
|
|
157
|
+
if (exactExternals.has(specifier) || subpathExternals.has(specifier)) return true;
|
|
158
|
+
for (const external of subpathExternals) if (specifier.startsWith(`${external}/`)) return true;
|
|
190
159
|
return false;
|
|
191
160
|
}
|
|
192
161
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { afterAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { GraphClientEntryDiscovery } from "./clientEntryDiscovery";
|
|
6
|
+
|
|
7
|
+
const dirs: string[] = [];
|
|
8
|
+
|
|
9
|
+
afterAll(async () => {
|
|
10
|
+
await Promise.all(dirs.map((dir) => rm(dir, { recursive: true, force: true })));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("GraphClientEntryDiscovery", () => {
|
|
14
|
+
test("a walk an invalidation overtook answers its caller but leaves nothing stale for the next one", async () => {
|
|
15
|
+
for (const yields of [0, 5, 10, 20, 40, 80]) {
|
|
16
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "akan-discovery-"));
|
|
17
|
+
dirs.push(dir);
|
|
18
|
+
const file = (name: string) => path.join(dir, name);
|
|
19
|
+
await Bun.write(file("S.tsx"), 'import "./A";\n');
|
|
20
|
+
await Bun.write(file("A.tsx"), 'import "./B";\nimport "./D";\n');
|
|
21
|
+
await Bun.write(file("B.tsx"), "export const b = 1;\n");
|
|
22
|
+
await Bun.write(file("C.tsx"), '"use client";\nexport const C = () => null;\n');
|
|
23
|
+
await Bun.write(file("D.tsx"), "export const d = 1;\n");
|
|
24
|
+
const discovery = new GraphClientEntryDiscovery({ barrelImports: [] }, (async () => null) as never);
|
|
25
|
+
expect(await discovery.discover([file("S.tsx")])).toEqual([]);
|
|
26
|
+
|
|
27
|
+
await Bun.write(file("B.tsx"), 'import "./C";\nexport const b = 1;\n');
|
|
28
|
+
await Bun.write(file("D.tsx"), "export const d = 2;\n");
|
|
29
|
+
discovery.invalidate([file("D.tsx")]);
|
|
30
|
+
const walk = discovery.discover([file("S.tsx")]);
|
|
31
|
+
for (let tick = 0; tick < yields; tick++) await Promise.resolve();
|
|
32
|
+
discovery.invalidate([file("B.tsx")]);
|
|
33
|
+
await walk;
|
|
34
|
+
expect(await discovery.discover([file("S.tsx")])).toEqual([file("C.tsx")]);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const tempDir = async () => {
|
|
39
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "akan-discovery-"));
|
|
40
|
+
dirs.push(dir);
|
|
41
|
+
return (name: string) => path.join(dir, name);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
test("an entry moved into a folder, or to another extension, is found once the move is invalidated", async () => {
|
|
45
|
+
const file = await tempDir();
|
|
46
|
+
await Bun.write(file("S.tsx"), 'import "./ui/Chart";\nimport "./ui/Card";\n');
|
|
47
|
+
await Bun.write(file("ui/Chart.tsx"), '"use client";\nexport const Chart = () => null;\n');
|
|
48
|
+
await Bun.write(file("ui/Card.tsx"), '"use client";\nexport const Card = () => null;\n');
|
|
49
|
+
const discovery = new GraphClientEntryDiscovery({ barrelImports: [] }, (async () => null) as never);
|
|
50
|
+
expect(await discovery.discover([file("S.tsx")])).toEqual([file("ui/Card.tsx"), file("ui/Chart.tsx")]);
|
|
51
|
+
|
|
52
|
+
await rm(file("ui/Chart.tsx"));
|
|
53
|
+
await Bun.write(file("ui/Chart/index.tsx"), '"use client";\nexport const Chart = () => null;\n');
|
|
54
|
+
await rm(file("ui/Card.tsx"));
|
|
55
|
+
await Bun.write(file("ui/Card.ts"), '"use client";\nexport const Card = () => null;\n');
|
|
56
|
+
discovery.invalidate([file("ui/Chart.tsx"), file("ui/Chart/index.tsx"), file("ui/Card.tsx"), file("ui/Card.ts")]);
|
|
57
|
+
expect(await discovery.discover([file("S.tsx")])).toEqual([file("ui/Card.ts"), file("ui/Chart/index.tsx")]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("a resolution still in flight when its target appears is not what the next walk reads", async () => {
|
|
61
|
+
const file = await tempDir();
|
|
62
|
+
await Bun.write(file("S.tsx"), 'import "./B";\n');
|
|
63
|
+
await Bun.write(file("B.tsx"), 'import "@demo/C";\nexport const b = 1;\n');
|
|
64
|
+
const gate = Promise.withResolvers<void>();
|
|
65
|
+
const seen = Promise.withResolvers<void>();
|
|
66
|
+
let calls = 0;
|
|
67
|
+
const resolvePackage = async () => {
|
|
68
|
+
calls += 1;
|
|
69
|
+
const exists = await Bun.file(file("C.tsx")).exists();
|
|
70
|
+
if (calls === 1) {
|
|
71
|
+
seen.resolve();
|
|
72
|
+
await gate.promise;
|
|
73
|
+
}
|
|
74
|
+
return exists ? { entryFile: file("C.tsx") } : null;
|
|
75
|
+
};
|
|
76
|
+
const discovery = new GraphClientEntryDiscovery({ barrelImports: [] }, resolvePackage as never);
|
|
77
|
+
const walk = discovery.discover([file("S.tsx")]);
|
|
78
|
+
await seen.promise;
|
|
79
|
+
await Bun.write(file("C.tsx"), '"use client";\nexport const C = () => null;\n');
|
|
80
|
+
discovery.invalidate([file("C.tsx")]);
|
|
81
|
+
gate.resolve();
|
|
82
|
+
await walk;
|
|
83
|
+
expect(await discovery.discover([file("S.tsx")])).toEqual([file("C.tsx")]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("a dense import cycle is walked once per file, not once per path", async () => {
|
|
87
|
+
const file = await tempDir();
|
|
88
|
+
const names = Array.from({ length: 12 }, (_, idx) => `M${idx}.tsx`);
|
|
89
|
+
for (const name of names)
|
|
90
|
+
await Bun.write(file(name), names.map((other) => `import "./${other.replace(".tsx", "")}";\n`).join(""));
|
|
91
|
+
await Bun.write(file("C.tsx"), '"use client";\nexport const C = () => null;\n');
|
|
92
|
+
await Bun.write(
|
|
93
|
+
file("M11.tsx"),
|
|
94
|
+
`${names.map((other) => `import "./${other.replace(".tsx", "")}";\n`).join("")}import "./C";\n`,
|
|
95
|
+
);
|
|
96
|
+
const discovery = new GraphClientEntryDiscovery({ barrelImports: [] }, (async () => null) as never);
|
|
97
|
+
const started = performance.now();
|
|
98
|
+
expect(await discovery.discover([file("M0.tsx")])).toEqual([file("C.tsx")]);
|
|
99
|
+
expect(performance.now() - started).toBeLessThan(2_000);
|
|
100
|
+
expect(await discovery.discover([file("M5.tsx")])).toEqual([file("C.tsx")]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("a file walked inside an import cycle keeps no partial result for a walk that starts at it", async () => {
|
|
104
|
+
const file = await tempDir();
|
|
105
|
+
await Bun.write(file("A.tsx"), 'import "./B";\nimport "./C";\n');
|
|
106
|
+
await Bun.write(file("B.tsx"), 'import "./A";\n');
|
|
107
|
+
await Bun.write(file("C.tsx"), '"use client";\nexport const C = () => null;\n');
|
|
108
|
+
const discovery = new GraphClientEntryDiscovery({ barrelImports: [] }, (async () => null) as never);
|
|
109
|
+
expect(await discovery.discover([file("A.tsx")])).toEqual([file("C.tsx")]);
|
|
110
|
+
expect(await discovery.discover([file("B.tsx")])).toEqual([file("C.tsx")]);
|
|
111
|
+
});
|
|
112
|
+
});
|