@akanjs/devkit 3.0.0-alpha.9 → 3.0.0-alpha.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/DEV_RUNTIME_KNOBS.md +27 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/agentsIndex.test.ts +10 -0
- package/agentsIndex.ts +21 -1
- package/aiEditor.ts +1 -1
- package/akanApp/BackendImportGraph.test.ts +120 -0
- package/akanApp/BackendImportGraph.ts +167 -0
- package/akanApp/akanApp.host.test.ts +16 -678
- package/akanApp/akanApp.host.ts +113 -578
- package/akanApp/devHostPolicy.test.ts +542 -0
- package/akanApp/devHostPolicy.ts +442 -0
- package/akanApp/index.ts +2 -0
- package/akanConfig/akanConfig.test.ts +182 -14
- package/akanConfig/akanConfig.ts +143 -47
- package/akanConfig/types.ts +9 -0
- package/akanContext.ts +30 -14
- package/akanMcpContract.ts +25 -4
- package/appSelectionMemory.ts +28 -0
- package/applicationBuildRunner.test.ts +1 -1
- package/applicationBuildRunner.ts +45 -21
- package/artifact/implicitRootLayout.test.ts +67 -0
- package/artifact/implicitRootLayout.ts +27 -7
- package/artifact/routeSeedIndex.test.ts +1 -0
- package/biome.base.json +86 -47
- package/biomeBase.ts +6 -5
- package/capacitorApp.ts +10 -0
- package/cloud/globalConfig.ts +9 -2
- package/codegenLock.test.ts +137 -0
- package/codegenLock.ts +136 -0
- package/commandDecorators/argMeta.ts +9 -1
- package/commandDecorators/command.ts +94 -7
- package/commandDecorators/commandBuilder.ts +20 -5
- package/commandDecorators/commandDecorators.test.ts +115 -3
- package/commandDecorators/helpFormatter.ts +4 -0
- package/dependencyScanner.test.ts +99 -0
- package/dependencyScanner.ts +27 -24
- package/devkitUtils.test.ts +0 -104
- package/executors.test.ts +129 -4
- package/executors.ts +105 -24
- package/fileEditor.ts +19 -19
- package/fileSys.ts +19 -1
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/autoImportSync.test.ts +58 -0
- package/frontendBuild/autoImportSync.ts +7 -2
- package/frontendBuild/buildRouteClient.test.ts +47 -24
- package/frontendBuild/clientBuildTypes.ts +4 -0
- package/frontendBuild/clientEntriesBundler.ts +4 -1
- package/frontendBuild/clientEntryDiscovery.ts +2 -2
- package/frontendBuild/csrArtifactBuilder.ts +116 -84
- package/frontendBuild/cssCompiler.ts +123 -12
- package/frontendBuild/cssImportResolver.ts +8 -7
- package/frontendBuild/devGeneratedIndexSync.ts +3 -2
- package/frontendBuild/fontOptimizer.ts +37 -18
- package/frontendBuild/fontPruner.test.ts +220 -0
- package/frontendBuild/fontPruner.ts +206 -0
- package/frontendBuild/frontendBuild.test.ts +178 -10
- package/frontendBuild/hmrWatcher.ts +1 -1
- package/frontendBuild/index.ts +1 -1
- package/frontendBuild/pagesBundleBuilder.ts +3 -3
- package/frontendBuild/pagesEntrySourceGenerator.ts +11 -88
- package/frontendBuild/routeClientBuilder.ts +12 -5
- package/frontendBuild/sourceMtimeIndex.ts +1 -1
- package/frontendBuild/ssrBaseArtifactBuilder.ts +21 -4
- package/frontendBuild/styleContract.ts +14 -20
- package/frontendBuild/themeValidator.ts +22 -17
- package/frontendBuild/vendorSpecifiers.ts +1 -0
- package/frontendBuild/watchRootResolver.test.ts +67 -0
- package/frontendBuild/watchRootResolver.ts +13 -1
- package/incrementalBuilder/devWatchBatch.test.ts +18 -20
- package/incrementalBuilder/devWatchBatch.ts +6 -2
- package/incrementalBuilder/incrementalBuilder.host.ts +51 -7
- package/incrementalBuilder/incrementalBuilder.proc.ts +17 -6
- package/index.ts +0 -7
- package/integration/devStabilityHarness.ts +2 -10
- package/libSource.test.ts +112 -0
- package/libSource.ts +126 -0
- package/lint/__fixtures__/README.md +40 -0
- package/lint/__fixtures__/no-arbitrary-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-arbitrary-color/good.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/bad.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/good.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/fixture.json +1 -0
- package/lint/__fixtures__/no-bang-comment-in-client/good.tsx +3 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +7 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/good.tsx +5 -0
- package/lint/__fixtures__/no-deep-internal-import/bad.tsx +3 -0
- package/lint/__fixtures__/no-deep-internal-import/fixture.json +1 -0
- package/lint/__fixtures__/no-deep-internal-import/good.tsx +3 -0
- package/lint/__fixtures__/no-deprecated-log-level/bad.tsx +4 -0
- package/lint/__fixtures__/no-deprecated-log-level/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-functions/bad.tsx +2 -0
- package/lint/__fixtures__/no-import-client-functions/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-in-server/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-client-in-server/good.tsx +5 -0
- package/lint/__fixtures__/no-import-external-library/bad.tsx +4 -0
- package/lint/__fixtures__/no-import-external-library/good.tsx +5 -0
- package/lint/__fixtures__/no-import-server-in-client/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-server-in-client/good.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/fixture.json +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/good.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/good.tsx +5 -0
- package/lint/__fixtures__/no-inline-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-inline-color/good.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/bad.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/good.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/bad.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/bad.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/good.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/bad.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/good.tsx +8 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/bad.ts +10 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/fixture.json +1 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/good.ts +6 -0
- package/lint/__fixtures__/no-return-in-store-action/bad.tsx +2 -0
- package/lint/__fixtures__/no-return-in-store-action/good.tsx +6 -0
- package/lint/__fixtures__/no-throw-raw-error/bad.tsx +4 -0
- package/lint/__fixtures__/no-throw-raw-error/good.tsx +4 -0
- package/lint/__fixtures__/no-unpublished-form-setter/bad.tsx +3 -0
- package/lint/__fixtures__/no-unpublished-form-setter/good.tsx +5 -0
- package/lint/__fixtures__/no-use-client-in-server/bad.tsx +3 -0
- package/lint/__fixtures__/no-use-client-in-server/good.tsx +2 -0
- package/lint/__fixtures__/non-scalar-props-restricted/bad.tsx +5 -0
- package/lint/__fixtures__/non-scalar-props-restricted/good.tsx +4 -0
- package/lint/gritRules.test.ts +178 -0
- package/lint/no-arbitrary-color.grit +1 -1
- package/lint/no-async-component-in-ui.grit +35 -0
- package/lint/no-bang-comment-in-client.grit +23 -10
- package/lint/no-daisyui-legacy-class.grit +26 -9
- package/lint/no-deprecated-log-level.grit +17 -0
- package/lint/no-init-fetch-in-client.grit +47 -0
- package/lint/no-inline-color.grit +10 -8
- package/lint/no-interpolated-arbitrary-class.grit +3 -3
- package/lint/no-model-type-in-util-zone.grit +58 -0
- package/lint/no-raw-palette-class.grit +3 -3
- package/lint/no-unpublished-form-setter.grit +41 -0
- package/lint/non-scalar-props-restricted.grit +16 -7
- package/linter.test.ts +80 -0
- package/linter.ts +99 -25
- package/package.json +5 -8
- package/prompter.ts +9 -4
- package/qualityScanner.test.ts +116 -0
- package/qualityScanner.ts +89 -21
- package/recipeScanner.ts +4 -1
- package/scanInfo.ts +32 -26
- package/semver.test.ts +26 -0
- package/semver.ts +31 -0
- package/slicePlanner.test.ts +182 -0
- package/slicePlanner.ts +235 -0
- package/ssrScanner.test.ts +301 -0
- package/subspace.test.ts +418 -0
- package/subspace.ts +723 -0
- package/subspaceConfig.ts +76 -0
- package/transforms/asyncDefaultExportDetector.ts +103 -0
- package/transforms/barrelImportsPlugin.ts +1 -1
- package/transforms/externalizeFrameworkPlugin.ts +0 -1
- package/transforms/transforms.test.ts +5 -5
- package/transforms/tsconfigPackageResolver.test.ts +230 -0
- package/tsconfig.json +6 -2
- package/typeChecker.ts +1 -1
- package/types.ts +1 -0
- package/uploadRelease.ts +2 -2
- package/workflow/executor.test.ts +146 -0
- package/workspaceLayout.test.ts +56 -4
- package/workspaceLayout.ts +48 -2
- package/builder.ts +0 -164
- package/extractDeps.ts +0 -86
- package/frontendBuild/styleGuard.test.ts +0 -165
- package/frontendBuild/styleGuard.ts +0 -322
- package/getCredentials.ts +0 -19
- package/getModelFileData.ts +0 -62
- package/src/capacitorApp.ts +0 -282
- package/streamAi.ts +0 -45
- package/ui/MultiScrollList.tsx +0 -242
- package/ui/ScrollList.tsx +0 -107
- package/ui/index.ts +0 -2
- package/useStdoutDimensions.ts +0 -20
|
@@ -377,7 +377,7 @@ export class SourceMtimeIndex {
|
|
|
377
377
|
return !name || name.startsWith(".") || name === "node_modules";
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
-
/** `WatchRootResolver` can return `apps/<app>/page` alongside `apps
|
|
380
|
+
/** `WatchRootResolver` can return `apps/<app>/page` alongside `apps/<app>`; walking both doubles the work. */
|
|
381
381
|
static #pruneNestedRoots(roots: string[]): string[] {
|
|
382
382
|
const resolved = [...new Set(roots.map((root) => path.resolve(root)))].sort();
|
|
383
383
|
return resolved.filter(
|
|
@@ -5,7 +5,7 @@ import { resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
|
|
|
5
5
|
import { computeRouteSeedIndex, type RouteSeedIndex, saveRouteSeedIndex } from "../artifact/routeSeedIndex";
|
|
6
6
|
import type { App } from "../commandDecorators";
|
|
7
7
|
import { ClientEntriesBundler } from "./clientEntriesBundler";
|
|
8
|
-
import { CssCompiler } from "./cssCompiler";
|
|
8
|
+
import { CssCompiler, type ImportedStylesheet } from "./cssCompiler";
|
|
9
9
|
import { FontOptimizer } from "./fontOptimizer";
|
|
10
10
|
import { PagesBundleBuilder } from "./pagesBundleBuilder";
|
|
11
11
|
import { RouteClientBuilder } from "./routeClientBuilder";
|
|
@@ -77,6 +77,7 @@ export class SsrBaseArtifactBuilder {
|
|
|
77
77
|
branches: [...akanConfig.branches],
|
|
78
78
|
i18n: akanConfig.i18n,
|
|
79
79
|
imageConfig: akanConfig.images,
|
|
80
|
+
web: akanConfig.web,
|
|
80
81
|
deepLinkAssociations: Object.values(akanConfig.mobile.targets)
|
|
81
82
|
.filter((target) => (target.deepLinks?.domains?.length ?? 0) > 0)
|
|
82
83
|
.map((target) => ({
|
|
@@ -110,7 +111,7 @@ export class SsrBaseArtifactBuilder {
|
|
|
110
111
|
const ssrBundle = await new ClientEntriesBundler({
|
|
111
112
|
app: this.#app,
|
|
112
113
|
entries: [rscSegmentOutletEntry],
|
|
113
|
-
...RouteClientBuilder.
|
|
114
|
+
...RouteClientBuilder.resolveSsrClientBundleOptions(this.#command),
|
|
114
115
|
outputSubdir: "client-ssr",
|
|
115
116
|
command: this.#command,
|
|
116
117
|
}).bundle();
|
|
@@ -192,7 +193,7 @@ export class SsrBaseArtifactBuilder {
|
|
|
192
193
|
Object.entries(cssByBasePath).flatMap(([basePath, baseCssText]) => {
|
|
193
194
|
const cssText = [baseCssText, optimizedFonts.css].filter(Boolean).join("\n");
|
|
194
195
|
if (!cssText) return [];
|
|
195
|
-
return [this.#writeCssAsset(basePath, cssText)];
|
|
196
|
+
return [this.#writeCssAsset(basePath, cssText, cssCompiler.importedStylesheetsByBasePath[basePath] ?? [])];
|
|
196
197
|
}),
|
|
197
198
|
),
|
|
198
199
|
);
|
|
@@ -201,7 +202,7 @@ export class SsrBaseArtifactBuilder {
|
|
|
201
202
|
return { cssCompiler, optimizedFonts, cssAssets };
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
async #writeCssAsset(basePath: string, cssText: string) {
|
|
205
|
+
async #writeCssAsset(basePath: string, cssText: string, imported: ImportedStylesheet[]) {
|
|
205
206
|
const cssAssetName = basePath || "root";
|
|
206
207
|
const preparedCssText = await prepareCssAsset(this.#command, basePath, cssText);
|
|
207
208
|
const cssHash = Bun.hash(`${basePath}\n${preparedCssText}`).toString(36);
|
|
@@ -210,7 +211,23 @@ export class SsrBaseArtifactBuilder {
|
|
|
210
211
|
`/_akan/styles/${cssAssetName}-${cssHash}.css`,
|
|
211
212
|
];
|
|
212
213
|
await Bun.write(path.join(this.#absArtifactDir, cssRelPath), preparedCssText);
|
|
214
|
+
SsrBaseArtifactBuilder.#warnDroppedImports(this.#app, cssRelPath, preparedCssText, imported);
|
|
213
215
|
this.#app.verbose(`[base-artifact] wrote ${preparedCssText.length} bytes of CSS for ${basePath} -> ${cssRelPath}`);
|
|
214
216
|
return [basePath, { cssUrl, cssRelPath }] as const;
|
|
215
217
|
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Checked against the file written here rather than against the compiled text, because this is the stylesheet
|
|
221
|
+
* `base-artifact.json` points at and therefore the only one an SSR render serves. A declaration can survive
|
|
222
|
+
* the compile and still be missing from the asset — a build that ships CSS to the CSR bundle and not to the
|
|
223
|
+
* server is indistinguishable, in the browser, from a theme that was never written.
|
|
224
|
+
*/
|
|
225
|
+
static #warnDroppedImports(app: App, cssRelPath: string, css: string, imported: ImportedStylesheet[]) {
|
|
226
|
+
for (const { cssPath, declaredNames } of imported) {
|
|
227
|
+
if (declaredNames.length === 0 || declaredNames.some((name) => css.includes(`${name}:`))) continue;
|
|
228
|
+
app.logger.warn(
|
|
229
|
+
`[base-artifact] @import ${cssPath} declares ${declaredNames.length} custom propert${declaredNames.length === 1 ? "y" : "ies"} and none of them are in ${cssRelPath}`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
216
233
|
}
|
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Reports the style contract that `akan lint` enforces, in the shape build/dev/lint share.
|
|
3
|
+
*
|
|
4
|
+
* Only contrast lives here. The vocabulary closure — raw palette classes, arbitrary colors, dropped
|
|
5
|
+
* daisyUI slots, inline color literals, interpolated arbitrary values — is enforced by the grit plugins
|
|
6
|
+
* in `lint/*.grit` during the biome run, so it is not re-scanned. Contrast cannot be a lint rule at all:
|
|
7
|
+
* it is arithmetic over resolved token *values*, which no syntactic pattern can reach.
|
|
5
8
|
*/
|
|
6
|
-
import type { StyleGuardViolation } from "./styleGuard";
|
|
7
9
|
import type { ThemeContrastViolation } from "./themeValidator";
|
|
8
10
|
|
|
9
11
|
export interface StyleContractViolations {
|
|
10
|
-
style: StyleGuardViolation[];
|
|
11
12
|
theme: ThemeContrastViolation[];
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
export const countBlocking = (
|
|
15
|
-
v.style.filter((s) => s.severity === "error").length + v.theme.length;
|
|
15
|
+
export const countBlocking = (violations: StyleContractViolations): number => violations.theme.length;
|
|
16
16
|
|
|
17
|
-
export const formatStyleContract = (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
for (const t of v.theme) {
|
|
25
|
-
lines.push(` [error] contrast ${t.scope} ${t.pair} = ${t.ratio}:1 (min ${t.threshold}:1)`);
|
|
26
|
-
lines.push(` → ${t.suggestion}`);
|
|
27
|
-
}
|
|
28
|
-
return lines.join("\n");
|
|
29
|
-
};
|
|
17
|
+
export const formatStyleContract = (violations: StyleContractViolations): string =>
|
|
18
|
+
violations.theme
|
|
19
|
+
.flatMap((theme) => [
|
|
20
|
+
` [error] contrast ${theme.scope} ${theme.pair} = ${theme.ratio}:1 (min ${theme.threshold}:1)`,
|
|
21
|
+
` → ${theme.suggestion}`,
|
|
22
|
+
])
|
|
23
|
+
.join("\n");
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* WCAG contrast checking over the semantic token pairs. No dependencies, pure functions, and no akanjs
|
|
3
|
+
* runtime import — it has to run inside the lint path.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
5
|
+
* The `--x` / `--x-foreground` pairing is what makes the check possible at all: it says which two values
|
|
6
|
+
* are going to end up on top of each other. That is the guard against a generated palette shipping a site
|
|
7
|
+
* nobody can read.
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Thresholds (WCAG 2.1):
|
|
10
|
+
* - body and primary surfaces (background, primary, secondary, accent, neutral, card, popover): 4.5:1,
|
|
11
|
+
* the AA floor for normal text
|
|
12
|
+
* - status and secondary pairs (info, success, warning, destructive, open, muted): 3:1, the AA floor for
|
|
13
|
+
* UI components and large text
|
|
14
|
+
* The shipped light/dark palette in styles.css clears all of them.
|
|
11
15
|
*/
|
|
12
16
|
|
|
13
17
|
export interface ThemeContrastViolation {
|
|
@@ -42,13 +46,13 @@ const PAIRS: PairDef[] = [
|
|
|
42
46
|
{ base: "open", fg: "open-foreground", threshold: 3 },
|
|
43
47
|
];
|
|
44
48
|
|
|
45
|
-
//
|
|
49
|
+
// Only these scopes are paired up; tokens scoped to something else (`.campaign-x`, …) are left alone.
|
|
46
50
|
const THEME_SCOPES = new Set([":root", '[data-theme="dark"]', '[data-theme="light"]']);
|
|
47
51
|
|
|
48
52
|
export type ThemeTokensByScope = Record<string, Record<string, string>>;
|
|
49
53
|
|
|
50
54
|
export class ThemeValidator {
|
|
51
|
-
/**
|
|
55
|
+
/** Extracts tokens from the css text and checks every scope it recognizes. */
|
|
52
56
|
validate(css: string): ThemeContrastViolation[] {
|
|
53
57
|
const tokensByScope = ThemeValidator.parseThemeTokens(css);
|
|
54
58
|
const violations: ThemeContrastViolation[] = [];
|
|
@@ -67,7 +71,7 @@ export class ThemeValidator {
|
|
|
67
71
|
if (!bg || !front) continue;
|
|
68
72
|
const bgRgb = ThemeValidator.parseHex(bg);
|
|
69
73
|
const fgRgb = ThemeValidator.parseHex(front);
|
|
70
|
-
if (!bgRgb || !fgRgb) continue; // var()
|
|
74
|
+
if (!bgRgb || !fgRgb) continue; // a var() or non-hex value has no ratio to compute
|
|
71
75
|
const ratio = ThemeValidator.contrastRatio(bgRgb, fgRgb);
|
|
72
76
|
if (ratio >= threshold) continue;
|
|
73
77
|
violations.push({
|
|
@@ -77,20 +81,21 @@ export class ThemeValidator {
|
|
|
77
81
|
foreground: front,
|
|
78
82
|
ratio: Math.round(ratio * 100) / 100,
|
|
79
83
|
threshold,
|
|
80
|
-
suggestion:
|
|
84
|
+
suggestion: `In ${scope}, --${base} (${bg}) against --${fg} (${front}) is ${ratio.toFixed(2)}:1, under the ${threshold}:1 minimum. Lighten or darken one of them until it clears.`,
|
|
81
85
|
});
|
|
82
86
|
}
|
|
83
87
|
return violations;
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
/**
|
|
87
|
-
* `:root` / `[data-theme="…"]`
|
|
88
|
-
* (`:root, [data-theme="dark"] { … }`)
|
|
89
|
-
*
|
|
91
|
+
* Reads `--token: value` out of `:root` / `[data-theme="…"]` blocks. A grouped selector
|
|
92
|
+
* (`:root, [data-theme="dark"] { … }`) distributes the same tokens to each selector, and a scope that
|
|
93
|
+
* appears twice keeps the later value — so passing framework css first and the app's second reflects the
|
|
94
|
+
* app's overrides.
|
|
90
95
|
*/
|
|
91
96
|
static parseThemeTokens(css: string): ThemeTokensByScope {
|
|
92
97
|
const result: ThemeTokensByScope = {};
|
|
93
|
-
//
|
|
98
|
+
// Flat rule blocks only; an at-rule (@theme, @keyframes) carries `@` in the selector and is skipped.
|
|
94
99
|
const blockRe = /(?:^|})\s*([^{}@]+?)\s*\{([^{}]*)\}/g;
|
|
95
100
|
for (const block of css.matchAll(blockRe)) {
|
|
96
101
|
const selectors = block[1].split(",").map((s) => s.trim());
|
|
@@ -109,11 +114,11 @@ export class ThemeValidator {
|
|
|
109
114
|
}
|
|
110
115
|
|
|
111
116
|
static #normalizeScope(selector: string): string {
|
|
112
|
-
//
|
|
117
|
+
// Quote normalization: [data-theme=dark] / [data-theme='dark'] -> [data-theme="dark"]
|
|
113
118
|
return selector.replace(/\[data-theme=['"]?([\w-]+)['"]?\]/g, '[data-theme="$1"]').trim();
|
|
114
119
|
}
|
|
115
120
|
|
|
116
|
-
/** #rgb / #rgba / #rrggbb / #rrggbbaa
|
|
121
|
+
/** #rgb / #rgba / #rrggbb / #rrggbbaa -> [r,g,b], alpha ignored. Null for anything not hex. */
|
|
117
122
|
static parseHex(value: string): [number, number, number] | null {
|
|
118
123
|
const v = value.trim();
|
|
119
124
|
if (!v.startsWith("#")) return null;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type { App } from "../commandDecorators";
|
|
6
|
+
import { WatchRootResolver } from "./watchRootResolver";
|
|
7
|
+
|
|
8
|
+
const roots: string[] = [];
|
|
9
|
+
|
|
10
|
+
const makeWorkspace = async (dirs: string[]) => {
|
|
11
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "akan-watch-roots-"));
|
|
12
|
+
roots.push(root);
|
|
13
|
+
await Promise.all(dirs.map((dir) => mkdir(path.join(root, dir), { recursive: true })));
|
|
14
|
+
return root;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const makeApp = (workspaceRoot: string, name: string, paths: Record<string, string[]>) =>
|
|
18
|
+
({
|
|
19
|
+
cwdPath: path.join(workspaceRoot, "apps", name),
|
|
20
|
+
workspace: { workspaceRoot },
|
|
21
|
+
getTsConfig: async () => ({ compilerOptions: { paths } }),
|
|
22
|
+
}) as unknown as App;
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("WatchRootResolver", () => {
|
|
29
|
+
test("narrows the apps container to the app being served", async () => {
|
|
30
|
+
const workspaceRoot = await makeWorkspace(["apps/app1/page", "apps/app2/page", "libs/util"]);
|
|
31
|
+
const resolved = await new WatchRootResolver(
|
|
32
|
+
makeApp(workspaceRoot, "app1", { "@apps/*": ["./apps/*"], "@libs/*": ["./libs/*"] }),
|
|
33
|
+
).resolve();
|
|
34
|
+
|
|
35
|
+
expect(resolved).toContain(path.join(workspaceRoot, "apps/app1"));
|
|
36
|
+
expect(resolved).not.toContain(path.join(workspaceRoot, "apps"));
|
|
37
|
+
expect(resolved).not.toContain(path.join(workspaceRoot, "apps/app2"));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("keeps the libs container whole", async () => {
|
|
41
|
+
const workspaceRoot = await makeWorkspace(["apps/app1/page", "libs/util", "libs/shared"]);
|
|
42
|
+
const resolved = await new WatchRootResolver(makeApp(workspaceRoot, "app1", { "@libs/*": ["./libs/*"] })).resolve();
|
|
43
|
+
|
|
44
|
+
expect(resolved).toContain(path.join(workspaceRoot, "libs"));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("resolves a package alias to its own directory, filename and glob stripped", async () => {
|
|
48
|
+
const workspaceRoot = await makeWorkspace(["apps/app1/page", "pkgs/akanjs/base"]);
|
|
49
|
+
const resolved = await new WatchRootResolver(
|
|
50
|
+
makeApp(workspaceRoot, "app1", {
|
|
51
|
+
akanjs: ["./pkgs/akanjs/index.ts"],
|
|
52
|
+
"akanjs/*": ["./pkgs/akanjs/*"],
|
|
53
|
+
missing: ["./pkgs/nothing/index.ts"],
|
|
54
|
+
}),
|
|
55
|
+
).resolve();
|
|
56
|
+
|
|
57
|
+
expect(resolved).toContain(path.join(workspaceRoot, "pkgs/akanjs"));
|
|
58
|
+
expect(resolved).not.toContain(path.join(workspaceRoot, "pkgs/nothing"));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("always watches the app's page tree", async () => {
|
|
62
|
+
const workspaceRoot = await makeWorkspace(["apps/app1/page"]);
|
|
63
|
+
const resolved = await new WatchRootResolver(makeApp(workspaceRoot, "app1", {})).resolve();
|
|
64
|
+
|
|
65
|
+
expect(resolved).toEqual([path.join(workspaceRoot, "apps/app1/page")]);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -2,6 +2,15 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { App } from "../commandDecorators";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* The directories one app's dev watcher follows, resolved from its tsconfig `paths`.
|
|
7
|
+
*
|
|
8
|
+
* `@apps/*` strips down to the `apps/` container, so taking it verbatim puts every sibling app under the
|
|
9
|
+
* watcher: with two dev servers up, a save in app2 rebuilds, restarts and reloads app1, and both builders
|
|
10
|
+
* rewrite the same generated barrels. Apps are leaves of the workspace graph — never one another's
|
|
11
|
+
* dependencies — so the container is replaced by this app's own directory. `libs/` stays whole, because a
|
|
12
|
+
* lib becomes a dependency the moment someone types the import, with no restart in between.
|
|
13
|
+
*/
|
|
5
14
|
export class WatchRootResolver {
|
|
6
15
|
#app: App;
|
|
7
16
|
|
|
@@ -11,6 +20,8 @@ export class WatchRootResolver {
|
|
|
11
20
|
|
|
12
21
|
async resolve(): Promise<string[]> {
|
|
13
22
|
const tsconfig = await this.#app.getTsConfig();
|
|
23
|
+
const appDir = path.resolve(this.#app.cwdPath);
|
|
24
|
+
const appsContainer = path.dirname(appDir);
|
|
14
25
|
const set = new Set<string>();
|
|
15
26
|
set.add(path.resolve(`${this.#app.cwdPath}/page`));
|
|
16
27
|
for (const targets of Object.values(tsconfig.compilerOptions.paths ?? {})) {
|
|
@@ -20,7 +31,8 @@ export class WatchRootResolver {
|
|
|
20
31
|
// Strip the trailing filename and glob so we watch the package root dir.
|
|
21
32
|
const cleaned = target.replace(/\/?\*+.*$/, "").replace(/\/[^/]+\.[^/]+$/, "");
|
|
22
33
|
const resolved = path.resolve(this.#app.workspace.workspaceRoot, cleaned);
|
|
23
|
-
|
|
34
|
+
const root = resolved === appsContainer ? appDir : resolved;
|
|
35
|
+
if (fs.existsSync(root)) set.add(root);
|
|
24
36
|
}
|
|
25
37
|
}
|
|
26
38
|
return [...set];
|
|
@@ -22,27 +22,25 @@ describe("prepareDevWatchBatch", () => {
|
|
|
22
22
|
expect(prepared.event.devPlan?.files).toEqual(prepared.files);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
test.each([
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
indexSync: { changedFiles: [generatedIndex], errors: [] },
|
|
38
|
-
changePlanner: new DevChangePlanner({ workspaceRoot: root }),
|
|
39
|
-
});
|
|
25
|
+
test.each(["common", "srvkit", "ui", "webkit"])(
|
|
26
|
+
"keeps %s facet add/delete generated index in the same generation",
|
|
27
|
+
(facet) => {
|
|
28
|
+
const root = "/repo";
|
|
29
|
+
const changedFile = `${root}/libs/shared/${facet}/tmpExample.ts`;
|
|
30
|
+
const generatedIndex = `${root}/libs/shared/${facet}/index.ts`;
|
|
31
|
+
const prepared = prepareDevWatchBatch({
|
|
32
|
+
generation: 20,
|
|
33
|
+
batch: { files: [changedFile], kinds: new Set(["code"]) },
|
|
34
|
+
indexSync: { changedFiles: [generatedIndex], errors: [] },
|
|
35
|
+
changePlanner: new DevChangePlanner({ workspaceRoot: root }),
|
|
36
|
+
});
|
|
40
37
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
expect(new Set(prepared.files)).toEqual(new Set([changedFile, generatedIndex]));
|
|
39
|
+
expect(prepared.event.devPlan?.generatedFiles).toEqual([generatedIndex]);
|
|
40
|
+
expect(prepared.event.devPlan?.roles).toContain("barrel");
|
|
41
|
+
expect(prepared.event.devPlan?.actions).toContain("sync-generated");
|
|
42
|
+
},
|
|
43
|
+
);
|
|
46
44
|
|
|
47
45
|
test("marks failed generated index sync as an error generation", () => {
|
|
48
46
|
const root = "/repo";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BuilderEvent, ChangeBatch } from "akanjs/server";
|
|
1
|
+
import type { BuilderEvent, ChangeBatch, DevChangePlan } from "akanjs/server";
|
|
2
2
|
import type { DevChangePlanner, GeneratedIndexSyncResult } from "../frontendBuild";
|
|
3
3
|
|
|
4
4
|
export interface PrepareDevWatchBatchOptions {
|
|
@@ -12,6 +12,8 @@ export interface PreparedDevWatchBatch {
|
|
|
12
12
|
files: string[];
|
|
13
13
|
kinds: ("code" | "css" | "config")[];
|
|
14
14
|
expandedBatch: ChangeBatch;
|
|
15
|
+
/** The same plan the event carries, where `devPlan` is optional — read it here, not off the event. */
|
|
16
|
+
devPlan: DevChangePlan;
|
|
15
17
|
event: Extract<BuilderEvent, { type: "invalidate" }>;
|
|
16
18
|
hasSyncErrors: boolean;
|
|
17
19
|
}
|
|
@@ -35,13 +37,15 @@ export const prepareDevWatchBatch = ({
|
|
|
35
37
|
});
|
|
36
38
|
|
|
37
39
|
if (indexSync.errors.length > 0 && !devPlan.actions.includes("report-error")) {
|
|
38
|
-
|
|
40
|
+
const withReport: DevChangePlan["actions"] = [...devPlan.actions, "report-error"];
|
|
41
|
+
devPlan.actions = withReport.sort();
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
return {
|
|
42
45
|
files,
|
|
43
46
|
kinds,
|
|
44
47
|
expandedBatch,
|
|
48
|
+
devPlan,
|
|
45
49
|
event: { type: "invalidate", kinds, files, generation, devPlan },
|
|
46
50
|
hasSyncErrors: indexSync.errors.length > 0,
|
|
47
51
|
};
|
|
@@ -14,11 +14,25 @@ const builderMsgTypeSet = new Set<BuilderMessage["type"]>([
|
|
|
14
14
|
"build-status",
|
|
15
15
|
"builder-metrics",
|
|
16
16
|
]);
|
|
17
|
+
/**
|
|
18
|
+
* Where a dev child process writes. `"pipe"` is not only about keeping a TUI's frame clean: a Bun child
|
|
19
|
+
* that inherits the controlling terminal snapshots its termios at spawn and writes that snapshot back
|
|
20
|
+
* when it exits, so a builder spawned while something holds the terminal raw turns it raw again on the
|
|
21
|
+
* first recycle. See the XXX comment on `Spinner.oraOptions`.
|
|
22
|
+
*/
|
|
23
|
+
export type DevStdioMode = "inherit" | "pipe";
|
|
24
|
+
|
|
17
25
|
interface IncrementalBuilderHostOptions {
|
|
18
26
|
app: App;
|
|
19
27
|
entry: string;
|
|
20
28
|
env: Record<string, string>;
|
|
29
|
+
stdio?: DevStdioMode;
|
|
21
30
|
onMessage: (message: BuilderMessage) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Required when `stdio` is `"pipe"`: an undrained pipe fills and blocks the builder's next write.
|
|
33
|
+
* Text arrives as decoded chunks, not lines — a consumer that needs lines splits them itself.
|
|
34
|
+
*/
|
|
35
|
+
onOutput?: (kind: "stdout" | "stderr", text: string) => void;
|
|
22
36
|
}
|
|
23
37
|
|
|
24
38
|
/**
|
|
@@ -63,7 +77,9 @@ export class IncrementalBuilderHost {
|
|
|
63
77
|
app: App;
|
|
64
78
|
ready = false;
|
|
65
79
|
readonly #onMessage: (message: BuilderMessage) => void;
|
|
66
|
-
#
|
|
80
|
+
readonly #stdio: DevStdioMode;
|
|
81
|
+
readonly #onOutput: ((kind: "stdout" | "stderr", text: string) => void) | null;
|
|
82
|
+
#proc: Bun.Subprocess<"ignore", "inherit" | "pipe", "inherit" | "pipe"> | null = null;
|
|
67
83
|
#status: IncrementalBuilderStatus = "stopped";
|
|
68
84
|
#restartAttempts = 0;
|
|
69
85
|
#restartTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -83,11 +99,13 @@ export class IncrementalBuilderHost {
|
|
|
83
99
|
*/
|
|
84
100
|
readonly #inFlight = new Map<number, "build-route" | "build-csr">();
|
|
85
101
|
#startOptions: IncrementalBuilderStartOptions = {};
|
|
86
|
-
constructor({ app, entry, env, onMessage }: IncrementalBuilderHostOptions) {
|
|
102
|
+
constructor({ app, entry, env, stdio = "inherit", onMessage, onOutput }: IncrementalBuilderHostOptions) {
|
|
87
103
|
this.app = app;
|
|
88
104
|
this.entry = entry;
|
|
89
105
|
this.env = env;
|
|
106
|
+
this.#stdio = stdio;
|
|
90
107
|
this.#onMessage = onMessage;
|
|
108
|
+
this.#onOutput = onOutput ?? null;
|
|
91
109
|
}
|
|
92
110
|
get status() {
|
|
93
111
|
return this.#status;
|
|
@@ -115,11 +133,11 @@ export class IncrementalBuilderHost {
|
|
|
115
133
|
// the flag is what tells it to re-announce what it booted with.
|
|
116
134
|
const afterRecycle = this.#spawnAfterRecycle;
|
|
117
135
|
this.#spawnAfterRecycle = false;
|
|
118
|
-
let proc!: Bun.Subprocess<"ignore", "inherit", "inherit">;
|
|
136
|
+
let proc!: Bun.Subprocess<"ignore", "inherit" | "pipe", "inherit" | "pipe">;
|
|
119
137
|
proc = Bun.spawn(["bun", this.entry], {
|
|
120
138
|
cwd: this.app.cwdPath,
|
|
121
139
|
env: { ...this.env, AKAN_WATCH: "1", ...(afterRecycle ? { AKAN_BUILDER_ANNOUNCE_BOOT: "1" } : {}) },
|
|
122
|
-
stdio: ["ignore",
|
|
140
|
+
stdio: ["ignore", this.#stdio, this.#stdio],
|
|
123
141
|
ipc: (msg: BuilderMessage) => {
|
|
124
142
|
if (this.#proc !== proc) return;
|
|
125
143
|
if (!msg || typeof msg !== "object") return;
|
|
@@ -167,8 +185,25 @@ export class IncrementalBuilderHost {
|
|
|
167
185
|
},
|
|
168
186
|
});
|
|
169
187
|
this.#proc = proc;
|
|
188
|
+
// Re-attached per spawn, not once per host: a recycle or a crash-restart brings new pipes.
|
|
189
|
+
if (this.#stdio === "pipe" && this.#onOutput) {
|
|
190
|
+
void this.#drain(proc.stdout as unknown as ReadableStream<Uint8Array> | undefined, "stdout");
|
|
191
|
+
void this.#drain(proc.stderr as unknown as ReadableStream<Uint8Array> | undefined, "stderr");
|
|
192
|
+
}
|
|
170
193
|
this.logger.verbose(`builder spawned pid=${proc.pid} entry=${this.entry}${isRestart ? " restart=1" : ""}`);
|
|
171
194
|
}
|
|
195
|
+
async #drain(stream: ReadableStream<Uint8Array> | undefined | null, kind: "stdout" | "stderr") {
|
|
196
|
+
if (!stream) return;
|
|
197
|
+
const decoder = new TextDecoder();
|
|
198
|
+
try {
|
|
199
|
+
for await (const chunk of stream) {
|
|
200
|
+
const text = decoder.decode(chunk, { stream: true });
|
|
201
|
+
if (text) this.#onOutput?.(kind, text);
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
// The stream closes when the builder exits; nothing further to surface here.
|
|
205
|
+
}
|
|
206
|
+
}
|
|
172
207
|
#scheduleRestart() {
|
|
173
208
|
if (this.#manualStop || this.#restartTimer) return;
|
|
174
209
|
this.#status = "restarting";
|
|
@@ -200,7 +235,7 @@ export class IncrementalBuilderHost {
|
|
|
200
235
|
// one at a time, into the dev error page a recycle is supposed to be invisible to. `ready` the
|
|
201
236
|
// field is deliberately untouched: `onExit` reads it to tell a planned exit from a boot failure.
|
|
202
237
|
this.#status = "recycling";
|
|
203
|
-
this.logger.
|
|
238
|
+
this.logger.debug(`recycling builder pid=${proc.pid} (${reason})`);
|
|
204
239
|
this.#recycleTimer = setTimeout(() => {
|
|
205
240
|
this.#recycleTimer = null;
|
|
206
241
|
if (this.#proc !== proc) return;
|
|
@@ -278,7 +313,15 @@ export class IncrementalBuilderHost {
|
|
|
278
313
|
this.ready = false;
|
|
279
314
|
this.#status = "stopped";
|
|
280
315
|
}
|
|
281
|
-
static async create(
|
|
316
|
+
static async create(
|
|
317
|
+
app: App,
|
|
318
|
+
env: Record<string, string>,
|
|
319
|
+
onMessage: (message: BuilderMessage) => void,
|
|
320
|
+
{
|
|
321
|
+
stdio = "inherit",
|
|
322
|
+
onOutput,
|
|
323
|
+
}: { stdio?: DevStdioMode; onOutput?: (kind: "stdout" | "stderr", text: string) => void } = {},
|
|
324
|
+
) {
|
|
282
325
|
const candidates = [
|
|
283
326
|
path.join(app.workspace.workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts"),
|
|
284
327
|
path.join(
|
|
@@ -289,7 +332,8 @@ export class IncrementalBuilderHost {
|
|
|
289
332
|
path.join(import.meta.dir, "incrementalBuilder.proc.ts"),
|
|
290
333
|
];
|
|
291
334
|
for (const c of candidates)
|
|
292
|
-
if (await Bun.file(c).exists())
|
|
335
|
+
if (await Bun.file(c).exists())
|
|
336
|
+
return new IncrementalBuilderHost({ app, entry: c, env, stdio, onMessage, onOutput });
|
|
293
337
|
throw new Error(`[cli] frontend builder entry not found; looked in: ${candidates.join(", ")}`);
|
|
294
338
|
}
|
|
295
339
|
}
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
// barrel reaches `cssCompiler`/`ssrBaseArtifactBuilder`, which pull tailwindcss + @tailwindcss/node
|
|
5
5
|
// (~40MB) into a process that then holds them for the whole dev session. Phase 2 moved css compilation
|
|
6
6
|
// into the batch worker, so this process has no use for them — `entryModuleGraph.test.ts` keeps it that way.
|
|
7
|
+
import { CodegenLock } from "@akanjs/devkit/codegenLock";
|
|
7
8
|
import type { App } from "@akanjs/devkit/commandDecorators";
|
|
8
9
|
import { AppExecutor, type PageRoot, WorkspaceExecutor } from "@akanjs/devkit/executors";
|
|
9
10
|
import { AutoImportSync } from "@akanjs/devkit/frontendBuild/autoImportSync";
|
|
@@ -185,7 +186,12 @@ class IncrementalBuilder {
|
|
|
185
186
|
if (this.#shuttingDown) return;
|
|
186
187
|
this.#shuttingDown = true;
|
|
187
188
|
const started = Date.now();
|
|
188
|
-
this.#logger.
|
|
189
|
+
this.#logger.debug(`shutdown requested (${reason}); draining ${this.#inFlight} work item(s)`);
|
|
190
|
+
// Stopped before the drain, or a save landing mid-drain would enqueue a batch behind the queue tail
|
|
191
|
+
// this method already awaited — and `process.exit(0)` would cut that batch off partway through
|
|
192
|
+
// writing its artifacts. The replacement rebuilds every artifact from its boot build anyway, so the
|
|
193
|
+
// batch is not lost; a half-written one would be.
|
|
194
|
+
this.#watcher?.stop();
|
|
189
195
|
if (this.#cssRebuildTimer) {
|
|
190
196
|
// Only reachable if a css batch landed between the idle report and this request: the fresh
|
|
191
197
|
// boot build recompiles css from scratch anyway, so dropping the debounce loses nothing.
|
|
@@ -200,7 +206,7 @@ class IncrementalBuilder {
|
|
|
200
206
|
// flushed — a `css-updated` relayed milliseconds before this line would be dropped with no error
|
|
201
207
|
// anywhere, leaving the backend serving the previous bundle. See `BuilderChannel`.
|
|
202
208
|
const flushed = await BuilderChannel.drain();
|
|
203
|
-
this.#logger.
|
|
209
|
+
this.#logger.debug(
|
|
204
210
|
`drained in ${Date.now() - started}ms${flushed ? ` after flushing ${flushed} ipc write(s)` : ""}; exiting for recycle`,
|
|
205
211
|
);
|
|
206
212
|
process.exit(0);
|
|
@@ -294,22 +300,27 @@ class IncrementalBuilder {
|
|
|
294
300
|
//* Insert framework imports that are used but omitted (e.g. `Int` in *.constant.ts, `fetch` in
|
|
295
301
|
//* *.store.ts) before regenerating barrels. Edits land on files already in this batch, so they
|
|
296
302
|
//* rebuild in this same generation; the write is idempotent so it does not re-trigger the watcher.
|
|
297
|
-
const autoImport = await
|
|
303
|
+
const [autoImport, indexSync] = await CodegenLock.run(
|
|
304
|
+
this.#app.workspace.workspaceRoot,
|
|
305
|
+
`hmr-batch:${this.#app.name}`,
|
|
306
|
+
async () => {
|
|
307
|
+
const auto = await this.#autoImportSync.syncForBatch(batch.files);
|
|
308
|
+
return [auto, await this.#generatedIndexSync.syncForBatch(batch.files)] as const;
|
|
309
|
+
},
|
|
310
|
+
);
|
|
298
311
|
for (const error of autoImport.errors) this.#logger.error(error);
|
|
299
312
|
if (autoImport.changedFiles.length > 0)
|
|
300
313
|
this.#logger.verbose(`[auto-import] inserted imports into ${autoImport.changedFiles.length} file(s)`);
|
|
301
|
-
const indexSync = await this.#generatedIndexSync.syncForBatch(batch.files);
|
|
302
314
|
//* Both passes above write source files, and this generation's build consumes what they wrote. Hand
|
|
303
315
|
//* them to the watcher so its verification scan does not read them back as a user edit and spend a
|
|
304
316
|
//* second generation rebuilding identical content.
|
|
305
317
|
await this.#watcher?.absorb([...autoImport.changedFiles, ...indexSync.changedFiles]);
|
|
306
|
-
const { files, kinds, expandedBatch, event, hasSyncErrors } = prepareDevWatchBatch({
|
|
318
|
+
const { files, kinds, expandedBatch, devPlan, event, hasSyncErrors } = prepareDevWatchBatch({
|
|
307
319
|
generation,
|
|
308
320
|
batch,
|
|
309
321
|
indexSync,
|
|
310
322
|
changePlanner: this.#changePlanner,
|
|
311
323
|
});
|
|
312
|
-
const devPlan = event.devPlan;
|
|
313
324
|
this.#logger.verbose(
|
|
314
325
|
`[hmr] batch generation=${generation} kinds=${kinds.join(",")} files=${files.length} generated=${indexSync.changedFiles.length} roles=${devPlan.roles.join(",") || "(none)"} actions=${devPlan.actions.join(",") || "(none)"}`,
|
|
315
326
|
);
|
package/index.ts
CHANGED
|
@@ -22,19 +22,15 @@ export type * from "./applicationBuildRunner";
|
|
|
22
22
|
export type * from "./applicationReleasePackager";
|
|
23
23
|
export type * from "./applicationTestPreload";
|
|
24
24
|
export type * from "./artifact";
|
|
25
|
-
export type * from "./builder";
|
|
26
25
|
export type * from "./capacitorApp";
|
|
27
26
|
export type * from "./cloud";
|
|
28
27
|
export type * from "./commandDecorators";
|
|
29
28
|
export type * from "./createTunnel";
|
|
30
29
|
export type * from "./dependencyScanner";
|
|
31
30
|
export type * from "./executors";
|
|
32
|
-
export type * from "./extractDeps";
|
|
33
31
|
export type * from "./fileSys";
|
|
34
32
|
export type * from "./frontendBuild";
|
|
35
|
-
export type * from "./getCredentials";
|
|
36
33
|
export type * from "./getDirname";
|
|
37
|
-
export type * from "./getModelFileData";
|
|
38
34
|
export type * from "./getRelatedCnsts";
|
|
39
35
|
export type * from "./guideline";
|
|
40
36
|
export type * from "./incrementalBuilder";
|
|
@@ -46,11 +42,8 @@ export type * from "./recipeScanner";
|
|
|
46
42
|
export type * from "./scanInfo";
|
|
47
43
|
export type * from "./selectModel";
|
|
48
44
|
export type * from "./spinner";
|
|
49
|
-
export type * from "./streamAi";
|
|
50
45
|
export type * from "./transforms";
|
|
51
46
|
export type * from "./typeChecker";
|
|
52
47
|
export type * from "./types";
|
|
53
|
-
export type * from "./ui";
|
|
54
48
|
export type * from "./uploadRelease";
|
|
55
|
-
export type * from "./useStdoutDimensions";
|
|
56
49
|
export type * from "./workflow";
|