@akanjs/devkit 3.0.0-beta.2 → 3.0.0-beta.20
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/README.md +1 -0
- 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 +1 -3
- package/akanApp/akanApp.host.ts +82 -254
- package/akanApp/devHostPolicy.test.ts +10 -34
- package/akanApp/devHostPolicy.ts +12 -98
- package/akanConfig/akanConfig.test.ts +85 -69
- package/akanConfig/akanConfig.ts +69 -153
- package/akanConfig/types.ts +1 -0
- package/akanContext.ts +106 -159
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.test.ts +5 -3
- package/applicationBuildRunner.ts +54 -61
- package/applicationReleasePackager.ts +24 -44
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +9 -30
- package/artifact/implicitRootLayout.ts +34 -106
- package/artifact/routeSeedIndex.test.ts +7 -6
- package/artifact/routeSeedIndex.ts +11 -44
- package/biome.base.json +6 -1
- package/biomeBase.ts +2 -5
- package/capacitorApp.test.ts +32 -16
- package/capacitorApp.ts +110 -238
- 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 +70 -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/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 +132 -106
- package/executors.ts +194 -484
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +5 -3
- package/fileSys.ts +6 -8
- 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 +62 -37
- package/frontendBuild/bundleDefine.ts +12 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.ts +6 -41
- package/frontendBuild/csrArtifactBuilder.ts +6 -25
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +29 -78
- 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/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 +46 -69
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +8 -32
- package/frontendBuild/hmrWatcher.ts +14 -71
- package/frontendBuild/pagesBundleBuilder.ts +4 -29
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/routeClientBuilder.ts +23 -46
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
- 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 +22 -57
- package/incrementalBuilder/buildBatchProtocol.ts +8 -36
- package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
- package/incrementalBuilder/buildBatchRunner.ts +10 -31
- 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/devWatchBatch.test.ts +21 -24
- package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
- package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +49 -161
- package/index.ts +5 -17
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +47 -194
- 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/gritRules.test.ts +8 -24
- package/lint/no-deep-internal-import.grit +2 -2
- package/lint/no-redeclare-predefined-endpoint.grit +2 -1
- package/linter.test.ts +1 -2
- package/linter.ts +26 -116
- package/mobile/mobileTarget.ts +14 -13
- package/package.json +10 -5
- 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 +197 -285
- 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 +87 -216
- 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 +30 -25
- package/subspace.ts +81 -115
- package/subspaceConfig.ts +11 -9
- 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 +35 -20
- package/transforms/transforms.test.ts +153 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +2 -22
- package/typeChecker.ts +7 -63
- package/types.ts +1 -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.ts +4 -19
- package/abstractCompactor.ts +0 -112
- package/aiEditor.test.ts +0 -60
- package/aiEditor.ts +0 -387
- 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
|
@@ -2,64 +2,19 @@ import path from "node:path";
|
|
|
2
2
|
import type { BunPlugin } from "bun";
|
|
3
3
|
import type { App } from "../commandDecorators";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Keep framework/runtime singletons external while bundling authored
|
|
7
|
-
* workspace sources and ordinary npm dependencies into the pages bundle.
|
|
8
|
-
*
|
|
9
|
-
* Why a plugin instead of `Bun.build({ external })` or
|
|
10
|
-
* `Bun.build({ packages: "external" })`:
|
|
11
|
-
* - `packages: "external"` externalizes every bare specifier, including
|
|
12
|
-
* `@apps/*` / `@libs/*` — workspace packages that must stay bundled
|
|
13
|
-
* so the `"use client"` plugin can rewrite their exports — and ordinary
|
|
14
|
-
* npm dependencies like `dayjs` / `tailwind-merge`, which the production runtime
|
|
15
|
-
* package.json does not install for the SSR pages artifact.
|
|
16
|
-
* - Top-level `external: [...]` applies to macro-time module resolution
|
|
17
|
-
* too; any `with { type: "macro" }` import chain that transitively
|
|
18
|
-
* resolves `react` during build then fails with `ENOENT "react"`.
|
|
19
|
-
*
|
|
20
|
-
* An `onResolve` plugin only participates in the output-graph walk, so
|
|
21
|
-
* macros evaluate against the real modules in node_modules while the
|
|
22
|
-
* emitted bundle keeps bare specifiers only for runtime singletons that
|
|
23
|
-
* are installed in the generated production package.json.
|
|
24
|
-
*
|
|
25
|
-
* Externalization rules (applied in order):
|
|
26
|
-
* 1. Relative specifiers (`./`, `../`) are NEVER externalized — they
|
|
27
|
-
* always resolve to the current package's source tree and must be
|
|
28
|
-
* inlined so transitive `"use client"` stubs work.
|
|
29
|
-
* 2. Specifiers listed in `include` (workspace aliases like
|
|
30
|
-
* `@apps/*`, `@libs/*`) are NEVER externalized.
|
|
31
|
-
* 3. React / RSC runtime packages and build-time tooling packages are
|
|
32
|
-
* externalized so the runtime supplies a single shared instance.
|
|
33
|
-
* 4. Other bare npm packages are bundled so the Docker runtime does not
|
|
34
|
-
* need to install every transitive page dependency separately.
|
|
35
|
-
*/
|
|
36
5
|
export interface ExternalizeFrameworkOptions {
|
|
37
6
|
app: App;
|
|
38
|
-
/**
|
|
39
|
-
* Prefixes that should be BUNDLED (kept internal). Defaults to
|
|
40
|
-
* `@apps/` and `@libs/` which are the workspace aliases used by the
|
|
41
|
-
* framework's app template.
|
|
42
|
-
*/
|
|
7
|
+
/** Prefixes to bundle rather than externalize; defaults to `akanjs/`, `@apps/`, `@libs/`. */
|
|
43
8
|
include?: string[];
|
|
44
|
-
/**
|
|
45
|
-
* Extra bare specifiers to force-externalize beyond the allowlist
|
|
46
|
-
* defaults. Not commonly needed since the default rule already
|
|
47
|
-
* externalizes every non-workspace specifier.
|
|
48
|
-
*/
|
|
9
|
+
/** Bare specifiers to force-externalize, overriding `include`. */
|
|
49
10
|
extra?: string[];
|
|
50
11
|
}
|
|
51
12
|
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
// the RSC worker would resolve them at runtime — skipping the
|
|
55
|
-
// transforms entirely, which re-introduces the react-dom/client
|
|
56
|
-
// side-effects we wanted to strip from the server graph.
|
|
13
|
+
// Bundled so they pass the barrel and `"use client"` transforms; resolved at runtime they would drag
|
|
14
|
+
// react-dom/client side effects into the server graph.
|
|
57
15
|
const DEFAULT_INCLUDE = ["akanjs/", "@apps/", "@libs/"];
|
|
58
16
|
|
|
59
|
-
//
|
|
60
|
-
// otherwise bundle them. These are runtime hosts or shared singleton
|
|
61
|
-
// framework packages; ordinary npm dependencies intentionally fall through
|
|
62
|
-
// to Bun's resolver and get bundled.
|
|
17
|
+
// Runtime hosts and singleton framework packages stay external; ordinary npm dependencies are bundled.
|
|
63
18
|
const DEFAULT_EXCLUDE_EXACT = new Set<string>(["akanjs/webkit", "@akanjs/cli", "@akanjs/devkit"]);
|
|
64
19
|
const DEFAULT_EXCLUDE_PREFIX = ["@akanjs/cli/", "@akanjs/devkit/"];
|
|
65
20
|
const OPTIONAL_BACKEND_EXTERNAL_EXACT = new Set<string>([
|
|
@@ -81,19 +36,17 @@ const RUNTIME_EXTERNAL_EXACT = new Set<string>([
|
|
|
81
36
|
]);
|
|
82
37
|
const RUNTIME_EXTERNAL_PREFIX = ["react-dom/", "react-server-dom-webpack/"];
|
|
83
38
|
|
|
84
|
-
//
|
|
85
|
-
// extensionless path. Ordered to prefer TS over JS, since workspace
|
|
86
|
-
// sources are authored in TypeScript.
|
|
39
|
+
// TS before JS: workspace sources are authored in TypeScript.
|
|
87
40
|
const CANDIDATE_EXTS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
|
|
88
41
|
|
|
42
|
+
// Rejected `Bun.build` options: `external` also applies at macro time (ENOENT "react"), and `packages: "external"`
|
|
43
|
+
// externalizes workspace and npm packages the runtime does not install. onResolve shapes only the output graph.
|
|
89
44
|
export async function createExternalizeFrameworkPlugin(options: ExternalizeFrameworkOptions): Promise<BunPlugin> {
|
|
90
45
|
const tsconfig = await options.app.getTsConfig();
|
|
91
46
|
const includePrefixes = options.include ?? DEFAULT_INCLUDE;
|
|
92
47
|
const extraExact = new Set(options.extra ?? []);
|
|
93
48
|
const workspaceRoot = options.app.workspace.workspaceRoot;
|
|
94
49
|
const tsconfigPaths = tsconfig.compilerOptions.paths ?? {};
|
|
95
|
-
// Pre-compute the tsconfig root-only entries (`akanjs/client` → …)
|
|
96
|
-
// so we can resolve their subpaths by concatenating the remainder.
|
|
97
50
|
const rootEntries = Object.entries(tsconfigPaths)
|
|
98
51
|
.filter(([k]) => !k.endsWith("/*"))
|
|
99
52
|
.map(([k, v]) => ({ pkg: k, entryFile: v[0] ?? null }))
|
|
@@ -105,7 +58,6 @@ export async function createExternalizeFrameworkPlugin(options: ExternalizeFrame
|
|
|
105
58
|
|
|
106
59
|
async function resolveWorkspaceSubpath(spec: string): Promise<string | null> {
|
|
107
60
|
if (!workspaceRoot) return null;
|
|
108
|
-
// Wildcard `@apps/*` / `@libs/*` → direct filesystem path.
|
|
109
61
|
for (const { prefix, replacements } of wildcardEntries) {
|
|
110
62
|
if (!spec.startsWith(prefix)) continue;
|
|
111
63
|
const suffix = spec.slice(prefix.length);
|
|
@@ -117,12 +69,8 @@ export async function createExternalizeFrameworkPlugin(options: ExternalizeFrame
|
|
|
117
69
|
if (hit) return hit;
|
|
118
70
|
}
|
|
119
71
|
}
|
|
120
|
-
// Root-only tsconfig entries — resolve the subpath against the
|
|
121
|
-
// package directory. E.g. `akanjs/client` → `pkgs/akanjs/client/index.ts`
|
|
122
|
-
// so `akanjs/client/cookie` → `pkgs/akanjs/client/cookie`.
|
|
123
72
|
for (const { pkg, entryFile } of rootEntries) {
|
|
124
|
-
if (
|
|
125
|
-
if (spec === pkg) continue; // the package root itself — let Bun's tsconfig resolver handle it
|
|
73
|
+
if (!spec.startsWith(`${pkg}/`)) continue;
|
|
126
74
|
const suffix = spec.slice(pkg.length + 1);
|
|
127
75
|
const pkgDir = path.dirname(path.resolve(workspaceRoot, entryFile));
|
|
128
76
|
const candidate = path.join(pkgDir, suffix);
|
|
@@ -137,13 +85,8 @@ export async function createExternalizeFrameworkPlugin(options: ExternalizeFrame
|
|
|
137
85
|
setup(build) {
|
|
138
86
|
build.onResolve({ filter: /.*/ }, async (args) => {
|
|
139
87
|
const spec = args.path;
|
|
140
|
-
// Relative imports always stay inlined — they anchor to the
|
|
141
|
-
// current package's source tree and cannot leak cross-package.
|
|
142
|
-
// `.` and `..` (without trailing slash) are valid relative
|
|
143
|
-
// specifiers too (equivalent to `./index` / `../index`).
|
|
144
88
|
if (spec === "." || spec === ".." || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/"))
|
|
145
89
|
return undefined;
|
|
146
|
-
// Force-external first (overrides the include allowlist).
|
|
147
90
|
if (extraExact.has(spec) || DEFAULT_EXCLUDE_EXACT.has(spec)) return { path: spec, external: true };
|
|
148
91
|
for (const prefix of DEFAULT_EXCLUDE_PREFIX) {
|
|
149
92
|
if (spec.startsWith(prefix)) return { path: spec, external: true };
|
|
@@ -153,38 +96,21 @@ export async function createExternalizeFrameworkPlugin(options: ExternalizeFrame
|
|
|
153
96
|
for (const prefix of RUNTIME_EXTERNAL_PREFIX) {
|
|
154
97
|
if (spec.startsWith(prefix)) return { path: spec, external: true };
|
|
155
98
|
}
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// tsconfig-paths entry exists for `akanjs/client`) and get
|
|
161
|
-
// silently left as externals.
|
|
162
|
-
for (const prefix of includePrefixes) {
|
|
163
|
-
if (!spec.startsWith(prefix)) continue;
|
|
164
|
-
const resolved = await resolveWorkspaceSubpath(spec);
|
|
165
|
-
if (resolved) return { path: resolved };
|
|
166
|
-
// Fall through to let Bun attempt its own resolution for the
|
|
167
|
-
// package root (`akanjs/client`) — tsconfig `paths` handles
|
|
168
|
-
// that case directly.
|
|
169
|
-
return undefined;
|
|
170
|
-
}
|
|
171
|
-
// Everything else (ordinary npm dependencies like dayjs / tailwind-merge /
|
|
172
|
-
// immer) is bundled into the pages artifact.
|
|
173
|
-
return undefined;
|
|
99
|
+
// Bun resolves only root tsconfig `paths`, so a rewritten `akanjs/client/cookie` would silently stay external.
|
|
100
|
+
if (!includePrefixes.some((prefix) => spec.startsWith(prefix))) return undefined;
|
|
101
|
+
const resolved = await resolveWorkspaceSubpath(spec);
|
|
102
|
+
return resolved ? { path: resolved } : undefined;
|
|
174
103
|
});
|
|
175
104
|
},
|
|
176
105
|
};
|
|
177
106
|
}
|
|
178
107
|
|
|
179
108
|
async function firstExisting(basePath: string): Promise<string | null> {
|
|
180
|
-
// Direct file with its own extension.
|
|
181
109
|
if (await Bun.file(basePath).exists()) return basePath;
|
|
182
|
-
// Try each candidate extension.
|
|
183
110
|
for (const ext of CANDIDATE_EXTS) {
|
|
184
111
|
const candidate = `${basePath}${ext}`;
|
|
185
112
|
if (await Bun.file(candidate).exists()) return candidate;
|
|
186
113
|
}
|
|
187
|
-
// Directory with `index.<ext>` inside.
|
|
188
114
|
for (const ext of CANDIDATE_EXTS) {
|
|
189
115
|
const candidate = path.join(basePath, `index${ext}`);
|
|
190
116
|
if (await Bun.file(candidate).exists()) return candidate;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface NamedItem {
|
|
2
|
+
imported: string;
|
|
3
|
+
local: string;
|
|
4
|
+
isType: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const loaderFor = (absPath: string): "ts" | "tsx" | "js" | "jsx" => {
|
|
8
|
+
if (absPath.endsWith(".tsx")) return "tsx";
|
|
9
|
+
if (absPath.endsWith(".jsx")) return "jsx";
|
|
10
|
+
if (absPath.endsWith(".ts")) return "ts";
|
|
11
|
+
return "js";
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const parseNamedList = (listBody: string): NamedItem[] => {
|
|
15
|
+
const out: NamedItem[] = [];
|
|
16
|
+
for (const raw of listBody.split(",")) {
|
|
17
|
+
let rest = raw.trim();
|
|
18
|
+
if (!rest) continue;
|
|
19
|
+
let isType = false;
|
|
20
|
+
if (rest.startsWith("type ")) {
|
|
21
|
+
isType = true;
|
|
22
|
+
rest = rest.slice(5).trim();
|
|
23
|
+
}
|
|
24
|
+
const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(rest);
|
|
25
|
+
if (asMatch) {
|
|
26
|
+
out.push({ imported: asMatch[1] ?? "", local: asMatch[2] ?? "", isType });
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (/^\w+$/.test(rest)) out.push({ imported: rest, local: rest, isType });
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
};
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { loaderFor } from "./moduleSyntax";
|
|
2
4
|
|
|
3
|
-
// Pure "use client" transform: when `source` starts with the `"use client"`
|
|
4
|
-
// directive, replace its exports with `registerClientReference` stubs so the
|
|
5
|
-
// RSC renderer can serialize them as client component references instead of
|
|
6
|
-
// trying to run them on the server.
|
|
7
|
-
|
|
8
|
-
// Matches `"use client"` or `'use client'` at the start of a file,
|
|
9
|
-
// optionally after leading whitespace and JS comments.
|
|
10
5
|
const USE_CLIENT_RE = /^\s*(?:\/\*[\s\S]*?\*\/\s*|\/\/[^\n]*\n\s*)*["']use client["']/;
|
|
11
6
|
const IMPLICIT_ROOT_LAYOUT_RE =
|
|
12
7
|
/[/\\]\.akan[/\\]generated[/\\](?:implicit-root-layout|root-layouts[/\\].*__root_layout)\.(tsx|ts|jsx|js)$/;
|
|
8
|
+
const STAR_REEXPORT_HINT_RE = /\bexport\s*\*\s*from\b/;
|
|
13
9
|
|
|
14
10
|
export interface UseClientTransformArgs {
|
|
15
11
|
path: string;
|
|
@@ -20,15 +16,41 @@ export function toClientReferencePath(absPath: string, workspaceRoot: string): s
|
|
|
20
16
|
return path.relative(path.resolve(workspaceRoot), path.resolve(absPath)).split(path.sep).join("/");
|
|
21
17
|
}
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
const starReexportSpecifiers = (source: string, filePath: string) =>
|
|
20
|
+
ts
|
|
21
|
+
.createSourceFile(filePath, source, ts.ScriptTarget.Latest)
|
|
22
|
+
.statements.filter(
|
|
23
|
+
(statement): statement is ts.ExportDeclaration & { moduleSpecifier: ts.StringLiteral } =>
|
|
24
|
+
ts.isExportDeclaration(statement) &&
|
|
25
|
+
!statement.exportClause &&
|
|
26
|
+
!statement.isTypeOnly &&
|
|
27
|
+
!!statement.moduleSpecifier &&
|
|
28
|
+
ts.isStringLiteral(statement.moduleSpecifier),
|
|
29
|
+
)
|
|
30
|
+
.map((statement) => statement.moduleSpecifier.text);
|
|
31
|
+
|
|
32
|
+
// `Bun.Transpiler.scan` reports no name through `export *`, so the server could register none of a star's names as
|
|
33
|
+
// client references; they would be undefined in a server component. Refused as Next.js refuses it.
|
|
34
|
+
export const scanUseClientExports = (source: string, filePath: string, workspaceRoot?: string): string[] => {
|
|
35
|
+
const stars =
|
|
36
|
+
USE_CLIENT_RE.test(source) && STAR_REEXPORT_HINT_RE.test(source) ? starReexportSpecifiers(source, filePath) : [];
|
|
37
|
+
if (stars.length) {
|
|
38
|
+
const file = workspaceRoot ? toClientReferencePath(filePath, workspaceRoot) : filePath;
|
|
39
|
+
const listed = stars.slice(0, 3).map((specifier) => `\`export * from ${JSON.stringify(specifier)}\``);
|
|
40
|
+
const written = stars.length > 3 ? `${listed.join(", ")} (and ${stars.length - 3} more)` : listed.join(", ");
|
|
41
|
+
throw new Error(
|
|
42
|
+
`${file} is a "use client" module, so it cannot ${written}: the server sees only the names a client module ` +
|
|
43
|
+
`declares, and a star re-export declares none. Re-export them by name — \`export { … } from ${JSON.stringify(stars[0])}\`.`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return new Bun.Transpiler({ loader: loaderFor(filePath) }).scan(source).exports;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** `null` unless `source` is a `"use client"` module; the stub source is valid under loader `"ts"`. */
|
|
27
50
|
export function transformUseClient(source: string, args: UseClientTransformArgs): string | null {
|
|
28
51
|
if (!USE_CLIENT_RE.test(source)) return null;
|
|
29
52
|
if (IMPLICIT_ROOT_LAYOUT_RE.test(args.path)) return null;
|
|
30
|
-
const
|
|
31
|
-
const { exports } = transpiler.scan(source);
|
|
53
|
+
const exports = scanUseClientExports(source, args.path, args.workspaceRoot);
|
|
32
54
|
if (exports.length === 0) return null;
|
|
33
55
|
|
|
34
56
|
const referencePath = args.workspaceRoot ? toClientReferencePath(args.path, args.workspaceRoot) : args.path;
|
|
@@ -50,10 +72,3 @@ export function transformUseClient(source: string, args: UseClientTransformArgs)
|
|
|
50
72
|
|
|
51
73
|
return lines.join("\n");
|
|
52
74
|
}
|
|
53
|
-
|
|
54
|
-
function loaderFor(absPath: string): "ts" | "tsx" | "js" | "jsx" {
|
|
55
|
-
if (absPath.endsWith(".tsx")) return "tsx";
|
|
56
|
-
if (absPath.endsWith(".jsx")) return "jsx";
|
|
57
|
-
if (absPath.endsWith(".ts")) return "ts";
|
|
58
|
-
return "js";
|
|
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", () => {
|
|
@@ -62,6 +51,65 @@ describe("transformUseClient", () => {
|
|
|
62
51
|
});
|
|
63
52
|
});
|
|
64
53
|
|
|
54
|
+
describe('export * in a "use client" module', () => {
|
|
55
|
+
test("is refused with the file, the specifier and the fix, since the server would see none of its names", () => {
|
|
56
|
+
const source = [
|
|
57
|
+
'"use client";',
|
|
58
|
+
'export * from "../../ui/UserLeave";',
|
|
59
|
+
"export const General = () => null;",
|
|
60
|
+
"",
|
|
61
|
+
].join("\n");
|
|
62
|
+
expect(() =>
|
|
63
|
+
transformUseClient(source, { path: "/repo/libs/shared/lib/user/User.Template.tsx", workspaceRoot: "/repo" }),
|
|
64
|
+
).toThrow(
|
|
65
|
+
'libs/shared/lib/user/User.Template.tsx is a "use client" module, so it cannot `export * from "../../ui/UserLeave"`',
|
|
66
|
+
);
|
|
67
|
+
expect(() => scanUseClientExports('"use client";\nexport * from "./a";\n', "/repo/ui/Barrel.ts")).toThrow(
|
|
68
|
+
"Re-export them by name",
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("leaves every other export shape alone, and a module that is not a client boundary", () => {
|
|
73
|
+
const file = "/repo/ui/Barrel.tsx";
|
|
74
|
+
const shapes = [
|
|
75
|
+
'"use client";',
|
|
76
|
+
'export type * from "./types";',
|
|
77
|
+
'export * as ns from "./namespace";',
|
|
78
|
+
'export { LeaveInfo, Voc } from "./leave";',
|
|
79
|
+
"// export * from './commented';",
|
|
80
|
+
"export const text = \"export * from './a string'\";",
|
|
81
|
+
"",
|
|
82
|
+
].join("\n");
|
|
83
|
+
expect(scanUseClientExports(shapes, file).sort()).toEqual(["LeaveInfo", "Voc", "ns", "text"]);
|
|
84
|
+
expect(scanUseClientExports('export * from "./a";\nexport const X = 1;\n', file)).toEqual(["X"]);
|
|
85
|
+
const stub = transformUseClient(shapes, { path: file, workspaceRoot: "/repo" });
|
|
86
|
+
expect(stub).toContain("export const LeaveInfo = registerClientReference");
|
|
87
|
+
expect(stub).toContain("export const Voc = registerClientReference");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("fails the server bundle, and the build report carries the reason out of the bundler's error", async () => {
|
|
91
|
+
const root = await makeTempRoot();
|
|
92
|
+
await write(path.join(root, "Leave.tsx"), '"use client";\nexport const LeaveInfo = () => null;\n');
|
|
93
|
+
await write(
|
|
94
|
+
path.join(root, "Template.tsx"),
|
|
95
|
+
'"use client";\nexport * from "./Leave";\nexport const General = () => null;\n',
|
|
96
|
+
);
|
|
97
|
+
await write(path.join(root, "page.tsx"), 'import * as Template from "./Template";\nexport const T = Template;\n');
|
|
98
|
+
const failure = await Bun.build({
|
|
99
|
+
entrypoints: [path.join(root, "page.tsx")],
|
|
100
|
+
target: "bun",
|
|
101
|
+
external: ["react-server-dom-webpack/server.node"],
|
|
102
|
+
plugins: [createUseClientBundlePlugin({ workspaceRoot: root })],
|
|
103
|
+
}).then(
|
|
104
|
+
() => null,
|
|
105
|
+
(error: unknown) => error,
|
|
106
|
+
);
|
|
107
|
+
expect(ApplicationBuildReporter.formatError(failure)).toContain(
|
|
108
|
+
'Template.tsx is a "use client" module, so it cannot `export * from "./Leave"`',
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
65
113
|
describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
66
114
|
test("analyzes local, named, aliased, and star re-exports while skipping type and namespace exports", async () => {
|
|
67
115
|
const root = await makeTempRoot();
|
|
@@ -97,14 +145,83 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
|
97
145
|
expect(map?.has("ns")).toBe(false);
|
|
98
146
|
});
|
|
99
147
|
|
|
148
|
+
test("a name a module exports itself wins over the same name its star re-exports bring, as ES decides", async () => {
|
|
149
|
+
const root = await makeTempRoot();
|
|
150
|
+
const pkgDir = path.join(root, "pkg");
|
|
151
|
+
await write(
|
|
152
|
+
path.join(pkgDir, "index.ts"),
|
|
153
|
+
[
|
|
154
|
+
'export * from "./a";',
|
|
155
|
+
'export { X } from "./c";',
|
|
156
|
+
'export * from "./nested";',
|
|
157
|
+
"export const Local = 1;",
|
|
158
|
+
"",
|
|
159
|
+
].join("\n"),
|
|
160
|
+
);
|
|
161
|
+
await write(path.join(pkgDir, "a.ts"), "export const X = 2;\nexport const Local = 2;\nexport const OnlyA = 2;\n");
|
|
162
|
+
await write(path.join(pkgDir, "c.ts"), "export const X = 1;\n");
|
|
163
|
+
await write(path.join(pkgDir, "nested", "index.ts"), 'export * from "./deep";\nexport { Y } from "./y";\n');
|
|
164
|
+
await write(path.join(pkgDir, "nested", "deep.ts"), "export const Y = 2;\n");
|
|
165
|
+
await write(path.join(pkgDir, "nested", "y.ts"), "export const Y = 1;\n");
|
|
166
|
+
const analyzer = new BarrelAnalyzer({
|
|
167
|
+
resolvePackage: async () => ({ pkgName: "@scope/pkg", entryFile: path.join(pkgDir, "index.ts"), pkgDir }),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const runtime = (await import(path.join(pkgDir, "index.ts"))) as Record<string, number>;
|
|
171
|
+
expect([runtime.X, runtime.Local, runtime.Y, runtime.OnlyA]).toEqual([1, 1, 1, 2]);
|
|
172
|
+
const map = await analyzer.analyze("@scope/pkg");
|
|
173
|
+
expect(map?.get("X")).toEqual({ subpath: "@scope/pkg/c", originalName: "X" });
|
|
174
|
+
expect(map?.get("Local")).toEqual({ subpath: "@scope/pkg", originalName: "Local" });
|
|
175
|
+
expect(map?.get("Y")).toEqual({ subpath: "@scope/pkg/nested/y", originalName: "Y" });
|
|
176
|
+
expect(map?.get("OnlyA")).toEqual({ subpath: "@scope/pkg/a", originalName: "OnlyA" });
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("two analyses that interleave each keep their own place in their barrel", async () => {
|
|
180
|
+
const root = await makeTempRoot();
|
|
181
|
+
const barrels = { first: ["a1", "a2", "a3"], second: ["b1", "b2"] } as const;
|
|
182
|
+
for (const [pkgName, names] of Object.entries(barrels)) {
|
|
183
|
+
const lines = names.map((name) => `export { ${name} } from "./${name}";`);
|
|
184
|
+
// Parks the second walk past the end of the first barrel, so a shared regex position would end the first early.
|
|
185
|
+
const padding = pkgName === "second" ? [`// ${"-".repeat(200)}`] : [];
|
|
186
|
+
await write(path.join(root, pkgName, "index.ts"), [...padding, ...lines, ""].join("\n"));
|
|
187
|
+
for (const name of names) await write(path.join(root, pkgName, `${name}.ts`), `export const ${name} = 1;\n`);
|
|
188
|
+
}
|
|
189
|
+
const parked = { first: Promise.withResolvers<void>(), second: Promise.withResolvers<void>() };
|
|
190
|
+
const released = { first: Promise.withResolvers<void>(), second: Promise.withResolvers<void>() };
|
|
191
|
+
const analyzer = new BarrelAnalyzer({
|
|
192
|
+
resolvePackage: async (pkgName) => ({
|
|
193
|
+
pkgName,
|
|
194
|
+
entryFile: path.join(root, pkgName, "index.ts"),
|
|
195
|
+
pkgDir: path.join(root, pkgName),
|
|
196
|
+
}),
|
|
197
|
+
resolveRelative: async (fromFile, relSpec) => {
|
|
198
|
+
const pkgName = path.basename(path.dirname(fromFile)) as keyof typeof barrels;
|
|
199
|
+
if (relSpec === `./${barrels[pkgName][0]}`) {
|
|
200
|
+
parked[pkgName].resolve();
|
|
201
|
+
await released[pkgName].promise;
|
|
202
|
+
}
|
|
203
|
+
return path.join(path.dirname(fromFile), `${relSpec.slice(2)}.ts`);
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const first = analyzer.analyze("first");
|
|
208
|
+
await parked.first.promise;
|
|
209
|
+
const second = analyzer.analyze("second");
|
|
210
|
+
await parked.second.promise;
|
|
211
|
+
released.first.resolve();
|
|
212
|
+
const firstMap = await first;
|
|
213
|
+
released.second.resolve();
|
|
214
|
+
const secondMap = await second;
|
|
215
|
+
|
|
216
|
+
expect(firstMap?.get("a3")).toEqual({ subpath: "first/a3", originalName: "a3" });
|
|
217
|
+
expect(secondMap?.get("b2")).toEqual({ subpath: "second/b2", originalName: "b2" });
|
|
218
|
+
});
|
|
219
|
+
|
|
100
220
|
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;
|
|
221
|
+
const analyzer = fakeAnalyzer([
|
|
222
|
+
["A", { subpath: "@scope/pkg/leaf", originalName: "A" }],
|
|
223
|
+
["Bee", { subpath: "@scope/pkg/leaf", originalName: "B" }],
|
|
224
|
+
]);
|
|
108
225
|
|
|
109
226
|
const rewritten = await rewriteBarrelImports(
|
|
110
227
|
'import DefaultExport, { A, Bee as LocalBee, type Shape, Missing } from "@scope/pkg";\nconsole.log(A);',
|
|
@@ -126,9 +243,7 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
|
126
243
|
});
|
|
127
244
|
|
|
128
245
|
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;
|
|
246
|
+
const analyzer = fakeAnalyzer([["st", { subpath: "@apps/demo/lib/st", originalName: "st" }]]);
|
|
132
247
|
|
|
133
248
|
const rewritten = await rewriteBarrelImports(
|
|
134
249
|
'import { st } from "@apps/demo/client";\nvoid st;\n',
|
|
@@ -142,9 +257,7 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
|
142
257
|
});
|
|
143
258
|
|
|
144
259
|
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;
|
|
260
|
+
const analyzer = fakeAnalyzer([["AkanApp", { subpath: "akanjs/server/akanApp", originalName: "AkanApp" }]]);
|
|
148
261
|
|
|
149
262
|
const source = [
|
|
150
263
|
'import { Code } from "@apps/docs/ui";',
|
|
@@ -164,13 +277,10 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
|
164
277
|
});
|
|
165
278
|
|
|
166
279
|
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;
|
|
280
|
+
const analyzer = fakeAnalyzer([
|
|
281
|
+
["AkanOption", { subpath: "akanjs/server/akanOption", originalName: "AkanOption" }],
|
|
282
|
+
["Try", { subpath: "akanjs/server/decorators", originalName: "Try" }],
|
|
283
|
+
]);
|
|
174
284
|
|
|
175
285
|
const rewritten = await rewriteBarrelImports(
|
|
176
286
|
'import { AkanOption, Try } from "akanjs/server";\nexport const option = new AkanOption();\n',
|
|
@@ -188,10 +298,9 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
|
188
298
|
});
|
|
189
299
|
|
|
190
300
|
test("rewrites single-package Akan facet barrels to leaf subpaths", async () => {
|
|
191
|
-
const analyzer =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
} as unknown as BarrelAnalyzer;
|
|
301
|
+
const analyzer = fakeAnalyzer([
|
|
302
|
+
["BottomInset", { subpath: "akanjs/ui/Layout/BottomInset", originalName: "BottomInset" }],
|
|
303
|
+
]);
|
|
195
304
|
|
|
196
305
|
const rewritten = await rewriteBarrelImports('import { BottomInset } from "akanjs/ui";\n', ["akanjs/ui"], analyzer);
|
|
197
306
|
|
|
@@ -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,21 +1,8 @@
|
|
|
1
1
|
import type { BunPlugin } from "bun";
|
|
2
|
+
import { loaderFor } from "./moduleSyntax";
|
|
2
3
|
import { transformUseClient } from "./rscUseClientTransform";
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
-
* BunPlugin that stubs `"use client"` modules so the server bundle sees
|
|
6
|
-
* `registerClientReference(proxy, absPath, name)` stubs instead of the
|
|
7
|
-
* original client code.
|
|
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
|
-
*/
|
|
5
|
+
/** `workspaceRoot` makes reference keys workspace-relative; they must match the client manifest's keys. */
|
|
19
6
|
export function createUseClientBundlePlugin(options: { workspaceRoot?: string } = {}): BunPlugin {
|
|
20
7
|
return {
|
|
21
8
|
name: "akan-use-client-bundle",
|
|
@@ -38,10 +25,3 @@ export function createUseClientBundlePlugin(options: { workspaceRoot?: string }
|
|
|
38
25
|
},
|
|
39
26
|
};
|
|
40
27
|
}
|
|
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
|
-
}
|