Skip to content
This repository was archived by the owner on Jul 6, 2025. It is now read-only.

Commit bc95f38

Browse files
committed
Refactor UnoCSS integration
1 parent 09d46b2 commit bc95f38

File tree

6 files changed

+136
-83
lines changed

6 files changed

+136
-83
lines changed

framework/core/style.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
const deno = typeof Deno === "object" && Deno !== null && typeof Deno.env === "object";
1+
// deno-lint-ignore-file ban-ts-comment
22

33
export function applyCSS(url: string, css: string) {
4-
if (!deno) {
5-
const { document } = window;
4+
const { document } = globalThis;
5+
if (document) {
66
const ssrEl = Array.from<Element>(document.head.children).find((el: Element) =>
77
el.getAttribute("data-module-id") === url &&
88
el.hasAttribute("ssr")
@@ -24,3 +24,42 @@ export function applyCSS(url: string, css: string) {
2424
}
2525
}
2626
}
27+
28+
export function applyUnoCSS(url: string, css: string) {
29+
let unocssSheet: CSSStyleSheet | null = null;
30+
if (globalThis.document?.styleSheets) {
31+
for (const sheet of document.styleSheets) {
32+
if (sheet.ownerNode && (sheet.ownerNode as HTMLStyleElement).hasAttribute("data-unocss")) {
33+
unocssSheet = sheet;
34+
break;
35+
}
36+
}
37+
}
38+
39+
if (unocssSheet) {
40+
const tokens = new Set(
41+
Array.from(unocssSheet.cssRules).map((rule) => {
42+
// @ts-ignore
43+
return rule.selectorText || rule.cssText.split("{")[0].trim();
44+
}),
45+
);
46+
try {
47+
const sheet = new CSSStyleSheet();
48+
// @ts-ignore
49+
sheet.replaceSync(css);
50+
for (const rule of sheet.cssRules) {
51+
// @ts-ignore
52+
const selectorText = rule.selectorText || rule.cssText.split("{")[0].trim();
53+
if (!tokens.has(selectorText)) {
54+
unocssSheet.insertRule(rule.cssText, unocssSheet.cssRules.length);
55+
}
56+
}
57+
return;
58+
} catch (error) {
59+
console.error(error);
60+
}
61+
}
62+
63+
// fallback to create a new style element
64+
applyCSS(url, css);
65+
}

server/graph.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
export type Module = {
22
readonly specifier: string;
33
readonly version: number;
4-
readonly sourceCode?: string;
54
readonly deps?: ReadonlyArray<DependencyDescriptor>;
65
readonly inlineCSS?: string;
7-
readonly atomicCSS?: boolean;
6+
readonly atomicCSS?: {
7+
readonly tokens: ReadonlyArray<string>;
8+
};
89
};
910

1011
export type DependencyDescriptor = {
@@ -52,7 +53,6 @@ export class DependencyGraph {
5253

5354
const mod: Module = {
5455
specifier,
55-
sourceCode: "",
5656
version: this.#initialVersion,
5757
...props,
5858
};

server/helpers.ts

Lines changed: 53 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { AlephConfig, ImportMap, JSXConfig, ModuleLoader } from "./types.ts
1010
export const regFullVersion = /@\d+\.\d+\.\d+/;
1111
export const builtinModuleExts = ["tsx", "ts", "mts", "jsx", "js", "mjs"];
1212

13+
/** Stores and returns the `fn` output in the `globalThis` object */
1314
export async function globalIt<T>(name: string, fn: () => Promise<T>): Promise<T> {
1415
const cache: T | undefined = Reflect.get(globalThis, name);
1516
if (cache !== undefined) {
@@ -22,6 +23,7 @@ export async function globalIt<T>(name: string, fn: () => Promise<T>): Promise<T
2223
return ret;
2324
}
2425

26+
/** Stores and returns the `fn` output in the `globalThis` object synchronously. */
2527
export function globalItSync<T>(name: string, fn: () => T): T {
2628
const cache: T | undefined = Reflect.get(globalThis, name);
2729
if (cache !== undefined) {
@@ -34,6 +36,7 @@ export function globalItSync<T>(name: string, fn: () => T): T {
3436
return ret;
3537
}
3638

39+
/* Get Aleph.js package URI. */
3740
export function getAlephPkgUri(): string {
3841
return globalItSync("__ALEPH_PKG_URI", () => {
3942
const uriFromEnv = Deno.env.get("ALEPH_PKG_URI");
@@ -49,16 +52,21 @@ export function getAlephPkgUri(): string {
4952
});
5053
}
5154

55+
/** Get the UnoCSS generator, return `null` if the presets are empty. */
5256
export function getUnoGenerator(): UnoGenerator | null {
57+
const config: AlephConfig | undefined = Reflect.get(globalThis, "__ALEPH_CONFIG");
58+
if (config === undefined) {
59+
return null;
60+
}
5361
return globalItSync("__UNO_GENERATOR", () => {
54-
const config: AlephConfig | undefined = Reflect.get(globalThis, "__ALEPH_CONFIG");
5562
if (config?.unocss?.presets) {
5663
return createGenerator(config.unocss);
5764
}
5865
return null;
5966
});
6067
}
6168

69+
/** Get the deployment ID. */
6270
export function getDeploymentId(): string | null {
6371
return Deno.env.get("DENO_DEPLOYMENT_ID") ?? null;
6472
}
@@ -101,6 +109,40 @@ export function restoreUrl(pathname: string): string {
101109
return `${protocol}://${host}${port ? ":" + port : ""}/${rest.join("/")}`;
102110
}
103111

112+
/** init loaders in `CLI` mode, or use prebuild loaders */
113+
export async function initModuleLoaders(importMap: ImportMap): Promise<ModuleLoader[]> {
114+
const loaders: ModuleLoader[] = Reflect.get(globalThis, "__ALEPH_MODULE_LOADERS") || [];
115+
if (Deno.env.get("ALEPH_CLI")) {
116+
for (const key in importMap.imports) {
117+
if (/^\*\.{?(\w+, ?)*\w+}?$/i.test(key)) {
118+
let src = importMap.imports[key];
119+
if (src.endsWith("!loader")) {
120+
src = util.trimSuffix(src, "!loader");
121+
if (src.startsWith("./") || src.startsWith("../")) {
122+
src = "file://" + join(dirname(importMap.__filename), src);
123+
}
124+
let { default: loader } = await import(src);
125+
if (typeof loader === "function") {
126+
loader = new loader();
127+
}
128+
if (loader !== null && typeof loader === "object" && typeof loader.load === "function") {
129+
const glob = "/**/" + key;
130+
const reg = globToRegExp(glob);
131+
const Loader = {
132+
meta: { src, glob },
133+
test: (pathname: string) => reg.test(pathname),
134+
load: (pathname: string, env: Record<string, unknown>) => loader.load(pathname, env),
135+
};
136+
loaders.push(Loader);
137+
}
138+
}
139+
}
140+
}
141+
}
142+
return loaders;
143+
}
144+
145+
/** Load the JSX config base the given import maps and the existing deno config. */
104146
export async function loadJSXConfig(importMap: ImportMap): Promise<JSXConfig> {
105147
const jsxConfig: JSXConfig = {};
106148
const denoConfigFile = await findFile(["deno.jsonc", "deno.json", "tsconfig.json"]);
@@ -181,6 +223,7 @@ export async function loadJSXConfig(importMap: ImportMap): Promise<JSXConfig> {
181223
return jsxConfig;
182224
}
183225

226+
/** Load the import maps from the working directory. */
184227
export async function loadImportMap(): Promise<ImportMap> {
185228
const importMap: ImportMap = { __filename: "", imports: {}, scopes: {} };
186229

@@ -219,37 +262,17 @@ export async function loadImportMap(): Promise<ImportMap> {
219262
return importMap;
220263
}
221264

222-
/** init loaders in `CLI` mode, or use prebuild loaders */
223-
export async function initModuleLoaders(importMap: ImportMap): Promise<ModuleLoader[]> {
224-
const loaders: ModuleLoader[] = Reflect.get(globalThis, "__ALEPH_MODULE_LOADERS") || [];
225-
if (Deno.env.get("ALEPH_CLI")) {
226-
for (const key in importMap.imports) {
227-
if (/^\*\.{?(\w+, ?)*\w+}?$/i.test(key)) {
228-
let src = importMap.imports[key];
229-
if (src.endsWith("!loader")) {
230-
src = util.trimSuffix(src, "!loader");
231-
if (src.startsWith("./") || src.startsWith("../")) {
232-
src = "file://" + join(dirname(importMap.__filename), src);
233-
}
234-
let { default: loader } = await import(src);
235-
if (typeof loader === "function") {
236-
loader = new loader();
237-
}
238-
if (loader !== null && typeof loader === "object" && typeof loader.load === "function") {
239-
const glob = "/**/" + key;
240-
const reg = globToRegExp(glob);
241-
const Loader = {
242-
meta: { src, glob },
243-
test: (pathname: string) => reg.test(pathname),
244-
load: (pathname: string, env: Record<string, unknown>) => loader.load(pathname, env),
245-
};
246-
loaders.push(Loader);
247-
}
248-
}
249-
}
265+
export function applyImportMap(specifier: string, importMap: ImportMap): string {
266+
if (specifier in importMap.imports) {
267+
return importMap.imports[specifier];
268+
}
269+
for (const key in importMap.imports) {
270+
if (key.endsWith("/") && specifier.startsWith(key)) {
271+
return importMap.imports[key] + specifier.slice(key.length);
250272
}
251273
}
252-
return loaders;
274+
// todo: support scopes
275+
return specifier;
253276
}
254277

255278
export async function parseJSONFile(jsonFile: string): Promise<Record<string, unknown>> {
@@ -274,19 +297,6 @@ export async function parseImportMap(importMapFile: string): Promise<ImportMap>
274297
return importMap;
275298
}
276299

277-
export function applyImportMap(specifier: string, importMap: ImportMap): string {
278-
if (specifier in importMap.imports) {
279-
return importMap.imports[specifier];
280-
}
281-
for (const key in importMap.imports) {
282-
if (key.endsWith("/") && specifier.startsWith(key)) {
283-
return importMap.imports[key] + specifier.slice(key.length);
284-
}
285-
}
286-
// todo: support scopes
287-
return specifier;
288-
}
289-
290300
function toStringMap(v: unknown): Record<string, string> {
291301
const m: Record<string, string> = {};
292302
if (util.isPlainObject(v)) {

server/proxy_modules.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { serveDir } from "../lib/serve.ts";
66
import util from "../lib/util.ts";
77
import { bundleCSS } from "./bundle_css.ts";
88
import { DependencyGraph } from "./graph.ts";
9-
import { builtinModuleExts } from "./helpers.ts";
10-
import type { ImportMap, ModuleLoader, ModuleLoaderContent, ModuleLoaderEnv } from "./types.ts";
9+
import { builtinModuleExts, getUnoGenerator } from "./helpers.ts";
10+
import type { ImportMap, ModuleLoader, ModuleLoaderEnv, ModuleLoaderOutput } from "./types.ts";
1111

1212
const cssModuleLoader = async (pathname: string, env: ModuleLoaderEnv) => {
1313
const specifier = "." + pathname;
@@ -38,19 +38,26 @@ const cssModuleLoader = async (pathname: string, env: ModuleLoaderEnv) => {
3838
};
3939
};
4040

41-
const esModuleLoader = async (input: { pathname: string } & ModuleLoaderContent, env: ModuleLoaderEnv) => {
42-
const { code: sourceCode, pathname, lang, inlineCSS } = input;
43-
const specifier = "." + pathname;
44-
const atomicCSS = input.atomicCSS || pathname.endsWith(".jsx") || pathname.endsWith(".tsx");
45-
const contentType = lang ? getContentType(`file.${lang}`) : undefined;
41+
const esModuleLoader = async (input: { pathname: string } & ModuleLoaderOutput, env: ModuleLoaderEnv) => {
4642
const serverDependencyGraph: DependencyGraph | undefined = Reflect.get(globalThis, "serverDependencyGraph");
4743
if (!serverDependencyGraph) {
4844
throw new Error("The `serverDependencyGraph` is not defined");
4945
}
50-
const deps = await parseDeps(specifier, sourceCode, { importMap: JSON.stringify(env.importMap) });
51-
serverDependencyGraph.mark(specifier, { sourceCode, deps, inlineCSS, atomicCSS });
46+
47+
const { code, pathname, lang, inlineCSS, isTemplateLanguage } = input;
48+
const specifier = "." + pathname;
49+
const contentType = lang ? getContentType(`file.${lang}`) : undefined;
50+
const unoGenerator = isTemplateLanguage || lang === "jsx" || lang === "tsx" || pathname.endsWith(".tsx") ||
51+
pathname.endsWith(".jsx")
52+
? getUnoGenerator()
53+
: null;
54+
const [deps, atomicCSS] = await Promise.all([
55+
parseDeps(specifier, code, { importMap: JSON.stringify(env.importMap) }),
56+
unoGenerator ? unoGenerator.generate(code).then((ret) => ({ tokens: [...ret.matched] })) : undefined,
57+
]);
58+
serverDependencyGraph.mark(specifier, { deps, inlineCSS, atomicCSS });
5259
if (deps.length) {
53-
const s = new MagicString(sourceCode);
60+
const s = new MagicString(code);
5461
deps.forEach((dep) => {
5562
const { specifier, importUrl, loc } = dep;
5663
if (loc) {
@@ -69,7 +76,7 @@ const esModuleLoader = async (input: { pathname: string } & ModuleLoaderContent,
6976
return { content: s.toString(), contentType };
7077
}
7178
return {
72-
content: sourceCode,
79+
content: code,
7380
contentType,
7481
};
7582
};

server/renderer.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -81,22 +81,17 @@ export default {
8181
signal: req.signal,
8282
bootstrapScripts: [bootstrapScript],
8383
onError: (_error: unknown) => {
84-
// todo: handle suspense error
84+
// todo: handle suspense ssr error
8585
},
8686
};
8787
const body = await render(ssrContext);
8888
const serverDependencyGraph: DependencyGraph | undefined = Reflect.get(globalThis, "serverDependencyGraph");
8989
if (serverDependencyGraph) {
90-
const atomicCSSSource: Promise<string>[] = [];
90+
const unocssTokens: ReadonlyArray<string>[] = [];
9191
const lookupModuleStyle = (mod: Module) => {
92-
const { specifier, sourceCode, atomicCSS, inlineCSS } = mod;
92+
const { specifier, atomicCSS, inlineCSS } = mod;
9393
if (atomicCSS) {
94-
atomicCSSSource.push(
95-
sourceCode ? Promise.resolve(sourceCode) : Deno.readTextFile(specifier).then((text) => {
96-
Object.assign(mod, { sourceCode: text });
97-
return text;
98-
}),
99-
);
94+
unocssTokens.push(atomicCSS.tokens);
10095
}
10196
if (inlineCSS) {
10297
headCollection.push(`<style data-module-id="${specifier}">${inlineCSS}</style>`);
@@ -111,20 +106,17 @@ export default {
111106
break;
112107
}
113108
}
114-
if (atomicCSSSource.length > 0) {
115-
// todo: cache the atomic CSS in production mode
109+
if (unocssTokens.length > 0) {
116110
const unoGenerator = getUnoGenerator();
117111
if (unoGenerator) {
118112
const start = performance.now();
119-
const input = (await Promise.all(atomicCSSSource)).join("\n");
120-
const { css } = await unoGenerator.generate(input, {
113+
const { css } = await unoGenerator.generate(new Set(unocssTokens.flat()), {
121114
minify: !isDev,
122115
});
123116
if (css) {
117+
const buildTime = performance.now() - start;
124118
headCollection.push(
125-
`<style data-unocss="${unoGenerator.version}" data-build-time="${
126-
performance.now() - start
127-
}ms">${css}</style>`,
119+
`<style data-unocss="${unoGenerator.version}" data-build-time="${buildTime}ms">${css}</style>`,
128120
);
129121
}
130122
}

server/transformer.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,22 +123,27 @@ export default {
123123
});
124124
}
125125
let { code, map, deps } = ret;
126-
let inlineCSS = loaded?.inlineCSS;
126+
let hasInlineCSS = false;
127127
if (uno) {
128128
const unoGenerator = getUnoGenerator();
129129
if (unoGenerator) {
130130
const { css } = await unoGenerator.generate(sourceCode, { id: specifier, minify: !isDev });
131-
if (inlineCSS) {
132-
inlineCSS = `${inlineCSS}\n${css}`;
133-
} else {
134-
inlineCSS = css;
131+
132+
if (css) {
133+
code += `\nimport { applyUnoCSS as __applyUnoCSS } from "${
134+
toLocalPath(alephPkgUri)
135+
}/framework/core/style.ts";\n__applyUnoCSS(${JSON.stringify(specifier)}, ${JSON.stringify(css)});\n`;
136+
hasInlineCSS = true;
135137
}
136138
}
137139
}
138-
if (inlineCSS) {
140+
if (loaded?.inlineCSS) {
139141
code += `\nimport { applyCSS as __applyCSS } from "${
140142
toLocalPath(alephPkgUri)
141-
}/framework/core/style.ts";\n__applyCSS(${JSON.stringify(specifier)}, ${JSON.stringify(inlineCSS)});\n`;
143+
}/framework/core/style.ts";\n__applyCSS(${JSON.stringify(specifier)}, ${JSON.stringify(loaded?.inlineCSS)});\n`;
144+
hasInlineCSS = true;
145+
}
146+
if (hasInlineCSS) {
142147
deps = [...(deps || []), { specifier: alephPkgUri + "/framework/core/style.ts" }] as typeof deps;
143148
}
144149
clientDependencyGraph?.mark(specifier, { deps });

0 commit comments

Comments
 (0)