@akanjs/devkit 3.0.0-alpha.5 → 3.0.0-alpha.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/agentsIndex.test.ts +10 -0
- package/agentsIndex.ts +47 -1
- package/aiEditor.ts +1 -1
- package/akanConfig/akanConfig.test.ts +47 -10
- package/akanConfig/akanConfig.ts +23 -16
- package/akanContext.ts +53 -11
- package/applicationBuildRunner.test.ts +1 -1
- package/applicationBuildRunner.ts +0 -1
- package/biome.base.json +321 -0
- package/biomeBase.ts +9 -0
- package/capacitorApp.test.ts +8 -0
- package/capacitorApp.ts +112 -18
- package/executors.test.ts +29 -5
- package/executors.ts +23 -5
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/buildRouteClient.test.ts +28 -2
- package/frontendBuild/clientBuildTypes.ts +4 -0
- package/frontendBuild/clientEntriesBundler.ts +4 -1
- package/frontendBuild/cssCandidateCache.test.ts +38 -0
- package/frontendBuild/cssCandidateCache.ts +15 -3
- package/frontendBuild/cssCompiler.ts +122 -11
- package/frontendBuild/cssImportResolver.ts +8 -7
- package/frontendBuild/frontendBuild.test.ts +88 -1
- package/frontendBuild/hmrWatcher.ts +1 -1
- package/frontendBuild/routeClientBuilder.ts +12 -5
- package/frontendBuild/ssrBaseArtifactBuilder.ts +20 -4
- package/frontendBuild/styleGuard.test.ts +15 -0
- package/frontendBuild/styleGuard.ts +17 -0
- package/frontendBuild/vendorSpecifiers.ts +1 -0
- package/getCredentials.ts +1 -3
- package/incrementalBuilder/devWatchBatch.test.ts +18 -20
- package/incrementalBuilder/incrementalBuilder.host.ts +1 -1
- package/incrementalBuilder/incrementalBuilder.proc.ts +2 -2
- package/integration/devStabilityHarness.ts +1 -5
- package/lint/no-daisyui-legacy-class.grit +26 -9
- package/lint/no-import-client-in-server.grit +48 -0
- package/lint/no-import-server-in-client.grit +45 -0
- package/lint/no-model-type-in-util-zone.grit +58 -0
- package/lint/no-return-in-store-action.grit +35 -0
- package/lint/no-unpublished-form-setter.grit +41 -0
- package/linter.ts +17 -12
- package/package.json +4 -4
- package/qualityScanner.test.ts +52 -0
- package/qualityScanner.ts +48 -37
- package/repoIdentity.ts +42 -0
- package/routeSourceValidator.test.ts +41 -0
- package/routeSourceValidator.ts +2 -44
- package/scanInfo.ts +21 -20
- package/transforms/externalizeFrameworkPlugin.ts +0 -1
- package/workspaceLayout.test.ts +56 -4
- package/workspaceLayout.ts +49 -4
|
@@ -106,14 +106,15 @@ export class CssImportResolver {
|
|
|
106
106
|
const pkg = await Bun.file(pkgPath).json();
|
|
107
107
|
const subpath = id === pkgName ? "." : `.${id.slice(pkgName.length)}`;
|
|
108
108
|
const exportValue = pkg.exports?.[subpath];
|
|
109
|
-
const
|
|
110
|
-
|
|
109
|
+
const exportedEntry =
|
|
110
|
+
typeof exportValue === "string"
|
|
111
111
|
? exportValue
|
|
112
|
-
: exportValue?.style || exportValue?.import || exportValue?.default
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return await this.#firstExisting(path.resolve(pkgDir,
|
|
112
|
+
: exportValue?.style || exportValue?.import || exportValue?.default;
|
|
113
|
+
if (exportedEntry) return await this.#firstExisting(path.resolve(pkgDir, exportedEntry));
|
|
114
|
+
//* A subpath names a file inside the package, so it resolves literally. Falling back to the package's own
|
|
115
|
+
//* style entry here would load a different stylesheet than the author asked for and report success.
|
|
116
|
+
if (subpath !== ".") return await this.#firstExisting(path.resolve(pkgDir, subpath));
|
|
117
|
+
return await this.#firstExisting(path.resolve(pkgDir, pkg.exports?.["."]?.style || pkg.style || "index.css"));
|
|
117
118
|
} catch {
|
|
118
119
|
return null;
|
|
119
120
|
}
|
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import type { RoutesManifest } from "akanjs/server";
|
|
7
7
|
import { CsrArtifactBuilder } from "./csrArtifactBuilder";
|
|
8
|
-
import { CssCompiler, isIgnoredNodeModuleSource } from "./cssCompiler";
|
|
8
|
+
import { CssCompiler, declaredCustomProperties, isIgnoredNodeModuleSource } from "./cssCompiler";
|
|
9
9
|
import { CssImportResolver } from "./cssImportResolver";
|
|
10
10
|
import { DevChangePlanner } from "./devChangePlanner";
|
|
11
11
|
import { DevGeneratedIndexSync } from "./devGeneratedIndexSync";
|
|
@@ -391,6 +391,20 @@ describe("CssImportResolver", () => {
|
|
|
391
391
|
expect(await resolver.resolve("@libs/ui/missing", root)).toBeNull();
|
|
392
392
|
});
|
|
393
393
|
|
|
394
|
+
test("never substitutes the package stylesheet for a subpath that does not exist", async () => {
|
|
395
|
+
const root = await makeTempRoot();
|
|
396
|
+
await write(
|
|
397
|
+
path.join(root, "node_modules/vendor/package.json"),
|
|
398
|
+
JSON.stringify({ name: "vendor", style: "index.css" }),
|
|
399
|
+
);
|
|
400
|
+
await write(path.join(root, "node_modules/vendor/index.css"), ".vendor {}\n");
|
|
401
|
+
|
|
402
|
+
const resolver = new CssImportResolver(root, {});
|
|
403
|
+
|
|
404
|
+
expect(await resolver.resolve("vendor", root)).toBe(path.join(root, "node_modules/vendor/index.css"));
|
|
405
|
+
expect(await resolver.resolve("vendor/ui/tokens.css", root)).toBeNull();
|
|
406
|
+
});
|
|
407
|
+
|
|
394
408
|
test("resolves css from single-package Akan workspace subpaths", async () => {
|
|
395
409
|
const root = await makeTempRoot();
|
|
396
410
|
await write(path.join(root, "pkgs/akanjs/ui/styles.css"), "body {}\n");
|
|
@@ -428,4 +442,77 @@ describe("CssCompiler", () => {
|
|
|
428
442
|
|
|
429
443
|
expect(css).toContain(".text-fuchsia-500");
|
|
430
444
|
});
|
|
445
|
+
|
|
446
|
+
test("reads declarations that prove a stylesheet arrived, ignoring theme variables that may not", () => {
|
|
447
|
+
expect(declaredCustomProperties(":root { --kakao: #fee500; --naver: #1ec800; }")).toEqual(["--kakao", "--naver"]);
|
|
448
|
+
expect(declaredCustomProperties("@theme inline {\n --color-brand: var(--brand);\n}\n")).toEqual([]);
|
|
449
|
+
expect(declaredCustomProperties("@theme { --color-x: initial; }\n:root { --brand: #111; }")).toEqual(["--brand"]);
|
|
450
|
+
expect(declaredCustomProperties(".a { color: var(--kakao); }")).toEqual([]);
|
|
451
|
+
expect(declaredCustomProperties(":root{--a:#111}\n@media (min-width:1px){:root{--a:#222;--b:#333}}")).toEqual([
|
|
452
|
+
"--a",
|
|
453
|
+
"--b",
|
|
454
|
+
]);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("fails loudly on a stylesheet import that resolves to nothing", async () => {
|
|
458
|
+
const root = await makeTempRoot();
|
|
459
|
+
const cssPath = path.join(root, "apps/demo/page/styles.css");
|
|
460
|
+
await write(cssPath, '@import "../../../libs/shared/ui/tokens.css";\n');
|
|
461
|
+
|
|
462
|
+
const compiler = new CssCompiler({
|
|
463
|
+
workspace: { workspaceRoot: root },
|
|
464
|
+
cwdPath: path.join(root, "apps/demo"),
|
|
465
|
+
getTsConfig: async () => ({ compilerOptions: { paths: {} } }),
|
|
466
|
+
} as never);
|
|
467
|
+
|
|
468
|
+
await expect(compiler.compileCss([cssPath], [])).rejects.toThrow(
|
|
469
|
+
/failed to resolve stylesheet import "\.\.\/\.\.\/\.\.\/libs\/shared\/ui\/tokens\.css"/,
|
|
470
|
+
);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("reports every @import per base path so the written asset can be checked against it", async () => {
|
|
474
|
+
const root = await makeTempRoot();
|
|
475
|
+
const appDir = path.join(root, "apps/demo");
|
|
476
|
+
await write(path.join(appDir, "page/_index.tsx"), 'import "./styles.css";\nexport default () => null;\n');
|
|
477
|
+
await write(path.join(appDir, "page/styles.css"), '@import "../../../libs/shared/ui/brand.css";\n');
|
|
478
|
+
await write(path.join(root, "libs/shared/ui/brand.css"), ":root { --kakao: #fee500; --naver: #1ec800; }\n");
|
|
479
|
+
|
|
480
|
+
const compiler = new CssCompiler({
|
|
481
|
+
workspace: { workspaceRoot: root },
|
|
482
|
+
cwdPath: appDir,
|
|
483
|
+
getPageKeys: async () => ["./_index.tsx"],
|
|
484
|
+
getConfig: async () => ({ barrelImports: [], basePaths: [] }),
|
|
485
|
+
getTsConfig: async () => ({ compilerOptions: { paths: {} } }),
|
|
486
|
+
} as never);
|
|
487
|
+
const cssByBasePath = await compiler.getCssByBasePath();
|
|
488
|
+
|
|
489
|
+
expect(cssByBasePath[""]).toContain("--kakao");
|
|
490
|
+
expect(compiler.importedStylesheetsByBasePath[""]).toEqual([
|
|
491
|
+
{ cssPath: path.join(root, "libs/shared/ui/brand.css"), declaredNames: ["--kakao", "--naver"] },
|
|
492
|
+
]);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("compiles lib-owned tokens ahead of the app stylesheets that may override them", async () => {
|
|
496
|
+
const root = await makeTempRoot();
|
|
497
|
+
const appDir = path.join(root, "apps/demo");
|
|
498
|
+
await write(
|
|
499
|
+
path.join(appDir, "page/_index.tsx"),
|
|
500
|
+
'import "./styles.css";\nimport { Card } from "@libs/shared/ui";\nexport default () => <Card />;\n',
|
|
501
|
+
);
|
|
502
|
+
await write(path.join(appDir, "page/styles.css"), ":root { --brand: #111111; }\n");
|
|
503
|
+
await write(path.join(root, "libs/shared/ui/index.ts"), "export const Card = () => null;\n");
|
|
504
|
+
await write(path.join(root, "libs/shared/ui/tokens.css"), ":root { --kakao: #fee500; }\n");
|
|
505
|
+
await write(path.join(root, "libs/unused/ui/tokens.css"), ":root { --unused: #000000; }\n");
|
|
506
|
+
|
|
507
|
+
const compiler = new CssCompiler({
|
|
508
|
+
workspace: { workspaceRoot: root },
|
|
509
|
+
cwdPath: appDir,
|
|
510
|
+
getPageKeys: async () => ["./_index.tsx"],
|
|
511
|
+
getConfig: async () => ({ barrelImports: [] }),
|
|
512
|
+
getTsConfig: async () => ({ compilerOptions: { paths: { "@libs/*": ["./libs/*"] } } }),
|
|
513
|
+
} as never);
|
|
514
|
+
const { cssPaths } = await compiler.discoverCssAndSources();
|
|
515
|
+
|
|
516
|
+
expect(cssPaths).toEqual([path.join(root, "libs/shared/ui/tokens.css"), path.join(appDir, "page/styles.css")]);
|
|
517
|
+
});
|
|
431
518
|
});
|
|
@@ -209,7 +209,7 @@ export class HmrWatcher {
|
|
|
209
209
|
// per-batch detail stays at verbose because a save-all trips this on every save.
|
|
210
210
|
if (!this.#reportedCompensating) {
|
|
211
211
|
this.#reportedCompensating = true;
|
|
212
|
-
this.#logger.
|
|
212
|
+
this.#logger.verbose(
|
|
213
213
|
`[hmr] recovered ${unreported} change(s) that fs.watch did not report; Bun coalesces concurrent saves and drops all but one, so changes are resolved by mtime`,
|
|
214
214
|
);
|
|
215
215
|
}
|
|
@@ -4,7 +4,7 @@ import type { BaseBuildArtifact, ClientManifest, SsrManifest } from "akanjs/serv
|
|
|
4
4
|
import type { App } from "../commandDecorators";
|
|
5
5
|
import { createBarrelImportsPlugin } from "../transforms/barrelImportsPlugin";
|
|
6
6
|
import { toClientReferencePath } from "../transforms/rscUseClientTransform";
|
|
7
|
-
import type { ClientEntryDiscovery } from "./clientBuildTypes";
|
|
7
|
+
import type { ClientBundleTarget, ClientEntryDiscovery } from "./clientBuildTypes";
|
|
8
8
|
import { ClientEntriesBundler } from "./clientEntriesBundler";
|
|
9
9
|
import { GraphClientEntryDiscovery } from "./clientEntryDiscovery";
|
|
10
10
|
import { VENDOR_SPECIFIERS } from "./vendorSpecifiers";
|
|
@@ -163,12 +163,11 @@ export class RouteClientBuilder {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
async #buildSsrBundle(bootstrapEntries: BootstrapEntries) {
|
|
166
|
-
const externalOptions = RouteClientBuilder.resolveSsrClientExternalOptions(this.#command);
|
|
167
166
|
return new ClientEntriesBundler({
|
|
168
167
|
app: this.#app,
|
|
169
168
|
entries: bootstrapEntries.buildEntries,
|
|
170
169
|
plugins: [await createBarrelImportsPlugin(this.#app)],
|
|
171
|
-
...
|
|
170
|
+
...RouteClientBuilder.resolveSsrClientBundleOptions(this.#command),
|
|
172
171
|
outputSubdir: "client-ssr",
|
|
173
172
|
command: this.#command,
|
|
174
173
|
}).bundle();
|
|
@@ -239,20 +238,28 @@ export class RouteClientBuilder {
|
|
|
239
238
|
return { [Bun.resolveSync("akanjs/fetch", serverEntry)]: "akanjs/fetch" };
|
|
240
239
|
}
|
|
241
240
|
|
|
242
|
-
|
|
241
|
+
/**
|
|
242
|
+
* `target: "bun"` is load-bearing: these chunks are `await import()`-ed by the SSR renderer, so a dependency
|
|
243
|
+
* resolved through its `browser` export condition can touch `document` at module scope and throw mid-render,
|
|
244
|
+
* degrading the whole document to client rendering. Bun's `conditions` only adds to the target's defaults —
|
|
245
|
+
* `browser` still wins — so the target itself has to say server.
|
|
246
|
+
*/
|
|
247
|
+
static resolveSsrClientBundleOptions(command: "build" | "start"): {
|
|
248
|
+
target: ClientBundleTarget;
|
|
243
249
|
external: readonly string[];
|
|
244
250
|
externalSubpaths?: readonly string[];
|
|
245
251
|
externalAliases?: Record<string, string>;
|
|
246
252
|
} {
|
|
247
253
|
if (command === "start") {
|
|
248
254
|
return {
|
|
255
|
+
target: "bun",
|
|
249
256
|
external: SSR_CLIENT_EXTERNALS,
|
|
250
257
|
externalSubpaths: ["akanjs/fetch"],
|
|
251
258
|
externalAliases: RouteClientBuilder.resolveSsrClientRuntimeAliases(),
|
|
252
259
|
};
|
|
253
260
|
}
|
|
254
261
|
|
|
255
|
-
return { external: SSR_CLIENT_ALIAS_EXTERNALS };
|
|
262
|
+
return { target: "bun", external: SSR_CLIENT_ALIAS_EXTERNALS };
|
|
256
263
|
}
|
|
257
264
|
|
|
258
265
|
static resolveAkanServerEntry(): string {
|
|
@@ -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";
|
|
@@ -110,7 +110,7 @@ export class SsrBaseArtifactBuilder {
|
|
|
110
110
|
const ssrBundle = await new ClientEntriesBundler({
|
|
111
111
|
app: this.#app,
|
|
112
112
|
entries: [rscSegmentOutletEntry],
|
|
113
|
-
...RouteClientBuilder.
|
|
113
|
+
...RouteClientBuilder.resolveSsrClientBundleOptions(this.#command),
|
|
114
114
|
outputSubdir: "client-ssr",
|
|
115
115
|
command: this.#command,
|
|
116
116
|
}).bundle();
|
|
@@ -192,7 +192,7 @@ export class SsrBaseArtifactBuilder {
|
|
|
192
192
|
Object.entries(cssByBasePath).flatMap(([basePath, baseCssText]) => {
|
|
193
193
|
const cssText = [baseCssText, optimizedFonts.css].filter(Boolean).join("\n");
|
|
194
194
|
if (!cssText) return [];
|
|
195
|
-
return [this.#writeCssAsset(basePath, cssText)];
|
|
195
|
+
return [this.#writeCssAsset(basePath, cssText, cssCompiler.importedStylesheetsByBasePath[basePath] ?? [])];
|
|
196
196
|
}),
|
|
197
197
|
),
|
|
198
198
|
);
|
|
@@ -201,7 +201,7 @@ export class SsrBaseArtifactBuilder {
|
|
|
201
201
|
return { cssCompiler, optimizedFonts, cssAssets };
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
async #writeCssAsset(basePath: string, cssText: string) {
|
|
204
|
+
async #writeCssAsset(basePath: string, cssText: string, imported: ImportedStylesheet[]) {
|
|
205
205
|
const cssAssetName = basePath || "root";
|
|
206
206
|
const preparedCssText = await prepareCssAsset(this.#command, basePath, cssText);
|
|
207
207
|
const cssHash = Bun.hash(`${basePath}\n${preparedCssText}`).toString(36);
|
|
@@ -210,7 +210,23 @@ export class SsrBaseArtifactBuilder {
|
|
|
210
210
|
`/_akan/styles/${cssAssetName}-${cssHash}.css`,
|
|
211
211
|
];
|
|
212
212
|
await Bun.write(path.join(this.#absArtifactDir, cssRelPath), preparedCssText);
|
|
213
|
+
SsrBaseArtifactBuilder.#warnDroppedImports(this.#app, cssRelPath, preparedCssText, imported);
|
|
213
214
|
this.#app.verbose(`[base-artifact] wrote ${preparedCssText.length} bytes of CSS for ${basePath} -> ${cssRelPath}`);
|
|
214
215
|
return [basePath, { cssUrl, cssRelPath }] as const;
|
|
215
216
|
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Checked against the file written here rather than against the compiled text, because this is the stylesheet
|
|
220
|
+
* `base-artifact.json` points at and therefore the only one an SSR render serves. A declaration can survive
|
|
221
|
+
* the compile and still be missing from the asset — a build that ships CSS to the CSR bundle and not to the
|
|
222
|
+
* server is indistinguishable, in the browser, from a theme that was never written.
|
|
223
|
+
*/
|
|
224
|
+
static #warnDroppedImports(app: App, cssRelPath: string, css: string, imported: ImportedStylesheet[]) {
|
|
225
|
+
for (const { cssPath, declaredNames } of imported) {
|
|
226
|
+
if (declaredNames.length === 0 || declaredNames.some((name) => css.includes(`${name}:`))) continue;
|
|
227
|
+
app.logger.warn(
|
|
228
|
+
`[base-artifact] @import ${cssPath} declares ${declaredNames.length} custom propert${declaredNames.length === 1 ? "y" : "ies"} and none of them are in ${cssRelPath}`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
216
232
|
}
|
|
@@ -67,6 +67,21 @@ describe("StyleGuard daisyui-legacy", () => {
|
|
|
67
67
|
test("does not flag bare ambiguous names that collide with Tailwind", () => {
|
|
68
68
|
expect(scan('<div className="card input badge btn" />')).toHaveLength(0);
|
|
69
69
|
});
|
|
70
|
+
|
|
71
|
+
test("flags daisyUI colour slots the vocabulary dropped", () => {
|
|
72
|
+
expect(rules('<div className="bg-base-100" />')).toEqual(["daisyui-legacy"]);
|
|
73
|
+
expect(rules('<div className="text-base-content/70" />')).toEqual(["daisyui-legacy"]);
|
|
74
|
+
expect(rules('<div className="border-t-base-300" />')).toEqual(["daisyui-legacy"]);
|
|
75
|
+
expect(rules('<div className="text-primary-content" />')).toEqual(["daisyui-legacy"]);
|
|
76
|
+
expect(rules('<div className="border-error/30 bg-error/5" />')).toEqual(["daisyui-legacy", "daisyui-legacy"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("does not flag colour slots that survived into the semantic vocabulary", () => {
|
|
80
|
+
expect(scan('<div className="bg-primary text-primary-foreground" />')).toHaveLength(0);
|
|
81
|
+
expect(scan('<div className="bg-neutral text-info border-warning" />')).toHaveLength(0);
|
|
82
|
+
expect(scan('<div className="bg-destructive/10 text-destructive" />')).toHaveLength(0);
|
|
83
|
+
expect(scan('<div className="content-center justify-content" />')).toHaveLength(0);
|
|
84
|
+
});
|
|
70
85
|
});
|
|
71
86
|
|
|
72
87
|
// The fixtures below must contain a literal `${`. Writing it inside a plain string trips biome's
|
|
@@ -90,6 +90,13 @@ const DAISYUI_LEGACY_RE = new RegExp(
|
|
|
90
90
|
"g",
|
|
91
91
|
);
|
|
92
92
|
|
|
93
|
+
// daisyUI 색 슬롯. 어휘 폐쇄가 이 이름들을 지웠으므로 CSS 가 생성되지 않는다 — 컴포넌트 클래스와 같은
|
|
94
|
+
// 무증상 실패다. 시맨틱 어휘에 살아남은 슬롯(primary·secondary·accent·neutral·info·success·warning)은
|
|
95
|
+
// 정상이므로 제외하고, 사라진 것(base-*, *-content, error)만 잡는다.
|
|
96
|
+
const DAISYUI_TOKEN =
|
|
97
|
+
"(?:base-(?:100|200|300|content)|(?:primary|secondary|accent|neutral|info|success|warning|error)-content|error)";
|
|
98
|
+
const DAISYUI_TOKEN_RE = new RegExp(`${LEAD}(?:${PREFIX})-${DAISYUI_TOKEN}${TAIL}`, "g");
|
|
99
|
+
|
|
93
100
|
const ALL_RULES: StyleGuardRule[] = [
|
|
94
101
|
"raw-palette",
|
|
95
102
|
"arbitrary-color",
|
|
@@ -247,6 +254,16 @@ export class StyleGuard {
|
|
|
247
254
|
}),
|
|
248
255
|
);
|
|
249
256
|
}
|
|
257
|
+
for (const m of scan.matchAll(DAISYUI_TOKEN_RE)) {
|
|
258
|
+
out.push(
|
|
259
|
+
this.#violation(file, m.index ?? 0, {
|
|
260
|
+
rule: "daisyui-legacy",
|
|
261
|
+
severity: "error",
|
|
262
|
+
suggestion:
|
|
263
|
+
"daisyUI 색 슬롯은 어휘에서 제거됐습니다 — base-100/200/300→background/muted/border, base-content→foreground, *-content→*-foreground, error→destructive.",
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
266
|
+
}
|
|
250
267
|
for (const m of scan.matchAll(INTERPOLATED_ARBITRARY_RE)) {
|
|
251
268
|
out.push(
|
|
252
269
|
this.#violation(file, m.index ?? 0, {
|
package/getCredentials.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import yaml from "js-yaml";
|
|
2
|
-
|
|
3
1
|
import type { AppExecutor } from "./executors";
|
|
4
2
|
import { FileSys } from "./fileSys";
|
|
5
3
|
|
|
@@ -12,7 +10,7 @@ interface Secret {
|
|
|
12
10
|
|
|
13
11
|
export const getCredentials = async (app: AppExecutor, environment: string): Promise<AppSecret> => {
|
|
14
12
|
const content = await FileSys.readText(`${app.workspace.workspaceRoot}/infra/app/values/${app.name}-secret.yaml`);
|
|
15
|
-
const secret =
|
|
13
|
+
const secret = Bun.YAML.parse(content) as Secret;
|
|
16
14
|
const appSecret = secret[environment];
|
|
17
15
|
if (!appSecret) throw new Error(`No secret found for ${app.name} in ${environment}`);
|
|
18
16
|
return appSecret;
|
|
@@ -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";
|
|
@@ -200,7 +200,7 @@ export class IncrementalBuilderHost {
|
|
|
200
200
|
// one at a time, into the dev error page a recycle is supposed to be invisible to. `ready` the
|
|
201
201
|
// field is deliberately untouched: `onExit` reads it to tell a planned exit from a boot failure.
|
|
202
202
|
this.#status = "recycling";
|
|
203
|
-
this.logger.
|
|
203
|
+
this.logger.debug(`recycling builder pid=${proc.pid} (${reason})`);
|
|
204
204
|
this.#recycleTimer = setTimeout(() => {
|
|
205
205
|
this.#recycleTimer = null;
|
|
206
206
|
if (this.#proc !== proc) return;
|
|
@@ -185,7 +185,7 @@ class IncrementalBuilder {
|
|
|
185
185
|
if (this.#shuttingDown) return;
|
|
186
186
|
this.#shuttingDown = true;
|
|
187
187
|
const started = Date.now();
|
|
188
|
-
this.#logger.
|
|
188
|
+
this.#logger.debug(`shutdown requested (${reason}); draining ${this.#inFlight} work item(s)`);
|
|
189
189
|
if (this.#cssRebuildTimer) {
|
|
190
190
|
// Only reachable if a css batch landed between the idle report and this request: the fresh
|
|
191
191
|
// boot build recompiles css from scratch anyway, so dropping the debounce loses nothing.
|
|
@@ -200,7 +200,7 @@ class IncrementalBuilder {
|
|
|
200
200
|
// flushed — a `css-updated` relayed milliseconds before this line would be dropped with no error
|
|
201
201
|
// anywhere, leaving the backend serving the previous bundle. See `BuilderChannel`.
|
|
202
202
|
const flushed = await BuilderChannel.drain();
|
|
203
|
-
this.#logger.
|
|
203
|
+
this.#logger.debug(
|
|
204
204
|
`drained in ${Date.now() - started}ms${flushed ? ` after flushing ${flushed} ipc write(s)` : ""}; exiting for recycle`,
|
|
205
205
|
);
|
|
206
206
|
process.exit(0);
|
|
@@ -127,11 +127,7 @@ export const env = {
|
|
|
127
127
|
),
|
|
128
128
|
this.writeFile(
|
|
129
129
|
"env/env.server.ts",
|
|
130
|
-
`
|
|
131
|
-
|
|
132
|
-
export const env = {
|
|
133
|
-
...getEnv(),
|
|
134
|
-
} as const;
|
|
130
|
+
`export const env = {} as const;
|
|
135
131
|
`,
|
|
136
132
|
),
|
|
137
133
|
this.writeFile(
|
|
@@ -2,19 +2,36 @@ engine biome(1.0)
|
|
|
2
2
|
language js(typescript, jsx)
|
|
3
3
|
|
|
4
4
|
// daisyUI was removed from the UI system; its component classes (`btn-primary`, `card-body`,
|
|
5
|
-
// `mockup-code`, ...)
|
|
6
|
-
//
|
|
5
|
+
// `mockup-code`, ...) and its colour slots (`bg-base-100`, `text-base-content`, `bg-error`)
|
|
6
|
+
// no longer have any CSS behind them and silently render unstyled. Grit port of styleGuard's
|
|
7
|
+
// `daisyui-legacy` rule (frontendBuild/styleGuard.ts) — keep in sync.
|
|
7
8
|
// Only high-signal compound names are matched; bare ambiguous words that collide with
|
|
8
|
-
// Tailwind or app classes (`card`, `input`, `badge`, `btn`) are deliberately not
|
|
9
|
+
// Tailwind or app classes (`card`, `input`, `badge`, `btn`, `divider`) are deliberately not
|
|
10
|
+
// flagged, and colour slots that survived into the semantic vocabulary (`primary`, `info`,
|
|
11
|
+
// `success`, `warning`, `neutral`) are not either — only the slots the vocabulary dropped.
|
|
12
|
+
// Biome keeps only the first diagnostic a single plugin registers on a node, so a string
|
|
13
|
+
// carrying both halves reports the component one and surfaces the colour one on the next run.
|
|
9
14
|
or {
|
|
10
15
|
JsxString() as $s,
|
|
11
16
|
JsStringLiteralExpression() as $s,
|
|
12
17
|
JsTemplateChunkElement() as $s
|
|
13
18
|
} where {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
any {
|
|
20
|
+
and {
|
|
21
|
+
$s <: r"[\s\S]*(?:^|[\s\"'`{(\[:!])(?:btn-(?:primary|secondary|accent|neutral|info|success|warning|error|ghost|link|outline|square|circle|wide|block|xs|sm|md|lg)|badge-(?:primary|secondary|accent|neutral|info|success|warning|error|ghost|outline)|alert-(?:info|success|warning|error)|input-(?:bordered|primary|secondary|accent|ghost|error)|select-(?:bordered|primary|ghost)|textarea-(?:bordered|primary|ghost)|checkbox-(?:primary|secondary|accent)|toggle-(?:primary|secondary|accent)|loading-(?:spinner|dots|ring|ball|bars|infinity)|card-(?:body|title|actions)|modal-(?:box|action|backdrop)|collapse-(?:title|content|arrow|plus)|dropdown-(?:content|end|start|hover)|stat-(?:title|value|desc)|tabs-(?:boxed|lifted|bordered)|tab-active|menu-(?:title|dropdown)|steps-(?:horizontal|vertical)|join-item|mockup-(?:code|phone|browser|window)|drawer-(?:side|content|toggle))(?:[\s\"'`})\]:/!,]|$)[\s\S]*",
|
|
22
|
+
register_diagnostic(
|
|
23
|
+
span = $s,
|
|
24
|
+
message = "daisyUI legacy class - daisyUI was removed, so this renders unstyled. Use akanjs/ui components and recipes (Button, buttonRecipe, badgeRecipe, ...) with semantic tokens instead.",
|
|
25
|
+
severity = "error"
|
|
26
|
+
)
|
|
27
|
+
},
|
|
28
|
+
and {
|
|
29
|
+
$s <: r"[\s\S]*(?:^|[\s\"'`{(\[:!])(?:bg|text|border(?:-[tblrxy])?(?:-[se])?|ring(?:-offset)?|fill|stroke|shadow|from|to|via|divide|outline|decoration|placeholder|caret|accent)-(?:base-(?:100|200|300|content)|(?:primary|secondary|accent|neutral|info|success|warning|error)-content|error)(?:[\s\"'`})\]:/!,]|$)[\s\S]*",
|
|
30
|
+
register_diagnostic(
|
|
31
|
+
span = $s,
|
|
32
|
+
message = "daisyUI colour slot - the closed colour vocabulary has no CSS for it, so this renders unstyled. base-100/200/300 -> background/muted/border, base-content -> foreground, <colour>-content -> <colour>-foreground, error -> destructive.",
|
|
33
|
+
severity = "error"
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
20
37
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
engine biome(1.0)
|
|
2
|
+
language js(typescript, jsx)
|
|
3
|
+
|
|
4
|
+
// Server files (*.document.ts, *.dictionary.ts, *.service.ts, *.signal.ts, srvkit/) and shared files
|
|
5
|
+
// (common/, *.constant.ts) run in Bun with no DOM and no bundler, and every CLI command, worker, and
|
|
6
|
+
// migration that loads a service loads whatever the service imports. Reaching into the client graph
|
|
7
|
+
// pulls React, the store, and the browser globals they touch into that process, and it runs the
|
|
8
|
+
// dependency backwards: the client entrypoint is built on top of cnst and sig, not the other way round.
|
|
9
|
+
//
|
|
10
|
+
// `import type` is erased before bundling and stays legal. A mixed value-and-type import is not exempt.
|
|
11
|
+
or {
|
|
12
|
+
JsModuleSource() as $source where {
|
|
13
|
+
$source <: within JsImport() as $import,
|
|
14
|
+
not $import <: r"import\s+type[\s\S]*",
|
|
15
|
+
$source <: or {
|
|
16
|
+
r"\".*\.store\"",
|
|
17
|
+
r"\".*\.(?:Template|Unit|Util|View|Zone)\""
|
|
18
|
+
},
|
|
19
|
+
register_diagnostic(
|
|
20
|
+
span = $source,
|
|
21
|
+
message = "Client module. A server or shared file must not import a *.store or a module component (*.Template, *.Unit, *.Util, *.View, *.Zone) — server code reaches the model through cnst and db, never through client state or JSX.",
|
|
22
|
+
severity = "error"
|
|
23
|
+
)
|
|
24
|
+
},
|
|
25
|
+
JsModuleSource() as $source where {
|
|
26
|
+
$source <: within JsImport() as $import,
|
|
27
|
+
not $import <: r"import\s+type[\s\S]*",
|
|
28
|
+
$source <: or {
|
|
29
|
+
r"\"(?:.*/)?(?:ui|webkit)(?:/.*)?\"",
|
|
30
|
+
r"\".*/client(?:/.*)?\""
|
|
31
|
+
},
|
|
32
|
+
register_diagnostic(
|
|
33
|
+
span = $source,
|
|
34
|
+
message = "Client entrypoint. A server or shared file must not import ui/, webkit/, or a package client entrypoint such as '@libs/<lib>/client' or 'akanjs/client' — import the server entrypoint or a common/ helper instead.",
|
|
35
|
+
severity = "error"
|
|
36
|
+
)
|
|
37
|
+
},
|
|
38
|
+
JsModuleSource() as $source where {
|
|
39
|
+
$source <: within JsImport() as $import,
|
|
40
|
+
not $import <: r"import\s+type[\s\S]*",
|
|
41
|
+
$source <: r"\"(?:.*/)?(?:st|store|useClient)\"",
|
|
42
|
+
register_diagnostic(
|
|
43
|
+
span = $source,
|
|
44
|
+
message = "Client barrel. A server or shared file must not import st, store, or useClient — server code holds no client state, and Err comes from dict on the server.",
|
|
45
|
+
severity = "error"
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
engine biome(1.0)
|
|
2
|
+
language js(typescript, jsx)
|
|
3
|
+
|
|
4
|
+
// Client files (ui/, webkit/, page/, *.store.ts, every .tsx) and shared files (common/, *.constant.ts)
|
|
5
|
+
// are compiled into the browser bundle, so a single value import of a server module drags its whole
|
|
6
|
+
// graph along — the database driver, node:crypto, a secret resolved from process.env. The boundary has
|
|
7
|
+
// to hold at the import statement, because by the call site the module is already bundled.
|
|
8
|
+
//
|
|
9
|
+
// `import type` is erased before bundling and stays legal, so a shared file may still name a server-side
|
|
10
|
+
// type. A mixed value-and-type import is not exempt: it emits a real edge.
|
|
11
|
+
or {
|
|
12
|
+
JsModuleSource() as $source where {
|
|
13
|
+
$source <: within JsImport() as $import,
|
|
14
|
+
not $import <: r"import\s+type[\s\S]*",
|
|
15
|
+
$source <: r"\".*\.(?:document|dictionary|service|signal)\"",
|
|
16
|
+
register_diagnostic(
|
|
17
|
+
span = $source,
|
|
18
|
+
message = "Server module. A client or shared file must not import a *.document, *.dictionary, *.service, or *.signal file — take the model from the package client entrypoint ('@libs/<lib>/client') or from its *.constant instead. Write 'import type' if only the type is needed.",
|
|
19
|
+
severity = "error"
|
|
20
|
+
)
|
|
21
|
+
},
|
|
22
|
+
JsModuleSource() as $source where {
|
|
23
|
+
$source <: within JsImport() as $import,
|
|
24
|
+
not $import <: r"import\s+type[\s\S]*",
|
|
25
|
+
$source <: or {
|
|
26
|
+
r"\"(?:.*/)?srvkit(?:/.*)?\"",
|
|
27
|
+
r"\".*/server(?:/.*)?\""
|
|
28
|
+
},
|
|
29
|
+
register_diagnostic(
|
|
30
|
+
span = $source,
|
|
31
|
+
message = "Server entrypoint. A client or shared file must not import srvkit/ or a package server entrypoint such as '@apps/<app>/server' or 'akanjs/server' — import the matching client entrypoint instead.",
|
|
32
|
+
severity = "error"
|
|
33
|
+
)
|
|
34
|
+
},
|
|
35
|
+
JsModuleSource() as $source where {
|
|
36
|
+
$source <: within JsImport() as $import,
|
|
37
|
+
not $import <: r"import\s+type[\s\S]*",
|
|
38
|
+
$source <: r"\"(?:.*/)?(?:db|srv|sig|dict|option|useServer)\"",
|
|
39
|
+
register_diagnostic(
|
|
40
|
+
span = $source,
|
|
41
|
+
message = "Server barrel. A client or shared file must not import db, srv, sig, dict, option, or useServer — read models from cnst, state from st, and Err from the package client entrypoint.",
|
|
42
|
+
severity = "error"
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
engine biome(1.0)
|
|
2
|
+
language js(typescript, jsx)
|
|
3
|
+
|
|
4
|
+
// `Util` and `Zone` are always client components, so a *prop* typed as a `cnst` model is a hydrated
|
|
5
|
+
// class instance the server has to hand across the boundary — the methods are stripped on the way and
|
|
6
|
+
// what arrives is a plain object wearing the model's type.
|
|
7
|
+
//
|
|
8
|
+
// Only prop positions are reported: the component's `*Props` interface / type alias, or the inline
|
|
9
|
+
// object type on its parameter. A `cnst` type anywhere else in the file never crosses the boundary and
|
|
10
|
+
// stays legal — a local annotation, a callback parameter the framework already types with the model
|
|
11
|
+
// (`renderItem`, `renderList`), a module-scope helper, a non-`Props` local shape, and the props of a
|
|
12
|
+
// component nested inside another one. A function-typed prop is client-internal too: a closure cannot
|
|
13
|
+
// cross the RSC boundary at all, so whoever passes it already holds the value it takes.
|
|
14
|
+
//
|
|
15
|
+
// The exempt generics are the framework props types whose model parameter never lands on a prop as
|
|
16
|
+
// data: `ClientInit` / `ClientView` / `ClientEdit` (mapped to plain `GetStateObject`) and `ModelsProps`,
|
|
17
|
+
// whose only use of the model is `onClickItem?: (model: M) => unknown` — a callback, so the caller
|
|
18
|
+
// already holds the value. `ModelProps<"x", cnst.LightX>` is *not* exempt: it spreads the model onto
|
|
19
|
+
// the props themselves.
|
|
20
|
+
TsReferenceType() as $ref where {
|
|
21
|
+
$ref <: r"cnst\.[A-Za-z_$][A-Za-z0-9_$]*",
|
|
22
|
+
not $ref <: within TsIndexedAccessType() as $indexed where {
|
|
23
|
+
$indexed <: r"cnst\.[A-Za-z_$][A-Za-z0-9_$]*\[.value.\][\s\S]*"
|
|
24
|
+
},
|
|
25
|
+
not $ref <: within TsReferenceType() as $wrapper where {
|
|
26
|
+
$wrapper <: r"(?:(?:Client|Server)(?:Init|View|Edit)|ModelsProps)<[\s\S]*"
|
|
27
|
+
},
|
|
28
|
+
not $ref <: within TsFunctionType(),
|
|
29
|
+
or {
|
|
30
|
+
$ref <: within TsInterfaceDeclaration() as $propsInterface where {
|
|
31
|
+
$propsInterface <: r"interface\s+[A-Za-z0-9_$]*Props[\s\S]*"
|
|
32
|
+
},
|
|
33
|
+
$ref <: within TsTypeAliasDeclaration() as $propsAlias where {
|
|
34
|
+
$propsAlias <: r"type\s+[A-Za-z0-9_$]*Props[\s\S]*"
|
|
35
|
+
},
|
|
36
|
+
and {
|
|
37
|
+
$ref <: within JsFormalParameter(),
|
|
38
|
+
// A parameter is a sibling of the body, so this only drops the ones belonging to a function
|
|
39
|
+
// declared *inside* a component — a nested component or a local callback.
|
|
40
|
+
not $ref <: within JsFunctionBody(),
|
|
41
|
+
// Same for a callback handed straight to a call, which an expression-bodied component leaves
|
|
42
|
+
// outside every function body: `rows.map((row: cnst.LightTicket) => …)`.
|
|
43
|
+
not $ref <: within JsCallArguments(),
|
|
44
|
+
or {
|
|
45
|
+
$ref <: within JsVariableDeclarator() as $component where {
|
|
46
|
+
$component <: r"[A-Z][A-Za-z0-9_$]*[\s\S]*"
|
|
47
|
+
},
|
|
48
|
+
$ref <: within JsFunctionDeclaration() as $componentFn where {
|
|
49
|
+
$componentFn <: r"(?:async\s+)?function\s+[A-Z][A-Za-z0-9_$]*[\s\S]*"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
register_diagnostic(
|
|
55
|
+
span = $ref,
|
|
56
|
+
message = "Util and Zone are client components, so a cnst model type on a prop is a class instance crossing the server boundary. Take an id instead (bannerId: string), or hand server data across as ClientInit / ClientView. An enum union such as cnst.AdminRole['value'] is allowed, and so is a cnst type that stays inside the file — a local annotation or a callback parameter."
|
|
57
|
+
)
|
|
58
|
+
}
|