@akanjs/devkit 3.0.0-beta.2 → 3.0.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +6 -8
- package/agentsIndex.test.ts +4 -7
- package/agentsIndex.ts +6 -22
- package/akanApp/BackendImportGraph.test.ts +8 -22
- package/akanApp/BackendImportGraph.ts +1 -20
- package/akanApp/akanApp.host.test.ts +1 -3
- package/akanApp/akanApp.host.ts +82 -254
- package/akanApp/devHostPolicy.test.ts +10 -34
- package/akanApp/devHostPolicy.ts +12 -98
- package/akanConfig/akanConfig.test.ts +85 -69
- package/akanConfig/akanConfig.ts +69 -153
- package/akanConfig/types.ts +1 -0
- package/akanContext.ts +106 -159
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.test.ts +5 -3
- package/applicationBuildRunner.ts +54 -61
- package/applicationReleasePackager.ts +24 -44
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +9 -30
- package/artifact/implicitRootLayout.ts +34 -106
- package/artifact/routeSeedIndex.test.ts +7 -6
- package/artifact/routeSeedIndex.ts +11 -44
- package/biome.base.json +6 -1
- package/biomeBase.ts +2 -5
- package/capacitorApp.test.ts +32 -16
- package/capacitorApp.ts +110 -238
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +77 -39
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +26 -5
- package/cloud/globalConfig.ts +70 -32
- package/codeAgent/agent/AkanCodeServices.ts +97 -0
- package/codeAgent/agent/AkanContextFiles.ts +94 -0
- package/codeAgent/agent/CodeAgent.ts +551 -0
- package/codeAgent/agent/CodeAgentAsks.ts +66 -0
- package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
- package/codeAgent/agent/CodeAgentGate.ts +93 -0
- package/codeAgent/agent/CodeAgentProxy.ts +82 -0
- package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
- package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
- package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
- package/codeAgent/agent/CodeAgentUi.ts +47 -0
- package/codeAgent/agent/CodeMailbox.ts +148 -0
- package/codeAgent/agent/CodeSessionFork.ts +56 -0
- package/codeAgent/agent/CodeSessionIndex.ts +78 -0
- package/codeAgent/agent/SubagentPool.ts +214 -0
- package/codeAgent/agent/akanCodeModel.ts +47 -0
- package/codeAgent/agent/akanCodePaths.ts +31 -0
- package/codeAgent/agent/akanContextFiles.test.ts +114 -0
- package/codeAgent/agent/akanSystemPrompt.ts +46 -0
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +523 -0
- package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
- package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
- package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
- package/codeAgent/agent/codeMailbox.test.ts +76 -0
- package/codeAgent/agent/codeSessionFork.test.ts +58 -0
- package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
- package/codeAgent/agent/consoleToStderr.ts +8 -0
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +82 -0
- package/codeAgent/feedback/PreviewChrome.ts +82 -0
- package/codeAgent/feedback/PreviewFeedback.ts +46 -0
- package/codeAgent/feedback/PreviewView.ts +95 -0
- package/codeAgent/feedback/TurnFeedback.ts +98 -0
- package/codeAgent/feedback/VerifyFeedback.ts +40 -0
- package/codeAgent/feedback/previewChrome.test.ts +73 -0
- package/codeAgent/index.ts +34 -0
- package/codeAgent/skills/akan-code/SKILL.md +130 -0
- package/codeAgent/skills/akan-debug/SKILL.md +42 -0
- package/codeAgent/skills/akan-deps/SKILL.md +50 -0
- package/codeAgent/skills/akan-err/SKILL.md +39 -0
- package/codeAgent/skills/akan-module/SKILL.md +45 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
- package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
- package/codeAgent/skills/akan-service/SKILL.md +34 -0
- package/codeAgent/skills/akan-store/SKILL.md +67 -0
- package/codeAgent/skills/akan-ui/SKILL.md +63 -0
- package/codeAgent/skills/akan-validate/SKILL.md +44 -0
- package/codeAgent/tools/AkanCli.ts +50 -0
- package/codeAgent/tools/AkanCodePlugins.ts +136 -0
- package/codeAgent/tools/AkanEditScope.ts +48 -0
- package/codeAgent/tools/AkanEnvKeys.ts +50 -0
- package/codeAgent/tools/AkanToolPack.ts +92 -0
- package/codeAgent/tools/AkanVerifier.ts +48 -0
- package/codeAgent/tools/AskToolPack.ts +99 -0
- package/codeAgent/tools/MailToolPack.ts +76 -0
- package/codeAgent/tools/McpClient.ts +187 -0
- package/codeAgent/tools/McpOAuth.ts +233 -0
- package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
- package/codeAgent/tools/McpServerConfig.ts +129 -0
- package/codeAgent/tools/McpSignIn.ts +158 -0
- package/codeAgent/tools/McpTokenStore.ts +81 -0
- package/codeAgent/tools/McpToolPack.ts +199 -0
- package/codeAgent/tools/SessionToolPack.ts +91 -0
- package/codeAgent/tools/WebToolPack.ts +147 -0
- package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
- package/codeAgent/tools/askToolPack.test.ts +116 -0
- package/codeAgent/tools/mcpOAuth.test.ts +612 -0
- package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
- package/codegenLock.test.ts +4 -13
- package/codegenLock.ts +3 -17
- package/commandDecorators/argMeta.ts +1 -5
- package/commandDecorators/camelToKebabCase.ts +1 -0
- package/commandDecorators/command.ts +46 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +2 -10
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +68 -145
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +31 -80
- package/devkitUtils.test.ts +48 -20
- package/executors.linkFallback.test.ts +103 -0
- package/executors.test.ts +132 -106
- package/executors.ts +194 -484
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +5 -3
- package/fileSys.ts +6 -8
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +27 -38
- package/frontendBuild/autoImportSync.ts +31 -65
- package/frontendBuild/buildRouteClient.test.ts +62 -37
- package/frontendBuild/bundleDefine.ts +12 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.ts +6 -41
- package/frontendBuild/csrArtifactBuilder.ts +6 -25
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +29 -78
- package/frontendBuild/cssImportResolver.ts +6 -19
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/fontOptimizer.test.ts +6 -14
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +46 -69
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +8 -32
- package/frontendBuild/hmrWatcher.ts +14 -71
- package/frontendBuild/pagesBundleBuilder.ts +4 -29
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/routeClientBuilder.ts +23 -46
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
- package/frontendBuild/styleContract.ts +1 -8
- package/frontendBuild/themeValidator.ts +4 -26
- package/frontendBuild/watchRootResolver.test.ts +23 -42
- package/frontendBuild/watchRootResolver.ts +5 -27
- package/getRelatedCnsts.ts +12 -92
- package/incrementalBuilder/buildBatch.proc.ts +22 -57
- package/incrementalBuilder/buildBatchProtocol.ts +8 -36
- package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
- package/incrementalBuilder/buildBatchRunner.ts +10 -31
- package/incrementalBuilder/builderChannel.test.ts +24 -49
- package/incrementalBuilder/builderChannel.ts +6 -37
- package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
- package/incrementalBuilder/builderRequestRouter.ts +5 -24
- package/incrementalBuilder/devWatchBatch.test.ts +21 -24
- package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
- package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +49 -161
- package/index.ts +5 -17
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +47 -194
- package/integration/devStabilityHarness.test.ts +13 -28
- package/integration/devStabilityHarness.ts +65 -330
- package/integration/ssrMemoryProbe.ts +41 -167
- package/libSource.test.ts +4 -15
- package/libSource.ts +19 -31
- package/lint/gritRules.test.ts +8 -24
- package/lint/no-deep-internal-import.grit +2 -2
- package/lint/no-redeclare-predefined-endpoint.grit +2 -1
- package/linter.test.ts +1 -2
- package/linter.ts +26 -116
- package/mobile/mobileTarget.ts +14 -13
- package/package.json +10 -5
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +46 -0
- package/platformTest/BunTestReport.test.ts +60 -0
- package/platformTest/BunTestReport.ts +65 -0
- package/platformTest/IdleWatchedProcess.test.ts +56 -0
- package/platformTest/IdleWatchedProcess.ts +73 -0
- package/platformTest/LinuxDockerTarget.ts +105 -0
- package/platformTest/PlatformTestRun.test.ts +64 -0
- package/platformTest/PlatformTestRun.ts +141 -0
- package/platformTest/PlatformTestTarget.ts +142 -0
- package/platformTest/TestSnapshot.test.ts +72 -0
- package/platformTest/TestSnapshot.ts +103 -0
- package/platformTest/WindowsSshTarget.ts +151 -0
- package/prompter.ts +3 -13
- package/qualityScanner.test.ts +9 -26
- package/qualityScanner.ts +197 -285
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.test.ts +0 -1
- package/routeSourceValidator.ts +7 -42
- package/scanInfo.ts +87 -216
- package/selectModel.ts +1 -2
- package/semver.ts +3 -5
- package/slicePlanner.test.ts +6 -24
- package/slicePlanner.ts +11 -32
- package/spinner.test.ts +3 -8
- package/spinner.ts +4 -16
- package/sshTunnel.ts +1 -6
- package/ssrScanner.test.ts +1 -6
- package/ssrScanner.ts +103 -113
- package/stripAnsi.ts +3 -0
- package/subspace.test.ts +30 -25
- package/subspace.ts +81 -115
- package/subspaceConfig.ts +11 -9
- package/testHelpers.ts +194 -0
- package/transforms/asyncDefaultExportDetector.ts +3 -11
- package/transforms/barrelAnalyzer.ts +43 -135
- package/transforms/barrelImportsPlugin.ts +38 -175
- package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
- package/transforms/externalizeFrameworkPlugin.ts +13 -87
- package/transforms/moduleSyntax.ts +32 -0
- package/transforms/rscUseClientTransform.ts +35 -20
- package/transforms/transforms.test.ts +153 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +2 -22
- package/typeChecker.ts +7 -63
- package/types.ts +1 -0
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +101 -110
- package/workflow/index.ts +0 -1
- package/workflow/moduleIndex.test.ts +4 -15
- package/workflow/moduleIndex.ts +9 -49
- package/workflow/plan.ts +18 -25
- package/workflow/render.ts +80 -96
- package/workflow/rolloutGate.test.ts +1 -1
- package/workflow/source.ts +52 -69
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.ts +4 -19
- package/abstractCompactor.ts +0 -112
- package/aiEditor.test.ts +0 -60
- package/aiEditor.ts +0 -387
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/guideline.ts +0 -19
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
|
@@ -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";
|
|
@@ -15,33 +15,19 @@ import { PagesEntrySourceGenerator } from "./pagesEntrySourceGenerator";
|
|
|
15
15
|
import { RoutesManifestArtifactSerializer } from "./routesManifestArtifactSerializer";
|
|
16
16
|
import { prepareCssAsset } 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
|
[
|
|
@@ -333,13 +314,14 @@ describe("RoutesManifestArtifactSerializer", () => {
|
|
|
333
314
|
describe("HmrChangeClassifier", () => {
|
|
334
315
|
test("classifies code, css, config, and ignored files", () => {
|
|
335
316
|
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("/
|
|
317
|
+
const app = path.resolve("/repo/apps/demo");
|
|
318
|
+
expect(classifier.classify(path.join(app, "page/_index.tsx"))).toBe("code");
|
|
319
|
+
expect(classifier.classify(path.join(app, "page/styles.css"))).toBe("css");
|
|
320
|
+
expect(classifier.classify(path.join(app, "akan.config.ts"))).toBe("config");
|
|
321
|
+
expect(classifier.classify(path.join(app, ".DS_Store"))).toBe("ignore");
|
|
322
|
+
expect(classifier.classify(path.join(app, "node_modules/pkg/index.ts"))).toBe("ignore");
|
|
323
|
+
expect(classifier.classify(path.join(app, ".akan/generated/page.tsx"))).toBe("ignore");
|
|
324
|
+
expect(classifier.classify(path.join(app, "public/logo.png"))).toBe("ignore");
|
|
343
325
|
});
|
|
344
326
|
});
|
|
345
327
|
|
|
@@ -394,16 +376,16 @@ describe("DevGeneratedIndexSync", () => {
|
|
|
394
376
|
|
|
395
377
|
describe("DevChangePlanner", () => {
|
|
396
378
|
test("classifies server, client, shared, and generated barrel changes", () => {
|
|
397
|
-
const root = "/repo";
|
|
379
|
+
const root = path.resolve("/repo");
|
|
398
380
|
const planner = new DevChangePlanner({ workspaceRoot: root });
|
|
399
|
-
const generatedIndex =
|
|
381
|
+
const generatedIndex = path.join(root, "libs/shared/common/index.ts");
|
|
400
382
|
const plan = planner.plan({
|
|
401
383
|
generation: 7,
|
|
402
384
|
files: [
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
385
|
+
path.join(root, "libs/shared/lib/admin/admin.service.ts"),
|
|
386
|
+
path.join(root, "libs/shared/lib/admin/Admin.Template.tsx"),
|
|
387
|
+
path.join(root, "libs/shared/lib/admin/admin.constant.ts"),
|
|
388
|
+
path.join(root, "libs/shared/common/foo.ts"),
|
|
407
389
|
],
|
|
408
390
|
kinds: ["code"],
|
|
409
391
|
generatedFiles: [generatedIndex],
|
|
@@ -429,21 +411,22 @@ describe("DevChangePlanner", () => {
|
|
|
429
411
|
});
|
|
430
412
|
|
|
431
413
|
test("recycles builder for macro-backed dictionary and signal metadata changes", () => {
|
|
432
|
-
const root = "/repo";
|
|
414
|
+
const root = path.resolve("/repo");
|
|
433
415
|
const planner = new DevChangePlanner({ workspaceRoot: root });
|
|
416
|
+
const dictionaryFile = path.join(root, "apps/demo/lib/_demo/demo.dictionary.ts");
|
|
434
417
|
const dictionaryPlan = planner.plan({
|
|
435
418
|
generation: 3,
|
|
436
|
-
files: [
|
|
419
|
+
files: [dictionaryFile],
|
|
437
420
|
kinds: ["code"],
|
|
438
421
|
});
|
|
439
422
|
const signalPlan = planner.plan({
|
|
440
423
|
generation: 4,
|
|
441
|
-
files: [
|
|
424
|
+
files: [path.join(root, "libs/shared/lib/admin/admin.signal.ts")],
|
|
442
425
|
kinds: ["code"],
|
|
443
426
|
});
|
|
444
427
|
|
|
445
428
|
expect(dictionaryPlan.actions).toEqual(["rebuild-client", "restart-backend", "restart-builder"]);
|
|
446
|
-
expect(dictionaryPlan.reasonByFile[
|
|
429
|
+
expect(dictionaryPlan.reasonByFile[dictionaryFile]).toContain("runtime-metadata");
|
|
447
430
|
expect(signalPlan.actions).toContain("restart-builder");
|
|
448
431
|
});
|
|
449
432
|
});
|
|
@@ -499,6 +482,14 @@ describe("CssImportResolver", () => {
|
|
|
499
482
|
});
|
|
500
483
|
|
|
501
484
|
describe("CssCompiler", () => {
|
|
485
|
+
const cssCompilerFor = (root: string, cwdPath: string, app: object = {}) =>
|
|
486
|
+
new CssCompiler({
|
|
487
|
+
workspace: { workspaceRoot: root },
|
|
488
|
+
cwdPath,
|
|
489
|
+
getTsConfig: async () => ({ compilerOptions: { paths: {} } }),
|
|
490
|
+
...app,
|
|
491
|
+
} as never);
|
|
492
|
+
|
|
502
493
|
test("scans installed akanjs sources while ignoring other node_modules", async () => {
|
|
503
494
|
expect(isIgnoredNodeModuleSource("/repo/node_modules/react/index.js")).toBe(true);
|
|
504
495
|
expect(isIgnoredNodeModuleSource("/repo/node_modules/akanjs/ui/Button.tsx")).toBe(false);
|
|
@@ -512,13 +503,8 @@ describe("CssCompiler", () => {
|
|
|
512
503
|
await write(cssPath, `@import ${JSON.stringify(tailwindCssPath)};\n@source "./**/*";\n`);
|
|
513
504
|
await write(uiSource, 'export const Button = () => <button className="text-fuchsia-500" />;\n');
|
|
514
505
|
|
|
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);
|
|
506
|
+
// The candidate cache lands under `<cwdPath>/.akan/cache`, so keep it inside this test's temp root.
|
|
507
|
+
const compiler = cssCompilerFor(root, root);
|
|
522
508
|
const css = await compiler.compileCss([cssPath], []);
|
|
523
509
|
|
|
524
510
|
expect(css).toContain(".text-fuchsia-500");
|
|
@@ -540,11 +526,7 @@ describe("CssCompiler", () => {
|
|
|
540
526
|
const cssPath = path.join(root, "apps/demo/page/styles.css");
|
|
541
527
|
await write(cssPath, '@import "../../../libs/shared/ui/tokens.css";\n');
|
|
542
528
|
|
|
543
|
-
const compiler =
|
|
544
|
-
workspace: { workspaceRoot: root },
|
|
545
|
-
cwdPath: path.join(root, "apps/demo"),
|
|
546
|
-
getTsConfig: async () => ({ compilerOptions: { paths: {} } }),
|
|
547
|
-
} as never);
|
|
529
|
+
const compiler = cssCompilerFor(root, path.join(root, "apps/demo"));
|
|
548
530
|
|
|
549
531
|
await expect(compiler.compileCss([cssPath], [])).rejects.toThrow(
|
|
550
532
|
/failed to resolve stylesheet import "\.\.\/\.\.\/\.\.\/libs\/shared\/ui\/tokens\.css"/,
|
|
@@ -558,13 +540,10 @@ describe("CssCompiler", () => {
|
|
|
558
540
|
await write(path.join(appDir, "page/styles.css"), '@import "../../../libs/shared/ui/brand.css";\n');
|
|
559
541
|
await write(path.join(root, "libs/shared/ui/brand.css"), ":root { --kakao: #fee500; --naver: #1ec800; }\n");
|
|
560
542
|
|
|
561
|
-
const compiler =
|
|
562
|
-
workspace: { workspaceRoot: root },
|
|
563
|
-
cwdPath: appDir,
|
|
543
|
+
const compiler = cssCompilerFor(root, appDir, {
|
|
564
544
|
getPageKeys: async () => ["./_index.tsx"],
|
|
565
545
|
getConfig: async () => ({ barrelImports: [], basePaths: [] }),
|
|
566
|
-
|
|
567
|
-
} as never);
|
|
546
|
+
});
|
|
568
547
|
const cssByBasePath = await compiler.getCssByBasePath();
|
|
569
548
|
|
|
570
549
|
expect(cssByBasePath[""]).toContain("--kakao");
|
|
@@ -585,13 +564,11 @@ describe("CssCompiler", () => {
|
|
|
585
564
|
await write(path.join(root, "libs/shared/ui/tokens.css"), ":root { --kakao: #fee500; }\n");
|
|
586
565
|
await write(path.join(root, "libs/unused/ui/tokens.css"), ":root { --unused: #000000; }\n");
|
|
587
566
|
|
|
588
|
-
const compiler =
|
|
589
|
-
workspace: { workspaceRoot: root },
|
|
590
|
-
cwdPath: appDir,
|
|
567
|
+
const compiler = cssCompilerFor(root, appDir, {
|
|
591
568
|
getPageKeys: async () => ["./_index.tsx"],
|
|
592
569
|
getConfig: async () => ({ barrelImports: [] }),
|
|
593
570
|
getTsConfig: async () => ({ compilerOptions: { paths: { "@libs/*": ["./libs/*"] } } }),
|
|
594
|
-
}
|
|
571
|
+
});
|
|
595
572
|
const { cssPaths } = await compiler.discoverCssAndSources();
|
|
596
573
|
|
|
597
574
|
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;
|
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { chmod, mkdir,
|
|
3
|
-
import os from "node:os";
|
|
2
|
+
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import type { Logger } from "akanjs/common";
|
|
5
|
+
import { tempDirs } from "../testHelpers";
|
|
6
6
|
import { type ChangeBatch, HmrWatcher } from "./hmrWatcher";
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
* These run against the real Bun watcher rather than a fake, because the behaviour under test only
|
|
10
|
-
* exists there: Bun 1.3.14's recursive `fs.watch` reports about one path per ~200ms coalescing window and
|
|
11
|
-
* discards the rest (`local/optimize-resource/06-watcher-dropped-event.md`). A fake that delivered every
|
|
12
|
-
* event would pass no matter what the watcher did with them.
|
|
13
|
-
*/
|
|
8
|
+
// The real Bun watcher, not a fake: the dropped-event behaviour under test only exists there.
|
|
14
9
|
const STREAM_WARMUP_MS = 600;
|
|
15
10
|
const SETTLE_MS = 1_500;
|
|
16
11
|
const TEST_TIMEOUT_MS = 15_000;
|
|
17
12
|
|
|
18
13
|
const started: HmrWatcher[] = [];
|
|
19
|
-
const roots: string[] = [];
|
|
20
14
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
-
/** Every level `HmrWatcher` can reach, so adding a log line cannot fail a test for the wrong reason. */
|
|
22
15
|
const silentLogger = {
|
|
23
16
|
trace: () => undefined,
|
|
24
17
|
verbose: () => undefined,
|
|
@@ -29,12 +22,6 @@ const silentLogger = {
|
|
|
29
22
|
error: () => undefined,
|
|
30
23
|
} as unknown as Logger;
|
|
31
24
|
|
|
32
|
-
const makeRoot = async () => {
|
|
33
|
-
const root = await mkdtemp(path.join(os.tmpdir(), "akan-hmr-watcher-"));
|
|
34
|
-
roots.push(root);
|
|
35
|
-
return root;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
25
|
const seed = async (root: string, rel: string, content = "export const x = 1;\n") => {
|
|
39
26
|
const abs = path.join(root, rel);
|
|
40
27
|
await mkdir(path.dirname(abs), { recursive: true });
|
|
@@ -56,10 +43,10 @@ const watch = async (root: string) => {
|
|
|
56
43
|
return { watcher, batches, seen: () => new Set(batches.flatMap((batch) => batch.files)) };
|
|
57
44
|
};
|
|
58
45
|
|
|
59
|
-
afterEach(
|
|
46
|
+
afterEach(() => {
|
|
60
47
|
for (const watcher of started.splice(0)) watcher.stop();
|
|
61
|
-
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
62
48
|
});
|
|
49
|
+
const makeRoot = tempDirs("akan-hmr-watcher-");
|
|
63
50
|
|
|
64
51
|
describe("HmrWatcher", () => {
|
|
65
52
|
test(
|
|
@@ -69,15 +56,12 @@ describe("HmrWatcher", () => {
|
|
|
69
56
|
const files = await Promise.all([0, 1, 2, 3, 4].map((i) => seed(root, `lib/File${i}.ts`)));
|
|
70
57
|
const { batches, seen, watcher } = await watch(root);
|
|
71
58
|
|
|
72
|
-
// No gaps between writes, so they share one coalescing window and Bun names at most one of them.
|
|
73
59
|
for (const [i, abs] of files.entries()) await writeFile(abs, `export const x = ${i}00;\n`);
|
|
74
60
|
await sleep(SETTLE_MS);
|
|
75
61
|
|
|
76
62
|
expect(batches.length).toBeGreaterThan(0);
|
|
77
63
|
for (const abs of files) expect([...seen()]).toContain(abs);
|
|
78
|
-
// Not
|
|
79
|
-
// events, so this suite keeps passing if the upstream defect is ever fixed. The counter is the
|
|
80
|
-
// signal for when the compensation can be removed.
|
|
64
|
+
// Not `> 0`: the suite must keep passing if Bun ever stops dropping events.
|
|
81
65
|
expect(watcher.unreportedChanges).toBeGreaterThanOrEqual(0);
|
|
82
66
|
},
|
|
83
67
|
TEST_TIMEOUT_MS,
|
|
@@ -90,8 +74,6 @@ describe("HmrWatcher", () => {
|
|
|
90
74
|
const source = await seed(root, "lib/a.ts");
|
|
91
75
|
const { seen, batches } = await watch(root);
|
|
92
76
|
|
|
93
|
-
// What every build ends with: a burst under `.akan/`, which the classifier ignores. Before this
|
|
94
|
-
// was handled, the burst was the one path Bun reported and the save right after it was invisible.
|
|
95
77
|
const artifactDir = path.join(root, ".akan", "artifact", "server");
|
|
96
78
|
await mkdir(artifactDir, { recursive: true });
|
|
97
79
|
for (let i = 0; i < 60; i++) await writeFile(path.join(artifactDir, `chunk-${i}.js`), "x".repeat(8192));
|
|
@@ -100,8 +82,6 @@ describe("HmrWatcher", () => {
|
|
|
100
82
|
await sleep(SETTLE_MS);
|
|
101
83
|
|
|
102
84
|
expect([...seen()]).toContain(source);
|
|
103
|
-
// Exactly one batch for one save. Bun does deliver a real event for some paths the scan has already
|
|
104
|
-
// emitted, and counting both produced a second generation and a second build for no change.
|
|
105
85
|
expect(batches.filter((batch) => batch.files.includes(source))).toHaveLength(1);
|
|
106
86
|
},
|
|
107
87
|
TEST_TIMEOUT_MS,
|
|
@@ -119,7 +99,6 @@ describe("HmrWatcher", () => {
|
|
|
119
99
|
for (let i = 0; i < 20; i++) await writeFile(path.join(artifactDir, `chunk-${i}.js`), "x".repeat(4096));
|
|
120
100
|
await sleep(SETTLE_MS);
|
|
121
101
|
|
|
122
|
-
// The verification scan is triggered by ignored paths, so it must not invent work from them either.
|
|
123
102
|
expect(batches).toEqual([]);
|
|
124
103
|
},
|
|
125
104
|
TEST_TIMEOUT_MS,
|
|
@@ -161,7 +140,7 @@ describe("HmrWatcher", () => {
|
|
|
161
140
|
TEST_TIMEOUT_MS,
|
|
162
141
|
);
|
|
163
142
|
|
|
164
|
-
test.skipIf(process.getuid?.() === 0)(
|
|
143
|
+
test.skipIf(process.getuid?.() === 0 || process.platform === "win32")(
|
|
165
144
|
"warns at startup when part of the tree cannot be read",
|
|
166
145
|
async () => {
|
|
167
146
|
const root = await makeRoot();
|
|
@@ -176,12 +155,9 @@ describe("HmrWatcher", () => {
|
|
|
176
155
|
});
|
|
177
156
|
started.push(watcher);
|
|
178
157
|
await watcher.start();
|
|
179
|
-
// Restored before asserting
|
|
180
|
-
// cannot traverse, and the leak would fail the *next* test instead of this one.
|
|
158
|
+
// Restored before asserting: a failed assertion would leave a directory `rm` cannot traverse.
|
|
181
159
|
await chmod(path.dirname(hidden), 0o755);
|
|
182
160
|
|
|
183
|
-
// At startup rather than at the first save, because an unreadable root means edits under it never
|
|
184
|
-
// rebuild — waiting for a save to reveal that means waiting for the save that silently does nothing.
|
|
185
161
|
expect(warnings).toHaveLength(1);
|
|
186
162
|
expect(warnings[0]).toContain("will not rebuild");
|
|
187
163
|
expect(warnings[0]).toContain("EACCES");
|