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

Commit 5c59e1f

Browse files
committed
Support CSS modules
1 parent f0b3b2b commit 5c59e1f

File tree

5 files changed

+180
-57
lines changed

5 files changed

+180
-57
lines changed

server/app.ts

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
buildChecksum,
1515
ImportMap,
1616
parseExportNames,
17-
ReactResolve,
1817
SourceType,
1918
transform,
2019
TransformOptions
@@ -31,12 +30,11 @@ import {
3130
import log from '../shared/log.ts'
3231
import util from '../shared/util.ts'
3332
import type {
34-
Config,
3533
RouterURL,
3634
ServerApplication
3735
} from '../types.ts'
3836
import { VERSION } from '../version.ts'
39-
import { defaultConfig, loadConfig, loadImportMap } from './config.ts'
37+
import { defaultConfig, loadConfig, loadImportMap, RequiredConfig } from './config.ts'
4038
import { CSSProcessor } from './css.ts'
4139
import {
4240
computeHash,
@@ -72,7 +70,7 @@ type TransformFn = (url: string, code: string) => string
7270
export class Application implements ServerApplication {
7371
readonly workingDir: string
7472
readonly mode: 'development' | 'production'
75-
readonly config: Required<Config & { react: ReactResolve }>
73+
readonly config: RequiredConfig
7674
readonly importMap: ImportMap
7775
readonly ready: Promise<void>
7876

@@ -113,7 +111,7 @@ export class Application implements ServerApplication {
113111
Object.assign(this.config, config)
114112
Object.assign(this.importMap, importMap)
115113
this.#pageRouting.config(this.config)
116-
this.#cssProcesser.config(!this.isDev, this.config.postcss.plugins)
114+
this.#cssProcesser.config(!this.isDev, this.config.css)
117115

118116
// inject env variables
119117
Deno.env.set('ALEPH_VERSION', VERSION)
@@ -145,15 +143,18 @@ export class Application implements ServerApplication {
145143
const buildManifestFile = join(this.buildDir, 'build.manifest.json')
146144
const plugins = computeHash(JSON.stringify({
147145
plugins: this.config.plugins.filter(isLoaderPlugin).map(({ name }) => name),
148-
postcssPlugins: this.config.postcss.plugins.map(p => {
149-
if (util.isString(p)) {
150-
return p
151-
} else if (util.isArray(p)) {
152-
return p[0] + JSON.stringify(p[1])
153-
} else {
154-
p.toString()
155-
}
156-
}),
146+
css: {
147+
modules: this.config.css.modules,
148+
postcss: this.config.css.postcss.plugins.map(p => {
149+
if (util.isString(p)) {
150+
return p
151+
} else if (util.isArray(p)) {
152+
return p[0] + JSON.stringify(p[1])
153+
} else {
154+
p.toString()
155+
}
156+
})
157+
},
157158
react: this.config.react,
158159
}, (key: string, value: any) => {
159160
if (key === 'inlineStylePreprocess') {
@@ -711,7 +712,6 @@ export class Application implements ServerApplication {
711712
const { code, type } = await loader.transform({ url: key, content: (new TextEncoder).encode(tpl) })
712713
if (type === 'css') {
713714
tpl = code
714-
break
715715
}
716716
}
717717
}
@@ -918,9 +918,11 @@ export class Application implements ServerApplication {
918918
sourceContent: Uint8Array,
919919
contentType: string | null
920920
): Promise<{ code: string, type: SourceType, map: string | null } | null> {
921-
let sourceCode = (new TextDecoder).decode(sourceContent)
921+
const encoder = new TextEncoder()
922+
const decoder = new TextDecoder()
923+
922924
let sourceType: SourceType | null = null
923-
let sourceMap: string | null = null
925+
let sourceMap: Uint8Array | null = null
924926

925927
if (contentType !== null) {
926928
switch (contentType.split(';')[0].trim()) {
@@ -944,10 +946,10 @@ export class Application implements ServerApplication {
944946

945947
for (const loader of this.loaders) {
946948
if (loader.test.test(url) && loader.transform) {
947-
const { code, type = 'js', map } = await loader.transform({ url, content: sourceContent })
948-
sourceCode = code
949+
const { code, type = 'js', map } = await loader.transform({ url, content: sourceContent, map: sourceMap ?? undefined })
950+
sourceContent = encoder.encode(code)
949951
if (map) {
950-
sourceMap = map
952+
sourceMap = encoder.encode(map)
951953
}
952954
switch (type) {
953955
case 'js':
@@ -966,7 +968,6 @@ export class Application implements ServerApplication {
966968
sourceType = SourceType.CSS
967969
break
968970
}
969-
break
970971
}
971972
}
972973

@@ -985,6 +986,7 @@ export class Application implements ServerApplication {
985986
case 'tsx':
986987
sourceType = SourceType.TSX
987988
break
989+
case 'postcss':
988990
case 'pcss':
989991
case 'css':
990992
sourceType = SourceType.CSS
@@ -995,15 +997,19 @@ export class Application implements ServerApplication {
995997
}
996998

997999
if (sourceType === SourceType.CSS) {
998-
const { code, map } = await this.#cssProcesser.transform(url, sourceCode)
999-
sourceCode = code
1000+
const { code, map } = await this.#cssProcesser.transform(url, (new TextDecoder).decode(sourceContent))
1001+
sourceContent = encoder.encode(code)
10001002
sourceType = SourceType.JS
10011003
if (map) {
1002-
sourceMap = map
1004+
sourceMap = encoder.encode(map)
10031005
}
10041006
}
10051007

1006-
return { code: sourceCode, type: sourceType, map: sourceMap }
1008+
return {
1009+
code: decoder.decode(sourceContent),
1010+
type: sourceType,
1011+
map: sourceMap ? decoder.decode(sourceMap) : null
1012+
}
10071013
}
10081014

10091015
/** compile a moudle by given url, then cache on the disk. */

server/config.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@ import { defaultReactVersion } from '../shared/constants.ts'
44
import { existsFileSync, existsDirSync } from '../shared/fs.ts'
55
import log from '../shared/log.ts'
66
import util from '../shared/util.ts'
7-
import type { Config, PostCSSPlugin } from '../types.ts'
7+
import type { Config, PostCSSPlugin, CSSOptions } from '../types.ts'
88
import { getAlephPkgUri, reLocaleID } from './helper.ts'
99

10-
export const defaultConfig: Readonly<Required<Config> & { react: ReactResolve }> = {
10+
export interface RequiredConfig extends Required<Omit<Config, 'css'>> {
11+
react: ReactResolve,
12+
css: CSSOptions
13+
}
14+
15+
export const defaultConfig: Readonly<RequiredConfig> = {
1116
framework: 'react',
1217
buildTarget: 'es2015',
1318
baseUrl: '/',
@@ -18,12 +23,15 @@ export const defaultConfig: Readonly<Required<Config> & { react: ReactResolve }>
1823
rewrites: {},
1924
ssr: {},
2025
plugins: [],
21-
postcss: { plugins: ['autoprefixer'] },
26+
css: {
27+
modules: false,
28+
postcss: { plugins: ['autoprefixer'] },
29+
},
2230
headers: {},
2331
env: {},
2432
react: {
2533
version: defaultReactVersion,
26-
esmShBuildVersion: 38,
34+
esmShBuildVersion: 39,
2735
}
2836
}
2937

@@ -64,7 +72,7 @@ export async function loadConfig(workingDir: string): Promise<Config> {
6472
ssr,
6573
rewrites,
6674
plugins,
67-
postcss,
75+
css,
6876
headers,
6977
env,
7078
} = data
@@ -115,10 +123,11 @@ export async function loadConfig(workingDir: string): Promise<Config> {
115123
if (util.isNEArray(plugins)) {
116124
config.plugins = plugins
117125
}
118-
if (isPostcssConfig(postcss)) {
119-
config.postcss = postcss
120-
} else {
121-
config.postcss = await loadPostCSSConfig(workingDir)
126+
if (util.isPlainObject(css)) {
127+
config.css = {
128+
modules: util.isPlainObject(css.modules) ? css.modules : false,
129+
postcss: isPostcssConfig(css.postcss) ? css.postcss : defaultConfig.css.postcss
130+
}
122131
}
123132

124133
return config

server/css.ts

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,76 @@
11
import util from '../shared/util.ts'
2-
import { PostCSSPlugin } from '../types.ts'
2+
import { PostCSSPlugin, CSSOptions } from '../types.ts'
33

44
const postcssVersion = '8.2.8'
55
const productionOnlyPostcssPlugins = ['autoprefixer']
66

77
export class CSSProcessor {
88
#isProd: boolean
9-
#postcssPlugins: PostCSSPlugin[]
9+
#options: CSSOptions
1010
#postcss: any
1111
#cleanCSS: any
12+
#modulesJSON: Record<string, Record<string, string>>
1213

1314
constructor() {
1415
this.#isProd = false
15-
this.#postcssPlugins = []
16+
this.#options = {
17+
modules: false,
18+
postcss: { plugins: ['autoprefixer'] },
19+
}
1620
this.#postcss = null
1721
this.#cleanCSS = null
22+
this.#modulesJSON = {}
1823
}
1924

20-
config(isProd: boolean, postcssPlugins: PostCSSPlugin[]) {
25+
config(isProd: boolean, options: CSSOptions) {
2126
this.#isProd = isProd
22-
this.#postcssPlugins = postcssPlugins
27+
if (util.isPlainObject(options.modules)) {
28+
options.postcss.plugins = options.postcss.plugins.filter(p => {
29+
if (p === 'postcss-modules' || (Array.isArray(p) && p[0] === 'postcss-modules')) {
30+
return false
31+
}
32+
return true
33+
})
34+
options.postcss.plugins.push(['postcss-modules', {
35+
...options.modules,
36+
getJSON: (url: string, json: Record<string, string>) => {
37+
this.#modulesJSON = { [url]: json }
38+
},
39+
}])
40+
}
41+
this.#options = options
42+
}
43+
44+
private getModulesJSON(url: string) {
45+
const json = this.#modulesJSON[url] || {}
46+
if (url in this.#modulesJSON) {
47+
delete this.#modulesJSON[url]
48+
}
49+
return json
2350
}
2451

25-
async transform(url: string, content: string): Promise<{ code: string, map?: string }> {
52+
async transform(url: string, content: string): Promise<{ code: string, map?: string, classNames?: Record<string, string> }> {
2653
if (util.isLikelyHttpURL(url)) {
2754
return {
2855
code: [
2956
`import { applyCSS } from "https://deno.land/x/aleph/framework/core/style.ts"`,
30-
`applyCSS(${JSON.stringify(url)})`
57+
`applyCSS(${JSON.stringify(url)})`,
58+
`export default { __url$: ${JSON.stringify(url)} }`
3159
].join('\n')
3260
}
3361
}
3462

3563
if (this.#postcss == null) {
3664
const [postcss, cleanCSS] = await Promise.all([
37-
initPostCSS(this.#postcssPlugins),
65+
initPostCSS(this.#options.postcss.plugins),
3866
this.#isProd ? initCleanCSS() : Promise.resolve(null)
3967
])
4068
this.#postcss = postcss
4169
this.#cleanCSS = cleanCSS
4270
}
4371

44-
const { content: pcss } = await this.#postcss.process(content).async()
72+
const { content: pcss } = await this.#postcss.process(content, { from: url }).async()
73+
const modulesJSON = this.getModulesJSON(url)
4574
const css = this.#isProd ? this.#cleanCSS.minify(pcss).styles : pcss
4675

4776
if (url.startsWith('#inline-style-')) {
@@ -54,7 +83,9 @@ export class CSSProcessor {
5483
return {
5584
code: [
5685
`import { applyCSS } from "https://deno.land/x/aleph/framework/core/style.ts"`,
57-
`applyCSS(${JSON.stringify(url)}, ${JSON.stringify(css)})`
86+
`const css = ${JSON.stringify(css)}`,
87+
`applyCSS(${JSON.stringify(url)}, css)`,
88+
`export default { __url$: ${JSON.stringify(url)}, __css$: css, ${util.trimSuffix(JSON.stringify(modulesJSON).slice(1), '}')}}`
5889
].join('\n'),
5990
// todo: generate map
6091
}
@@ -83,7 +114,10 @@ async function initPostCSS(plugins: PostCSSPlugin[]) {
83114
return await importPostcssPluginByName(p)
84115
} else if (Array.isArray(p)) {
85116
const Plugin = await importPostcssPluginByName(p[0])
86-
return [Plugin, p[1]]
117+
if (util.isFunction(Plugin)) {
118+
return Plugin(p[1])
119+
}
120+
return null
87121
} else {
88122
return p
89123
}

0 commit comments

Comments
 (0)