@akanjs/devkit 3.0.0-alpha.11 → 3.0.0-alpha.12

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.
@@ -48,6 +48,8 @@ export class CssCompiler {
48
48
  #resolvedSpecifierCache = new Map<string, Promise<string | null>>();
49
49
  /** Every stylesheet this compile reached, entry points and `@import` targets alike. */
50
50
  #discoveredCssPaths = new Set<string>();
51
+ /** `@import` targets of the compile in progress, by path, so the output can be checked against them. */
52
+ #importedStylesheets = new Map<string, string>();
51
53
 
52
54
  #fileExists(absPath: string): Promise<boolean> {
53
55
  let cached = this.#fileExistsCache.get(absPath);
@@ -233,6 +235,7 @@ export class CssCompiler {
233
235
  async compileCss(cssPaths: string[], sourcePaths: string[]): Promise<string> {
234
236
  if (cssPaths.length === 0) return "";
235
237
 
238
+ this.#importedStylesheets.clear();
236
239
  const compileStarted = Date.now();
237
240
  const compilers = await Promise.all(
238
241
  cssPaths.map(async (cssPath) => {
@@ -265,13 +268,33 @@ export class CssCompiler {
265
268
  this.#logger.verbose(
266
269
  `css compiled paths=${cssPaths.length} candidates=${candidates.length} in ${Date.now() - compileStarted}ms`,
267
270
  );
268
- return parts.join("\n");
271
+ const css = parts.join("\n");
272
+ this.#warnDroppedImports(css);
273
+ return css;
274
+ }
275
+
276
+ /**
277
+ * An `@import` can resolve, be read, and still contribute nothing — the one failure the existence check
278
+ * cannot see, and the one that reads as an unstyled component rather than as an error. Custom properties
279
+ * declared outside `@theme` are the tell: those pass through verbatim, so a stylesheet whose every
280
+ * declaration is missing from the build did not make it in.
281
+ */
282
+ #warnDroppedImports(css: string) {
283
+ for (const [cssPath, content] of this.#importedStylesheets) {
284
+ const names = declaredCustomProperties(content);
285
+ if (names.length === 0 || names.some((name) => css.includes(`${name}:`))) continue;
286
+ this.#logger.warn(
287
+ `css @import ${cssPath} was loaded but none of its ${names.length} declaration(s) reached the compiled CSS`,
288
+ );
289
+ }
269
290
  }
270
291
 
271
292
  async #loadStylesheet(id: string, fromBase: string) {
272
293
  const p = await this.#resolveCssImport(id, fromBase);
273
294
  this.#discoveredCssPaths.add(p);
274
295
  const content = await Bun.file(p).text();
296
+ this.#importedStylesheets.set(p, content);
297
+ this.#logger.verbose(`css import "${id}" from ${fromBase} -> ${p} (${content.length} bytes)`);
275
298
  return { path: p, base: path.dirname(p), content };
276
299
  }
277
300
 
@@ -388,6 +411,17 @@ export function isIgnoredNodeModuleSource(filePath: string): boolean {
388
411
  return NODE_MODULES_RE.test(filePath) && !AKANJS_NODE_MODULE_RE.test(filePath);
389
412
  }
390
413
 
414
+ /**
415
+ * `@theme` blocks are stripped first: those variables are emitted only when a utility uses one, so their
416
+ * absence from a build says nothing about whether the stylesheet arrived.
417
+ */
418
+ export function declaredCustomProperties(css: string): string[] {
419
+ const withoutThemeBlocks = css.replace(/@theme[^{]*\{[^}]*\}/g, "");
420
+ return [...new Set([...withoutThemeBlocks.matchAll(/(?:^|[\s;{])(--[\w-]+)\s*:/g)].map(([, name]) => name))].filter(
421
+ (name): name is string => !!name,
422
+ );
423
+ }
424
+
391
425
  function getPageKeyBasePath(pageKey: string, basePaths: string[]): string | null {
392
426
  const normalized = pageKey.split(path.sep).join("/").replace(/^\.\//, "");
393
427
  const segments = normalized.split("/");
@@ -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";
@@ -443,6 +443,17 @@ describe("CssCompiler", () => {
443
443
  expect(css).toContain(".text-fuchsia-500");
444
444
  });
445
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
+
446
457
  test("fails loudly on a stylesheet import that resolves to nothing", async () => {
447
458
  const root = await makeTempRoot();
448
459
  const cssPath = path.join(root, "apps/demo/page/styles.css");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "3.0.0-alpha.11",
3
+ "version": "3.0.0-alpha.12",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -45,7 +45,7 @@
45
45
  "@langchain/openai": "^1.4.6",
46
46
  "@tailwindcss/node": "^4.3.0",
47
47
  "@trapezedev/project": "^7.1.4",
48
- "akanjs": "3.0.0-alpha.11",
48
+ "akanjs": "3.0.0-alpha.12",
49
49
  "chalk": "^5.6.2",
50
50
  "commander": "^14.0.3",
51
51
  "dayjs": "^1.11.20",