@akanjs/devkit 3.0.0-beta.9 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEV_RUNTIME_KNOBS.md +44 -26
- package/README.ko.md +21 -7
- package/README.md +17 -8
- package/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +6 -8
- package/agentsIndex.test.ts +4 -7
- package/agentsIndex.ts +6 -22
- package/akanApp/BackendImportGraph.test.ts +8 -22
- package/akanApp/BackendImportGraph.ts +1 -20
- package/akanApp/akanApp.host.test.ts +56 -3
- package/akanApp/akanApp.host.ts +218 -278
- package/akanApp/devBootLatch.test.ts +39 -0
- package/akanApp/devBootLatch.ts +39 -0
- package/akanApp/devHostPolicy.test.ts +81 -35
- package/akanApp/devHostPolicy.ts +41 -102
- package/akanConfig/akanBin.test.ts +62 -0
- package/akanConfig/akanBin.ts +78 -0
- package/akanConfig/akanConfig.test.ts +439 -141
- package/akanConfig/akanConfig.ts +359 -275
- package/akanConfig/index.ts +1 -0
- package/akanConfig/types.ts +22 -8
- package/akanContext.ts +110 -160
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.test.ts +41 -5
- package/applicationBuildRunner.ts +71 -63
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +32 -30
- package/artifact/implicitRootLayout.ts +58 -106
- package/artifact/routeSeedIndex.test.ts +7 -6
- package/artifact/routeSeedIndex.ts +11 -44
- package/biome.base.json +35 -2
- package/biomeBase.ts +2 -5
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +77 -39
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +26 -5
- package/cloud/globalConfig.ts +71 -32
- package/codeAgent/agent/AkanCodeServices.ts +97 -0
- package/codeAgent/agent/AkanContextFiles.ts +94 -0
- package/codeAgent/agent/CodeAgent.ts +551 -0
- package/codeAgent/agent/CodeAgentAsks.ts +66 -0
- package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
- package/codeAgent/agent/CodeAgentGate.ts +93 -0
- package/codeAgent/agent/CodeAgentProxy.ts +82 -0
- package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
- package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
- package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
- package/codeAgent/agent/CodeAgentUi.ts +47 -0
- package/codeAgent/agent/CodeMailbox.ts +148 -0
- package/codeAgent/agent/CodeSessionFork.ts +56 -0
- package/codeAgent/agent/CodeSessionIndex.ts +78 -0
- package/codeAgent/agent/SubagentPool.ts +214 -0
- package/codeAgent/agent/akanCodeModel.ts +47 -0
- package/codeAgent/agent/akanCodePaths.ts +31 -0
- package/codeAgent/agent/akanContextFiles.test.ts +114 -0
- package/codeAgent/agent/akanSystemPrompt.ts +46 -0
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +523 -0
- package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
- package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
- package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
- package/codeAgent/agent/codeMailbox.test.ts +76 -0
- package/codeAgent/agent/codeSessionFork.test.ts +58 -0
- package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
- package/codeAgent/agent/consoleToStderr.ts +8 -0
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +82 -0
- package/codeAgent/feedback/PreviewChrome.ts +82 -0
- package/codeAgent/feedback/PreviewFeedback.ts +46 -0
- package/codeAgent/feedback/PreviewView.ts +95 -0
- package/codeAgent/feedback/TurnFeedback.ts +98 -0
- package/codeAgent/feedback/VerifyFeedback.ts +40 -0
- package/codeAgent/feedback/previewChrome.test.ts +73 -0
- package/codeAgent/index.ts +34 -0
- package/codeAgent/skills/akan-code/SKILL.md +130 -0
- package/codeAgent/skills/akan-debug/SKILL.md +42 -0
- package/codeAgent/skills/akan-deps/SKILL.md +50 -0
- package/codeAgent/skills/akan-err/SKILL.md +39 -0
- package/codeAgent/skills/akan-module/SKILL.md +45 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
- package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
- package/codeAgent/skills/akan-service/SKILL.md +34 -0
- package/codeAgent/skills/akan-store/SKILL.md +67 -0
- package/codeAgent/skills/akan-ui/SKILL.md +63 -0
- package/codeAgent/skills/akan-validate/SKILL.md +44 -0
- package/codeAgent/tools/AkanCli.ts +50 -0
- package/codeAgent/tools/AkanCodePlugins.ts +136 -0
- package/codeAgent/tools/AkanEditScope.ts +48 -0
- package/codeAgent/tools/AkanEnvKeys.ts +50 -0
- package/codeAgent/tools/AkanToolPack.ts +92 -0
- package/codeAgent/tools/AkanVerifier.ts +48 -0
- package/codeAgent/tools/AskToolPack.ts +99 -0
- package/codeAgent/tools/MailToolPack.ts +76 -0
- package/codeAgent/tools/McpClient.ts +187 -0
- package/codeAgent/tools/McpOAuth.ts +233 -0
- package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
- package/codeAgent/tools/McpServerConfig.ts +129 -0
- package/codeAgent/tools/McpSignIn.ts +158 -0
- package/codeAgent/tools/McpTokenStore.ts +81 -0
- package/codeAgent/tools/McpToolPack.ts +199 -0
- package/codeAgent/tools/SessionToolPack.ts +91 -0
- package/codeAgent/tools/WebToolPack.ts +147 -0
- package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
- package/codeAgent/tools/askToolPack.test.ts +116 -0
- package/codeAgent/tools/mcpOAuth.test.ts +612 -0
- package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
- package/codegenLock.test.ts +4 -13
- package/codegenLock.ts +3 -17
- package/commandDecorators/argMeta.ts +1 -5
- package/commandDecorators/camelToKebabCase.ts +1 -0
- package/commandDecorators/command.ts +46 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +2 -10
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +68 -145
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/csrE2e/csrE2eHarness.fixture.ts +505 -0
- package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
- package/csrE2e/csrPageStack.e2e.test.ts +252 -0
- package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
- package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
- package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
- package/csrE2e/registryVendor.e2e.test.ts +221 -0
- package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +31 -80
- package/devkitUtils.test.ts +48 -20
- package/executors.linkFallback.test.ts +103 -0
- package/executors.test.ts +176 -106
- package/executors.ts +212 -498
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +5 -3
- package/fileSys.ts +22 -9
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +27 -38
- package/frontendBuild/autoImportSync.ts +31 -65
- package/frontendBuild/buildRouteClient.test.ts +123 -37
- package/frontendBuild/bundleDefine.test.ts +12 -0
- package/frontendBuild/bundleDefine.ts +16 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
- package/frontendBuild/clientEntryDiscovery.ts +122 -110
- package/frontendBuild/csrArtifactBuilder.ts +9 -31
- package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
- package/frontendBuild/csrDevArtifactWriter.ts +307 -0
- package/frontendBuild/csrDevBundler.test.ts +66 -0
- package/frontendBuild/csrDevBundler.ts +95 -0
- package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
- package/frontendBuild/csrDevFactoryCheck.ts +39 -0
- package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
- package/frontendBuild/csrDevModuleCompiler.ts +422 -0
- package/frontendBuild/csrDevPatcher.test.ts +572 -0
- package/frontendBuild/csrDevPatcher.ts +548 -0
- package/frontendBuild/csrDevPaths.ts +145 -0
- package/frontendBuild/csrDevResolver.test.ts +239 -0
- package/frontendBuild/csrDevResolver.ts +363 -0
- package/frontendBuild/csrDevTypes.ts +61 -0
- package/frontendBuild/csrEntryFiles.ts +24 -0
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +31 -82
- package/frontendBuild/cssImportResolver.ts +6 -19
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/devRegistryBundler.ts +192 -0
- package/frontendBuild/fontOptimizer.test.ts +6 -14
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +57 -70
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +59 -32
- package/frontendBuild/hmrWatcher.ts +31 -74
- package/frontendBuild/index.ts +4 -0
- package/frontendBuild/pagesBundleBuilder.ts +28 -30
- package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
- package/frontendBuild/registryMetadataFingerprint.ts +28 -0
- package/frontendBuild/routeClientBuilder.ts +100 -62
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/serverGraphFile.test.ts +77 -0
- package/frontendBuild/serverGraphFile.ts +93 -0
- package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
- package/frontendBuild/ssrDevBundler.ts +85 -0
- package/frontendBuild/styleContract.ts +1 -8
- package/frontendBuild/themeValidator.ts +4 -26
- package/frontendBuild/watchRootResolver.test.ts +23 -42
- package/frontendBuild/watchRootResolver.ts +5 -27
- package/getRelatedCnsts.ts +12 -92
- package/incrementalBuilder/buildBatch.proc.ts +147 -63
- package/incrementalBuilder/buildBatchProtocol.ts +13 -38
- package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
- package/incrementalBuilder/buildBatchRunner.ts +26 -38
- package/incrementalBuilder/builderChannel.test.ts +24 -49
- package/incrementalBuilder/builderChannel.ts +6 -37
- package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
- package/incrementalBuilder/builderRequestRouter.ts +5 -24
- package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
- package/incrementalBuilder/builderWorkQueue.ts +126 -0
- package/incrementalBuilder/devWatchBatch.test.ts +21 -24
- package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
- package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
- package/index.ts +5 -19
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +105 -199
- package/integration/devStabilityHarness.test.ts +13 -28
- package/integration/devStabilityHarness.ts +65 -330
- package/integration/ssrMemoryProbe.ts +41 -167
- package/libSource.test.ts +4 -15
- package/libSource.ts +19 -31
- package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
- package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
- package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
- package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
- package/lint/gritRules.test.ts +8 -24
- package/lint/no-deep-internal-import.grit +2 -2
- package/lint/no-document-cookie.grit +29 -0
- package/lint/no-redeclare-predefined-endpoint.grit +2 -1
- package/lint/no-web-only-api-outside-webkit.grit +44 -0
- package/lint/no-web-storage.grit +30 -0
- package/linter.test.ts +1 -2
- package/linter.ts +26 -116
- package/mobile/desktopBin.test.ts +182 -0
- package/mobile/desktopBin.ts +144 -0
- package/mobile/desktopServerStage.test.ts +252 -0
- package/mobile/desktopServerStage.ts +178 -0
- package/mobile/index.ts +9 -0
- package/mobile/mobileTarget.test.ts +57 -8
- package/mobile/mobileTarget.ts +45 -26
- package/mobile/nativeApi.test.ts +30 -0
- package/mobile/nativeApi.ts +40 -0
- package/mobile/nativeApp.test.ts +184 -0
- package/mobile/nativeApp.ts +454 -0
- package/mobile/nativeAppLine.test.ts +69 -0
- package/mobile/nativeAppLine.ts +71 -0
- package/mobile/nativeConfig.test.ts +438 -0
- package/mobile/nativeConfig.ts +294 -0
- package/mobile/nativePluginFolders.test.ts +81 -0
- package/mobile/nativePluginFolders.ts +60 -0
- package/mobile/nativeWebDir.test.ts +80 -0
- package/mobile/nativeWebDir.ts +66 -0
- package/mobile/placeholderAppId.test.ts +13 -0
- package/mobile/placeholderAppId.ts +16 -0
- package/mobile/usageDescriptions.test.ts +27 -0
- package/mobile/usageDescriptions.ts +24 -0
- package/package.json +11 -11
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +46 -0
- package/platformTest/BunTestReport.test.ts +60 -0
- package/platformTest/BunTestReport.ts +65 -0
- package/platformTest/IdleWatchedProcess.test.ts +56 -0
- package/platformTest/IdleWatchedProcess.ts +73 -0
- package/platformTest/LinuxDockerTarget.ts +105 -0
- package/platformTest/PlatformTestRun.test.ts +64 -0
- package/platformTest/PlatformTestRun.ts +141 -0
- package/platformTest/PlatformTestTarget.ts +142 -0
- package/platformTest/TestSnapshot.test.ts +72 -0
- package/platformTest/TestSnapshot.ts +103 -0
- package/platformTest/WindowsSshTarget.ts +151 -0
- package/prompter.ts +3 -13
- package/qualityScanner.test.ts +9 -26
- package/qualityScanner.ts +200 -286
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.test.ts +0 -1
- package/routeSourceValidator.ts +7 -42
- package/scanInfo.ts +91 -225
- package/selectModel.ts +1 -2
- package/semver.ts +3 -5
- package/slicePlanner.test.ts +6 -24
- package/slicePlanner.ts +11 -32
- package/spinner.test.ts +3 -8
- package/spinner.ts +4 -16
- package/sshTunnel.ts +1 -6
- package/ssrScanner.test.ts +1 -6
- package/ssrScanner.ts +103 -113
- package/stripAnsi.ts +3 -0
- package/subspace.test.ts +9 -25
- package/subspace.ts +48 -115
- package/subspaceConfig.ts +3 -11
- package/testHelpers.ts +194 -0
- package/transforms/asyncDefaultExportDetector.ts +3 -11
- package/transforms/barrelAnalyzer.ts +43 -135
- package/transforms/barrelImportsPlugin.ts +38 -175
- package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
- package/transforms/externalizeFrameworkPlugin.ts +13 -87
- package/transforms/moduleSyntax.ts +32 -0
- package/transforms/rscUseClientTransform.ts +42 -20
- package/transforms/serverEnvPlugin.test.ts +103 -0
- package/transforms/serverEnvPlugin.ts +41 -0
- package/transforms/transforms.test.ts +166 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +6 -23
- package/typeChecker.ts +7 -63
- package/types.ts +2 -0
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +101 -110
- package/workflow/index.ts +0 -1
- package/workflow/moduleIndex.test.ts +4 -15
- package/workflow/moduleIndex.ts +9 -49
- package/workflow/plan.ts +18 -25
- package/workflow/render.ts +80 -96
- package/workflow/rolloutGate.test.ts +1 -1
- package/workflow/source.ts +52 -69
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.test.ts +18 -3
- package/workspaceLayout.ts +22 -23
- package/abstractCompactor.ts +0 -112
- package/aiEditor.test.ts +0 -60
- package/aiEditor.ts +0 -387
- package/applicationReleasePackager.ts +0 -209
- package/capacitorApp.test.ts +0 -372
- package/capacitorApp.ts +0 -1648
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/guideline.ts +0 -19
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir,
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, rm, stat, writeFile } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import type { App } from "../commandDecorators";
|
|
5
|
+
import { tempDirs } from "../testHelpers";
|
|
6
6
|
import { FontOptimizer } from "./fontOptimizer";
|
|
7
7
|
|
|
8
8
|
const SOURCE_FONT = path.resolve(import.meta.dir, "../../../../libs/shared/public/fonts/Assistant-Regular.woff2");
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const tempRoot = tempDirs("akan-devkit-font-");
|
|
11
11
|
|
|
12
12
|
const makeApp = async (layoutSource: string) => {
|
|
13
|
-
const root = await
|
|
14
|
-
tempRoots.push(root);
|
|
13
|
+
const root = await tempRoot();
|
|
15
14
|
const cwdPath = path.join(root, "apps/demo");
|
|
16
15
|
await mkdir(path.join(cwdPath, "page"), { recursive: true });
|
|
17
16
|
await mkdir(path.join(cwdPath, "public/fonts"), { recursive: true });
|
|
@@ -59,10 +58,6 @@ const chainFontList = (extra = "") => `[\n ${chainFontEntry(extra)},\n ]`;
|
|
|
59
58
|
|
|
60
59
|
const optimize = (app: App) => new FontOptimizer(app, "start").optimize();
|
|
61
60
|
|
|
62
|
-
afterEach(async () => {
|
|
63
|
-
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
64
|
-
});
|
|
65
|
-
|
|
66
61
|
describe("FontOptimizer cache", () => {
|
|
67
62
|
test("reuses subset output instead of resubsetting an unchanged font", async () => {
|
|
68
63
|
const { app } = await makeApp(layoutWith());
|
|
@@ -76,7 +71,6 @@ describe("FontOptimizer cache", () => {
|
|
|
76
71
|
expect(second.files).toEqual(first.files);
|
|
77
72
|
expect(second.css).toBe(first.css);
|
|
78
73
|
expect(second.fonts).toEqual(first.fonts);
|
|
79
|
-
// The output was reused, not rewritten — the whole point of the cache.
|
|
80
74
|
expect((await stat(second.files[0])).mtimeMs).toBe(writtenAt);
|
|
81
75
|
});
|
|
82
76
|
|
|
@@ -160,8 +154,6 @@ describe("FontOptimizer discovery", () => {
|
|
|
160
154
|
expect(second.css).toContain(".font-brand");
|
|
161
155
|
});
|
|
162
156
|
|
|
163
|
-
// A list the build cannot read subsets nothing while the runtime still preloads /_akan/fonts, so the 404s
|
|
164
|
-
// have to be announced at build time rather than found in a browser console.
|
|
165
157
|
test("warns when the chain is handed a font list it cannot read", async () => {
|
|
166
158
|
const { app, warnings } = await makeApp(chainLayoutWith("brandFonts"));
|
|
167
159
|
|
|
@@ -170,7 +162,7 @@ describe("FontOptimizer discovery", () => {
|
|
|
170
162
|
expect(result.fonts).toEqual([]);
|
|
171
163
|
expect(result.files).toEqual([]);
|
|
172
164
|
expect(warnings).toHaveLength(1);
|
|
173
|
-
expect(warnings[0]).toContain("page
|
|
165
|
+
expect(warnings[0]).toContain(path.join("page", "_layout.tsx"));
|
|
174
166
|
});
|
|
175
167
|
|
|
176
168
|
test("warns about an entry it cannot read and keeps the ones it can", async () => {
|
|
@@ -21,10 +21,6 @@ export interface OptimizeAppFontsResult {
|
|
|
21
21
|
files: string[];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
* Lets a boot that changed nothing about its fonts skip re-subsetting them. `files` is stored relative
|
|
26
|
-
* to the artifact root so the `build` and `start` roots keep independent, relocatable caches.
|
|
27
|
-
*/
|
|
28
24
|
interface FontOptimizerCache {
|
|
29
25
|
version: number;
|
|
30
26
|
key: string;
|
|
@@ -43,8 +39,6 @@ export class FontOptimizer {
|
|
|
43
39
|
#woff2Ready: Promise<void> | null = null;
|
|
44
40
|
|
|
45
41
|
static #ksX1001Text: string | null = null;
|
|
46
|
-
// 2: the key moved to sha256 over a deterministically ordered `auto` text, so v1 entries cannot be
|
|
47
|
-
// compared against and their subsets were built from `page`/`ui` only.
|
|
48
42
|
static readonly #cacheVersion = 2;
|
|
49
43
|
|
|
50
44
|
constructor(app: App, command: FontOptimizerCommand = "start") {
|
|
@@ -76,10 +70,7 @@ export class FontOptimizer {
|
|
|
76
70
|
return path.join(this.#artifactRoot, "fontCache.json");
|
|
77
71
|
}
|
|
78
72
|
|
|
79
|
-
|
|
80
|
-
* Null means "do not cache this run": a source we cannot stat is a source whose staleness we cannot
|
|
81
|
-
* detect, and the uncached path is also the one that warns about it.
|
|
82
|
-
*/
|
|
73
|
+
// null skips the cache: an unstat-able source's staleness is undetectable, and the uncached path warns about it.
|
|
83
74
|
async #buildCacheKey(fonts: ReactFont[]): Promise<string | null> {
|
|
84
75
|
const sources: unknown[] = [];
|
|
85
76
|
for (const font of fonts) {
|
|
@@ -96,18 +87,13 @@ export class FontOptimizer {
|
|
|
96
87
|
if (!stamp) return null;
|
|
97
88
|
sources.push({ subsetFile: filePath, ...stamp });
|
|
98
89
|
}
|
|
99
|
-
// `auto` derives the subset from app source text, which no font config hash can capture.
|
|
100
90
|
if (this.#getFontSubsets(font).includes("auto"))
|
|
101
91
|
sources.push({ autoSubsetText: this.#cacheDigest(await this.#collectAutoSubsetText()) });
|
|
102
92
|
}
|
|
103
93
|
return this.#cacheDigest({ version: FontOptimizer.#cacheVersion, fonts, sources });
|
|
104
94
|
}
|
|
105
95
|
|
|
106
|
-
|
|
107
|
-
* Cache keys get a cryptographic digest, not the 32-bit FNV `#hashFontConfig` computes for filenames:
|
|
108
|
-
* a collision there serves a stale subset as if it were current, while a filename only has to be short
|
|
109
|
-
* and stable.
|
|
110
|
-
*/
|
|
96
|
+
// sha256, not #hashFontConfig's 32-bit FNV: a key collision would serve a stale subset as current.
|
|
111
97
|
#cacheDigest(value: unknown) {
|
|
112
98
|
return new Bun.CryptoHasher("sha256").update(this.#stableStringify(value)).digest("hex");
|
|
113
99
|
}
|
|
@@ -152,13 +138,11 @@ export class FontOptimizer {
|
|
|
152
138
|
const file = Bun.file(filePath);
|
|
153
139
|
if (!(await file.exists())) return;
|
|
154
140
|
const source = await file.text();
|
|
155
|
-
// A `.fonts()` stage or a `fonts` declaration cannot exist in text that never mentions it, and parsing the
|
|
156
|
-
// route files that never declare one is what a cached optimize() otherwise spends its time on.
|
|
157
141
|
if (!source.includes("fonts")) return;
|
|
158
142
|
fonts.push(...this.#extractFontsExport(source, filePath));
|
|
159
143
|
}),
|
|
160
144
|
);
|
|
161
|
-
return
|
|
145
|
+
return [...new Map(fonts.map((font) => [JSON.stringify(font), font] as const)).values()];
|
|
162
146
|
}
|
|
163
147
|
|
|
164
148
|
async #optimizeFont(font: ReactFont) {
|
|
@@ -186,12 +170,6 @@ export class FontOptimizer {
|
|
|
186
170
|
if (faceCss.length > 0) this.#cssParts.push(...faceCss, this.#buildRootVariableRule(font));
|
|
187
171
|
}
|
|
188
172
|
|
|
189
|
-
/**
|
|
190
|
-
* The font list of a route file, from the `.fonts([…])` stage of its default-exported `rootLayout()` chain
|
|
191
|
-
* or from the legacy `export const fonts`. Only an inline literal is readable, because the build enumerates
|
|
192
|
-
* routes without evaluating them — and a list it cannot read is warned about rather than skipped in silence:
|
|
193
|
-
* the runtime still emits `/_akan/fonts` preloads for fonts nothing subset, and every one of them 404s.
|
|
194
|
-
*/
|
|
195
173
|
#extractFontsExport(source: string, filePath: string): ReactFont[] {
|
|
196
174
|
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
197
175
|
const declaration = this.#findFontsDeclaration(sourceFile);
|
|
@@ -203,7 +181,7 @@ export class FontOptimizer {
|
|
|
203
181
|
this.#app.logger.warn(
|
|
204
182
|
`[font] ${path.relative(this.#app.cwdPath, filePath)} declares fonts the build cannot read without evaluating the module — write the list inline, or nothing is subset for it and every /_akan/fonts request 404s`,
|
|
205
183
|
);
|
|
206
|
-
return fonts.map((font) =>
|
|
184
|
+
return fonts.map((font) => ({ ...font, subsets: font.subsets ?? [...DEFAULT_FONT_SUBSETS] }));
|
|
207
185
|
}
|
|
208
186
|
|
|
209
187
|
#isReadableFont(value: unknown): value is ReactFont {
|
|
@@ -231,7 +209,7 @@ export class FontOptimizer {
|
|
|
231
209
|
return null;
|
|
232
210
|
}
|
|
233
211
|
|
|
234
|
-
|
|
212
|
+
// Outermost call first, so `??=` keeps the stage that wins at runtime.
|
|
235
213
|
#findChainStageArgument(expression: ts.Expression, stageName: string): ts.Expression | null {
|
|
236
214
|
let node = this.#unwrapExpression(expression);
|
|
237
215
|
let argument: ts.Expression | null = null;
|
|
@@ -284,16 +262,6 @@ export class FontOptimizer {
|
|
|
284
262
|
return null;
|
|
285
263
|
}
|
|
286
264
|
|
|
287
|
-
#dedupeFonts(fonts: ReactFont[]) {
|
|
288
|
-
const map = new Map<string, ReactFont>();
|
|
289
|
-
for (const font of fonts) map.set(JSON.stringify(font), font);
|
|
290
|
-
return [...map.values()];
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
#withFontDefaults(font: ReactFont): ReactFont {
|
|
294
|
-
return { ...font, subsets: font.subsets ?? [...DEFAULT_FONT_SUBSETS] };
|
|
295
|
-
}
|
|
296
|
-
|
|
297
265
|
#getFontSubsets(font: ReactFont): ReactFontSubset[] {
|
|
298
266
|
return font.subsets ?? DEFAULT_FONT_SUBSETS;
|
|
299
267
|
}
|
|
@@ -310,12 +278,8 @@ export class FontOptimizer {
|
|
|
310
278
|
return font.optimize !== false;
|
|
311
279
|
}
|
|
312
280
|
|
|
313
|
-
#getFontStyles(font: ReactFont): ReactFontStyle[] {
|
|
314
|
-
return font.styles?.length ? font.styles : ["normal"];
|
|
315
|
-
}
|
|
316
|
-
|
|
317
281
|
#getFontFaces(font: ReactFont): ReactFontFace[] {
|
|
318
|
-
const enabledStyles = new Set(
|
|
282
|
+
const enabledStyles = new Set<ReactFontStyle>(font.styles?.length ? font.styles : ["normal"]);
|
|
319
283
|
return font.paths
|
|
320
284
|
.map((fontPath) => {
|
|
321
285
|
const style = fontPath.style ?? "normal";
|
|
@@ -393,8 +357,7 @@ export class FontOptimizer {
|
|
|
393
357
|
return null;
|
|
394
358
|
}
|
|
395
359
|
|
|
396
|
-
|
|
397
|
-
* cache hit — the common case once the cache exists — loads none of them. */
|
|
360
|
+
// The font libraries are imported lazily so a cache hit loads none of them.
|
|
398
361
|
async #buildFontBuffer(font: ReactFont, sourceBuffer: Buffer, sourcePath: string) {
|
|
399
362
|
if (font.subset === false) return this.#convertToWoff2(sourceBuffer, sourcePath);
|
|
400
363
|
const { default: subsetFont } = await import("subset-font");
|
|
@@ -403,16 +366,12 @@ export class FontOptimizer {
|
|
|
403
366
|
|
|
404
367
|
async #convertToWoff2(buffer: Buffer, sourcePath: string) {
|
|
405
368
|
const { createFont } = await import("fonteditor-core");
|
|
406
|
-
|
|
369
|
+
this.#woff2Ready ??= import("fonteditor-core").then(({ woff2 }) => woff2.init()).then(() => undefined);
|
|
370
|
+
await this.#woff2Ready;
|
|
407
371
|
const font = createFont(buffer, { type: this.#getFontType(sourcePath, buffer) });
|
|
408
372
|
return font.write({ type: "woff2", toBuffer: true });
|
|
409
373
|
}
|
|
410
374
|
|
|
411
|
-
async #initWoff2() {
|
|
412
|
-
this.#woff2Ready ??= import("fonteditor-core").then(({ woff2 }) => woff2.init()).then(() => undefined);
|
|
413
|
-
return this.#woff2Ready;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
375
|
#getFontType(sourcePath: string, buffer: Buffer) {
|
|
417
376
|
const signature = buffer.toString("ascii", 0, 4);
|
|
418
377
|
if (signature === "wOFF") return "woff";
|
|
@@ -443,25 +402,15 @@ export class FontOptimizer {
|
|
|
443
402
|
}
|
|
444
403
|
|
|
445
404
|
async #getSubsetPresetText(subset: ReactFontSubset) {
|
|
446
|
-
if (subset === "latin") return
|
|
447
|
-
if (subset === "latin-ext")
|
|
405
|
+
if (subset === "latin") return FontOptimizer.#rangeText(0x20, 0x7e);
|
|
406
|
+
if (subset === "latin-ext")
|
|
407
|
+
return `${FontOptimizer.#rangeText(0x20, 0x7e)}${FontOptimizer.#rangeText(0xa0, 0x024f)}`;
|
|
448
408
|
if (subset === "ks-x-1001") return FontOptimizer.#getKsX1001Text();
|
|
449
409
|
if (subset === "auto") return this.#collectAutoSubsetText();
|
|
450
410
|
return "";
|
|
451
411
|
}
|
|
452
412
|
|
|
453
|
-
|
|
454
|
-
* Every source that can put a glyph on screen, concatenated in a **stable** order.
|
|
455
|
-
*
|
|
456
|
-
* The order is load-bearing even though a glyph set is not: `#buildCacheKey` hashes this string, so
|
|
457
|
-
* reading the roots concurrently and pushing as each file resolved made the key depend on i/o
|
|
458
|
-
* scheduling — measured 8 distinct keys over 8 runs against unchanged sources, which means the cache
|
|
459
|
-
* never hit and every build re-subset the fonts.
|
|
460
|
-
*
|
|
461
|
-
* `lib` is in the roots because that is where user-facing text actually lives: a dictionary's
|
|
462
|
-
* `[en, ko]` pairs are the Korean in the app, and a subset built from `page` and `ui` alone renders
|
|
463
|
-
* them as tofu — while hashing the same partial text also stopped a new label from invalidating.
|
|
464
|
-
*/
|
|
413
|
+
// Keep the order stable: #buildCacheKey hashes this text. `lib` counts because dictionaries hold user-visible text.
|
|
465
414
|
async #collectAutoSubsetText() {
|
|
466
415
|
//* Synced lib pages hold app-visible text too, and a glob never crosses the symlink that mounts them.
|
|
467
416
|
const libPageRoots = (await this.#app.getPageRoots()).filter((root) => root.keyPrefix).map((root) => root.dir);
|
|
@@ -481,12 +430,6 @@ export class FontOptimizer {
|
|
|
481
430
|
return parts.join("");
|
|
482
431
|
}
|
|
483
432
|
|
|
484
|
-
#rangeText(start: number, end: number) {
|
|
485
|
-
let text = "";
|
|
486
|
-
for (let code = start; code <= end; code++) text += String.fromCodePoint(code);
|
|
487
|
-
return text;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
433
|
static #getKsX1001Text() {
|
|
491
434
|
if (FontOptimizer.#ksX1001Text) return FontOptimizer.#ksX1001Text;
|
|
492
435
|
try {
|
|
@@ -500,12 +443,12 @@ export class FontOptimizer {
|
|
|
500
443
|
}
|
|
501
444
|
FontOptimizer.#ksX1001Text = [...chars].join("");
|
|
502
445
|
} catch {
|
|
503
|
-
FontOptimizer.#ksX1001Text = FontOptimizer.#
|
|
446
|
+
FontOptimizer.#ksX1001Text = FontOptimizer.#rangeText(0xac00, 0xd7a3);
|
|
504
447
|
}
|
|
505
448
|
return FontOptimizer.#ksX1001Text;
|
|
506
449
|
}
|
|
507
450
|
|
|
508
|
-
static #
|
|
451
|
+
static #rangeText(start: number, end: number) {
|
|
509
452
|
let text = "";
|
|
510
453
|
for (let code = start; code <= end; code++) text += String.fromCodePoint(code);
|
|
511
454
|
return text;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir,
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import type { App } from "../commandDecorators";
|
|
5
|
+
import { tempDirs } from "../testHelpers";
|
|
6
6
|
import { FontPruner } from "./fontPruner";
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const tempRoot = tempDirs("akan-devkit-font-prune-");
|
|
9
9
|
|
|
10
10
|
interface Tree {
|
|
11
11
|
layout?: string;
|
|
@@ -23,8 +23,7 @@ const write = async (root: string, files: Record<string, string>) => {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
const makeApp = async ({ layout = layoutWith(), publicFiles = {}, artifactFiles = {}, csrFiles = {} }: Tree = {}) => {
|
|
26
|
-
const root = await
|
|
27
|
-
tempRoots.push(root);
|
|
26
|
+
const root = await tempRoot();
|
|
28
27
|
const cwdPath = path.join(root, "apps/demo");
|
|
29
28
|
const distPath = path.join(root, "dist/apps/demo");
|
|
30
29
|
await mkdir(path.join(cwdPath, "page"), { recursive: true });
|
|
@@ -58,10 +57,6 @@ export default function Layout() {
|
|
|
58
57
|
|
|
59
58
|
const exists = (distPath: string, rel: string) => Bun.file(path.join(distPath, "public", rel)).exists();
|
|
60
59
|
|
|
61
|
-
afterEach(async () => {
|
|
62
|
-
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
63
|
-
});
|
|
64
|
-
|
|
65
60
|
describe("FontPruner", () => {
|
|
66
61
|
test("drops a subset font's source and reports the bytes freed", async () => {
|
|
67
62
|
const { app, distPath } = await makeApp({
|
|
@@ -18,12 +18,7 @@ export interface FontPruneResult {
|
|
|
18
18
|
freedBytes: number;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
* Drops the font sources in a build's `public/` that no built surface reads. A font with `optimize` on is
|
|
23
|
-
* served from `/_akan/fonts` after subsetting, so its source is a build input — the image ships it and the
|
|
24
|
-
* runtime never opens it. Only `dist` is touched: an app's and a lib's own `public/` keep every file, because
|
|
25
|
-
* one lib's fonts are picked over differently by every app that mounts it and by other repos.
|
|
26
|
-
*/
|
|
21
|
+
/** Deletes the fonts in the build's `dist/…/public` that nothing reads; source `public/` trees are never touched. */
|
|
27
22
|
export class FontPruner {
|
|
28
23
|
#app: App;
|
|
29
24
|
#keepGlobs: string[];
|
|
@@ -92,15 +87,8 @@ export class FontPruner {
|
|
|
92
87
|
return candidates;
|
|
93
88
|
}
|
|
94
89
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
* absolute or relative `url()`, and the percent-encoded form a font whose filename carries a space needs.
|
|
98
|
-
*
|
|
99
|
-
* The build's own bundles are read on weaker terms than `public/` and the compiled CSS: every route file's
|
|
100
|
-
* `fonts` declaration is inlined into the pages bundle, the client chunks and the CSR shell, so a declared
|
|
101
|
-
* source is in all three whether or not anything loads it. A hit there is therefore ignored for a font the
|
|
102
|
-
* optimizer already subset — that string is the declaration, not a fetch.
|
|
103
|
-
*/
|
|
90
|
+
// Basename match (plain and percent-encoded) covers every url() spelling. The bundles inline each route's `fonts`
|
|
91
|
+
// declaration, so a bundle hit counts only for a font the optimizer did not subset.
|
|
104
92
|
async #findReferrers(
|
|
105
93
|
candidates: { rel: string; basename: string }[],
|
|
106
94
|
optimizedSrcs: Set<string>,
|
|
@@ -155,11 +143,8 @@ export class FontPruner {
|
|
|
155
143
|
for (const dir of dirs) await rmdir(path.join(this.#publicRoot, dir)).catch(() => undefined);
|
|
156
144
|
}
|
|
157
145
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
* service-worker precache manifest is the usual one — is a real reference and keeps every font it names, so
|
|
161
|
-
* without this line a build that pruned nothing looks the same as a build with nothing to prune.
|
|
162
|
-
*/
|
|
146
|
+
// The roll-up is `info`: a precache manifest listing all of public/ keeps every font, which otherwise looks like
|
|
147
|
+
// a build with nothing to prune.
|
|
163
148
|
#report(result: FontPruneResult) {
|
|
164
149
|
if (!result.removed.length && !result.kept.length) return;
|
|
165
150
|
for (const { file, bytes } of result.removed)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { readFile, rm } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
6
5
|
import type { RoutesManifest } from "akanjs/server";
|
|
6
|
+
import { tempDirs, writeText as write } from "../testHelpers";
|
|
7
7
|
import { CsrArtifactBuilder } from "./csrArtifactBuilder";
|
|
8
8
|
import { CssCompiler, declaredCustomProperties, isIgnoredNodeModuleSource } from "./cssCompiler";
|
|
9
9
|
import { CssImportResolver } from "./cssImportResolver";
|
|
@@ -13,35 +13,21 @@ import { HmrChangeClassifier } from "./hmrChangeClassifier";
|
|
|
13
13
|
import { PagesBundleBuilder } from "./pagesBundleBuilder";
|
|
14
14
|
import { PagesEntrySourceGenerator } from "./pagesEntrySourceGenerator";
|
|
15
15
|
import { RoutesManifestArtifactSerializer } from "./routesManifestArtifactSerializer";
|
|
16
|
-
import { prepareCssAsset } from "./ssrBaseArtifactBuilder";
|
|
16
|
+
import { prepareCssAsset, SsrBaseArtifactBuilder } from "./ssrBaseArtifactBuilder";
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
const makeTempRoot = async () => {
|
|
21
|
-
const root = await mkdtemp(path.join(os.tmpdir(), "akan-devkit-frontend-"));
|
|
22
|
-
tempRoots.push(root);
|
|
23
|
-
return root;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const write = async (filePath: string, content: string) => {
|
|
27
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
28
|
-
await writeFile(filePath, content);
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
afterEach(async () => {
|
|
32
|
-
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
33
|
-
});
|
|
18
|
+
const makeTempRoot = tempDirs("akan-devkit-frontend-");
|
|
34
19
|
|
|
35
20
|
describe("PagesEntrySourceGenerator", () => {
|
|
36
21
|
const toSpecifier = (absPath: string) => path.resolve(absPath).split(path.sep).join("/");
|
|
22
|
+
const indexAbs = path.resolve("/repo/apps/demo/page/_index.tsx");
|
|
23
|
+
const adminAbs = path.resolve("/repo/apps/demo/page/admin.tsx");
|
|
24
|
+
const entries = [
|
|
25
|
+
{ key: "./_index.tsx", moduleAbsPath: indexAbs },
|
|
26
|
+
{ key: "./admin.tsx", moduleAbsPath: adminAbs },
|
|
27
|
+
];
|
|
37
28
|
|
|
38
29
|
test("generates dynamic import source using forward-slash module paths", () => {
|
|
39
|
-
const
|
|
40
|
-
const adminAbs = path.resolve("/repo/apps/demo/page/admin.tsx");
|
|
41
|
-
const source = PagesEntrySourceGenerator.generate([
|
|
42
|
-
{ key: "./_index.tsx", moduleAbsPath: indexAbs },
|
|
43
|
-
{ key: "./admin.tsx", moduleAbsPath: adminAbs },
|
|
44
|
-
]);
|
|
30
|
+
const source = PagesEntrySourceGenerator.generate(entries);
|
|
45
31
|
|
|
46
32
|
expect(source).toBe(
|
|
47
33
|
[
|
|
@@ -55,12 +41,7 @@ describe("PagesEntrySourceGenerator", () => {
|
|
|
55
41
|
});
|
|
56
42
|
|
|
57
43
|
test("generates static import source for single-file CSR bundles", async () => {
|
|
58
|
-
const
|
|
59
|
-
const adminAbs = path.resolve("/repo/apps/demo/page/admin.tsx");
|
|
60
|
-
const source = await PagesEntrySourceGenerator.generateStatic([
|
|
61
|
-
{ key: "./_index.tsx", moduleAbsPath: indexAbs },
|
|
62
|
-
{ key: "./admin.tsx", moduleAbsPath: adminAbs },
|
|
63
|
-
]);
|
|
44
|
+
const source = await PagesEntrySourceGenerator.generateStatic(entries);
|
|
64
45
|
|
|
65
46
|
expect(source).toBe(
|
|
66
47
|
[
|
|
@@ -271,6 +252,16 @@ describe("CsrArtifactBuilder", () => {
|
|
|
271
252
|
});
|
|
272
253
|
|
|
273
254
|
describe("SsrBaseArtifactBuilder", () => {
|
|
255
|
+
test("a dev artifact serves every web surface, and a build the one akan.config.ts keeps", () => {
|
|
256
|
+
for (const web of [
|
|
257
|
+
{ ssr: true, csr: false },
|
|
258
|
+
{ ssr: false, csr: false },
|
|
259
|
+
]) {
|
|
260
|
+
expect(SsrBaseArtifactBuilder.servedWeb("start", web)).toEqual({ ssr: true, csr: true });
|
|
261
|
+
expect(SsrBaseArtifactBuilder.servedWeb("build", web)).toEqual(web);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
274
265
|
test("minifies CSS assets only for production builds", async () => {
|
|
275
266
|
const css = [
|
|
276
267
|
".card {",
|
|
@@ -333,13 +324,14 @@ describe("RoutesManifestArtifactSerializer", () => {
|
|
|
333
324
|
describe("HmrChangeClassifier", () => {
|
|
334
325
|
test("classifies code, css, config, and ignored files", () => {
|
|
335
326
|
const classifier = new HmrChangeClassifier();
|
|
336
|
-
|
|
337
|
-
expect(classifier.classify("
|
|
338
|
-
expect(classifier.classify("/
|
|
339
|
-
expect(classifier.classify("
|
|
340
|
-
expect(classifier.classify(
|
|
341
|
-
expect(classifier.classify(
|
|
342
|
-
expect(classifier.classify("/
|
|
327
|
+
const app = path.resolve("/repo/apps/demo");
|
|
328
|
+
expect(classifier.classify(path.join(app, "page/_index.tsx"))).toBe("code");
|
|
329
|
+
expect(classifier.classify(path.join(app, "page/styles.css"))).toBe("css");
|
|
330
|
+
expect(classifier.classify(path.join(app, "akan.config.ts"))).toBe("config");
|
|
331
|
+
expect(classifier.classify(path.join(app, ".DS_Store"))).toBe("ignore");
|
|
332
|
+
expect(classifier.classify(path.join(app, "node_modules/pkg/index.ts"))).toBe("ignore");
|
|
333
|
+
expect(classifier.classify(path.join(app, ".akan/generated/page.tsx"))).toBe("ignore");
|
|
334
|
+
expect(classifier.classify(path.join(app, "public/logo.png"))).toBe("ignore");
|
|
343
335
|
});
|
|
344
336
|
});
|
|
345
337
|
|
|
@@ -394,16 +386,16 @@ describe("DevGeneratedIndexSync", () => {
|
|
|
394
386
|
|
|
395
387
|
describe("DevChangePlanner", () => {
|
|
396
388
|
test("classifies server, client, shared, and generated barrel changes", () => {
|
|
397
|
-
const root = "/repo";
|
|
389
|
+
const root = path.resolve("/repo");
|
|
398
390
|
const planner = new DevChangePlanner({ workspaceRoot: root });
|
|
399
|
-
const generatedIndex =
|
|
391
|
+
const generatedIndex = path.join(root, "libs/shared/common/index.ts");
|
|
400
392
|
const plan = planner.plan({
|
|
401
393
|
generation: 7,
|
|
402
394
|
files: [
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
395
|
+
path.join(root, "libs/shared/lib/admin/admin.service.ts"),
|
|
396
|
+
path.join(root, "libs/shared/lib/admin/Admin.Template.tsx"),
|
|
397
|
+
path.join(root, "libs/shared/lib/admin/admin.constant.ts"),
|
|
398
|
+
path.join(root, "libs/shared/common/foo.ts"),
|
|
407
399
|
],
|
|
408
400
|
kinds: ["code"],
|
|
409
401
|
generatedFiles: [generatedIndex],
|
|
@@ -429,21 +421,22 @@ describe("DevChangePlanner", () => {
|
|
|
429
421
|
});
|
|
430
422
|
|
|
431
423
|
test("recycles builder for macro-backed dictionary and signal metadata changes", () => {
|
|
432
|
-
const root = "/repo";
|
|
424
|
+
const root = path.resolve("/repo");
|
|
433
425
|
const planner = new DevChangePlanner({ workspaceRoot: root });
|
|
426
|
+
const dictionaryFile = path.join(root, "apps/demo/lib/_demo/demo.dictionary.ts");
|
|
434
427
|
const dictionaryPlan = planner.plan({
|
|
435
428
|
generation: 3,
|
|
436
|
-
files: [
|
|
429
|
+
files: [dictionaryFile],
|
|
437
430
|
kinds: ["code"],
|
|
438
431
|
});
|
|
439
432
|
const signalPlan = planner.plan({
|
|
440
433
|
generation: 4,
|
|
441
|
-
files: [
|
|
434
|
+
files: [path.join(root, "libs/shared/lib/admin/admin.signal.ts")],
|
|
442
435
|
kinds: ["code"],
|
|
443
436
|
});
|
|
444
437
|
|
|
445
438
|
expect(dictionaryPlan.actions).toEqual(["rebuild-client", "restart-backend", "restart-builder"]);
|
|
446
|
-
expect(dictionaryPlan.reasonByFile[
|
|
439
|
+
expect(dictionaryPlan.reasonByFile[dictionaryFile]).toContain("runtime-metadata");
|
|
447
440
|
expect(signalPlan.actions).toContain("restart-builder");
|
|
448
441
|
});
|
|
449
442
|
});
|
|
@@ -499,6 +492,14 @@ describe("CssImportResolver", () => {
|
|
|
499
492
|
});
|
|
500
493
|
|
|
501
494
|
describe("CssCompiler", () => {
|
|
495
|
+
const cssCompilerFor = (root: string, cwdPath: string, app: object = {}) =>
|
|
496
|
+
new CssCompiler({
|
|
497
|
+
workspace: { workspaceRoot: root },
|
|
498
|
+
cwdPath,
|
|
499
|
+
getTsConfig: async () => ({ compilerOptions: { paths: {} } }),
|
|
500
|
+
...app,
|
|
501
|
+
} as never);
|
|
502
|
+
|
|
502
503
|
test("scans installed akanjs sources while ignoring other node_modules", async () => {
|
|
503
504
|
expect(isIgnoredNodeModuleSource("/repo/node_modules/react/index.js")).toBe(true);
|
|
504
505
|
expect(isIgnoredNodeModuleSource("/repo/node_modules/akanjs/ui/Button.tsx")).toBe(false);
|
|
@@ -512,13 +513,8 @@ describe("CssCompiler", () => {
|
|
|
512
513
|
await write(cssPath, `@import ${JSON.stringify(tailwindCssPath)};\n@source "./**/*";\n`);
|
|
513
514
|
await write(uiSource, 'export const Button = () => <button className="text-fuchsia-500" />;\n');
|
|
514
515
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
// The candidate cache lands under `<cwdPath>/.akan/cache`, so point it at this test's temp root
|
|
518
|
-
// rather than wherever the suite happens to run from.
|
|
519
|
-
cwdPath: root,
|
|
520
|
-
getTsConfig: async () => ({ compilerOptions: { paths: {} } }),
|
|
521
|
-
} as never);
|
|
516
|
+
// The candidate cache lands under `<cwdPath>/.akan/cache`, so keep it inside this test's temp root.
|
|
517
|
+
const compiler = cssCompilerFor(root, root);
|
|
522
518
|
const css = await compiler.compileCss([cssPath], []);
|
|
523
519
|
|
|
524
520
|
expect(css).toContain(".text-fuchsia-500");
|
|
@@ -540,11 +536,7 @@ describe("CssCompiler", () => {
|
|
|
540
536
|
const cssPath = path.join(root, "apps/demo/page/styles.css");
|
|
541
537
|
await write(cssPath, '@import "../../../libs/shared/ui/tokens.css";\n');
|
|
542
538
|
|
|
543
|
-
const compiler =
|
|
544
|
-
workspace: { workspaceRoot: root },
|
|
545
|
-
cwdPath: path.join(root, "apps/demo"),
|
|
546
|
-
getTsConfig: async () => ({ compilerOptions: { paths: {} } }),
|
|
547
|
-
} as never);
|
|
539
|
+
const compiler = cssCompilerFor(root, path.join(root, "apps/demo"));
|
|
548
540
|
|
|
549
541
|
await expect(compiler.compileCss([cssPath], [])).rejects.toThrow(
|
|
550
542
|
/failed to resolve stylesheet import "\.\.\/\.\.\/\.\.\/libs\/shared\/ui\/tokens\.css"/,
|
|
@@ -558,13 +550,10 @@ describe("CssCompiler", () => {
|
|
|
558
550
|
await write(path.join(appDir, "page/styles.css"), '@import "../../../libs/shared/ui/brand.css";\n');
|
|
559
551
|
await write(path.join(root, "libs/shared/ui/brand.css"), ":root { --kakao: #fee500; --naver: #1ec800; }\n");
|
|
560
552
|
|
|
561
|
-
const compiler =
|
|
562
|
-
workspace: { workspaceRoot: root },
|
|
563
|
-
cwdPath: appDir,
|
|
553
|
+
const compiler = cssCompilerFor(root, appDir, {
|
|
564
554
|
getPageKeys: async () => ["./_index.tsx"],
|
|
565
555
|
getConfig: async () => ({ barrelImports: [], basePaths: [] }),
|
|
566
|
-
|
|
567
|
-
} as never);
|
|
556
|
+
});
|
|
568
557
|
const cssByBasePath = await compiler.getCssByBasePath();
|
|
569
558
|
|
|
570
559
|
expect(cssByBasePath[""]).toContain("--kakao");
|
|
@@ -585,13 +574,11 @@ describe("CssCompiler", () => {
|
|
|
585
574
|
await write(path.join(root, "libs/shared/ui/tokens.css"), ":root { --kakao: #fee500; }\n");
|
|
586
575
|
await write(path.join(root, "libs/unused/ui/tokens.css"), ":root { --unused: #000000; }\n");
|
|
587
576
|
|
|
588
|
-
const compiler =
|
|
589
|
-
workspace: { workspaceRoot: root },
|
|
590
|
-
cwdPath: appDir,
|
|
577
|
+
const compiler = cssCompilerFor(root, appDir, {
|
|
591
578
|
getPageKeys: async () => ["./_index.tsx"],
|
|
592
579
|
getConfig: async () => ({ barrelImports: [] }),
|
|
593
580
|
getTsConfig: async () => ({ compilerOptions: { paths: { "@libs/*": ["./libs/*"] } } }),
|
|
594
|
-
}
|
|
581
|
+
});
|
|
595
582
|
const { cssPaths } = await compiler.discoverCssAndSources();
|
|
596
583
|
|
|
597
584
|
expect(cssPaths).toEqual([path.join(root, "libs/shared/ui/tokens.css"), path.join(appDir, "page/styles.css")]);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import type { ChangeKind } from "akanjs/server";
|
|
3
|
+
import { SOURCE_EXTS } from "../akanApp/devHostPolicy";
|
|
3
4
|
|
|
4
|
-
const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
5
5
|
const CSS_EXTS = new Set([".css"]);
|
|
6
6
|
const CONFIG_BASENAMES = new Set(["akan.config.ts", "bunfig.toml", "tsconfig.json", "package.json"]);
|
|
7
7
|
|
|
@@ -19,7 +19,7 @@ export class HmrChangeClassifier {
|
|
|
19
19
|
#isUninteresting(abs: string): boolean {
|
|
20
20
|
const base = path.basename(abs);
|
|
21
21
|
if (!base) return true;
|
|
22
|
-
if (base.startsWith(".")) return true;
|
|
22
|
+
if (base.startsWith(".")) return true;
|
|
23
23
|
if (base.endsWith("~") || base.endsWith(".swp") || base.endsWith(".swx") || base.endsWith(".tmp")) return true;
|
|
24
24
|
if (abs.includes(`${path.sep}node_modules${path.sep}`)) return true;
|
|
25
25
|
if (abs.includes(`${path.sep}.akan${path.sep}`)) return true;
|