Skip to content

Commit 0525f30

Browse files
jinghaihanautofix-ci[bot]sxzz
authored
feat(css): add CSS code splitting support (#654)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Kevin Deng <sxzz@sxzz.moe>
1 parent 6d22027 commit 0525f30

File tree

10 files changed

+322
-2
lines changed

10 files changed

+322
-2
lines changed

docs/.vitepress/config/theme.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export function getLocaleConfig(lang: string) {
106106
{ text: t('Package Exports'), link: '/package-exports.md' },
107107
{ text: t('Unbundle'), link: '/unbundle.md' },
108108
{ text: t('CJS Default Export'), link: '/cjs-default.md' },
109+
{ text: t('CSS'), link: '/css.md' },
109110
],
110111
},
111112
{

docs/options/css.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# CSS Support
2+
3+
CSS support in `tsdown` is still in a very early, experimental stage. While you can use some basic features, please be aware that the API and behavior may change in future releases.
4+
5+
> [!WARNING] Experimental Feature
6+
> CSS support is highly experimental. Please test thoroughly and report any issues you encounter. The API and behavior may change as the feature matures.
7+
8+
## Options
9+
10+
### Disabling CSS Code Splitting
11+
12+
By default, CSS may be split into multiple files based on your entry points. If you want to disable CSS code splitting and generate a single CSS file, you can set `css.splitting` to `false` in your configuration:
13+
14+
```ts
15+
export default defineConfig({
16+
css: {
17+
splitting: false,
18+
},
19+
})
20+
```
21+
22+
### Setting the Output CSS File Name
23+
24+
You can customize the name of the merged CSS file using the `css.fileName` option:
25+
26+
```ts
27+
export default defineConfig({
28+
css: {
29+
fileName: 'my-library.css',
30+
},
31+
})
32+
```
33+
34+
This will output your combined CSS as `my-library.css` in the output directory.

docs/zh-CN/options/css.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# CSS 支持
2+
3+
`tsdown` 的 CSS 支持仍处于非常早期的实验阶段。虽然您可以使用一些基础功能,但请注意,相关 API 和行为在未来版本中可能会发生变化。
4+
5+
> [!WARNING] 实验性功能
6+
> CSS 支持属于高度实验性特性。请务必充分测试,并反馈您遇到的任何问题。随着功能的完善,API 和行为可能会有所调整。
7+
8+
## 选项
9+
10+
### 禁用 CSS 代码分割
11+
12+
默认情况下,CSS 可能会根据入口文件被拆分为多个文件。如果您希望禁用 CSS 代码分割并生成单一 CSS 文件,可以在配置中将 `css.splitting` 设置为 `false`
13+
14+
```ts
15+
export default defineConfig({
16+
css: {
17+
splitting: false,
18+
},
19+
})
20+
```
21+
22+
### 设置输出 CSS 文件名
23+
24+
您可以通过 `css.fileName` 选项自定义合并后 CSS 文件的名称:
25+
26+
```ts
27+
export default defineConfig({
28+
css: {
29+
fileName: 'my-library.css',
30+
},
31+
})
32+
```
33+
34+
这样会在输出目录下生成名为 `my-library.css` 的合并 CSS 文件。

dts.snapshot.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"CopyEntry": "interface CopyEntry {\n from: string | string[]\n to?: string\n flatten?: boolean\n verbose?: boolean\n rename?: string | ((_: string, _: string, _: string) => string)\n}",
2222
"CopyOptions": "type CopyOptions = Arrayable<string | CopyEntry>",
2323
"CopyOptionsFn": "type CopyOptionsFn = (_: ResolvedConfig) => Awaitable<CopyOptions>",
24+
"CssOptions": "interface CssOptions {\n splitting?: boolean\n fileName?: string\n}",
2425
"DebugOptions": "interface DebugOptions extends NonNullable<InputOptions['debug']> {\n devtools?: boolean | Partial<StartOptions>\n clean?: boolean\n}",
2526
"ExportsOptions": "interface ExportsOptions {\n devExports?: boolean | string\n packageJson?: boolean\n all?: boolean\n exclude?: (RegExp | string)[]\n customExports?: (_: Record<string, any>, _: { pkg: PackageJson; chunks: ChunksByFormat; isPublish: boolean }) => Awaitable<Record<string, any>>\n}",
2627
"Format": "type Format = ModuleFormat",
@@ -52,14 +53,14 @@
5253
"PackageType": "type PackageType = 'module' | 'commonjs' | undefined",
5354
"ReportOptions": "interface ReportOptions {\n gzip?: boolean\n brotli?: boolean\n maxCompressSize?: number\n}",
5455
"ReportPlugin": "declare function ReportPlugin(_: ReportOptions, _: Logger, _: string, _: boolean, _: string, _: boolean): Plugin",
55-
"ResolvedConfig": "type ResolvedConfig = Overwrite<MarkPartial<Omit<UserConfig, 'workspace' | 'fromVite' | 'publicDir' | 'silent' | 'bundle' | 'removeNodeProtocol' | 'logLevel' | 'failOnWarn' | 'customLogger' | 'envFile' | 'envPrefix'>, 'globalName' | 'inputOptions' | 'outputOptions' | 'minify' | 'define' | 'alias' | 'external' | 'onSuccess' | 'outExtensions' | 'hooks' | 'copy' | 'loader' | 'name' | 'banner' | 'footer'>, { entry: Record<string, string>; nameLabel: string | undefined; format: NormalizedFormat; target?: string[]; clean: string[]; pkg?: PackageJsonWithPath; nodeProtocol: 'strip' | boolean; logger: Logger; ignoreWatch: Array<string | RegExp>; noExternal?: NoExternalFn; inlineOnly?: Array<string | RegExp>; dts: false | DtsOptions; report: false | ReportOptions; tsconfig: false | string; exports: false | ExportsOptions; debug: false | DebugOptions; publint: false | PublintOptions; attw: false | AttwOptions; unused: false | UnusedOptions }>",
56+
"ResolvedConfig": "type ResolvedConfig = Overwrite<MarkPartial<Omit<UserConfig, 'workspace' | 'fromVite' | 'publicDir' | 'silent' | 'bundle' | 'removeNodeProtocol' | 'logLevel' | 'failOnWarn' | 'customLogger' | 'envFile' | 'envPrefix'>, 'globalName' | 'inputOptions' | 'outputOptions' | 'minify' | 'define' | 'alias' | 'external' | 'onSuccess' | 'outExtensions' | 'hooks' | 'copy' | 'loader' | 'name' | 'banner' | 'footer'>, { entry: Record<string, string>; nameLabel: string | undefined; format: NormalizedFormat; target?: string[]; clean: string[]; pkg?: PackageJsonWithPath; nodeProtocol: 'strip' | boolean; logger: Logger; ignoreWatch: Array<string | RegExp>; noExternal?: NoExternalFn; inlineOnly?: Array<string | RegExp>; css: Required<CssOptions>; dts: false | DtsOptions; report: false | ReportOptions; tsconfig: false | string; exports: false | ExportsOptions; debug: false | DebugOptions; publint: false | PublintOptions; attw: false | AttwOptions; unused: false | UnusedOptions }>",
5657
"RolldownChunk": "type RolldownChunk = (OutputChunk | OutputAsset) & { outDir: string }",
5758
"RolldownContext": "interface RolldownContext {\n buildOptions: BuildOptions\n}",
5859
"Sourcemap": "type Sourcemap = boolean | 'inline' | 'hidden'",
5960
"TsdownBundle": "interface TsdownBundle extends AsyncDisposable {\n chunks: RolldownChunk[]\n config: ResolvedConfig\n}",
6061
"TsdownHooks": "interface TsdownHooks {\n 'build:prepare': (_: BuildContext) => void | Promise<void>\n 'build:before': (_: BuildContext & RolldownContext) => void | Promise<void>\n 'build:done': (_: BuildContext & { chunks: RolldownChunk[] }) => void | Promise<void>\n}",
6162
"TsdownInputOption": "type TsdownInputOption = string | string[] | Record<string, string | string[]>",
62-
"UserConfig": "interface UserConfig {\n entry?: TsdownInputOption\n external?: ExternalOption\n noExternal?: Arrayable<string | RegExp> | NoExternalFn\n inlineOnly?: Arrayable<string | RegExp>\n skipNodeModulesBundle?: boolean\n alias?: Record<string, string>\n tsconfig?: string | boolean\n platform?: 'node' | 'neutral' | 'browser'\n target?: string | string[] | false\n env?: Record<string, any>\n envFile?: string\n envPrefix?: string | string[]\n define?: Record<string, string>\n shims?: boolean\n treeshake?: boolean | TreeshakingOptions\n loader?: ModuleTypes\n removeNodeProtocol?: boolean\n nodeProtocol?: 'strip' | boolean\n plugins?: InputOptions['plugins']\n inputOptions?: InputOptions | ((_: InputOptions, _: NormalizedFormat, _: { cjsDts: boolean }) => Awaitable<InputOptions | void | null>)\n format?: Format | Format[] | Partial<Record<Format, Partial<ResolvedConfig>>>\n globalName?: string\n outDir?: string\n write?: boolean\n sourcemap?: Sourcemap\n clean?: boolean | string[]\n minify?: boolean | 'dce-only' | MinifyOptions\n footer?: ChunkAddon\n banner?: ChunkAddon\n unbundle?: boolean\n bundle?: boolean\n fixedExtension?: boolean\n outExtensions?: OutExtensionFactory\n hash?: boolean\n cjsDefault?: boolean\n outputOptions?: OutputOptions | ((_: OutputOptions, _: NormalizedFormat, _: { cjsDts: boolean }) => Awaitable<OutputOptions | void | null>)\n cwd?: string\n name?: string\n silent?: boolean\n logLevel?: LogLevel\n failOnWarn?: boolean | CIOption\n customLogger?: Logger\n fromVite?: boolean | 'vitest'\n watch?: boolean | Arrayable<string>\n ignoreWatch?: Arrayable<string | RegExp>\n debug?: WithEnabled<DebugOptions>\n onSuccess?: string | ((_: ResolvedConfig, _: AbortSignal) => void | Promise<void>)\n dts?: WithEnabled<DtsOptions>\n unused?: WithEnabled<UnusedOptions>\n publint?: WithEnabled<PublintOptions>\n attw?: WithEnabled<AttwOptions>\n report?: WithEnabled<ReportOptions>\n globImport?: boolean\n exports?: WithEnabled<ExportsOptions>\n publicDir?: CopyOptions | CopyOptionsFn\n copy?: CopyOptions | CopyOptionsFn\n hooks?: Partial<TsdownHooks> | ((_: Hookable<TsdownHooks>) => Awaitable<void>)\n workspace?: Workspace | Arrayable<string> | true\n}",
63+
"UserConfig": "interface UserConfig {\n entry?: TsdownInputOption\n external?: ExternalOption\n noExternal?: Arrayable<string | RegExp> | NoExternalFn\n inlineOnly?: Arrayable<string | RegExp>\n skipNodeModulesBundle?: boolean\n alias?: Record<string, string>\n tsconfig?: string | boolean\n platform?: 'node' | 'neutral' | 'browser'\n target?: string | string[] | false\n env?: Record<string, any>\n envFile?: string\n envPrefix?: string | string[]\n define?: Record<string, string>\n shims?: boolean\n treeshake?: boolean | TreeshakingOptions\n loader?: ModuleTypes\n removeNodeProtocol?: boolean\n nodeProtocol?: 'strip' | boolean\n plugins?: InputOptions['plugins']\n inputOptions?: InputOptions | ((_: InputOptions, _: NormalizedFormat, _: { cjsDts: boolean }) => Awaitable<InputOptions | void | null>)\n format?: Format | Format[] | Partial<Record<Format, Partial<ResolvedConfig>>>\n globalName?: string\n outDir?: string\n write?: boolean\n sourcemap?: Sourcemap\n clean?: boolean | string[]\n minify?: boolean | 'dce-only' | MinifyOptions\n footer?: ChunkAddon\n banner?: ChunkAddon\n unbundle?: boolean\n bundle?: boolean\n fixedExtension?: boolean\n outExtensions?: OutExtensionFactory\n hash?: boolean\n cjsDefault?: boolean\n outputOptions?: OutputOptions | ((_: OutputOptions, _: NormalizedFormat, _: { cjsDts: boolean }) => Awaitable<OutputOptions | void | null>)\n cwd?: string\n name?: string\n silent?: boolean\n logLevel?: LogLevel\n failOnWarn?: boolean | CIOption\n customLogger?: Logger\n fromVite?: boolean | 'vitest'\n watch?: boolean | Arrayable<string>\n ignoreWatch?: Arrayable<string | RegExp>\n debug?: WithEnabled<DebugOptions>\n onSuccess?: string | ((_: ResolvedConfig, _: AbortSignal) => void | Promise<void>)\n dts?: WithEnabled<DtsOptions>\n unused?: WithEnabled<UnusedOptions>\n publint?: WithEnabled<PublintOptions>\n attw?: WithEnabled<AttwOptions>\n report?: WithEnabled<ReportOptions>\n globImport?: boolean\n exports?: WithEnabled<ExportsOptions>\n css?: CssOptions\n publicDir?: CopyOptions | CopyOptionsFn\n copy?: CopyOptions | CopyOptionsFn\n hooks?: Partial<TsdownHooks> | ((_: Hookable<TsdownHooks>) => Awaitable<void>)\n workspace?: Workspace | Arrayable<string> | true\n}",
6364
"UserConfigExport": "type UserConfigExport = Awaitable<Arrayable<UserConfig> | UserConfigFn>",
6465
"UserConfigFn": "type UserConfigFn = (_: InlineConfig, _: { ci: boolean }) => Awaitable<Arrayable<UserConfig>>",
6566
"WithEnabled": "type WithEnabled<T> = boolean | undefined | CIOption | (T & { enabled?: boolean | CIOption })",
@@ -77,6 +78,7 @@
7778
"CopyEntry",
7879
"CopyOptions",
7980
"CopyOptionsFn",
81+
"CssOptions",
8082
"DebugOptions",
8183
"DtsOptions",
8284
"ExportsOptions",

src/config/options.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { createDefu } from 'defu'
77
import isInCi from 'is-in-ci'
88
import { createDebug } from 'obug'
99
import { resolveClean } from '../features/clean.ts'
10+
import { defaultCssBundleName } from '../features/css.ts'
1011
import { resolveEntry } from '../features/entry.ts'
1112
import { hasExportsTypes } from '../features/pkg/exports.ts'
1213
import { resolveTarget } from '../features/target.ts'
@@ -82,6 +83,7 @@ export async function resolveUserConfig(
8283
cjsDefault = true,
8384
globImport = true,
8485
inlineOnly,
86+
css,
8587
fixedExtension = platform === 'node',
8688
debug = false,
8789
write = true,
@@ -250,6 +252,11 @@ export async function resolveUserConfig(
250252
cjsDefault,
251253
clean,
252254
copy: publicDir || copy,
255+
css: {
256+
splitting: true,
257+
fileName: defaultCssBundleName,
258+
...css,
259+
},
253260
cwd,
254261
debug,
255262
dts,

src/config/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { CopyEntry, CopyOptions, CopyOptionsFn } from '../features/copy.ts'
2+
import type { CssOptions } from '../features/css.ts'
23
import type { DebugOptions } from '../features/debug.ts'
34
import type {
45
BuildContext,
@@ -75,6 +76,7 @@ export type {
7576
CopyEntry,
7677
CopyOptions,
7778
CopyOptionsFn,
79+
CssOptions,
7880
DebugOptions,
7981
DtsOptions,
8082
ExportsOptions,
@@ -507,6 +509,11 @@ export interface UserConfig {
507509
*/
508510
exports?: WithEnabled<ExportsOptions>
509511

512+
/**
513+
* **[experimental]** CSS options.
514+
*/
515+
css?: CssOptions
516+
510517
/**
511518
* @deprecated Alias for `copy`, will be removed in the future.
512519
*/
@@ -608,6 +615,7 @@ export type ResolvedConfig = Overwrite<
608615
ignoreWatch: Array<string | RegExp>
609616
noExternal?: NoExternalFn
610617
inlineOnly?: Array<string | RegExp>
618+
css: Required<CssOptions>
611619

612620
dts: false | DtsOptions
613621
report: false | ReportOptions

src/features/css.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { RE_CSS } from 'rolldown-plugin-dts/filename'
2+
import type { ResolvedConfig } from '../config/index.ts'
3+
import type { OutputAsset, OutputChunk, Plugin } from 'rolldown'
4+
5+
export interface CssOptions {
6+
/**
7+
* Enable/disable CSS code splitting.
8+
* When set to `false`, all CSS in the entire project will be extracted into a single CSS file.
9+
* When set to `true`, CSS imported in async JS chunks will be preserved as chunks.
10+
* @default true
11+
*/
12+
splitting?: boolean
13+
14+
/**
15+
* Specify the name of the CSS file.
16+
* @default 'style.css'
17+
*/
18+
fileName?: string
19+
}
20+
21+
// Regular expressions for file matching
22+
const RE_CSS_HASH = /-[\w-]+\.css$/
23+
const RE_CHUNK_HASH = /-[\w-]+\.(m?js|cjs)$/
24+
const RE_CHUNK_EXT = /\.(m?js|cjs)$/
25+
26+
export const defaultCssBundleName = 'style.css'
27+
28+
/**
29+
* Normalize CSS file name by removing hash pattern and extension.
30+
* e.g., "async-DcjEOEdU.css" -> "async"
31+
*/
32+
function normalizeCssFileName(cssFileName: string): string {
33+
return cssFileName.replace(RE_CSS_HASH, '').replace(RE_CSS, '')
34+
}
35+
36+
/**
37+
* Normalize chunk file name by removing hash pattern and extension.
38+
* e.g., "async-CvIfFAic.mjs" -> "async"
39+
*/
40+
function normalizeChunkFileName(chunkFileName: string): string {
41+
return chunkFileName.replace(RE_CHUNK_HASH, '').replace(RE_CHUNK_EXT, '')
42+
}
43+
44+
/**
45+
* CSS Code Split Plugin
46+
*
47+
* When css.splitting is false, this plugin merges all CSS files into a single file.
48+
* When css.splitting is true (default), CSS code splitting is preserved.
49+
* Based on Vite's implementation.
50+
*/
51+
export function CssCodeSplitPlugin(
52+
config: Pick<ResolvedConfig, 'css'>,
53+
): Plugin | undefined {
54+
const { splitting, fileName } = config.css
55+
if (splitting) return
56+
57+
let hasEmitted = false
58+
59+
return {
60+
name: 'tsdown:css-code-split',
61+
62+
renderStart() {
63+
// Reset state for each build for watch mode
64+
hasEmitted = false
65+
},
66+
67+
generateBundle(_outputOptions, bundle) {
68+
if (hasEmitted) return
69+
70+
// Collect all CSS assets and their content
71+
const cssAssets = new Map<string, string>()
72+
73+
for (const [fileName, asset] of Object.entries(bundle)) {
74+
if (asset.type === 'asset' && RE_CSS.test(fileName)) {
75+
const source =
76+
typeof asset.source === 'string'
77+
? asset.source
78+
: new TextDecoder('utf-8').decode(asset.source)
79+
cssAssets.set(fileName, source)
80+
}
81+
}
82+
83+
if (!cssAssets.size) return
84+
85+
// Build a map from chunk fileName to its associated CSS fileName(s)
86+
// Match CSS assets to chunks by analyzing module IDs and file names
87+
const chunkCSSMap = new Map<string, string[]>()
88+
89+
// Identify which chunks contain CSS modules
90+
for (const [chunkFileName, item] of Object.entries(bundle)) {
91+
if (item.type === 'chunk') {
92+
for (const moduleId of Object.keys(item.modules)) {
93+
if (RE_CSS.test(moduleId)) {
94+
if (!chunkCSSMap.has(chunkFileName)) {
95+
chunkCSSMap.set(chunkFileName, [])
96+
}
97+
break
98+
}
99+
}
100+
}
101+
}
102+
103+
// Match CSS assets to chunks by comparing base names
104+
for (const [cssFileName] of cssAssets) {
105+
const cssBaseName = normalizeCssFileName(cssFileName)
106+
for (const [chunkFileName] of chunkCSSMap) {
107+
const chunkBaseName = normalizeChunkFileName(chunkFileName)
108+
if (
109+
chunkBaseName === cssBaseName ||
110+
chunkFileName.startsWith(`${cssBaseName}-`)
111+
) {
112+
chunkCSSMap.get(chunkFileName)?.push(cssFileName)
113+
break
114+
}
115+
}
116+
}
117+
118+
let extractedCss = ''
119+
const collected = new Set<OutputChunk>()
120+
const dynamicImports = new Set<string>()
121+
122+
function collect(chunk: OutputChunk | OutputAsset | undefined) {
123+
if (!chunk || chunk.type !== 'chunk' || collected.has(chunk)) return
124+
collected.add(chunk)
125+
126+
// Collect all styles from synchronous imports (lowest priority)
127+
chunk.imports.forEach((importName) => {
128+
collect(bundle[importName])
129+
})
130+
131+
// Save dynamic imports to add styles later (highest priority)
132+
chunk.dynamicImports.forEach((importName) => {
133+
dynamicImports.add(importName)
134+
})
135+
136+
// Collect the styles of the current chunk
137+
const files = chunkCSSMap.get(chunk.fileName)
138+
if (files && files.length > 0) {
139+
for (const filename of files) {
140+
extractedCss += cssAssets.get(filename) ?? ''
141+
}
142+
}
143+
}
144+
145+
// Collect CSS from all entry chunks first
146+
for (const chunk of Object.values(bundle)) {
147+
if (chunk.type === 'chunk' && chunk.isEntry) {
148+
collect(chunk)
149+
}
150+
}
151+
152+
// Collect CSS from dynamic imports (highest priority)
153+
for (const chunkName of dynamicImports) {
154+
collect(bundle[chunkName])
155+
}
156+
157+
if (extractedCss) {
158+
hasEmitted = true
159+
160+
// Remove all individual CSS assets from bundle
161+
for (const fileName of cssAssets.keys()) {
162+
delete bundle[fileName]
163+
}
164+
165+
this.emitFile({
166+
type: 'asset',
167+
source: extractedCss,
168+
fileName,
169+
// this file is an implicit entry point, use `style.css` as the original file name
170+
originalFileName: defaultCssBundleName,
171+
})
172+
}
173+
},
174+
}
175+
}

src/features/rolldown.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { mergeUserOptions } from '../config/options.ts'
1616
import { lowestCommonAncestor } from '../utils/fs.ts'
1717
import { importWithError } from '../utils/general.ts'
1818
import { LogLevels } from '../utils/logger.ts'
19+
import { CssCodeSplitPlugin } from './css.ts'
1920
import { ExternalPlugin } from './external.ts'
2021
import { LightningCSSPlugin } from './lightningcss.ts'
2122
import { NodeProtocolPlugin } from './node-protocol.ts'
@@ -154,6 +155,11 @@ async function resolveInputOptions(
154155
await LightningCSSPlugin({ target }),
155156
)
156157
}
158+
// Add CSS code split plugin after LightningCSS to merge generated CSS files
159+
const cssPlugin = CssCodeSplitPlugin(config)
160+
if (cssPlugin) {
161+
plugins.push(cssPlugin)
162+
}
157163
plugins.push(ShebangPlugin(logger, cwd, nameLabel, isDualFormat))
158164
if (globImport) {
159165
plugins.push(importGlobPlugin({ root: cwd }))
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
## async-CvIfFAic.mjs
2+
3+
```mjs
4+
export { };
5+
```
6+
7+
## index.css
8+
9+
```css
10+
body { color: red }
11+
.async { color: blue }
12+
13+
```
14+
15+
## index.mjs
16+
17+
```mjs
18+
//#region index.ts
19+
const loadAsync = () => import("./async-CvIfFAic.mjs");
20+
21+
//#endregion
22+
export { loadAsync };
23+
```

0 commit comments

Comments
 (0)