Skip to content

Commit 28aa3b4

Browse files
feat: browser entrypoint. (#75)
1 parent 1c0d075 commit 28aa3b4

9 files changed

Lines changed: 518 additions & 14 deletions

File tree

package-lock.json

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/css/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
{
22
"name": "@knighted/css",
3-
"version": "1.2.0-rc.1",
3+
"version": "1.2.0-rc.2",
44
"description": "A build-time utility that traverses JavaScript/TypeScript module dependency graphs to extract, compile, and optimize all imported CSS into a single, in-memory string.",
55
"type": "module",
66
"main": "./dist/css.js",
77
"types": "./types.d.ts",
88
"typesVersions": {
99
"*": {
10+
"browser": [
11+
"./dist/browser.d.ts"
12+
],
1013
"loader": [
1114
"./dist/loader.d.ts"
1215
],
@@ -36,6 +39,10 @@
3639
"import": "./dist/css.js",
3740
"require": "./dist/cjs/css.cjs"
3841
},
42+
"./browser": {
43+
"types": "./dist/browser.d.ts",
44+
"import": "./dist/browser.js"
45+
},
3946
"./loader": {
4047
"types": "./dist/loader.d.ts",
4148
"import": "./dist/loader.js",

packages/css/src/browser.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
export type BrowserDialect = 'css' | 'sass' | 'less' | 'module'
2+
3+
export type CssFromSourceResult = {
4+
ok: true
5+
css: string
6+
exports?: Record<string, string | string[]>
7+
}
8+
9+
export type CssFromSourceError = {
10+
ok: false
11+
error: {
12+
message: string
13+
code?: string
14+
}
15+
}
16+
17+
export type CssFromSourceResponse = CssFromSourceResult | CssFromSourceError
18+
19+
export type SassLike = {
20+
compile?: (
21+
source: string,
22+
options?: Record<string, unknown>,
23+
) => { css: string } | { css: { toString: () => string } }
24+
compileString?: (
25+
source: string,
26+
options?: Record<string, unknown>,
27+
) => { css: string } | { css: { toString: () => string } }
28+
compileStringAsync?: (
29+
source: string,
30+
options?: Record<string, unknown>,
31+
) => Promise<{ css: string } | { css: { toString: () => string } }>
32+
}
33+
34+
export type LessLike = {
35+
render: (source: string, options?: Record<string, unknown>) => Promise<{ css: string }>
36+
}
37+
38+
export type LightningCssWasm = {
39+
transform: (options: { filename?: string; code: Uint8Array; cssModules?: boolean }) => {
40+
code: Uint8Array
41+
exports?: Record<string, string | string[]>
42+
}
43+
}
44+
45+
export type CssFromSourceOptions = {
46+
dialect: BrowserDialect
47+
filename?: string
48+
sass?: SassLike
49+
less?: LessLike
50+
lightningcss?: LightningCssWasm
51+
sassOptions?: Record<string, unknown>
52+
lessOptions?: Record<string, unknown>
53+
}
54+
55+
const defaultFilename = 'input.css'
56+
57+
function resolveCssText(value: { css: unknown }): string {
58+
const raw = value.css
59+
if (typeof raw === 'string') {
60+
return raw
61+
}
62+
if (raw && typeof (raw as { toString?: unknown }).toString === 'function') {
63+
return String((raw as { toString: () => string }).toString())
64+
}
65+
return ''
66+
}
67+
68+
function toErrorResult(error: unknown): CssFromSourceError {
69+
if (error && typeof error === 'object') {
70+
const message =
71+
'message' in error && typeof (error as { message?: unknown }).message === 'string'
72+
? (error as { message: string }).message
73+
: 'Unknown error'
74+
const code =
75+
'code' in error && typeof (error as { code?: unknown }).code === 'string'
76+
? (error as { code: string }).code
77+
: undefined
78+
return { ok: false, error: { message, code } }
79+
}
80+
return { ok: false, error: { message: String(error) } }
81+
}
82+
83+
async function cssFromSourceInternal(
84+
source: string,
85+
options: CssFromSourceOptions,
86+
): Promise<CssFromSourceResult> {
87+
const filename = options.filename ?? defaultFilename
88+
89+
if (options.dialect === 'css') {
90+
return { ok: true, css: source }
91+
}
92+
93+
if (options.dialect === 'sass') {
94+
if (!options.sass) {
95+
throw new Error('@knighted/css: Missing Sass compiler for browser usage.')
96+
}
97+
if (typeof options.sass.compileStringAsync === 'function') {
98+
const result = await options.sass.compileStringAsync(source, options.sassOptions)
99+
return { ok: true, css: resolveCssText(result) }
100+
}
101+
if (typeof options.sass.compileString === 'function') {
102+
const result = options.sass.compileString(source, options.sassOptions)
103+
return { ok: true, css: resolveCssText(result) }
104+
}
105+
if (typeof options.sass.compile === 'function') {
106+
const result = options.sass.compile(source, options.sassOptions)
107+
return { ok: true, css: resolveCssText(result) }
108+
}
109+
throw new Error(
110+
'@knighted/css: Sass compiler does not expose compileStringAsync, compileString, or compile APIs.',
111+
)
112+
}
113+
114+
if (options.dialect === 'less') {
115+
if (!options.less) {
116+
throw new Error('@knighted/css: Missing Less compiler for browser usage.')
117+
}
118+
const result = await options.less.render(source, options.lessOptions)
119+
return { ok: true, css: result.css }
120+
}
121+
122+
if (options.dialect === 'module') {
123+
if (!options.lightningcss) {
124+
throw new Error('@knighted/css: Missing Lightning CSS WASM compiler.')
125+
}
126+
const encoder = new TextEncoder()
127+
const decoder = new TextDecoder()
128+
const result = options.lightningcss.transform({
129+
filename,
130+
code: encoder.encode(source),
131+
cssModules: true,
132+
})
133+
return {
134+
ok: true,
135+
css: decoder.decode(result.code),
136+
exports: result.exports,
137+
}
138+
}
139+
140+
return { ok: true, css: source }
141+
}
142+
143+
export async function cssFromSource(
144+
source: string,
145+
options: CssFromSourceOptions,
146+
): Promise<CssFromSourceResponse> {
147+
try {
148+
return await cssFromSourceInternal(source, options)
149+
} catch (error) {
150+
return toErrorResult(error)
151+
}
152+
}

0 commit comments

Comments
 (0)