@akanjs/devkit 1.0.20 → 2.1.0-rc.1
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.ko.md +65 -0
- package/README.md +62 -6
- package/aiEditor.ts +304 -0
- package/akanApp/akanApp.host.ts +393 -0
- package/akanApp/index.ts +1 -0
- package/akanConfig/akanConfig.test.ts +236 -0
- package/akanConfig/akanConfig.ts +384 -0
- package/akanConfig/index.ts +2 -0
- package/akanConfig/types.ts +23 -0
- package/applicationBuildReporter.ts +69 -0
- package/applicationBuildRunner.ts +302 -0
- package/applicationReleasePackager.ts +206 -0
- package/artifact/implicitRootLayout.ts +155 -0
- package/artifact/index.ts +1 -0
- package/artifact/routeSeedIndex.test.ts +98 -0
- package/artifact/routeSeedIndex.ts +130 -0
- package/auth.ts +41 -0
- package/builder.ts +164 -0
- package/capacitor.base.config.ts +88 -0
- package/capacitorApp.ts +440 -0
- package/commandDecorators/argMeta.ts +102 -0
- package/commandDecorators/command.ts +351 -0
- package/commandDecorators/commandBuilder.ts +224 -0
- package/commandDecorators/commandDecorators.test.ts +212 -0
- package/commandDecorators/commandMeta.ts +7 -0
- package/commandDecorators/dependencyBuilder.ts +100 -0
- package/{esm/src/commandDecorators/helpFormatter.js → commandDecorators/helpFormatter.ts} +100 -47
- package/{esm/src/commandDecorators/index.js → commandDecorators/index.ts} +4 -2
- package/commandDecorators/targetMeta.ts +31 -0
- package/commandDecorators/types.ts +10 -0
- package/constants.ts +25 -0
- package/createTunnel.ts +36 -0
- package/dependencyScanner.ts +357 -0
- package/devkitUtils.test.ts +259 -0
- package/executors.test.ts +315 -0
- package/executors.ts +1390 -0
- package/{esm/src/extractDeps.js → extractDeps.ts} +26 -20
- package/{esm/src/fileEditor.js → fileEditor.ts} +51 -32
- package/fileSys.ts +39 -0
- package/frontendBuild/allRoutesBuilder.ts +103 -0
- package/frontendBuild/buildRouteClient.test.ts +190 -0
- package/frontendBuild/clientBuildTypes.ts +114 -0
- package/frontendBuild/clientEntriesBundler.ts +303 -0
- package/frontendBuild/clientEntryDiscovery.ts +199 -0
- package/frontendBuild/csrArtifactBuilder.ts +237 -0
- package/frontendBuild/cssCompiler.ts +286 -0
- package/frontendBuild/cssImportResolver.ts +116 -0
- package/frontendBuild/fontOptimizer.ts +427 -0
- package/frontendBuild/frontendBuild.test.ts +204 -0
- package/frontendBuild/hmrChangeClassifier.ts +28 -0
- package/frontendBuild/hmrWatcher.ts +102 -0
- package/frontendBuild/index.ts +18 -0
- package/frontendBuild/pagesBundleBuilder.ts +137 -0
- package/frontendBuild/pagesEntrySourceGenerator.ts +37 -0
- package/frontendBuild/precompressArtifacts.ts +59 -0
- package/frontendBuild/routeClientBuilder.ts +290 -0
- package/frontendBuild/routesManifestArtifactSerializer.ts +62 -0
- package/frontendBuild/ssrBaseArtifactBuilder.ts +139 -0
- package/frontendBuild/vendorSpecifiers.ts +16 -0
- package/frontendBuild/watchRootResolver.ts +28 -0
- package/getCredentials.ts +19 -0
- package/getDirname.ts +3 -0
- package/getModelFileData.ts +59 -0
- package/getRelatedCnsts.ts +313 -0
- package/guideline.ts +19 -0
- package/incrementalBuilder/incrementalBuilder.host.test.ts +51 -0
- package/incrementalBuilder/incrementalBuilder.host.ts +152 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +331 -0
- package/incrementalBuilder/index.ts +1 -0
- package/{esm/src/index.js → index.ts} +28 -15
- package/lint/no-deep-internal-import.grit +25 -0
- package/lint/no-import-client-functions.grit +32 -0
- package/lint/no-import-external-library.grit +21 -0
- package/lint/no-js-private-class-method.grit +42 -0
- package/lint/no-use-client-in-server.grit +7 -0
- package/lint/non-scalar-props-restricted.grit +13 -0
- package/linter.ts +271 -0
- package/mobile/index.ts +1 -0
- package/mobile/mobileTarget.test.ts +53 -0
- package/mobile/mobileTarget.ts +88 -0
- package/package.json +48 -31
- package/prompter.ts +72 -0
- package/scanInfo.ts +606 -0
- package/selectModel.ts +11 -0
- package/{esm/src/spinner.js → spinner.ts} +22 -28
- package/{esm/src/capacitorApp.js → src/capacitorApp.ts} +82 -81
- package/sshTunnel.ts +152 -0
- package/{esm/src/streamAi.js → streamAi.ts} +18 -12
- package/transforms/barrelAnalyzer.ts +278 -0
- package/transforms/barrelImportsPlugin.ts +504 -0
- package/transforms/externalizeFrameworkPlugin.ts +185 -0
- package/transforms/index.ts +5 -0
- package/transforms/rscUseClientTransform.ts +59 -0
- package/transforms/transforms.test.ts +208 -0
- package/transforms/useClientBundlePlugin.ts +47 -0
- package/tsconfig.json +37 -0
- package/typeChecker.ts +264 -0
- package/types.ts +44 -0
- package/ui/MultiScrollList.tsx +242 -0
- package/ui/ScrollList.tsx +107 -0
- package/ui/index.ts +2 -0
- package/{esm/src/uploadRelease.js → uploadRelease.ts} +50 -34
- package/{esm/src/useStdoutDimensions.js → useStdoutDimensions.ts} +5 -5
- package/cjs/index.js +0 -21
- package/cjs/src/aiEditor.js +0 -311
- package/cjs/src/auth.js +0 -72
- package/cjs/src/builder.js +0 -114
- package/cjs/src/capacitorApp.js +0 -313
- package/cjs/src/commandDecorators/argMeta.js +0 -88
- package/cjs/src/commandDecorators/command.js +0 -324
- package/cjs/src/commandDecorators/commandMeta.js +0 -30
- package/cjs/src/commandDecorators/helpFormatter.js +0 -211
- package/cjs/src/commandDecorators/index.js +0 -31
- package/cjs/src/commandDecorators/targetMeta.js +0 -57
- package/cjs/src/commandDecorators/types.js +0 -15
- package/cjs/src/constants.js +0 -46
- package/cjs/src/createTunnel.js +0 -49
- package/cjs/src/dependencyScanner.js +0 -220
- package/cjs/src/executors.js +0 -964
- package/cjs/src/extractDeps.js +0 -103
- package/cjs/src/fileEditor.js +0 -120
- package/cjs/src/getCredentials.js +0 -44
- package/cjs/src/getDirname.js +0 -38
- package/cjs/src/getModelFileData.js +0 -66
- package/cjs/src/getRelatedCnsts.js +0 -260
- package/cjs/src/guideline.js +0 -15
- package/cjs/src/index.js +0 -65
- package/cjs/src/linter.js +0 -238
- package/cjs/src/prompter.js +0 -85
- package/cjs/src/scanInfo.js +0 -491
- package/cjs/src/selectModel.js +0 -46
- package/cjs/src/spinner.js +0 -93
- package/cjs/src/streamAi.js +0 -62
- package/cjs/src/typeChecker.js +0 -207
- package/cjs/src/types.js +0 -15
- package/cjs/src/uploadRelease.js +0 -112
- package/cjs/src/useStdoutDimensions.js +0 -43
- package/esm/index.js +0 -1
- package/esm/src/aiEditor.js +0 -282
- package/esm/src/auth.js +0 -42
- package/esm/src/builder.js +0 -81
- package/esm/src/commandDecorators/argMeta.js +0 -54
- package/esm/src/commandDecorators/command.js +0 -290
- package/esm/src/commandDecorators/commandMeta.js +0 -7
- package/esm/src/commandDecorators/targetMeta.js +0 -33
- package/esm/src/commandDecorators/types.js +0 -0
- package/esm/src/constants.js +0 -17
- package/esm/src/createTunnel.js +0 -26
- package/esm/src/dependencyScanner.js +0 -187
- package/esm/src/executors.js +0 -928
- package/esm/src/getCredentials.js +0 -11
- package/esm/src/getDirname.js +0 -5
- package/esm/src/getModelFileData.js +0 -33
- package/esm/src/getRelatedCnsts.js +0 -221
- package/esm/src/guideline.js +0 -0
- package/esm/src/linter.js +0 -205
- package/esm/src/prompter.js +0 -51
- package/esm/src/scanInfo.js +0 -455
- package/esm/src/selectModel.js +0 -13
- package/esm/src/typeChecker.js +0 -174
- package/esm/src/types.js +0 -0
- package/index.d.ts +0 -1
- package/src/aiEditor.d.ts +0 -50
- package/src/auth.d.ts +0 -9
- package/src/builder.d.ts +0 -18
- package/src/capacitorApp.d.ts +0 -39
- package/src/commandDecorators/argMeta.d.ts +0 -67
- package/src/commandDecorators/command.d.ts +0 -2
- package/src/commandDecorators/commandMeta.d.ts +0 -2
- package/src/commandDecorators/helpFormatter.d.ts +0 -3
- package/src/commandDecorators/index.d.ts +0 -6
- package/src/commandDecorators/targetMeta.d.ts +0 -19
- package/src/commandDecorators/types.d.ts +0 -1
- package/src/constants.d.ts +0 -26
- package/src/createTunnel.d.ts +0 -8
- package/src/dependencyScanner.d.ts +0 -23
- package/src/executors.d.ts +0 -296
- package/src/extractDeps.d.ts +0 -7
- package/src/fileEditor.d.ts +0 -16
- package/src/getCredentials.d.ts +0 -12
- package/src/getDirname.d.ts +0 -1
- package/src/getModelFileData.d.ts +0 -16
- package/src/getRelatedCnsts.d.ts +0 -53
- package/src/guideline.d.ts +0 -19
- package/src/index.d.ts +0 -23
- package/src/linter.d.ts +0 -109
- package/src/prompter.d.ts +0 -14
- package/src/scanInfo.d.ts +0 -82
- package/src/selectModel.d.ts +0 -1
- package/src/spinner.d.ts +0 -20
- package/src/streamAi.d.ts +0 -6
- package/src/typeChecker.d.ts +0 -52
- package/src/types.d.ts +0 -31
- package/src/uploadRelease.d.ts +0 -10
- package/src/useStdoutDimensions.d.ts +0 -1
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { BunPlugin } from "bun";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import type { App } from "../commandDecorators";
|
|
5
|
+
import { BarrelAnalyzer, type BarrelExportMap, type PackageEntry } from "./barrelAnalyzer";
|
|
6
|
+
|
|
7
|
+
export interface BarrelImportsPluginOptions {
|
|
8
|
+
/** Absolute paths whose content should be returned unchanged (e.g. node_modules). */
|
|
9
|
+
skipPath?: (absPath: string) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Optional transform applied after the barrel rewrite in the same onLoad
|
|
12
|
+
* pass. Useful to chain further transforms (e.g. `"use client"` stubbing)
|
|
13
|
+
* that would otherwise be blocked — Bun's `onLoad` cannot fall through to a
|
|
14
|
+
* second plugin once a response is returned.
|
|
15
|
+
* Return the transformed source, or `null` to indicate no change.
|
|
16
|
+
*/
|
|
17
|
+
pipeAfter?: (source: string, args: { path: string }) => string | Promise<string | null> | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const createBarrelImportsPlugin = async (
|
|
21
|
+
app: App,
|
|
22
|
+
{ skipPath = defaultSkipPath, pipeAfter }: BarrelImportsPluginOptions = {},
|
|
23
|
+
): Promise<BunPlugin> => {
|
|
24
|
+
const akanConfig = await app.getConfig();
|
|
25
|
+
const barrels = [...new Set(akanConfig.barrelImports)].filter(Boolean);
|
|
26
|
+
const analyzer = new BarrelAnalyzer({
|
|
27
|
+
resolvePackage: await createTsconfigPackageResolver(app),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
name: "barrel-imports",
|
|
32
|
+
setup(build) {
|
|
33
|
+
// Exclude third-party node_modules, but keep node_modules/akanjs in generated
|
|
34
|
+
// workspaces so framework `"use client"` modules are still stubbed for RSC.
|
|
35
|
+
//
|
|
36
|
+
// The optional `(\?v=\d+)?` tail lets the filter match paths that HMR
|
|
37
|
+
// has version-tagged (`./_index.tsx?v=3`). We strip the query before
|
|
38
|
+
// reading and normalize it away from the path we pass downstream so
|
|
39
|
+
// only the cache-bust `pipeAfter` step (if any) knows about it.
|
|
40
|
+
build.onLoad(
|
|
41
|
+
{
|
|
42
|
+
filter:
|
|
43
|
+
/^(?:(?!.*[\\/]node_modules[\\/]).*|.*[\\/]node_modules[\\/]akanjs[\\/].*)\.(tsx|ts|jsx|js)(\?v=\d+)?$/,
|
|
44
|
+
},
|
|
45
|
+
async (args) => {
|
|
46
|
+
const realPath = args.path.replace(/\?v=\d+$/, "");
|
|
47
|
+
const loader = loaderFor(realPath);
|
|
48
|
+
if (skipPath(realPath)) {
|
|
49
|
+
const raw = await Bun.file(realPath).text();
|
|
50
|
+
return { contents: raw, loader };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let source = await Bun.file(realPath).text();
|
|
54
|
+
|
|
55
|
+
// Bun's macro evaluator has a race condition when the plugin returns
|
|
56
|
+
// rewritten source for a module that also contains
|
|
57
|
+
// `with { type: "macro" }` imports *and* another macro-host file is
|
|
58
|
+
// being evaluated concurrently in the same graph: one of the macro
|
|
59
|
+
// identifiers ends up undefined at runtime (e.g. `getSerializedSignal
|
|
60
|
+
// is not defined`). Returning the unmodified source for macro hosts
|
|
61
|
+
// sidesteps the race. The trade-off is that a handful of `useClient`-
|
|
62
|
+
// style files keep their original barrel imports; the rest of the
|
|
63
|
+
// tree still benefits from flattening.
|
|
64
|
+
const hasMacroAttr = MACRO_ATTR_RE.test(source);
|
|
65
|
+
|
|
66
|
+
if (!hasMacroAttr && barrels.length > 0) {
|
|
67
|
+
// Fast pre-check to avoid tokenizing every file in the workspace.
|
|
68
|
+
let maybe = false;
|
|
69
|
+
for (const b of barrels) {
|
|
70
|
+
if (source.includes(b)) {
|
|
71
|
+
maybe = true;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (maybe) {
|
|
76
|
+
const rewritten = await rewriteBarrelImports(source, barrels, analyzer);
|
|
77
|
+
if (rewritten !== null) source = rewritten;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (pipeAfter) {
|
|
82
|
+
const piped = await pipeAfter(source, { path: realPath });
|
|
83
|
+
if (piped !== null) source = piped;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { contents: source, loader };
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Build a `resolvePackage` that maps a package specifier (like `akanjs/ui`)
|
|
95
|
+
* to its barrel entry file using the workspace tsconfig `paths`. If no direct
|
|
96
|
+
* mapping exists, falls back to node_modules resolution.
|
|
97
|
+
*/
|
|
98
|
+
export const createTsconfigPackageResolver = async (
|
|
99
|
+
app: App,
|
|
100
|
+
): Promise<(pkgName: string) => Promise<PackageEntry | null>> => {
|
|
101
|
+
const tsconfig = await app.getTsConfig();
|
|
102
|
+
const tsconfigPaths = tsconfig.compilerOptions.paths ?? {};
|
|
103
|
+
// Pre-compute wildcard entries so we don't walk the full map per lookup.
|
|
104
|
+
// Longer prefixes sort first so `@libs/util/*` wins over `@libs/*`.
|
|
105
|
+
const wildcardEntries = Object.entries(tsconfigPaths)
|
|
106
|
+
.filter(([k]) => k.endsWith("/*"))
|
|
107
|
+
.map(([k, v]) => ({
|
|
108
|
+
prefix: k.slice(0, -1), // keep trailing `/`
|
|
109
|
+
replacements: v,
|
|
110
|
+
}))
|
|
111
|
+
.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
112
|
+
|
|
113
|
+
return async (pkgName) => {
|
|
114
|
+
const exact = tsconfigPaths[pkgName];
|
|
115
|
+
if (exact && exact.length > 0) {
|
|
116
|
+
const raw = exact[0];
|
|
117
|
+
if (!raw) return null;
|
|
118
|
+
const entryFile = path.resolve(app.workspace.workspaceRoot, raw);
|
|
119
|
+
if (!(await Bun.file(entryFile).exists())) return null;
|
|
120
|
+
// Detect "facet" barrels: specifiers like `@libs/util/server` whose entry
|
|
121
|
+
// file is a sibling inside the parent directory (`libs/util/server.ts`)
|
|
122
|
+
// rather than an `index.*` inside a dedicated package directory
|
|
123
|
+
// (`libs/util/server/index.ts`). For facets, the subpath the analyzer
|
|
124
|
+
// generates for a leaf like `libs/util/lib/sig.ts` must be computed
|
|
125
|
+
// against the parent package (`@libs/util`) so that the rewritten
|
|
126
|
+
// import resolves via the workspace's `@libs/*` tsconfig wildcard
|
|
127
|
+
// (`libs/util/lib/sig`). Using the raw `pkgName` (`@libs/util/server`)
|
|
128
|
+
// would generate `@libs/util/server/lib/sig`, a path that does not exist
|
|
129
|
+
// on disk and cannot be imported.
|
|
130
|
+
const parsed = path.parse(entryFile);
|
|
131
|
+
const lastSlash = pkgName.lastIndexOf("/");
|
|
132
|
+
if (parsed.name !== "index" && lastSlash !== -1) {
|
|
133
|
+
const facet = pkgName.slice(lastSlash + 1);
|
|
134
|
+
const parentSpec = pkgName.slice(0, lastSlash);
|
|
135
|
+
if (facet === parsed.name && parentSpec.length > 0) {
|
|
136
|
+
return { pkgName: parentSpec, entryFile, pkgDir: parsed.dir };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { pkgName, entryFile, pkgDir: path.dirname(entryFile) };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Wildcard fallback: tsconfig entries like `@libs/*` → `./libs/*` map a
|
|
143
|
+
// whole family of specifiers to workspace directories. Without this,
|
|
144
|
+
// barrels such as `@libs/util/ui` or `@apps/minimal/client` never resolve
|
|
145
|
+
// to an entry file — the analyzer returns `null`, the plugin skips them,
|
|
146
|
+
// and the consumer falls back to Bun's default resolution which loads the
|
|
147
|
+
// full barrel (pulling the entire transitive macro / side-effect graph).
|
|
148
|
+
for (const { prefix, replacements } of wildcardEntries) {
|
|
149
|
+
if (!pkgName.startsWith(prefix)) continue;
|
|
150
|
+
const suffix = pkgName.slice(prefix.length);
|
|
151
|
+
for (const repl of replacements) {
|
|
152
|
+
if (!repl) continue;
|
|
153
|
+
const replPath = repl.endsWith("/*") ? repl.slice(0, -1) : repl;
|
|
154
|
+
const candidate = path.resolve(app.workspace.workspaceRoot, replPath + suffix);
|
|
155
|
+
// Try `candidate.<ext>` first (facet-barrel: sibling file like
|
|
156
|
+
// `apps/minimal/client.ts`). If it exists we treat the PARENT directory
|
|
157
|
+
// as `pkgDir` and the PARENT specifier (`@apps/minimal`) as `pkgName`
|
|
158
|
+
// so a leaf at `apps/minimal/lib/useClient.ts` rewrites to
|
|
159
|
+
// `@apps/minimal/lib/useClient` and resolves via the same `@apps/*`
|
|
160
|
+
// wildcard. Using the raw `pkgName` would yield
|
|
161
|
+
// `@apps/minimal/client/lib/useClient`, a path that does not exist.
|
|
162
|
+
for (const ext of CANDIDATE_EXTS) {
|
|
163
|
+
const file = `${candidate}${ext}`;
|
|
164
|
+
if (await Bun.file(file).exists()) {
|
|
165
|
+
const lastSlash = pkgName.lastIndexOf("/");
|
|
166
|
+
if (lastSlash !== -1) {
|
|
167
|
+
const parentSpec = pkgName.slice(0, lastSlash);
|
|
168
|
+
if (parentSpec.length > 0) {
|
|
169
|
+
return { pkgName: parentSpec, entryFile: file, pkgDir: path.dirname(file) };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { pkgName, entryFile: file, pkgDir: path.dirname(file) };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const ext of CANDIDATE_EXTS) {
|
|
176
|
+
const file = path.join(candidate, `index${ext}`);
|
|
177
|
+
if (await Bun.file(file).exists()) {
|
|
178
|
+
return { pkgName, entryFile: file, pkgDir: candidate };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// A prefix matched but nothing on disk — stop so a shorter, less specific
|
|
183
|
+
// prefix doesn't accidentally resolve to an unrelated location.
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Fallback: resolve package exports from node_modules. This supports
|
|
188
|
+
// single-package subpaths such as `akanjs/ui`, whose package.json lives at
|
|
189
|
+
// node_modules/akanjs/package.json rather than node_modules/akanjs/ui.
|
|
190
|
+
const exported = await resolveNodePackageExport(app.workspace.workspaceRoot, pkgName);
|
|
191
|
+
if (exported) return exported;
|
|
192
|
+
|
|
193
|
+
const pkgJsonPath = path.join(app.workspace.workspaceRoot, "node_modules", pkgName, "package.json");
|
|
194
|
+
if (!(await Bun.file(pkgJsonPath).exists())) return null;
|
|
195
|
+
try {
|
|
196
|
+
const pkgJson = JSON.parse(await Bun.file(pkgJsonPath).text()) as {
|
|
197
|
+
main?: string;
|
|
198
|
+
module?: string;
|
|
199
|
+
};
|
|
200
|
+
const rel = pkgJson.module ?? pkgJson.main ?? "index.js";
|
|
201
|
+
const entryFile = path.resolve(path.dirname(pkgJsonPath), rel);
|
|
202
|
+
if (!(await Bun.file(entryFile).exists())) return null;
|
|
203
|
+
return { pkgName, entryFile, pkgDir: path.dirname(pkgJsonPath) };
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const CANDIDATE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
211
|
+
|
|
212
|
+
const NODE_MODULES_RE = /[\\/]node_modules[\\/]/;
|
|
213
|
+
const AKANJS_NODE_MODULE_RE = /[\\/]node_modules[\\/]akanjs[\\/]/;
|
|
214
|
+
const defaultSkipPath = (absPath: string) => NODE_MODULES_RE.test(absPath) && !AKANJS_NODE_MODULE_RE.test(absPath);
|
|
215
|
+
|
|
216
|
+
type ExportValue = string | string[] | { [condition: string]: ExportValue | undefined };
|
|
217
|
+
|
|
218
|
+
const resolveNodePackageExport = async (workspaceRoot: string, specifier: string): Promise<PackageEntry | null> => {
|
|
219
|
+
const packageName = getPackageName(specifier);
|
|
220
|
+
if (!packageName) return null;
|
|
221
|
+
const pkgJsonPath = path.join(workspaceRoot, "node_modules", packageName, "package.json");
|
|
222
|
+
if (!(await Bun.file(pkgJsonPath).exists())) return null;
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const pkgDir = path.dirname(pkgJsonPath);
|
|
226
|
+
const pkgJson = JSON.parse(await Bun.file(pkgJsonPath).text()) as {
|
|
227
|
+
exports?: Record<string, ExportValue>;
|
|
228
|
+
module?: string;
|
|
229
|
+
main?: string;
|
|
230
|
+
};
|
|
231
|
+
const subpath = specifier === packageName ? "." : `.${specifier.slice(packageName.length)}`;
|
|
232
|
+
const exported = resolvePackageExport(pkgJson.exports, subpath);
|
|
233
|
+
const rel = exported ?? (subpath === "." ? (pkgJson.module ?? pkgJson.main ?? "index.js") : null);
|
|
234
|
+
if (!rel || !rel.startsWith(".")) return null;
|
|
235
|
+
const entryFile = await resolveFileCandidate(path.resolve(pkgDir, rel));
|
|
236
|
+
if (!entryFile) return null;
|
|
237
|
+
const pkgEntryName = specifier;
|
|
238
|
+
return { pkgName: pkgEntryName, entryFile, pkgDir: path.dirname(entryFile), preserveFilePath: true };
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const resolveExportValue = (value: ExportValue | undefined): string | null => {
|
|
245
|
+
if (!value) return null;
|
|
246
|
+
if (typeof value === "string") return value;
|
|
247
|
+
if (Array.isArray(value)) {
|
|
248
|
+
for (const item of value) {
|
|
249
|
+
const resolved = resolveExportValue(item);
|
|
250
|
+
if (resolved) return resolved;
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
for (const condition of ["source", "import", "default", "types"]) {
|
|
255
|
+
const resolved = resolveExportValue(value[condition]);
|
|
256
|
+
if (resolved) return resolved;
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const resolvePackageExport = (exportsMap: Record<string, ExportValue> | undefined, subpath: string): string | null => {
|
|
262
|
+
if (!exportsMap) return null;
|
|
263
|
+
const exact = resolveExportValue(exportsMap[subpath]);
|
|
264
|
+
if (exact) return exact;
|
|
265
|
+
|
|
266
|
+
for (const [key, value] of Object.entries(exportsMap)) {
|
|
267
|
+
const starIdx = key.indexOf("*");
|
|
268
|
+
if (starIdx === -1) continue;
|
|
269
|
+
const prefix = key.slice(0, starIdx);
|
|
270
|
+
const suffix = key.slice(starIdx + 1);
|
|
271
|
+
if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix)) continue;
|
|
272
|
+
const wildcard = subpath.slice(prefix.length, subpath.length - suffix.length);
|
|
273
|
+
const resolved = resolveExportValue(value);
|
|
274
|
+
if (resolved) return resolved.replace("*", wildcard);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return null;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const getPackageName = (specifier: string): string | null => {
|
|
281
|
+
const parts = specifier.split("/");
|
|
282
|
+
if (!parts[0]) return null;
|
|
283
|
+
if (specifier.startsWith("@")) return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
|
|
284
|
+
return parts[0];
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const resolveFileCandidate = async (candidate: string): Promise<string | null> => {
|
|
288
|
+
if (await Bun.file(candidate).exists()) return candidate;
|
|
289
|
+
if (path.extname(candidate)) return null;
|
|
290
|
+
for (const ext of CANDIDATE_EXTS) {
|
|
291
|
+
const file = `${candidate}${ext}`;
|
|
292
|
+
if (await Bun.file(file).exists()) return file;
|
|
293
|
+
}
|
|
294
|
+
for (const ext of CANDIDATE_EXTS) {
|
|
295
|
+
const file = path.join(candidate, `index${ext}`);
|
|
296
|
+
if (await Bun.file(file).exists()) return file;
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// Matches `with { type: "macro" }` import attributes (single or double quotes,
|
|
302
|
+
// tolerant of whitespace). Used to detect macro-host modules so the plugin
|
|
303
|
+
// can leave their source untouched.
|
|
304
|
+
const MACRO_ATTR_RE = /with\s*\{\s*type\s*:\s*["']macro["']\s*\}/;
|
|
305
|
+
|
|
306
|
+
/** Exposed for testing. */
|
|
307
|
+
export const rewriteBarrelImports = async (
|
|
308
|
+
source: string,
|
|
309
|
+
barrels: string[],
|
|
310
|
+
analyzer: BarrelAnalyzer,
|
|
311
|
+
): Promise<string | null> => {
|
|
312
|
+
const statements = findImportStatements(source);
|
|
313
|
+
if (statements.length === 0) return null;
|
|
314
|
+
|
|
315
|
+
// Walk import statements in reverse so replacements don't shift earlier ranges.
|
|
316
|
+
let changed = false;
|
|
317
|
+
let out = source;
|
|
318
|
+
for (let i = statements.length - 1; i >= 0; i--) {
|
|
319
|
+
const stmt = statements[i];
|
|
320
|
+
if (!stmt) continue;
|
|
321
|
+
if (!barrels.includes(stmt.specifier)) continue;
|
|
322
|
+
const map = await analyzer.analyze(stmt.specifier);
|
|
323
|
+
if (!map || map.size === 0) continue;
|
|
324
|
+
const replacement = rewriteSingleStatement(stmt, map);
|
|
325
|
+
if (replacement === null) continue;
|
|
326
|
+
out = out.slice(0, stmt.start) + replacement + out.slice(stmt.end);
|
|
327
|
+
changed = true;
|
|
328
|
+
}
|
|
329
|
+
return changed ? out : null;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
interface ImportStatement {
|
|
333
|
+
start: number;
|
|
334
|
+
end: number;
|
|
335
|
+
clause: string;
|
|
336
|
+
specifier: string;
|
|
337
|
+
trailingSemicolon: boolean;
|
|
338
|
+
raw: string;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const findImportStatements = (source: string): ImportStatement[] => {
|
|
342
|
+
const statements: ImportStatement[] = [];
|
|
343
|
+
const sourceFile = ts.createSourceFile("barrel-imports.tsx", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
344
|
+
for (const statement of sourceFile.statements) {
|
|
345
|
+
if (!ts.isImportDeclaration(statement)) continue;
|
|
346
|
+
if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
347
|
+
const importClause = statement.importClause;
|
|
348
|
+
if (!importClause) continue;
|
|
349
|
+
const statementStart = statement.getStart(sourceFile);
|
|
350
|
+
const statementEnd = statement.end;
|
|
351
|
+
statements.push({
|
|
352
|
+
start: statementStart,
|
|
353
|
+
end: statementEnd,
|
|
354
|
+
clause: source.slice(importClause.getStart(sourceFile), importClause.end).trim(),
|
|
355
|
+
specifier: statement.moduleSpecifier.text,
|
|
356
|
+
trailingSemicolon: source.slice(statement.moduleSpecifier.end, statement.end).includes(";"),
|
|
357
|
+
raw: source.slice(statementStart, statementEnd),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
return statements;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
interface NamedImportItem {
|
|
364
|
+
imported: string;
|
|
365
|
+
local: string;
|
|
366
|
+
isType: boolean;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
interface ParsedClause {
|
|
370
|
+
defaultImport?: string;
|
|
371
|
+
namespaceImport?: string;
|
|
372
|
+
named?: NamedImportItem[];
|
|
373
|
+
/** Whole clause is `import type { ... } from "..."`. */
|
|
374
|
+
typeOnly: boolean;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const parseImportClause = (clause: string): ParsedClause | null => {
|
|
378
|
+
let rest = clause.trim();
|
|
379
|
+
let typeOnly = false;
|
|
380
|
+
if (rest.startsWith("type ")) {
|
|
381
|
+
typeOnly = true;
|
|
382
|
+
rest = rest.slice(5).trim();
|
|
383
|
+
}
|
|
384
|
+
const parsed: ParsedClause = { typeOnly };
|
|
385
|
+
// Try pattern: default + rest
|
|
386
|
+
const commaMatch = /^(\w+)\s*,\s*(.+)$/.exec(rest);
|
|
387
|
+
if (commaMatch) {
|
|
388
|
+
parsed.defaultImport = commaMatch[1];
|
|
389
|
+
rest = commaMatch[2] ?? "";
|
|
390
|
+
} else if (/^\w+$/.test(rest)) {
|
|
391
|
+
parsed.defaultImport = rest;
|
|
392
|
+
return parsed;
|
|
393
|
+
}
|
|
394
|
+
if (rest.startsWith("*")) {
|
|
395
|
+
const ns = /^\*\s+as\s+(\w+)$/.exec(rest);
|
|
396
|
+
if (!ns) return null;
|
|
397
|
+
parsed.namespaceImport = ns[1];
|
|
398
|
+
return parsed;
|
|
399
|
+
}
|
|
400
|
+
if (rest.startsWith("{")) {
|
|
401
|
+
const close = rest.indexOf("}");
|
|
402
|
+
if (close === -1) return null;
|
|
403
|
+
const inner = rest.slice(1, close);
|
|
404
|
+
parsed.named = parseNamedImportList(inner);
|
|
405
|
+
return parsed;
|
|
406
|
+
}
|
|
407
|
+
return parsed;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const parseNamedImportList = (body: string): NamedImportItem[] => {
|
|
411
|
+
const out: NamedImportItem[] = [];
|
|
412
|
+
for (const raw of body.split(",")) {
|
|
413
|
+
const s = raw.trim();
|
|
414
|
+
if (!s) continue;
|
|
415
|
+
let isType = false;
|
|
416
|
+
let rest = s;
|
|
417
|
+
if (rest.startsWith("type ")) {
|
|
418
|
+
isType = true;
|
|
419
|
+
rest = rest.slice(5).trim();
|
|
420
|
+
}
|
|
421
|
+
const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(rest);
|
|
422
|
+
if (asMatch) {
|
|
423
|
+
out.push({ imported: asMatch[1] ?? "", local: asMatch[2] ?? "", isType });
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (/^\w+$/.test(rest)) {
|
|
427
|
+
out.push({ imported: rest, local: rest, isType });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return out;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const rewriteSingleStatement = (stmt: ImportStatement, map: BarrelExportMap): string | null => {
|
|
434
|
+
const clause = parseImportClause(stmt.clause);
|
|
435
|
+
if (!clause) return null;
|
|
436
|
+
// Namespace imports need the whole barrel — cannot rewrite safely.
|
|
437
|
+
if (clause.namespaceImport) return null;
|
|
438
|
+
// Pure type imports are erased at build; leave them alone.
|
|
439
|
+
if (clause.typeOnly && !clause.defaultImport) return null;
|
|
440
|
+
if (!clause.named || clause.named.length === 0) {
|
|
441
|
+
// Only default import — nothing to split.
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const remaining: NamedImportItem[] = [];
|
|
446
|
+
const rewrites = new Map<string, NamedImportItem[]>();
|
|
447
|
+
for (const item of clause.named) {
|
|
448
|
+
if (item.isType) {
|
|
449
|
+
remaining.push(item);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
const target = map.get(item.imported);
|
|
453
|
+
if (!target) {
|
|
454
|
+
remaining.push(item);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const list = rewrites.get(target.subpath) ?? [];
|
|
458
|
+
// Use the leaf's original name, keeping the consumer's local alias.
|
|
459
|
+
list.push({ imported: target.originalName, local: item.local, isType: false });
|
|
460
|
+
rewrites.set(target.subpath, list);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (rewrites.size === 0) return null;
|
|
464
|
+
|
|
465
|
+
const lines: string[] = [];
|
|
466
|
+
// Always emit trailing semicolons; safe even when the source omitted them.
|
|
467
|
+
const tail = ";";
|
|
468
|
+
|
|
469
|
+
if (shouldPreserveBarrelSideEffects(stmt.specifier)) {
|
|
470
|
+
lines.push(`import "${stmt.specifier}"${tail}`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Re-emit an import from the original barrel that carries whatever we could
|
|
474
|
+
// not flatten (default import, type-only items, unknown names).
|
|
475
|
+
if (clause.defaultImport || remaining.length > 0) {
|
|
476
|
+
const parts: string[] = [];
|
|
477
|
+
if (clause.defaultImport) parts.push(clause.defaultImport);
|
|
478
|
+
if (remaining.length > 0) {
|
|
479
|
+
parts.push(`{ ${remaining.map(serializeNamedItem).join(", ")} }`);
|
|
480
|
+
}
|
|
481
|
+
lines.push(`import ${parts.join(", ")} from "${stmt.specifier}"${tail}`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
for (const [subpath, items] of rewrites) {
|
|
485
|
+
lines.push(`import { ${items.map(serializeNamedItem).join(", ")} } from "${subpath}"${tail}`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return lines.join("\n");
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const shouldPreserveBarrelSideEffects = (specifier: string): boolean => /^@(apps|libs)\/[^/]+\/client$/.test(specifier);
|
|
492
|
+
|
|
493
|
+
const serializeNamedItem = (item: NamedImportItem): string => {
|
|
494
|
+
const prefix = item.isType ? "type " : "";
|
|
495
|
+
if (item.imported === item.local) return `${prefix}${item.imported}`;
|
|
496
|
+
return `${prefix}${item.imported} as ${item.local}`;
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const loaderFor = (absPath: string): "ts" | "tsx" | "js" | "jsx" => {
|
|
500
|
+
if (absPath.endsWith(".tsx")) return "tsx";
|
|
501
|
+
if (absPath.endsWith(".jsx")) return "jsx";
|
|
502
|
+
if (absPath.endsWith(".ts")) return "ts";
|
|
503
|
+
return "js";
|
|
504
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { BunPlugin } from "bun";
|
|
3
|
+
import type { App } from "../commandDecorators";
|
|
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` / `clsx`, 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 framework host 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
|
+
export interface ExternalizeFrameworkOptions {
|
|
37
|
+
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
|
+
*/
|
|
43
|
+
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
|
+
*/
|
|
49
|
+
extra?: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Workspace package prefixes that must be BUNDLED so their sources go
|
|
53
|
+
// through the barrel-imports + `"use client"` transforms. Without this
|
|
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.
|
|
57
|
+
const DEFAULT_INCLUDE = ["akanjs/", "@apps/", "@libs/"];
|
|
58
|
+
|
|
59
|
+
// Packages that must stay external even when a broad allowlist would
|
|
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.
|
|
63
|
+
const DEFAULT_EXCLUDE_EXACT = new Set<string>(["akanjs/webkit", "akanjs/server", "@akanjs/cli", "@akanjs/devkit"]);
|
|
64
|
+
const DEFAULT_EXCLUDE_PREFIX = ["akanjs/server/", "@akanjs/cli/", "@akanjs/devkit/"];
|
|
65
|
+
const RUNTIME_EXTERNAL_EXACT = new Set<string>([
|
|
66
|
+
"react",
|
|
67
|
+
"react-dom",
|
|
68
|
+
"react/jsx-runtime",
|
|
69
|
+
"react/jsx-dev-runtime",
|
|
70
|
+
"react-server-dom-webpack",
|
|
71
|
+
"react-server-dom-webpack/server.node",
|
|
72
|
+
"react-server-dom-webpack/client.node",
|
|
73
|
+
"react-server-dom-webpack/client.browser",
|
|
74
|
+
]);
|
|
75
|
+
const RUNTIME_EXTERNAL_PREFIX = ["react-dom/", "react-server-dom-webpack/"];
|
|
76
|
+
|
|
77
|
+
// File extensions Bun can load when a bare subpath points at an
|
|
78
|
+
// extensionless path. Ordered to prefer TS over JS, since workspace
|
|
79
|
+
// sources are authored in TypeScript.
|
|
80
|
+
const CANDIDATE_EXTS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
|
|
81
|
+
|
|
82
|
+
export async function createExternalizeFrameworkPlugin(options: ExternalizeFrameworkOptions): Promise<BunPlugin> {
|
|
83
|
+
const tsconfig = await options.app.getTsConfig();
|
|
84
|
+
const includePrefixes = options.include ?? DEFAULT_INCLUDE;
|
|
85
|
+
const extraExact = new Set(options.extra ?? []);
|
|
86
|
+
const workspaceRoot = options.app.workspace.workspaceRoot;
|
|
87
|
+
const tsconfigPaths = tsconfig.compilerOptions.paths ?? {};
|
|
88
|
+
// Pre-compute the tsconfig root-only entries (`akanjs/client` → …)
|
|
89
|
+
// so we can resolve their subpaths by concatenating the remainder.
|
|
90
|
+
const rootEntries = Object.entries(tsconfigPaths)
|
|
91
|
+
.filter(([k]) => !k.endsWith("/*"))
|
|
92
|
+
.map(([k, v]) => ({ pkg: k, entryFile: v[0] ?? null }))
|
|
93
|
+
.filter((e): e is { pkg: string; entryFile: string } => e.entryFile !== null);
|
|
94
|
+
const wildcardEntries = Object.entries(tsconfigPaths)
|
|
95
|
+
.filter(([k]) => k.endsWith("/*"))
|
|
96
|
+
.map(([k, v]) => ({ prefix: k.slice(0, -1), replacements: v }))
|
|
97
|
+
.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
98
|
+
|
|
99
|
+
async function resolveWorkspaceSubpath(spec: string): Promise<string | null> {
|
|
100
|
+
if (!workspaceRoot) return null;
|
|
101
|
+
// Wildcard `@apps/*` / `@libs/*` → direct filesystem path.
|
|
102
|
+
for (const { prefix, replacements } of wildcardEntries) {
|
|
103
|
+
if (!spec.startsWith(prefix)) continue;
|
|
104
|
+
const suffix = spec.slice(prefix.length);
|
|
105
|
+
for (const repl of replacements) {
|
|
106
|
+
const replPath = repl?.endsWith("/*") ? repl.slice(0, -1) : (repl ?? "");
|
|
107
|
+
if (!replPath) continue;
|
|
108
|
+
const candidate = path.resolve(workspaceRoot, replPath + suffix);
|
|
109
|
+
const hit = await firstExisting(candidate);
|
|
110
|
+
if (hit) return hit;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Root-only tsconfig entries — resolve the subpath against the
|
|
114
|
+
// package directory. E.g. `akanjs/client` → `pkgs/akanjs/client/index.ts`
|
|
115
|
+
// so `akanjs/client/cookie` → `pkgs/akanjs/client/cookie`.
|
|
116
|
+
for (const { pkg, entryFile } of rootEntries) {
|
|
117
|
+
if (spec !== pkg && !spec.startsWith(`${pkg}/`)) continue;
|
|
118
|
+
if (spec === pkg) continue; // the package root itself — let Bun's tsconfig resolver handle it
|
|
119
|
+
const suffix = spec.slice(pkg.length + 1);
|
|
120
|
+
const pkgDir = path.dirname(path.resolve(workspaceRoot, entryFile));
|
|
121
|
+
const candidate = path.join(pkgDir, suffix);
|
|
122
|
+
const hit = await firstExisting(candidate);
|
|
123
|
+
if (hit) return hit;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
name: "akan-externalize-framework",
|
|
130
|
+
setup(build) {
|
|
131
|
+
build.onResolve({ filter: /.*/ }, async (args) => {
|
|
132
|
+
const spec = args.path;
|
|
133
|
+
// Relative imports always stay inlined — they anchor to the
|
|
134
|
+
// current package's source tree and cannot leak cross-package.
|
|
135
|
+
// `.` and `..` (without trailing slash) are valid relative
|
|
136
|
+
// specifiers too (equivalent to `./index` / `../index`).
|
|
137
|
+
if (spec === "." || spec === ".." || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/"))
|
|
138
|
+
return undefined;
|
|
139
|
+
// Force-external first (overrides the include allowlist).
|
|
140
|
+
if (extraExact.has(spec) || DEFAULT_EXCLUDE_EXACT.has(spec)) return { path: spec, external: true };
|
|
141
|
+
for (const prefix of DEFAULT_EXCLUDE_PREFIX) {
|
|
142
|
+
if (spec.startsWith(prefix)) return { path: spec, external: true };
|
|
143
|
+
}
|
|
144
|
+
if (RUNTIME_EXTERNAL_EXACT.has(spec)) return { path: spec, external: true };
|
|
145
|
+
for (const prefix of RUNTIME_EXTERNAL_PREFIX) {
|
|
146
|
+
if (spec.startsWith(prefix)) return { path: spec, external: true };
|
|
147
|
+
}
|
|
148
|
+
// Workspace allowlist — resolve the subpath to an absolute
|
|
149
|
+
// on-disk file so Bun.build can read it. Without this, bare
|
|
150
|
+
// specifiers like `akanjs/client/cookie` (produced by the
|
|
151
|
+
// barrel rewriter) fail Bun's default resolution (only root
|
|
152
|
+
// tsconfig-paths entry exists for `akanjs/client`) and get
|
|
153
|
+
// silently left as externals.
|
|
154
|
+
for (const prefix of includePrefixes) {
|
|
155
|
+
if (!spec.startsWith(prefix)) continue;
|
|
156
|
+
const resolved = await resolveWorkspaceSubpath(spec);
|
|
157
|
+
if (resolved) return { path: resolved };
|
|
158
|
+
// Fall through to let Bun attempt its own resolution for the
|
|
159
|
+
// package root (`akanjs/client`) — tsconfig `paths` handles
|
|
160
|
+
// that case directly.
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
// Everything else (ordinary npm dependencies like dayjs / clsx /
|
|
164
|
+
// immer) is bundled into the pages artifact.
|
|
165
|
+
return undefined;
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function firstExisting(basePath: string): Promise<string | null> {
|
|
172
|
+
// Direct file with its own extension.
|
|
173
|
+
if (await Bun.file(basePath).exists()) return basePath;
|
|
174
|
+
// Try each candidate extension.
|
|
175
|
+
for (const ext of CANDIDATE_EXTS) {
|
|
176
|
+
const candidate = `${basePath}${ext}`;
|
|
177
|
+
if (await Bun.file(candidate).exists()) return candidate;
|
|
178
|
+
}
|
|
179
|
+
// Directory with `index.<ext>` inside.
|
|
180
|
+
for (const ext of CANDIDATE_EXTS) {
|
|
181
|
+
const candidate = path.join(basePath, `index${ext}`);
|
|
182
|
+
if (await Bun.file(candidate).exists()) return candidate;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|