@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,237 @@
|
|
|
1
|
+
import { mkdir, rm, unlink } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { BaseBuildArtifact } from "akanjs/server";
|
|
4
|
+
import { resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
|
|
5
|
+
import type { App } from "../commandDecorators";
|
|
6
|
+
import { PagesEntrySourceGenerator } from "./pagesEntrySourceGenerator";
|
|
7
|
+
|
|
8
|
+
export interface BuildCsrArtifactResult {
|
|
9
|
+
outputDir: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class CsrArtifactBuilder {
|
|
13
|
+
#app: App;
|
|
14
|
+
#command: "build" | "start";
|
|
15
|
+
#lang: string;
|
|
16
|
+
|
|
17
|
+
constructor(app: App, command: "build" | "start" = "start", lang = "en") {
|
|
18
|
+
this.#app = app;
|
|
19
|
+
this.#command = command;
|
|
20
|
+
this.#lang = lang;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async build(): Promise<BuildCsrArtifactResult | null> {
|
|
24
|
+
const pageKeys = await this.#app.getPageKeys();
|
|
25
|
+
if (pageKeys.length === 0) {
|
|
26
|
+
this.#app.log(`[cli] no route files under ${this.#app.cwdPath}/page — skipping CSR build`);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const pageEntries = await resolveSsrPageEntriesForApp(this.#app, pageKeys);
|
|
31
|
+
const akanConfig = await this.#app.getConfig();
|
|
32
|
+
const artifact = await this.#loadCsrArtifact();
|
|
33
|
+
const csrBasePaths = [...akanConfig.basePaths];
|
|
34
|
+
const htmlEntries = csrBasePaths.length > 0 ? csrBasePaths : ["index"];
|
|
35
|
+
await rm(this.#outputDir, { recursive: true, force: true });
|
|
36
|
+
await mkdir(path.join(this.#app.cwdPath, ".akan/generated/csr"), { recursive: true });
|
|
37
|
+
const generatedHtmlFiles = Object.fromEntries(htmlEntries.map((basePath) => this.#createHtmlFile(basePath)));
|
|
38
|
+
|
|
39
|
+
const result = await Bun.build({
|
|
40
|
+
target: "browser",
|
|
41
|
+
entrypoints: Object.keys(generatedHtmlFiles),
|
|
42
|
+
files: {
|
|
43
|
+
...generatedHtmlFiles,
|
|
44
|
+
[`${this.#app.cwdPath}/.akan/generated/csr/csr.tsx`]: `
|
|
45
|
+
import { bootCsr } from "akanjs/webkit";
|
|
46
|
+
${PagesEntrySourceGenerator.generateStatic(pageEntries)}
|
|
47
|
+
void bootCsr(pages);
|
|
48
|
+
`,
|
|
49
|
+
},
|
|
50
|
+
root: `${this.#app.cwdPath}/.akan/generated/csr`,
|
|
51
|
+
outdir: this.#outputDir,
|
|
52
|
+
splitting: false,
|
|
53
|
+
minify: true,
|
|
54
|
+
env: "AKAN_PUBLIC_*",
|
|
55
|
+
define: this.#define(),
|
|
56
|
+
optimizeImports: akanConfig.optimizeImports,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!result.success) {
|
|
60
|
+
const logs = result.logs.map((log) => log.message).join("\n");
|
|
61
|
+
throw new Error(`[csr-build] failed${logs ? `\n${logs}` : ""}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await this.#inlineCsrArtifacts(artifact.cssAssets ?? {});
|
|
65
|
+
this.#app.verbose(`[csr-build] output -> ${this.#outputDir}`);
|
|
66
|
+
return { outputDir: this.#outputDir };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get #outputDir(): string {
|
|
70
|
+
return path.join(
|
|
71
|
+
this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath,
|
|
72
|
+
this.#command === "build" ? "csr" : ".akan/artifact/csr",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#define(): Record<string, string> {
|
|
77
|
+
const nodeEnv = this.#command === "build" ? "production" : (process.env.NODE_ENV ?? "development");
|
|
78
|
+
return {
|
|
79
|
+
"process.env.NODE_ENV": JSON.stringify(nodeEnv),
|
|
80
|
+
"process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("csr"),
|
|
81
|
+
...Object.fromEntries(
|
|
82
|
+
Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]),
|
|
83
|
+
),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#createHtmlFile(basePath: string): readonly [string, string] {
|
|
88
|
+
const filename = `${basePath}.html`;
|
|
89
|
+
return [
|
|
90
|
+
`${this.#app.cwdPath}/.akan/generated/csr/${filename}`,
|
|
91
|
+
`<!doctype html>
|
|
92
|
+
<html lang="${this.#lang}">
|
|
93
|
+
<head>
|
|
94
|
+
<meta charset="utf-8" />
|
|
95
|
+
<title>${this.#app.name}</title>
|
|
96
|
+
<base href="/" />
|
|
97
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
98
|
+
</head>
|
|
99
|
+
<body>
|
|
100
|
+
<div id="root"></div>
|
|
101
|
+
<script type="module" src="./csr.tsx"></script>
|
|
102
|
+
</body>
|
|
103
|
+
</html>
|
|
104
|
+
`,
|
|
105
|
+
] as const;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async #loadCsrArtifact(): Promise<Pick<BaseBuildArtifact, "cssAssets">> {
|
|
109
|
+
const artifactDir = path.join(
|
|
110
|
+
this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath,
|
|
111
|
+
".akan/artifact",
|
|
112
|
+
);
|
|
113
|
+
const artifactFile = Bun.file(path.join(artifactDir, "base-artifact.json"));
|
|
114
|
+
if (!(await artifactFile.exists())) return { cssAssets: {} };
|
|
115
|
+
const artifact = (await artifactFile.json()) as Pick<BaseBuildArtifact, "cssAssets">;
|
|
116
|
+
return { cssAssets: artifact.cssAssets ?? {} };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async #inlineCsrArtifacts(cssAssets: Record<string, { cssUrl: string; cssRelPath: string }>): Promise<void> {
|
|
120
|
+
const jsFiles = new Set<string>();
|
|
121
|
+
const cssFiles = new Set<string>();
|
|
122
|
+
for (const htmlPath of await this.#htmlOutputPaths()) {
|
|
123
|
+
const htmlFile = Bun.file(htmlPath);
|
|
124
|
+
if (!(await htmlFile.exists())) continue;
|
|
125
|
+
const basePath = path.basename(htmlPath, ".html") === "index" ? "" : path.basename(htmlPath, ".html");
|
|
126
|
+
const inlined = await this.#inlineHtmlAssets(await htmlFile.text(), htmlPath, cssAssets[basePath]);
|
|
127
|
+
for (const filePath of inlined.jsFiles) jsFiles.add(filePath);
|
|
128
|
+
for (const filePath of inlined.cssFiles) cssFiles.add(filePath);
|
|
129
|
+
await Bun.write(htmlPath, inlined.html);
|
|
130
|
+
}
|
|
131
|
+
for (const filePath of jsFiles) await unlink(filePath).catch(() => undefined);
|
|
132
|
+
for (const filePath of cssFiles) await unlink(filePath).catch(() => undefined);
|
|
133
|
+
const remainingJs = await this.#listOutputFiles((filePath) => filePath.endsWith(".js"));
|
|
134
|
+
const remainingCss = await this.#listOutputFiles((filePath) => filePath.endsWith(".css"));
|
|
135
|
+
const remainingAssets = [...remainingJs, ...remainingCss];
|
|
136
|
+
if (remainingAssets.length > 0) {
|
|
137
|
+
throw new Error(`[csr-build] expected single-file HTML, but CSR assets remain:\n${remainingAssets.join("\n")}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async #inlineHtmlAssets(
|
|
142
|
+
html: string,
|
|
143
|
+
htmlPath: string,
|
|
144
|
+
cssAsset?: { cssUrl: string; cssRelPath: string },
|
|
145
|
+
): Promise<{ html: string; jsFiles: string[]; cssFiles: string[] }> {
|
|
146
|
+
const jsFiles: string[] = [];
|
|
147
|
+
const cssFiles = CsrArtifactBuilder.collectStylesheetHrefs(html).map((href) =>
|
|
148
|
+
CsrArtifactBuilder.resolveHtmlAssetPath(htmlPath, href),
|
|
149
|
+
);
|
|
150
|
+
let next = CsrArtifactBuilder.stripBundledStylesheetLinks(html);
|
|
151
|
+
next = await CsrArtifactBuilder.replaceModuleScriptSrc(next, async (src) => {
|
|
152
|
+
const jsPath = CsrArtifactBuilder.resolveHtmlAssetPath(htmlPath, src);
|
|
153
|
+
jsFiles.push(jsPath);
|
|
154
|
+
return await Bun.file(jsPath).text();
|
|
155
|
+
});
|
|
156
|
+
if (cssAsset) {
|
|
157
|
+
const cssPath = path.join(
|
|
158
|
+
this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath,
|
|
159
|
+
".akan/artifact",
|
|
160
|
+
cssAsset.cssRelPath,
|
|
161
|
+
);
|
|
162
|
+
const css = await Bun.file(cssPath).text();
|
|
163
|
+
const style = CsrArtifactBuilder.createInlineStyle(css);
|
|
164
|
+
if (!next.includes(style)) next = CsrArtifactBuilder.injectBeforeHeadEnd(next, style);
|
|
165
|
+
}
|
|
166
|
+
return { html: next, jsFiles, cssFiles };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async #htmlOutputPaths(): Promise<string[]> {
|
|
170
|
+
return await this.#listOutputFiles((filePath) => filePath.endsWith(".html"));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async #listOutputFiles(predicate: (filePath: string) => boolean): Promise<string[]> {
|
|
174
|
+
const glob = new Bun.Glob("**/*");
|
|
175
|
+
const files: string[] = [];
|
|
176
|
+
for await (const filePath of glob.scan({ cwd: this.#outputDir, absolute: true })) {
|
|
177
|
+
if (predicate(filePath)) files.push(filePath);
|
|
178
|
+
}
|
|
179
|
+
return files.sort();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
static injectBeforeHeadEnd(html: string, snippet: string): string {
|
|
183
|
+
const matches = [...html.matchAll(/<\/head\s*>/gi)];
|
|
184
|
+
const bodyStart = html.search(/<body(?:\s|>)/i);
|
|
185
|
+
const headEnd = matches
|
|
186
|
+
.filter((match) => match.index !== undefined && (bodyStart === -1 || match.index < bodyStart))
|
|
187
|
+
.at(-1);
|
|
188
|
+
if (!headEnd || headEnd.index === undefined) return `${snippet}\n${html}`;
|
|
189
|
+
return `${html.slice(0, headEnd.index)}${snippet}\n${html.slice(headEnd.index)}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
static stripBundledStylesheetLinks(html: string): string {
|
|
193
|
+
return html.replace(/<link\b(?=[^>]*\brel=["']stylesheet["'])[^>]*>\s*/gi, "");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
static collectStylesheetHrefs(html: string): string[] {
|
|
197
|
+
const linkRe = /<link\b(?=[^>]*\brel=["']stylesheet["'])(?=[^>]*\bhref=["']([^"']+)["'])[^>]*>/gi;
|
|
198
|
+
return [...html.matchAll(linkRe)].map((match) => match[1]).filter((href): href is string => !!href);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
static createInlineStyle(css: string): string {
|
|
202
|
+
return `<style data-akan-css="active">\n${css.replace(/<\/style/gi, "<\\/style")}\n</style>`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
static async replaceModuleScriptSrc(
|
|
206
|
+
html: string,
|
|
207
|
+
loadScript: (src: string) => Promise<string> | string,
|
|
208
|
+
): Promise<string> {
|
|
209
|
+
const scriptRe = /<script\b(?=[^>]*\btype=["']module["'])(?=[^>]*\bsrc=["']([^"']+)["'])[^>]*>\s*<\/script>/gi;
|
|
210
|
+
let result = "";
|
|
211
|
+
let lastIndex = 0;
|
|
212
|
+
let matched = false;
|
|
213
|
+
for (const match of html.matchAll(scriptRe)) {
|
|
214
|
+
const full = match[0];
|
|
215
|
+
const src = match[1];
|
|
216
|
+
if (match.index === undefined || !src) continue;
|
|
217
|
+
matched = true;
|
|
218
|
+
result += html.slice(lastIndex, match.index);
|
|
219
|
+
result += `<script type="module">\n${CsrArtifactBuilder.escapeInlineScript(await loadScript(src))}\n</script>`;
|
|
220
|
+
lastIndex = match.index + full.length;
|
|
221
|
+
}
|
|
222
|
+
if (!matched) return html;
|
|
223
|
+
return result + html.slice(lastIndex);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
static escapeInlineScript(source: string): string {
|
|
227
|
+
return source.replace(/<\/script/gi, "<\\/script");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
static resolveHtmlAssetPath(htmlPath: string, src: string): string {
|
|
231
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith("//")) {
|
|
232
|
+
throw new Error(`[csr-build] cannot inline external script: ${src}`);
|
|
233
|
+
}
|
|
234
|
+
const normalized = src.startsWith("/") ? src.slice(1) : src;
|
|
235
|
+
return path.resolve(path.dirname(htmlPath), normalized);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { Logger } from "akanjs/common";
|
|
3
|
+
import { compile } from "tailwindcss";
|
|
4
|
+
import type { App } from "../commandDecorators";
|
|
5
|
+
import { BarrelAnalyzer } from "../transforms/barrelAnalyzer";
|
|
6
|
+
import { createTsconfigPackageResolver, rewriteBarrelImports } from "../transforms/barrelImportsPlugin";
|
|
7
|
+
import { CssImportResolver } from "./cssImportResolver";
|
|
8
|
+
|
|
9
|
+
const SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"] as const;
|
|
10
|
+
const NON_SOURCE_EXT_RE = /\.(json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav)$/i;
|
|
11
|
+
const NODE_MODULES_RE = /[\\/]node_modules[\\/]/;
|
|
12
|
+
const AKANJS_NODE_MODULE_RE = /[\\/]node_modules[\\/]akanjs[\\/]/;
|
|
13
|
+
|
|
14
|
+
interface CssDiscovery {
|
|
15
|
+
cssPaths: string[];
|
|
16
|
+
sourcePaths: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class CssCompiler {
|
|
20
|
+
#logger = new Logger("CssCompiler");
|
|
21
|
+
#transpiler = new Bun.Transpiler({ loader: "tsx" });
|
|
22
|
+
#app: App;
|
|
23
|
+
#cssImportResolver: CssImportResolver | null = null;
|
|
24
|
+
constructor(app: App) {
|
|
25
|
+
this.#app = app;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#cssText: string | null = null;
|
|
29
|
+
#cssTextByBasePath: Record<string, string> | null = null;
|
|
30
|
+
async getCss({ refresh }: { refresh?: boolean } = {}) {
|
|
31
|
+
if (this.#cssText !== null && !refresh) return this.#cssText;
|
|
32
|
+
const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh });
|
|
33
|
+
this.#cssText = await this.compileCss(cssPaths, sourcePaths);
|
|
34
|
+
return this.#cssText;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async getCssByBasePath({ refresh }: { refresh?: boolean } = {}): Promise<Record<string, string>> {
|
|
38
|
+
if (this.#cssTextByBasePath !== null && !refresh) return this.#cssTextByBasePath;
|
|
39
|
+
const akanConfig = await this.#app.getConfig({ refresh });
|
|
40
|
+
const pageKeys = await this.#app.getPageKeys({ refresh });
|
|
41
|
+
const basePaths = [...akanConfig.basePaths];
|
|
42
|
+
const rootPageKeys = pageKeys.filter((pageKey) => getPageKeyBasePath(pageKey, basePaths) === null);
|
|
43
|
+
const cssEntries = await Promise.all([
|
|
44
|
+
(async () => {
|
|
45
|
+
if (rootPageKeys.length === 0) return ["", ""] as const;
|
|
46
|
+
const started = Date.now();
|
|
47
|
+
const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: rootPageKeys });
|
|
48
|
+
const css = await this.compileCss(cssPaths, sourcePaths);
|
|
49
|
+
this.#logger.verbose(
|
|
50
|
+
`css base=root paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`,
|
|
51
|
+
);
|
|
52
|
+
return ["", css] as const;
|
|
53
|
+
})(),
|
|
54
|
+
...basePaths.map(async (basePath) => {
|
|
55
|
+
const basePathPageKeys = pageKeys.filter((pageKey) => getPageKeyBasePath(pageKey, basePaths) === basePath);
|
|
56
|
+
if (basePathPageKeys.length === 0) return [basePath, ""] as const;
|
|
57
|
+
const started = Date.now();
|
|
58
|
+
const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: basePathPageKeys });
|
|
59
|
+
const css = await this.compileCss(cssPaths, sourcePaths);
|
|
60
|
+
this.#logger.verbose(
|
|
61
|
+
`css base=${basePath} paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`,
|
|
62
|
+
);
|
|
63
|
+
return [basePath, css] as const;
|
|
64
|
+
}),
|
|
65
|
+
]);
|
|
66
|
+
this.#cssTextByBasePath = Object.fromEntries(cssEntries);
|
|
67
|
+
return this.#cssTextByBasePath;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async discoverCss({ refresh }: { refresh?: boolean } = {}): Promise<string[]> {
|
|
71
|
+
const { cssPaths } = await this.discoverCssAndSources({ refresh });
|
|
72
|
+
return cssPaths;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async discoverCssAndSources({
|
|
76
|
+
refresh,
|
|
77
|
+
pageKeys,
|
|
78
|
+
}: {
|
|
79
|
+
refresh?: boolean;
|
|
80
|
+
pageKeys?: string[];
|
|
81
|
+
} = {}): Promise<CssDiscovery> {
|
|
82
|
+
pageKeys ??= await this.#app.getPageKeys({ refresh });
|
|
83
|
+
const seeds = pageKeys.map((key) => path.resolve(this.#app.cwdPath, "page", key));
|
|
84
|
+
const cssFiles = new Set<string>();
|
|
85
|
+
const sourceFiles = new Set<string>();
|
|
86
|
+
const queue = [...seeds];
|
|
87
|
+
const resolvePackage = await createTsconfigPackageResolver(this.#app);
|
|
88
|
+
const analyzer = new BarrelAnalyzer({ resolvePackage });
|
|
89
|
+
const akanConfig = await this.#app.getConfig({ refresh });
|
|
90
|
+
|
|
91
|
+
while (queue.length > 0) {
|
|
92
|
+
const filePath = queue.shift();
|
|
93
|
+
if (!filePath || sourceFiles.has(filePath) || isIgnoredNodeModuleSource(filePath)) continue;
|
|
94
|
+
sourceFiles.add(filePath);
|
|
95
|
+
|
|
96
|
+
let content: string;
|
|
97
|
+
try {
|
|
98
|
+
content = await Bun.file(filePath).text();
|
|
99
|
+
} catch {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let source = content;
|
|
104
|
+
if (akanConfig.barrelImports.length > 0) {
|
|
105
|
+
try {
|
|
106
|
+
const rewritten = await rewriteBarrelImports(content, akanConfig.barrelImports, analyzer);
|
|
107
|
+
if (rewritten !== null) source = rewritten;
|
|
108
|
+
} catch {
|
|
109
|
+
// best-effort: unresolved barrel rewrites should not stop CSS discovery
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let imports: Bun.Import[];
|
|
114
|
+
try {
|
|
115
|
+
imports = this.#transpiler.scanImports(source);
|
|
116
|
+
} catch {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const importerDir = path.dirname(filePath);
|
|
121
|
+
for (const imp of imports) {
|
|
122
|
+
const spec = imp.path;
|
|
123
|
+
if (!spec) continue;
|
|
124
|
+
if (spec.endsWith(".css")) {
|
|
125
|
+
const cssPath = await this.#resolveCssImport(spec, importerDir);
|
|
126
|
+
cssFiles.add(cssPath);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (NON_SOURCE_EXT_RE.test(spec)) continue;
|
|
130
|
+
const resolved = await this.#resolveSourceImport(spec, importerDir, resolvePackage);
|
|
131
|
+
if (!resolved || sourceFiles.has(resolved) || isIgnoredNodeModuleSource(resolved)) continue;
|
|
132
|
+
queue.push(resolved);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { cssPaths: [...cssFiles], sourcePaths: [...sourceFiles] };
|
|
137
|
+
}
|
|
138
|
+
async compileCss(cssPaths: string[], sourcePaths: string[]): Promise<string> {
|
|
139
|
+
if (cssPaths.length === 0) return "";
|
|
140
|
+
|
|
141
|
+
const compileStarted = Date.now();
|
|
142
|
+
const compilers = await Promise.all(
|
|
143
|
+
cssPaths.map(async (cssPath) => {
|
|
144
|
+
const css = await Bun.file(cssPath).text();
|
|
145
|
+
const base = path.dirname(cssPath);
|
|
146
|
+
const compiler = await compile(css, {
|
|
147
|
+
base,
|
|
148
|
+
loadStylesheet: (id, fromBase) => this.#loadStylesheet(id, fromBase),
|
|
149
|
+
loadModule: (id, fromBase) => this.#loadModule(id, fromBase),
|
|
150
|
+
});
|
|
151
|
+
return { cssPath, compiler };
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const sourceDirs = new Set<string>();
|
|
156
|
+
for (const entry of compilers) {
|
|
157
|
+
if (!entry) continue;
|
|
158
|
+
for (const s of entry.compiler.sources as { base: string }[]) sourceDirs.add(s.base);
|
|
159
|
+
}
|
|
160
|
+
const scanStarted = Date.now();
|
|
161
|
+
const candidates = await this.#scanCandidates(sourcePaths, [...sourceDirs]);
|
|
162
|
+
this.#logger.verbose(
|
|
163
|
+
`css candidates scanned count=${candidates.length} sources=${sourcePaths.length} dirs=${sourceDirs.size} in ${Date.now() - scanStarted}ms`,
|
|
164
|
+
);
|
|
165
|
+
const parts: string[] = [];
|
|
166
|
+
for (const entry of compilers) {
|
|
167
|
+
if (!entry) continue;
|
|
168
|
+
parts.push(entry.compiler.build(candidates));
|
|
169
|
+
}
|
|
170
|
+
this.#logger.verbose(
|
|
171
|
+
`css compiled paths=${cssPaths.length} candidates=${candidates.length} in ${Date.now() - compileStarted}ms`,
|
|
172
|
+
);
|
|
173
|
+
return parts.join("\n");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async #loadStylesheet(id: string, fromBase: string) {
|
|
177
|
+
const p = await this.#resolveCssImport(id, fromBase);
|
|
178
|
+
const content = await Bun.file(p).text();
|
|
179
|
+
return { path: p, base: path.dirname(p), content };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async #resolveCssImport(id: string, fromBase: string): Promise<string> {
|
|
183
|
+
if (id.startsWith(".") || id.startsWith("/")) return path.resolve(fromBase, id);
|
|
184
|
+
const resolver = await this.#getCssImportResolver();
|
|
185
|
+
const resolved = await resolver.resolve(id, fromBase);
|
|
186
|
+
if (resolved) return resolved;
|
|
187
|
+
throw new Error(`[css] failed to resolve stylesheet import "${id}" from ${fromBase}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async #getCssImportResolver() {
|
|
191
|
+
if (this.#cssImportResolver) return this.#cssImportResolver;
|
|
192
|
+
this.#cssImportResolver = await CssImportResolver.create(this.#app);
|
|
193
|
+
return this.#cssImportResolver;
|
|
194
|
+
}
|
|
195
|
+
async #loadModule(id: string, fromBase: string) {
|
|
196
|
+
const p = require.resolve(id, { paths: [fromBase] });
|
|
197
|
+
const mod = await import(p);
|
|
198
|
+
return { path: p, base: path.dirname(p), module: mod.default ?? mod };
|
|
199
|
+
}
|
|
200
|
+
async #resolveSourceImport(
|
|
201
|
+
id: string,
|
|
202
|
+
fromBase: string,
|
|
203
|
+
resolvePackage: Awaited<ReturnType<typeof createTsconfigPackageResolver>>,
|
|
204
|
+
): Promise<string | null> {
|
|
205
|
+
if (id.startsWith(".") || id.startsWith("/")) {
|
|
206
|
+
const abs = id.startsWith("/") ? id : path.resolve(fromBase, id);
|
|
207
|
+
return resolveSourceFileCandidate(abs);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const pkg = await resolvePackage(id);
|
|
211
|
+
if (pkg) return pkg.entryFile;
|
|
212
|
+
|
|
213
|
+
for (const resolve of [() => resolveSourceWithBun(id, fromBase), () => resolveSourceWithRequire(id, fromBase)]) {
|
|
214
|
+
const resolved = await resolve();
|
|
215
|
+
if (resolved) return resolved;
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
async #scanCandidates(sourcePaths: string[], dirs: string[]): Promise<string[]> {
|
|
220
|
+
const CANDIDATE_RE = /-?[\w@][\w:/.-]*(?:\[[^\]]+\][\w:/.-]*)*/g;
|
|
221
|
+
const candidates = new Set<string>();
|
|
222
|
+
const glob = new Bun.Glob("**/*.{tsx,ts,jsx,js,html}");
|
|
223
|
+
const files = new Set<string>(sourcePaths);
|
|
224
|
+
await Promise.all(
|
|
225
|
+
dirs.map(async (dir) => {
|
|
226
|
+
for await (const file of glob.scan({ cwd: dir, absolute: true })) {
|
|
227
|
+
if (isIgnoredNodeModuleSource(file)) continue;
|
|
228
|
+
files.add(file);
|
|
229
|
+
}
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
await Promise.all(
|
|
233
|
+
[...files].map(async (file) => {
|
|
234
|
+
const content = await Bun.file(file).text();
|
|
235
|
+
for (const m of content.matchAll(CANDIDATE_RE)) candidates.add(m[0]);
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
return [...candidates];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function resolveSourceFileCandidate(absPathNoExt: string): Promise<string | null> {
|
|
243
|
+
if (await Bun.file(absPathNoExt).exists()) return isSourceFile(absPathNoExt) ? absPathNoExt : null;
|
|
244
|
+
for (const ext of SOURCE_EXTS) {
|
|
245
|
+
const filePath = `${absPathNoExt}${ext}`;
|
|
246
|
+
if (await Bun.file(filePath).exists()) return filePath;
|
|
247
|
+
}
|
|
248
|
+
for (const ext of SOURCE_EXTS) {
|
|
249
|
+
const filePath = path.join(absPathNoExt, `index${ext}`);
|
|
250
|
+
if (await Bun.file(filePath).exists()) return filePath;
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function resolveSourceWithBun(id: string, fromBase: string): string | null {
|
|
256
|
+
try {
|
|
257
|
+
const resolved = Bun.resolveSync(id, fromBase);
|
|
258
|
+
return isSourceFile(resolved) ? resolved : null;
|
|
259
|
+
} catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function resolveSourceWithRequire(id: string, fromBase: string): string | null {
|
|
265
|
+
try {
|
|
266
|
+
const resolved = require.resolve(id, { paths: [fromBase] });
|
|
267
|
+
return isSourceFile(resolved) ? resolved : null;
|
|
268
|
+
} catch {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function isSourceFile(filePath: string) {
|
|
274
|
+
return SOURCE_EXTS.includes(path.extname(filePath) as (typeof SOURCE_EXTS)[number]);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function isIgnoredNodeModuleSource(filePath: string): boolean {
|
|
278
|
+
return NODE_MODULES_RE.test(filePath) && !AKANJS_NODE_MODULE_RE.test(filePath);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function getPageKeyBasePath(pageKey: string, basePaths: string[]): string | null {
|
|
282
|
+
const normalized = pageKey.split(path.sep).join("/").replace(/^\.\//, "");
|
|
283
|
+
const segments = normalized.split("/");
|
|
284
|
+
const firstPublicSegment = segments.find((segment) => segment !== "[lang]" && !/^\(.+\)$/.test(segment));
|
|
285
|
+
return firstPublicSegment && basePaths.includes(firstPublicSegment) ? firstPublicSegment : null;
|
|
286
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { App } from "../commandDecorators";
|
|
3
|
+
|
|
4
|
+
const CSS_IMPORT_EXTS = ["", ".css", "/styles.css", "/index.css"] as const;
|
|
5
|
+
|
|
6
|
+
export class CssImportResolver {
|
|
7
|
+
#workspaceRoot: string;
|
|
8
|
+
#paths: Record<string, string[]>;
|
|
9
|
+
#wildcardEntries: { prefix: string; replacements: string[] }[];
|
|
10
|
+
|
|
11
|
+
constructor(workspaceRoot: string, paths: Record<string, string[]> = {}) {
|
|
12
|
+
this.#workspaceRoot = workspaceRoot;
|
|
13
|
+
this.#paths = paths;
|
|
14
|
+
this.#wildcardEntries = Object.entries(paths)
|
|
15
|
+
.filter(([key]) => key.endsWith("/*"))
|
|
16
|
+
.map(([key, replacements]) => ({ prefix: key.slice(0, -1), replacements }))
|
|
17
|
+
.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static async create(app: App): Promise<CssImportResolver> {
|
|
21
|
+
const tsconfig = await app.getTsConfig();
|
|
22
|
+
return new CssImportResolver(app.workspace.workspaceRoot, tsconfig.compilerOptions.paths ?? {});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async resolve(id: string, fromBase: string): Promise<string | null> {
|
|
26
|
+
for (const resolve of [
|
|
27
|
+
() => this.#resolveWithBun(id, fromBase),
|
|
28
|
+
() => this.#resolveWithRequire(id, fromBase),
|
|
29
|
+
() => this.#resolveWithTsconfig(id),
|
|
30
|
+
() => this.#resolvePackageStyle(id, fromBase),
|
|
31
|
+
]) {
|
|
32
|
+
const resolved = await resolve();
|
|
33
|
+
if (resolved) return resolved;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#resolveWithBun(id: string, fromBase: string): string | null {
|
|
39
|
+
try {
|
|
40
|
+
const resolved = Bun.resolveSync(id, fromBase);
|
|
41
|
+
return CssImportResolver.isCssFile(resolved) ? resolved : null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#resolveWithRequire(id: string, fromBase: string): string | null {
|
|
48
|
+
try {
|
|
49
|
+
const resolved = require.resolve(id, { paths: [fromBase] });
|
|
50
|
+
return CssImportResolver.isCssFile(resolved) ? resolved : null;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async #resolveWithTsconfig(id: string): Promise<string | null> {
|
|
57
|
+
const exact = this.#paths[id];
|
|
58
|
+
if (exact) {
|
|
59
|
+
for (const repl of exact) {
|
|
60
|
+
const resolved = await this.#firstExisting(path.resolve(this.#workspaceRoot, repl));
|
|
61
|
+
if (resolved) return resolved;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const { prefix, replacements } of this.#wildcardEntries) {
|
|
66
|
+
if (!id.startsWith(prefix)) continue;
|
|
67
|
+
const suffix = id.slice(prefix.length);
|
|
68
|
+
for (const repl of replacements) {
|
|
69
|
+
const replPath = repl.endsWith("/*") ? repl.slice(0, -1) : repl;
|
|
70
|
+
const resolved = await this.#firstExisting(path.resolve(this.#workspaceRoot, replPath + suffix));
|
|
71
|
+
if (resolved) return resolved;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async #resolvePackageStyle(id: string, fromBase: string): Promise<string | null> {
|
|
78
|
+
const pkgName = CssImportResolver.getPackageName(id);
|
|
79
|
+
if (!pkgName) return null;
|
|
80
|
+
try {
|
|
81
|
+
const pkgPath = require.resolve(`${pkgName}/package.json`, { paths: [fromBase] });
|
|
82
|
+
const pkgDir = path.dirname(pkgPath);
|
|
83
|
+
const pkg = await Bun.file(pkgPath).json();
|
|
84
|
+
const subpath = id === pkgName ? "." : `.${id.slice(pkgName.length)}`;
|
|
85
|
+
const exportValue = pkg.exports?.[subpath];
|
|
86
|
+
const styleEntry =
|
|
87
|
+
(typeof exportValue === "string"
|
|
88
|
+
? exportValue
|
|
89
|
+
: exportValue?.style || exportValue?.import || exportValue?.default) ||
|
|
90
|
+
pkg.exports?.["."]?.style ||
|
|
91
|
+
pkg.style ||
|
|
92
|
+
"index.css";
|
|
93
|
+
return await this.#firstExisting(path.resolve(pkgDir, styleEntry));
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async #firstExisting(basePath: string): Promise<string | null> {
|
|
100
|
+
for (const suffix of CSS_IMPORT_EXTS) {
|
|
101
|
+
const candidate = `${basePath}${suffix}`;
|
|
102
|
+
if (await Bun.file(candidate).exists()) return candidate;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static getPackageName(id: string): string | null {
|
|
108
|
+
const parts = id.split("/");
|
|
109
|
+
if (id.startsWith("@")) return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
|
|
110
|
+
return parts[0] ?? null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static isCssFile(filePath: string): boolean {
|
|
114
|
+
return path.extname(filePath) === ".css";
|
|
115
|
+
}
|
|
116
|
+
}
|