@akanjs/devkit 3.0.0-alpha.12 → 3.0.0-alpha.13
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.
|
@@ -17,6 +17,12 @@ interface CssDiscovery {
|
|
|
17
17
|
sourcePaths: string[];
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** One `@import` target and the custom properties it declares, which is what proves it arrived downstream. */
|
|
21
|
+
export interface ImportedStylesheet {
|
|
22
|
+
cssPath: string;
|
|
23
|
+
declaredNames: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
export class CssCompiler {
|
|
21
27
|
#logger = new Logger("CssCompiler");
|
|
22
28
|
#transpiler = new Bun.Transpiler({ loader: "tsx" });
|
|
@@ -48,8 +54,11 @@ export class CssCompiler {
|
|
|
48
54
|
#resolvedSpecifierCache = new Map<string, Promise<string | null>>();
|
|
49
55
|
/** Every stylesheet this compile reached, entry points and `@import` targets alike. */
|
|
50
56
|
#discoveredCssPaths = new Set<string>();
|
|
51
|
-
/**
|
|
52
|
-
|
|
57
|
+
/**
|
|
58
|
+
* `@import` targets per base path, so whoever writes the asset can check the file it actually wrote — the
|
|
59
|
+
* compiled text and the written artifact are two different places a declaration can go missing.
|
|
60
|
+
*/
|
|
61
|
+
importedStylesheetsByBasePath: Record<string, ImportedStylesheet[]> = {};
|
|
53
62
|
|
|
54
63
|
#fileExists(absPath: string): Promise<boolean> {
|
|
55
64
|
let cached = this.#fileExistsCache.get(absPath);
|
|
@@ -81,8 +90,11 @@ export class CssCompiler {
|
|
|
81
90
|
async getCss({ refresh }: { refresh?: boolean } = {}) {
|
|
82
91
|
if (this.#cssText !== null && !refresh) return this.#cssText;
|
|
83
92
|
this.#discoveredCssPaths.clear();
|
|
93
|
+
this.importedStylesheetsByBasePath = {};
|
|
84
94
|
const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh });
|
|
85
|
-
|
|
95
|
+
const { css, imported } = await this.#compileWithImports(cssPaths, sourcePaths);
|
|
96
|
+
this.#cssText = css;
|
|
97
|
+
this.importedStylesheetsByBasePath = { "": imported };
|
|
86
98
|
await this.#warnUnreachableStylesheets();
|
|
87
99
|
return this.#cssText;
|
|
88
100
|
}
|
|
@@ -90,6 +102,7 @@ export class CssCompiler {
|
|
|
90
102
|
async getCssByBasePath({ refresh }: { refresh?: boolean } = {}): Promise<Record<string, string>> {
|
|
91
103
|
if (this.#cssTextByBasePath !== null && !refresh) return this.#cssTextByBasePath;
|
|
92
104
|
this.#discoveredCssPaths.clear();
|
|
105
|
+
this.importedStylesheetsByBasePath = {};
|
|
93
106
|
const akanConfig = await this.#app.getConfig({ refresh });
|
|
94
107
|
const pageKeys = await this.#app.getPageKeys({ refresh });
|
|
95
108
|
const basePaths = [...akanConfig.basePaths];
|
|
@@ -99,7 +112,8 @@ export class CssCompiler {
|
|
|
99
112
|
if (rootPageKeys.length === 0) return ["", ""] as const;
|
|
100
113
|
const started = Date.now();
|
|
101
114
|
const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: rootPageKeys });
|
|
102
|
-
const css = await this
|
|
115
|
+
const { css, imported } = await this.#compileWithImports(cssPaths, sourcePaths);
|
|
116
|
+
this.importedStylesheetsByBasePath[""] = imported;
|
|
103
117
|
this.#logger.verbose(
|
|
104
118
|
`css base=root paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`,
|
|
105
119
|
);
|
|
@@ -110,7 +124,8 @@ export class CssCompiler {
|
|
|
110
124
|
if (basePathPageKeys.length === 0) return [basePath, ""] as const;
|
|
111
125
|
const started = Date.now();
|
|
112
126
|
const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: basePathPageKeys });
|
|
113
|
-
const css = await this
|
|
127
|
+
const { css, imported } = await this.#compileWithImports(cssPaths, sourcePaths);
|
|
128
|
+
this.importedStylesheetsByBasePath[basePath] = imported;
|
|
114
129
|
this.#logger.verbose(
|
|
115
130
|
`css base=${basePath} paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`,
|
|
116
131
|
);
|
|
@@ -233,20 +248,32 @@ export class CssCompiler {
|
|
|
233
248
|
return tokenPaths.filter((tokensPath): tokensPath is string => !!tokensPath);
|
|
234
249
|
}
|
|
235
250
|
async compileCss(cssPaths: string[], sourcePaths: string[]): Promise<string> {
|
|
236
|
-
|
|
251
|
+
const { css } = await this.#compileWithImports(cssPaths, sourcePaths);
|
|
252
|
+
return css;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The collector is per compile rather than per compiler instance: `getCssByBasePath` compiles every base path
|
|
257
|
+
* concurrently, so instance state would mix one base path's imports into another's check.
|
|
258
|
+
*/
|
|
259
|
+
async #compileWithImports(
|
|
260
|
+
cssPaths: string[],
|
|
261
|
+
sourcePaths: string[],
|
|
262
|
+
): Promise<{ css: string; imported: ImportedStylesheet[] }> {
|
|
263
|
+
if (cssPaths.length === 0) return { css: "", imported: [] };
|
|
237
264
|
|
|
238
|
-
this.#importedStylesheets.clear();
|
|
239
265
|
const compileStarted = Date.now();
|
|
240
266
|
const compilers = await Promise.all(
|
|
241
267
|
cssPaths.map(async (cssPath) => {
|
|
242
268
|
const css = await Bun.file(cssPath).text();
|
|
243
269
|
const base = path.dirname(cssPath);
|
|
270
|
+
const imported = new Map<string, string>();
|
|
244
271
|
const compiler = await compile(css, {
|
|
245
272
|
base,
|
|
246
|
-
loadStylesheet: (id, fromBase) => this.#loadStylesheet(id, fromBase),
|
|
273
|
+
loadStylesheet: (id, fromBase) => this.#loadStylesheet(id, fromBase, imported),
|
|
247
274
|
loadModule: (id, fromBase) => this.#loadModule(id, fromBase),
|
|
248
275
|
});
|
|
249
|
-
return { cssPath, compiler };
|
|
276
|
+
return { cssPath, compiler, imported };
|
|
250
277
|
}),
|
|
251
278
|
);
|
|
252
279
|
|
|
@@ -261,39 +288,31 @@ export class CssCompiler {
|
|
|
261
288
|
`css candidates scanned count=${candidates.length} sources=${sourcePaths.length} dirs=${sourceDirs.size} in ${Date.now() - scanStarted}ms`,
|
|
262
289
|
);
|
|
263
290
|
const parts: string[] = [];
|
|
291
|
+
const imported: ImportedStylesheet[] = [];
|
|
264
292
|
for (const entry of compilers) {
|
|
265
293
|
if (!entry) continue;
|
|
266
|
-
|
|
294
|
+
const part = entry.compiler.build(candidates);
|
|
295
|
+
parts.push(part);
|
|
296
|
+
for (const [cssPath, content] of entry.imported) {
|
|
297
|
+
const declaredNames = declaredCustomProperties(content);
|
|
298
|
+
imported.push({ cssPath, declaredNames });
|
|
299
|
+
if (declaredNames.length === 0 || declaredNames.some((name) => part.includes(`${name}:`))) continue;
|
|
300
|
+
this.#logger.warn(
|
|
301
|
+
`css @import ${cssPath} was loaded by ${entry.cssPath} but none of its ${declaredNames.length} declaration(s) reached the compiled CSS`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
267
304
|
}
|
|
268
305
|
this.#logger.verbose(
|
|
269
306
|
`css compiled paths=${cssPaths.length} candidates=${candidates.length} in ${Date.now() - compileStarted}ms`,
|
|
270
307
|
);
|
|
271
|
-
|
|
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
|
-
}
|
|
308
|
+
return { css: parts.join("\n"), imported };
|
|
290
309
|
}
|
|
291
310
|
|
|
292
|
-
async #loadStylesheet(id: string, fromBase: string) {
|
|
311
|
+
async #loadStylesheet(id: string, fromBase: string, imported?: Map<string, string>) {
|
|
293
312
|
const p = await this.#resolveCssImport(id, fromBase);
|
|
294
313
|
this.#discoveredCssPaths.add(p);
|
|
295
314
|
const content = await Bun.file(p).text();
|
|
296
|
-
|
|
315
|
+
imported?.set(p, content);
|
|
297
316
|
this.#logger.verbose(`css import "${id}" from ${fromBase} -> ${p} (${content.length} bytes)`);
|
|
298
317
|
return { path: p, base: path.dirname(p), content };
|
|
299
318
|
}
|
|
@@ -470,6 +470,28 @@ describe("CssCompiler", () => {
|
|
|
470
470
|
);
|
|
471
471
|
});
|
|
472
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
|
+
|
|
473
495
|
test("compiles lib-owned tokens ahead of the app stylesheets that may override them", async () => {
|
|
474
496
|
const root = await makeTempRoot();
|
|
475
497
|
const appDir = path.join(root, "apps/demo");
|
|
@@ -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";
|
|
@@ -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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.13",
|
|
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.
|
|
48
|
+
"akanjs": "3.0.0-alpha.13",
|
|
49
49
|
"chalk": "^5.6.2",
|
|
50
50
|
"commander": "^14.0.3",
|
|
51
51
|
"dayjs": "^1.11.20",
|