@akanjs/devkit 3.0.0-alpha.11 → 3.0.0-alpha.110
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/CHANGELOG.md +15 -0
- package/DEV_RUNTIME_KNOBS.md +34 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/agentsIndex.test.ts +10 -0
- package/agentsIndex.ts +21 -1
- package/aiEditor.ts +1 -1
- package/akanApp/BackendImportGraph.test.ts +120 -0
- package/akanApp/BackendImportGraph.ts +167 -0
- package/akanApp/akanApp.host.test.ts +16 -678
- package/akanApp/akanApp.host.ts +113 -578
- package/akanApp/devHostPolicy.test.ts +542 -0
- package/akanApp/devHostPolicy.ts +442 -0
- package/akanApp/index.ts +2 -0
- package/akanConfig/akanConfig.test.ts +182 -14
- package/akanConfig/akanConfig.ts +143 -47
- package/akanConfig/types.ts +9 -0
- package/akanContext.ts +30 -14
- package/akanMcpContract.ts +25 -4
- package/appSelectionMemory.ts +28 -0
- package/applicationBuildRunner.test.ts +37 -2
- package/applicationBuildRunner.ts +52 -23
- package/artifact/implicitRootLayout.test.ts +70 -1
- package/artifact/implicitRootLayout.ts +41 -14
- package/artifact/routeSeedIndex.test.ts +1 -0
- package/biome.base.json +28 -1
- package/biomeBase.ts +1 -1
- package/capacitorApp.ts +10 -0
- package/cloud/globalConfig.ts +9 -2
- package/codegenLock.test.ts +137 -0
- package/codegenLock.ts +136 -0
- package/commandDecorators/argMeta.ts +9 -1
- package/commandDecorators/command.ts +94 -7
- package/commandDecorators/commandBuilder.ts +20 -5
- package/commandDecorators/commandDecorators.test.ts +115 -3
- package/commandDecorators/helpFormatter.ts +4 -0
- package/dependencyScanner.test.ts +99 -0
- package/dependencyScanner.ts +27 -24
- package/devkitUtils.test.ts +0 -104
- package/executors.test.ts +135 -7
- package/executors.ts +115 -25
- package/fileEditor.ts +19 -19
- package/fileSys.ts +19 -1
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/autoImportSync.test.ts +58 -0
- package/frontendBuild/autoImportSync.ts +7 -2
- package/frontendBuild/buildRouteClient.test.ts +47 -24
- package/frontendBuild/clientBuildTypes.ts +4 -0
- package/frontendBuild/clientEntriesBundler.ts +4 -1
- package/frontendBuild/clientEntryDiscovery.ts +2 -2
- package/frontendBuild/csrArtifactBuilder.ts +116 -84
- package/frontendBuild/cssCompiler.ts +63 -10
- package/frontendBuild/devGeneratedIndexSync.ts +3 -2
- package/frontendBuild/fontOptimizer.test.ts +75 -2
- package/frontendBuild/fontOptimizer.ts +98 -27
- package/frontendBuild/fontPruner.test.ts +220 -0
- package/frontendBuild/fontPruner.ts +206 -0
- package/frontendBuild/frontendBuild.test.ts +124 -10
- package/frontendBuild/hmrWatcher.ts +1 -1
- package/frontendBuild/index.ts +1 -1
- package/frontendBuild/pagesBundleBuilder.ts +3 -3
- package/frontendBuild/pagesEntrySourceGenerator.ts +11 -88
- package/frontendBuild/routeClientBuilder.ts +12 -5
- package/frontendBuild/sourceMtimeIndex.ts +1 -1
- package/frontendBuild/ssrBaseArtifactBuilder.ts +21 -4
- package/frontendBuild/styleContract.ts +14 -20
- package/frontendBuild/themeValidator.ts +22 -17
- package/frontendBuild/vendorSpecifiers.ts +1 -0
- package/frontendBuild/watchRootResolver.test.ts +129 -0
- package/frontendBuild/watchRootResolver.ts +66 -1
- package/incrementalBuilder/devWatchBatch.test.ts +18 -20
- package/incrementalBuilder/devWatchBatch.ts +6 -2
- package/incrementalBuilder/incrementalBuilder.host.ts +51 -7
- package/incrementalBuilder/incrementalBuilder.proc.ts +17 -6
- package/index.ts +0 -7
- package/integration/devStabilityHarness.ts +13 -19
- package/libSource.test.ts +112 -0
- package/libSource.ts +126 -0
- package/lint/__fixtures__/README.md +40 -0
- package/lint/__fixtures__/no-arbitrary-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-arbitrary-color/good.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/bad.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/good.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/fixture.json +1 -0
- package/lint/__fixtures__/no-bang-comment-in-client/good.tsx +3 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +7 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/good.tsx +5 -0
- package/lint/__fixtures__/no-deep-internal-import/bad.tsx +3 -0
- package/lint/__fixtures__/no-deep-internal-import/fixture.json +1 -0
- package/lint/__fixtures__/no-deep-internal-import/good.tsx +3 -0
- package/lint/__fixtures__/no-deprecated-log-level/bad.tsx +4 -0
- package/lint/__fixtures__/no-deprecated-log-level/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-functions/bad.tsx +2 -0
- package/lint/__fixtures__/no-import-client-functions/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-in-server/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-client-in-server/good.tsx +5 -0
- package/lint/__fixtures__/no-import-external-library/bad.tsx +4 -0
- package/lint/__fixtures__/no-import-external-library/good.tsx +5 -0
- package/lint/__fixtures__/no-import-server-in-client/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-server-in-client/good.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/fixture.json +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/good.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/good.tsx +5 -0
- package/lint/__fixtures__/no-inline-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-inline-color/good.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/bad.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/good.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/bad.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/bad.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/good.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/bad.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/good.tsx +8 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/bad.ts +10 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/fixture.json +1 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/good.ts +6 -0
- package/lint/__fixtures__/no-return-in-store-action/bad.tsx +2 -0
- package/lint/__fixtures__/no-return-in-store-action/good.tsx +6 -0
- package/lint/__fixtures__/no-throw-raw-error/bad.tsx +4 -0
- package/lint/__fixtures__/no-throw-raw-error/good.tsx +4 -0
- package/lint/__fixtures__/no-unpublished-form-setter/bad.tsx +3 -0
- package/lint/__fixtures__/no-unpublished-form-setter/good.tsx +5 -0
- package/lint/__fixtures__/no-use-client-in-server/bad.tsx +3 -0
- package/lint/__fixtures__/no-use-client-in-server/good.tsx +2 -0
- package/lint/__fixtures__/non-scalar-props-restricted/bad.tsx +5 -0
- package/lint/__fixtures__/non-scalar-props-restricted/good.tsx +4 -0
- package/lint/gritRules.test.ts +178 -0
- package/lint/no-arbitrary-color.grit +1 -1
- package/lint/no-async-component-in-ui.grit +35 -0
- package/lint/no-bang-comment-in-client.grit +23 -10
- package/lint/no-daisyui-legacy-class.grit +26 -9
- package/lint/no-deprecated-log-level.grit +17 -0
- package/lint/no-init-fetch-in-client.grit +47 -0
- package/lint/no-inline-color.grit +10 -8
- package/lint/no-interpolated-arbitrary-class.grit +3 -3
- package/lint/no-model-type-in-util-zone.grit +58 -0
- package/lint/no-raw-palette-class.grit +3 -3
- package/lint/no-unpublished-form-setter.grit +41 -0
- package/lint/non-scalar-props-restricted.grit +16 -7
- package/linter.test.ts +80 -0
- package/linter.ts +82 -13
- package/package.json +5 -8
- package/prompter.ts +9 -4
- package/qualityScanner.test.ts +80 -142
- package/qualityScanner.ts +89 -31
- package/recipeScanner.ts +4 -1
- package/routeSourceValidator.test.ts +63 -0
- package/routeSourceValidator.ts +123 -16
- package/scanInfo.ts +32 -26
- package/semver.test.ts +26 -0
- package/semver.ts +31 -0
- package/slicePlanner.test.ts +182 -0
- package/slicePlanner.ts +235 -0
- package/ssrScanner.test.ts +301 -0
- package/subspace.test.ts +418 -0
- package/subspace.ts +723 -0
- package/subspaceConfig.ts +76 -0
- package/transforms/asyncDefaultExportDetector.ts +105 -0
- package/transforms/barrelImportsPlugin.ts +1 -1
- package/transforms/externalizeFrameworkPlugin.ts +0 -1
- package/transforms/transforms.test.ts +5 -5
- package/transforms/tsconfigPackageResolver.test.ts +230 -0
- package/tsconfig.json +6 -2
- package/typeChecker.ts +1 -1
- package/types.ts +1 -0
- package/uploadRelease.ts +2 -2
- package/workflow/executor.test.ts +146 -0
- package/workspaceLayout.test.ts +56 -4
- package/workspaceLayout.ts +48 -2
- package/builder.ts +0 -164
- package/extractDeps.ts +0 -86
- package/frontendBuild/styleGuard.test.ts +0 -165
- package/frontendBuild/styleGuard.ts +0 -322
- package/getCredentials.ts +0 -19
- package/getModelFileData.ts +0 -62
- package/mcpScanner.ts +0 -217
- package/src/capacitorApp.ts +0 -282
- package/storeScanner.ts +0 -173
- package/streamAi.ts +0 -45
- package/ui/MultiScrollList.tsx +0 -242
- package/ui/ScrollList.tsx +0 -107
- package/ui/index.ts +0 -2
- package/useStdoutDimensions.ts +0 -20
|
@@ -82,16 +82,42 @@ describe("route client store bootstrap", () => {
|
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
test("bundles akan fetch into production SSR client chunks", () => {
|
|
85
|
-
expect(RouteClientBuilder.
|
|
85
|
+
expect(RouteClientBuilder.resolveSsrClientBundleOptions("start")).toMatchObject({
|
|
86
86
|
external: expect.arrayContaining(["akanjs/fetch"]),
|
|
87
87
|
externalSubpaths: ["akanjs/fetch"],
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
-
expect(RouteClientBuilder.
|
|
90
|
+
expect(RouteClientBuilder.resolveSsrClientBundleOptions("build")).toEqual({
|
|
91
|
+
target: "bun",
|
|
91
92
|
external: ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "react/jsx-dev-runtime"],
|
|
92
93
|
});
|
|
93
94
|
});
|
|
94
95
|
|
|
96
|
+
test("targets the server so SSR client chunks never resolve a browser export condition", async () => {
|
|
97
|
+
const root = await makeTempRoot();
|
|
98
|
+
const pkgDir = path.join(root, "node_modules/dom-conditioned-pkg");
|
|
99
|
+
await write(
|
|
100
|
+
path.join(pkgDir, "package.json"),
|
|
101
|
+
JSON.stringify({
|
|
102
|
+
name: "dom-conditioned-pkg",
|
|
103
|
+
type: "module",
|
|
104
|
+
exports: { ".": { browser: "./index.dom.js", default: "./index.js" } },
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
await write(path.join(pkgDir, "index.dom.js"), 'export const element = document.createElement("i");\n');
|
|
108
|
+
await write(path.join(pkgDir, "index.js"), "export const element = null;\n");
|
|
109
|
+
const entry = path.join(root, "entry.ts");
|
|
110
|
+
await write(entry, 'export { element } from "dom-conditioned-pkg";\n');
|
|
111
|
+
|
|
112
|
+
for (const command of ["start", "build"] as const) {
|
|
113
|
+
const { target } = RouteClientBuilder.resolveSsrClientBundleOptions(command);
|
|
114
|
+
const built = await Bun.build({ entrypoints: [entry], target, format: "esm" });
|
|
115
|
+
|
|
116
|
+
expect(built.success).toBe(true);
|
|
117
|
+
expect(await built.outputs[0].text()).not.toContain("document.createElement");
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
95
121
|
test("rewrites SSR external imports to runtime aliases", () => {
|
|
96
122
|
const source = [
|
|
97
123
|
'import React, { useState } from "react";',
|
|
@@ -133,28 +159,25 @@ describe("route client store bootstrap", () => {
|
|
|
133
159
|
await write(uiEntry, 'export { ClientPathWrapper } from "./System/Client";\n');
|
|
134
160
|
await write(clientEntry, '"use client";\nexport const ClientPathWrapper = () => null;\n');
|
|
135
161
|
|
|
136
|
-
const discovery = new GraphClientEntryDiscovery(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return null;
|
|
156
|
-
},
|
|
157
|
-
);
|
|
162
|
+
const discovery = new GraphClientEntryDiscovery({ barrelImports: ["akanjs/ui"] }, async (specifier) => {
|
|
163
|
+
if (specifier === "akanjs/ui") {
|
|
164
|
+
return {
|
|
165
|
+
pkgName: "akanjs/ui",
|
|
166
|
+
entryFile: uiEntry,
|
|
167
|
+
pkgDir: path.dirname(uiEntry),
|
|
168
|
+
preserveFilePath: true,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (specifier === "akanjs/ui/System/Client.tsx") {
|
|
172
|
+
return {
|
|
173
|
+
pkgName: "akanjs/ui/System/Client.tsx",
|
|
174
|
+
entryFile: clientEntry,
|
|
175
|
+
pkgDir: path.dirname(clientEntry),
|
|
176
|
+
preserveFilePath: true,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
});
|
|
158
181
|
|
|
159
182
|
expect(await discovery.discover([seed])).toEqual([clientEntry]);
|
|
160
183
|
});
|
|
@@ -55,6 +55,8 @@ export interface BuildClientResult {
|
|
|
55
55
|
rscClientUrl: string;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
export type ClientBundleTarget = "browser" | "bun";
|
|
59
|
+
|
|
58
60
|
export const CLIENT_BUNDLE_NAMING = {
|
|
59
61
|
entry: "[name]-[hash].[ext]",
|
|
60
62
|
chunk: "chunks/[hash].[ext]",
|
|
@@ -79,6 +81,8 @@ export interface BundleClientEntriesOptions {
|
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
export interface BundleClientEntriesInternalOptions extends BundleClientEntriesOptions {
|
|
84
|
+
/** Module-resolution target. `"bun"` for the server-executed `client-ssr` bundle, `"browser"` otherwise. */
|
|
85
|
+
target?: ClientBundleTarget;
|
|
82
86
|
external?: readonly string[];
|
|
83
87
|
externalSubpaths?: readonly string[];
|
|
84
88
|
externalAliases?: Partial<Record<string, string>>;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type BundleClientEntriesInternalOptions,
|
|
8
8
|
type BundleClientEntriesResult,
|
|
9
9
|
CLIENT_BUNDLE_NAMING,
|
|
10
|
+
type ClientBundleTarget,
|
|
10
11
|
type ClientManifest,
|
|
11
12
|
type MetafileOutput,
|
|
12
13
|
type OpaqueEntryAliases,
|
|
@@ -25,6 +26,7 @@ export class ClientEntriesBundler {
|
|
|
25
26
|
#externalSubpaths: readonly string[];
|
|
26
27
|
#externalAliases: Partial<Record<string, string>>;
|
|
27
28
|
#command: "build" | "start";
|
|
29
|
+
#target: ClientBundleTarget;
|
|
28
30
|
#outputSubdir: string;
|
|
29
31
|
#reactFastRefresh: boolean;
|
|
30
32
|
#artifactDir: string;
|
|
@@ -47,6 +49,7 @@ export class ClientEntriesBundler {
|
|
|
47
49
|
this.#externalSubpaths = options.externalSubpaths ?? [];
|
|
48
50
|
this.#externalAliases = options.externalAliases ?? {};
|
|
49
51
|
this.#command = options.command ?? "start";
|
|
52
|
+
this.#target = options.target ?? "browser";
|
|
50
53
|
this.#outputSubdir = options.outputSubdir ?? "client";
|
|
51
54
|
this.#reactFastRefresh = options.reactFastRefresh ?? false;
|
|
52
55
|
this.#artifactDir = `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
|
|
@@ -61,7 +64,7 @@ export class ClientEntriesBundler {
|
|
|
61
64
|
entrypoints: this.#opaqueEntries.entries,
|
|
62
65
|
outdir: this.#outdir,
|
|
63
66
|
splitting: true,
|
|
64
|
-
target:
|
|
67
|
+
target: this.#target,
|
|
65
68
|
format: "esm",
|
|
66
69
|
naming: CLIENT_BUNDLE_NAMING,
|
|
67
70
|
metafile: true,
|
|
@@ -34,7 +34,7 @@ const shouldSkipNodeModule = (absPath: string) => NODE_MODULES_RE.test(absPath)
|
|
|
34
34
|
* the traversal matches the module graph the bundler will actually see.
|
|
35
35
|
*/
|
|
36
36
|
export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
|
|
37
|
-
#akanConfig: AkanConfig
|
|
37
|
+
#akanConfig: Pick<AkanConfig, "barrelImports">;
|
|
38
38
|
#resolvePackage: PackageResolver;
|
|
39
39
|
#analyzer: BarrelAnalyzer;
|
|
40
40
|
#tsTranspiler = new Bun.Transpiler({ loader: "tsx" });
|
|
@@ -57,7 +57,7 @@ export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
|
|
|
57
57
|
#unresolvedPaths = new Set<string>();
|
|
58
58
|
#unresolvedSpecifiers = new Set<string>();
|
|
59
59
|
|
|
60
|
-
constructor(akanConfig: AkanConfig, resolvePackage: PackageResolver) {
|
|
60
|
+
constructor(akanConfig: Pick<AkanConfig, "barrelImports">, resolvePackage: PackageResolver) {
|
|
61
61
|
this.#akanConfig = akanConfig;
|
|
62
62
|
this.#resolvePackage = resolvePackage;
|
|
63
63
|
this.#analyzer = new BarrelAnalyzer({ resolvePackage });
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { mkdir, rm, unlink } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { BaseBuildArtifact } from "akanjs/server";
|
|
4
|
-
import { resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
|
|
4
|
+
import { type PageEntry, resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
|
|
5
5
|
import type { App } from "../commandDecorators";
|
|
6
|
+
import { getPageKeyBasePath } from "./cssCompiler";
|
|
7
|
+
import { PagesBundleBuilder } from "./pagesBundleBuilder";
|
|
6
8
|
import { PagesEntrySourceGenerator } from "./pagesEntrySourceGenerator";
|
|
7
9
|
|
|
8
10
|
export interface BuildCsrArtifactResult {
|
|
9
11
|
outputDir: string;
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
type CssAsset = NonNullable<BaseBuildArtifact["cssAssets"]>[string];
|
|
15
|
+
|
|
12
16
|
export class CsrArtifactBuilder {
|
|
13
17
|
#app: App;
|
|
14
18
|
#command: "build" | "start";
|
|
@@ -29,31 +33,39 @@ export class CsrArtifactBuilder {
|
|
|
29
33
|
|
|
30
34
|
const pageEntries = await resolveSsrPageEntriesForApp(this.#app, pageKeys);
|
|
31
35
|
const akanConfig = await this.#app.getConfig();
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
36
|
+
const cssAssets = await this.#loadCssAssets();
|
|
37
|
+
const basePaths = [...akanConfig.basePaths];
|
|
38
|
+
const htmlBasePaths = basePaths.length > 0 ? basePaths : [""];
|
|
35
39
|
await rm(this.#outputDir, { recursive: true, force: true });
|
|
36
|
-
await mkdir(
|
|
37
|
-
const
|
|
40
|
+
await mkdir(this.#generatedDir, { recursive: true });
|
|
41
|
+
const generatedFiles = Object.fromEntries(
|
|
42
|
+
(
|
|
43
|
+
await Promise.all(
|
|
44
|
+
htmlBasePaths.map(async (basePath) => [
|
|
45
|
+
this.#createHtmlFile(basePath),
|
|
46
|
+
await this.#createEntryFile(
|
|
47
|
+
basePath,
|
|
48
|
+
CsrArtifactBuilder.pageEntriesForBasePath(pageEntries, basePath, basePaths),
|
|
49
|
+
),
|
|
50
|
+
]),
|
|
51
|
+
)
|
|
52
|
+
).flat(),
|
|
53
|
+
);
|
|
38
54
|
|
|
39
55
|
const result = await Bun.build({
|
|
40
56
|
target: "browser",
|
|
41
|
-
entrypoints:
|
|
42
|
-
files:
|
|
43
|
-
|
|
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`,
|
|
57
|
+
entrypoints: htmlBasePaths.map((basePath) => this.#generatedPath(CsrArtifactBuilder.htmlFilename(basePath))),
|
|
58
|
+
files: generatedFiles,
|
|
59
|
+
root: this.#generatedDir,
|
|
51
60
|
outdir: this.#outputDir,
|
|
52
61
|
splitting: false,
|
|
53
62
|
minify: true,
|
|
54
63
|
env: "AKAN_PUBLIC_*",
|
|
55
64
|
define: this.#define(),
|
|
56
65
|
optimizeImports: akanConfig.optimizeImports,
|
|
66
|
+
// The base artifact's compiled sheet is the only stylesheet, as it is for SSR: a raw `.css` reached through
|
|
67
|
+
// the route graph is Tailwind source, and every root layout stylesheet in the graph would land in every HTML.
|
|
68
|
+
plugins: [PagesBundleBuilder.createCssStubPlugin()],
|
|
57
69
|
});
|
|
58
70
|
|
|
59
71
|
if (!result.success) {
|
|
@@ -61,11 +73,32 @@ void bootCsr(pages);
|
|
|
61
73
|
throw new Error(`[csr-build] failed${logs ? `\n${logs}` : ""}`);
|
|
62
74
|
}
|
|
63
75
|
|
|
64
|
-
await this.#inlineCsrArtifacts(
|
|
76
|
+
await this.#inlineCsrArtifacts(cssAssets);
|
|
65
77
|
this.#app.verbose(`[csr-build] output -> ${this.#outputDir}`);
|
|
66
78
|
return { outputDir: this.#outputDir };
|
|
67
79
|
}
|
|
68
80
|
|
|
81
|
+
/** The routes one basePath's HTML boots: its own plus every route outside any basePath, matching `bootCsr`. */
|
|
82
|
+
static pageEntriesForBasePath(pageEntries: PageEntry[], basePath: string, basePaths: string[]): PageEntry[] {
|
|
83
|
+
return pageEntries.filter((entry) => {
|
|
84
|
+
const entryBasePath = getPageKeyBasePath(entry.key, basePaths);
|
|
85
|
+
return entryBasePath === null || entryBasePath === basePath;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static htmlFilename(basePath: string): string {
|
|
90
|
+
return `${basePath || "index"}.html`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static entryFilename(basePath: string): string {
|
|
94
|
+
return `${basePath || "index"}.csr.tsx`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static basePathOfHtml(htmlPath: string): string {
|
|
98
|
+
const name = path.basename(htmlPath, ".html");
|
|
99
|
+
return name === "index" ? "" : name;
|
|
100
|
+
}
|
|
101
|
+
|
|
69
102
|
get #outputDir(): string {
|
|
70
103
|
return path.join(
|
|
71
104
|
this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath,
|
|
@@ -73,6 +106,18 @@ void bootCsr(pages);
|
|
|
73
106
|
);
|
|
74
107
|
}
|
|
75
108
|
|
|
109
|
+
get #generatedDir(): string {
|
|
110
|
+
return path.join(this.#app.cwdPath, ".akan/generated/csr");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get #artifactDir(): string {
|
|
114
|
+
return path.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, ".akan/artifact");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#generatedPath(filename: string): string {
|
|
118
|
+
return path.join(this.#generatedDir, filename);
|
|
119
|
+
}
|
|
120
|
+
|
|
76
121
|
#define(): Record<string, string> {
|
|
77
122
|
const nodeEnv = this.#command === "build" ? "production" : (process.env.NODE_ENV ?? "development");
|
|
78
123
|
return {
|
|
@@ -84,10 +129,19 @@ void bootCsr(pages);
|
|
|
84
129
|
};
|
|
85
130
|
}
|
|
86
131
|
|
|
132
|
+
async #createEntryFile(basePath: string, pageEntries: PageEntry[]): Promise<readonly [string, string]> {
|
|
133
|
+
return [
|
|
134
|
+
this.#generatedPath(CsrArtifactBuilder.entryFilename(basePath)),
|
|
135
|
+
`import { bootCsr } from "akanjs/webkit";
|
|
136
|
+
${await PagesEntrySourceGenerator.generateStatic(pageEntries)}
|
|
137
|
+
void bootCsr(pages);
|
|
138
|
+
`,
|
|
139
|
+
] as const;
|
|
140
|
+
}
|
|
141
|
+
|
|
87
142
|
#createHtmlFile(basePath: string): readonly [string, string] {
|
|
88
|
-
const filename = `${basePath}.html`;
|
|
89
143
|
return [
|
|
90
|
-
|
|
144
|
+
this.#generatedPath(CsrArtifactBuilder.htmlFilename(basePath)),
|
|
91
145
|
`<!doctype html>
|
|
92
146
|
<html lang="${this.#lang}">
|
|
93
147
|
<head>
|
|
@@ -98,41 +152,40 @@ void bootCsr(pages);
|
|
|
98
152
|
</head>
|
|
99
153
|
<body>
|
|
100
154
|
<div id="root"></div>
|
|
101
|
-
<script type="module" src="
|
|
155
|
+
<script type="module" src="./${CsrArtifactBuilder.entryFilename(basePath)}"></script>
|
|
102
156
|
</body>
|
|
103
157
|
</html>
|
|
104
158
|
`,
|
|
105
159
|
] as const;
|
|
106
160
|
}
|
|
107
161
|
|
|
108
|
-
async #
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
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 ?? {} };
|
|
162
|
+
async #loadCssAssets(): Promise<Record<string, CssAsset>> {
|
|
163
|
+
const artifactFile = Bun.file(path.join(this.#artifactDir, "base-artifact.json"));
|
|
164
|
+
if (!(await artifactFile.exists())) return {};
|
|
165
|
+
const artifact = (await artifactFile.json()) as Partial<Pick<BaseBuildArtifact, "cssAssets">>;
|
|
166
|
+
return artifact.cssAssets ?? {};
|
|
117
167
|
}
|
|
118
168
|
|
|
119
|
-
async #inlineCsrArtifacts(cssAssets: Record<string,
|
|
169
|
+
async #inlineCsrArtifacts(cssAssets: Record<string, CssAsset>): Promise<void> {
|
|
120
170
|
const jsFiles = new Set<string>();
|
|
121
|
-
const cssFiles = new Set<string>();
|
|
122
171
|
for (const htmlPath of await this.#htmlOutputPaths()) {
|
|
123
172
|
const htmlFile = Bun.file(htmlPath);
|
|
124
173
|
if (!(await htmlFile.exists())) continue;
|
|
125
|
-
const basePath =
|
|
126
|
-
const
|
|
174
|
+
const basePath = CsrArtifactBuilder.basePathOfHtml(htmlPath);
|
|
175
|
+
const cssAsset = cssAssets[basePath];
|
|
176
|
+
if (!cssAsset) {
|
|
177
|
+
this.#app.logger.warn(
|
|
178
|
+
`[csr-build] base-artifact.json has no compiled stylesheet for ${basePath || "root"}; ${path.basename(htmlPath)} ships without CSS`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const inlined = await this.#inlineHtmlAssets(await htmlFile.text(), htmlPath, cssAsset);
|
|
127
182
|
for (const filePath of inlined.jsFiles) jsFiles.add(filePath);
|
|
128
|
-
for (const filePath of inlined.cssFiles) cssFiles.add(filePath);
|
|
129
183
|
await Bun.write(htmlPath, inlined.html);
|
|
130
184
|
}
|
|
131
185
|
for (const filePath of jsFiles) await unlink(filePath).catch(() => undefined);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const remainingAssets = [...remainingJs, ...remainingCss];
|
|
186
|
+
const remainingAssets = await this.#listOutputFiles(
|
|
187
|
+
(filePath) => filePath.endsWith(".js") || filePath.endsWith(".css"),
|
|
188
|
+
);
|
|
136
189
|
if (remainingAssets.length > 0) {
|
|
137
190
|
throw new Error(`[csr-build] expected single-file HTML, but CSR assets remain:\n${remainingAssets.join("\n")}`);
|
|
138
191
|
}
|
|
@@ -141,44 +194,20 @@ void bootCsr(pages);
|
|
|
141
194
|
async #inlineHtmlAssets(
|
|
142
195
|
html: string,
|
|
143
196
|
htmlPath: string,
|
|
144
|
-
cssAsset?:
|
|
145
|
-
): Promise<{ html: string; jsFiles: string[]
|
|
197
|
+
cssAsset?: CssAsset,
|
|
198
|
+
): Promise<{ html: string; jsFiles: string[] }> {
|
|
199
|
+
let next = html;
|
|
200
|
+
if (cssAsset) {
|
|
201
|
+
const css = await Bun.file(path.join(this.#artifactDir, cssAsset.cssRelPath)).text();
|
|
202
|
+
next = CsrArtifactBuilder.injectBeforeHeadEnd(next, CsrArtifactBuilder.createInlineStyle(css));
|
|
203
|
+
}
|
|
146
204
|
const jsFiles: string[] = [];
|
|
147
|
-
const cssFiles = CsrArtifactBuilder.collectStylesheetHrefs(html).map((href) =>
|
|
148
|
-
CsrArtifactBuilder.resolveHtmlAssetPath(htmlPath, href),
|
|
149
|
-
);
|
|
150
|
-
let next = CsrArtifactBuilder.stripBundledStylesheetLinks(html);
|
|
151
205
|
next = await CsrArtifactBuilder.replaceModuleScriptSrc(next, async (src) => {
|
|
152
206
|
const jsPath = CsrArtifactBuilder.resolveHtmlAssetPath(htmlPath, src);
|
|
153
207
|
jsFiles.push(jsPath);
|
|
154
208
|
return await Bun.file(jsPath).text();
|
|
155
209
|
});
|
|
156
|
-
|
|
157
|
-
await Promise.all(
|
|
158
|
-
cssFiles.map((cssFile) =>
|
|
159
|
-
Bun.file(cssFile)
|
|
160
|
-
.text()
|
|
161
|
-
.catch(() => ""),
|
|
162
|
-
),
|
|
163
|
-
)
|
|
164
|
-
)
|
|
165
|
-
.filter(Boolean)
|
|
166
|
-
.join("\n");
|
|
167
|
-
if (bundledCss) {
|
|
168
|
-
const style = CsrArtifactBuilder.createInlineStyle(bundledCss);
|
|
169
|
-
if (!next.includes(style)) next = CsrArtifactBuilder.injectBeforeHeadEnd(next, style);
|
|
170
|
-
}
|
|
171
|
-
if (cssAsset) {
|
|
172
|
-
const cssPath = path.join(
|
|
173
|
-
this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath,
|
|
174
|
-
".akan/artifact",
|
|
175
|
-
cssAsset.cssRelPath,
|
|
176
|
-
);
|
|
177
|
-
const css = await Bun.file(cssPath).text();
|
|
178
|
-
const style = CsrArtifactBuilder.createInlineStyle(css);
|
|
179
|
-
if (!next.includes(style)) next = CsrArtifactBuilder.injectBeforeHeadEnd(next, style);
|
|
180
|
-
}
|
|
181
|
-
return { html: next, jsFiles, cssFiles };
|
|
210
|
+
return { html: next, jsFiles };
|
|
182
211
|
}
|
|
183
212
|
|
|
184
213
|
async #htmlOutputPaths(): Promise<string[]> {
|
|
@@ -194,23 +223,26 @@ void bootCsr(pages);
|
|
|
194
223
|
return files.sort();
|
|
195
224
|
}
|
|
196
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Bun's HTML bundler hoists the module script into `<head>`, so once that script is inline its source is part
|
|
228
|
+
* of the text being searched — and a React bundle contains `<body` and `</head>` as strings. Positions are
|
|
229
|
+
* taken on a copy with script, style and comment bodies blanked, and the snippet always lands after whatever
|
|
230
|
+
* was injected before it: prepending would reverse the cascade order the caller chose.
|
|
231
|
+
*/
|
|
197
232
|
static injectBeforeHeadEnd(html: string, snippet: string): string {
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
return `${html.slice(0, headEnd.index)}${snippet}\n${html.slice(headEnd.index)}`;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
static stripBundledStylesheetLinks(html: string): string {
|
|
208
|
-
return html.replace(/<link\b(?=[^>]*\brel=["']stylesheet["'])[^>]*>\s*/gi, "");
|
|
233
|
+
const scannable = CsrArtifactBuilder.blankEmbeddedContent(html);
|
|
234
|
+
const headEnd = scannable.search(/<\/head\s*>/i);
|
|
235
|
+
if (headEnd !== -1) return `${html.slice(0, headEnd)}${snippet}\n${html.slice(headEnd)}`;
|
|
236
|
+
const bodyStart = scannable.search(/<body(?:\s|>)/i);
|
|
237
|
+
if (bodyStart !== -1) return `${html.slice(0, bodyStart)}${snippet}\n${html.slice(bodyStart)}`;
|
|
238
|
+
return `${html}\n${snippet}`;
|
|
209
239
|
}
|
|
210
240
|
|
|
211
|
-
static
|
|
212
|
-
|
|
213
|
-
|
|
241
|
+
static blankEmbeddedContent(html: string): string {
|
|
242
|
+
return html.replace(
|
|
243
|
+
/<script\b[^>]*>[\s\S]*?<\/script\s*>|<style\b[^>]*>[\s\S]*?<\/style\s*>|<!--[\s\S]*?-->/gi,
|
|
244
|
+
(match) => " ".repeat(match.length),
|
|
245
|
+
);
|
|
214
246
|
}
|
|
215
247
|
|
|
216
248
|
static createInlineStyle(css: string): string {
|
|
@@ -17,6 +17,12 @@ interface CssDiscovery {
|
|
|
17
17
|
sourcePaths: string[];
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** One `@import` target and the custom properties it declares, which is what proves it arrived downstream. */
|
|
21
|
+
export interface ImportedStylesheet {
|
|
22
|
+
cssPath: string;
|
|
23
|
+
declaredNames: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
export class CssCompiler {
|
|
21
27
|
#logger = new Logger("CssCompiler");
|
|
22
28
|
#transpiler = new Bun.Transpiler({ loader: "tsx" });
|
|
@@ -48,6 +54,11 @@ export class CssCompiler {
|
|
|
48
54
|
#resolvedSpecifierCache = new Map<string, Promise<string | null>>();
|
|
49
55
|
/** Every stylesheet this compile reached, entry points and `@import` targets alike. */
|
|
50
56
|
#discoveredCssPaths = new Set<string>();
|
|
57
|
+
/**
|
|
58
|
+
* `@import` targets per base path, so whoever writes the asset can check the file it actually wrote — the
|
|
59
|
+
* compiled text and the written artifact are two different places a declaration can go missing.
|
|
60
|
+
*/
|
|
61
|
+
importedStylesheetsByBasePath: Record<string, ImportedStylesheet[]> = {};
|
|
51
62
|
|
|
52
63
|
#fileExists(absPath: string): Promise<boolean> {
|
|
53
64
|
let cached = this.#fileExistsCache.get(absPath);
|
|
@@ -79,8 +90,11 @@ export class CssCompiler {
|
|
|
79
90
|
async getCss({ refresh }: { refresh?: boolean } = {}) {
|
|
80
91
|
if (this.#cssText !== null && !refresh) return this.#cssText;
|
|
81
92
|
this.#discoveredCssPaths.clear();
|
|
93
|
+
this.importedStylesheetsByBasePath = {};
|
|
82
94
|
const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh });
|
|
83
|
-
|
|
95
|
+
const { css, imported } = await this.#compileWithImports(cssPaths, sourcePaths);
|
|
96
|
+
this.#cssText = css;
|
|
97
|
+
this.importedStylesheetsByBasePath = { "": imported };
|
|
84
98
|
await this.#warnUnreachableStylesheets();
|
|
85
99
|
return this.#cssText;
|
|
86
100
|
}
|
|
@@ -88,6 +102,7 @@ export class CssCompiler {
|
|
|
88
102
|
async getCssByBasePath({ refresh }: { refresh?: boolean } = {}): Promise<Record<string, string>> {
|
|
89
103
|
if (this.#cssTextByBasePath !== null && !refresh) return this.#cssTextByBasePath;
|
|
90
104
|
this.#discoveredCssPaths.clear();
|
|
105
|
+
this.importedStylesheetsByBasePath = {};
|
|
91
106
|
const akanConfig = await this.#app.getConfig({ refresh });
|
|
92
107
|
const pageKeys = await this.#app.getPageKeys({ refresh });
|
|
93
108
|
const basePaths = [...akanConfig.basePaths];
|
|
@@ -97,7 +112,8 @@ export class CssCompiler {
|
|
|
97
112
|
if (rootPageKeys.length === 0) return ["", ""] as const;
|
|
98
113
|
const started = Date.now();
|
|
99
114
|
const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: rootPageKeys });
|
|
100
|
-
const css = await this
|
|
115
|
+
const { css, imported } = await this.#compileWithImports(cssPaths, sourcePaths);
|
|
116
|
+
this.importedStylesheetsByBasePath[""] = imported;
|
|
101
117
|
this.#logger.verbose(
|
|
102
118
|
`css base=root paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`,
|
|
103
119
|
);
|
|
@@ -108,7 +124,8 @@ export class CssCompiler {
|
|
|
108
124
|
if (basePathPageKeys.length === 0) return [basePath, ""] as const;
|
|
109
125
|
const started = Date.now();
|
|
110
126
|
const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: basePathPageKeys });
|
|
111
|
-
const css = await this
|
|
127
|
+
const { css, imported } = await this.#compileWithImports(cssPaths, sourcePaths);
|
|
128
|
+
this.importedStylesheetsByBasePath[basePath] = imported;
|
|
112
129
|
this.#logger.verbose(
|
|
113
130
|
`css base=${basePath} paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`,
|
|
114
131
|
);
|
|
@@ -231,19 +248,32 @@ export class CssCompiler {
|
|
|
231
248
|
return tokenPaths.filter((tokensPath): tokensPath is string => !!tokensPath);
|
|
232
249
|
}
|
|
233
250
|
async compileCss(cssPaths: string[], sourcePaths: string[]): Promise<string> {
|
|
234
|
-
|
|
251
|
+
const { css } = await this.#compileWithImports(cssPaths, sourcePaths);
|
|
252
|
+
return css;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The collector is per compile rather than per compiler instance: `getCssByBasePath` compiles every base path
|
|
257
|
+
* concurrently, so instance state would mix one base path's imports into another's check.
|
|
258
|
+
*/
|
|
259
|
+
async #compileWithImports(
|
|
260
|
+
cssPaths: string[],
|
|
261
|
+
sourcePaths: string[],
|
|
262
|
+
): Promise<{ css: string; imported: ImportedStylesheet[] }> {
|
|
263
|
+
if (cssPaths.length === 0) return { css: "", imported: [] };
|
|
235
264
|
|
|
236
265
|
const compileStarted = Date.now();
|
|
237
266
|
const compilers = await Promise.all(
|
|
238
267
|
cssPaths.map(async (cssPath) => {
|
|
239
268
|
const css = await Bun.file(cssPath).text();
|
|
240
269
|
const base = path.dirname(cssPath);
|
|
270
|
+
const imported = new Map<string, string>();
|
|
241
271
|
const compiler = await compile(css, {
|
|
242
272
|
base,
|
|
243
|
-
loadStylesheet: (id, fromBase) => this.#loadStylesheet(id, fromBase),
|
|
273
|
+
loadStylesheet: (id, fromBase) => this.#loadStylesheet(id, fromBase, imported),
|
|
244
274
|
loadModule: (id, fromBase) => this.#loadModule(id, fromBase),
|
|
245
275
|
});
|
|
246
|
-
return { cssPath, compiler };
|
|
276
|
+
return { cssPath, compiler, imported };
|
|
247
277
|
}),
|
|
248
278
|
);
|
|
249
279
|
|
|
@@ -258,20 +288,32 @@ export class CssCompiler {
|
|
|
258
288
|
`css candidates scanned count=${candidates.length} sources=${sourcePaths.length} dirs=${sourceDirs.size} in ${Date.now() - scanStarted}ms`,
|
|
259
289
|
);
|
|
260
290
|
const parts: string[] = [];
|
|
291
|
+
const imported: ImportedStylesheet[] = [];
|
|
261
292
|
for (const entry of compilers) {
|
|
262
293
|
if (!entry) continue;
|
|
263
|
-
|
|
294
|
+
const part = entry.compiler.build(candidates);
|
|
295
|
+
parts.push(part);
|
|
296
|
+
for (const [cssPath, content] of entry.imported) {
|
|
297
|
+
const declaredNames = declaredCustomProperties(content);
|
|
298
|
+
imported.push({ cssPath, declaredNames });
|
|
299
|
+
if (declaredNames.length === 0 || declaredNames.some((name) => part.includes(`${name}:`))) continue;
|
|
300
|
+
this.#logger.warn(
|
|
301
|
+
`css @import ${cssPath} was loaded by ${entry.cssPath} but none of its ${declaredNames.length} declaration(s) reached the compiled CSS`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
264
304
|
}
|
|
265
305
|
this.#logger.verbose(
|
|
266
306
|
`css compiled paths=${cssPaths.length} candidates=${candidates.length} in ${Date.now() - compileStarted}ms`,
|
|
267
307
|
);
|
|
268
|
-
return parts.join("\n");
|
|
308
|
+
return { css: parts.join("\n"), imported };
|
|
269
309
|
}
|
|
270
310
|
|
|
271
|
-
async #loadStylesheet(id: string, fromBase: string) {
|
|
311
|
+
async #loadStylesheet(id: string, fromBase: string, imported?: Map<string, string>) {
|
|
272
312
|
const p = await this.#resolveCssImport(id, fromBase);
|
|
273
313
|
this.#discoveredCssPaths.add(p);
|
|
274
314
|
const content = await Bun.file(p).text();
|
|
315
|
+
imported?.set(p, content);
|
|
316
|
+
this.#logger.verbose(`css import "${id}" from ${fromBase} -> ${p} (${content.length} bytes)`);
|
|
275
317
|
return { path: p, base: path.dirname(p), content };
|
|
276
318
|
}
|
|
277
319
|
|
|
@@ -388,7 +430,18 @@ export function isIgnoredNodeModuleSource(filePath: string): boolean {
|
|
|
388
430
|
return NODE_MODULES_RE.test(filePath) && !AKANJS_NODE_MODULE_RE.test(filePath);
|
|
389
431
|
}
|
|
390
432
|
|
|
391
|
-
|
|
433
|
+
/**
|
|
434
|
+
* `@theme` blocks are stripped first: those variables are emitted only when a utility uses one, so their
|
|
435
|
+
* absence from a build says nothing about whether the stylesheet arrived.
|
|
436
|
+
*/
|
|
437
|
+
export function declaredCustomProperties(css: string): string[] {
|
|
438
|
+
const withoutThemeBlocks = css.replace(/@theme[^{]*\{[^}]*\}/g, "");
|
|
439
|
+
return [...new Set([...withoutThemeBlocks.matchAll(/(?:^|[\s;{])(--[\w-]+)\s*:/g)].map(([, name]) => name))].filter(
|
|
440
|
+
(name): name is string => !!name,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function getPageKeyBasePath(pageKey: string, basePaths: string[]): string | null {
|
|
392
445
|
const normalized = pageKey.split(path.sep).join("/").replace(/^\.\//, "");
|
|
393
446
|
const segments = normalized.split("/");
|
|
394
447
|
const firstPublicSegment = segments.find((segment) => segment !== "[lang]" && !/^\(.+\)$/.test(segment));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile, rm, stat
|
|
1
|
+
import { mkdir, readdir, readFile, rm, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { FileSys } from "../fileSys";
|
|
3
4
|
|
|
4
5
|
const BARREL_FACETS = new Set(["common", "srvkit", "ui", "webkit", "plugin"]);
|
|
5
6
|
const FACET_SOURCE_FILE_RE = /\.(ts|tsx)$/;
|
|
@@ -101,7 +102,7 @@ export class DevGeneratedIndexSync {
|
|
|
101
102
|
const current = await readFile(indexPath, "utf8").catch(() => null);
|
|
102
103
|
if (current === content) return false;
|
|
103
104
|
await mkdir(dir, { recursive: true });
|
|
104
|
-
await
|
|
105
|
+
await FileSys.writeTextAtomic(indexPath, content);
|
|
105
106
|
return true;
|
|
106
107
|
}
|
|
107
108
|
|