@esmx/rspack 3.0.0-rc.10

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.
@@ -0,0 +1,560 @@
1
+ import type { Esmx } from '@esmx/core';
2
+ import {
3
+ type LightningcssLoaderOptions,
4
+ type RuleSetUse,
5
+ type SwcLoaderOptions,
6
+ rspack
7
+ } from '@rspack/core';
8
+ import NodePolyfillPlugin from 'node-polyfill-webpack-plugin';
9
+ import {
10
+ type RspackAppConfigContext,
11
+ type RspackAppOptions,
12
+ createRspackApp
13
+ } from './app';
14
+ import type { BuildTarget } from './build-target';
15
+ import { RSPACK_LOADER } from './loader';
16
+
17
+ /**
18
+ * Rspack HTML 应用配置选项接口
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * // entry.node.ts
23
+ * export default {
24
+ * async devApp(esmx) {
25
+ * return import('@esmx/rspack').then((m) =>
26
+ * m.createRspackHtmlApp(esmx, {
27
+ * // 将 CSS 输出到独立的 CSS 文件中
28
+ * css: 'css',
29
+ * // 自定义 loader
30
+ * loaders: {
31
+ * styleLoader: 'vue-style-loader'
32
+ * },
33
+ * // 配置 CSS 相关 loader
34
+ * styleLoader: {
35
+ * injectType: 'singletonStyleTag'
36
+ * },
37
+ * cssLoader: {
38
+ * modules: true
39
+ * },
40
+ * // 配置构建目标
41
+ * target: {
42
+ * web: ['chrome>=87'],
43
+ * node: ['node>=16']
44
+ * },
45
+ * // 定义全局常量
46
+ * definePlugin: {
47
+ * 'process.env.APP_ENV': JSON.stringify('production')
48
+ * }
49
+ * })
50
+ * );
51
+ * }
52
+ * };
53
+ * ```
54
+ */
55
+ export interface RspackHtmlAppOptions extends RspackAppOptions {
56
+ /**
57
+ * CSS 输出模式配置
58
+ *
59
+ * @default 根据环境自动选择:
60
+ * - 生产环境: 'css',将CSS输出到独立文件中,有利于缓存和并行加载
61
+ * - 开发环境: 'js',将CSS打包到JS中以支持热更新(HMR),实现样式的即时更新
62
+ *
63
+ * - 'css': 将 CSS 输出到独立的 CSS 文件中
64
+ * - 'js': 将 CSS 打包到 JS 文件中,运行时动态插入样式
65
+ * - false: 关闭默认的 CSS 处理配置,需要手动配置 loader 规则
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * // 使用环境默认配置
70
+ * css: undefined
71
+ *
72
+ * // 强制输出到独立的 CSS 文件
73
+ * css: 'css'
74
+ *
75
+ * // 强制打包到 JS 中
76
+ * css: 'js'
77
+ *
78
+ * // 自定义 CSS 处理
79
+ * css: false
80
+ * ```
81
+ */
82
+ css?: 'css' | 'js' | false;
83
+
84
+ /**
85
+ * 自定义 loader 配置
86
+ *
87
+ * 允许替换默认的 loader 实现,可用于切换到特定框架的 loader
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * // 使用 Vue 的 style-loader
92
+ * loaders: {
93
+ * styleLoader: 'vue-style-loader'
94
+ * }
95
+ * ```
96
+ */
97
+ loaders?: Partial<Record<keyof typeof RSPACK_LOADER, string>>;
98
+
99
+ /**
100
+ * style-loader 配置项
101
+ *
102
+ * 用于配置样式注入方式,完整选项参考:
103
+ * https://github.com/webpack-contrib/style-loader
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * styleLoader: {
108
+ * injectType: 'singletonStyleTag',
109
+ * attributes: { id: 'app-styles' }
110
+ * }
111
+ * ```
112
+ */
113
+ styleLoader?: Record<string, any>;
114
+
115
+ /**
116
+ * css-loader 配置项
117
+ *
118
+ * 用于配置 CSS 模块化、URL 解析等,完整选项参考:
119
+ * https://github.com/webpack-contrib/css-loader
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * cssLoader: {
124
+ * modules: true,
125
+ * url: false
126
+ * }
127
+ * ```
128
+ */
129
+ cssLoader?: Record<string, any>;
130
+
131
+ /**
132
+ * less-loader 配置项
133
+ *
134
+ * 用于配置 Less 编译选项,完整选项参考:
135
+ * https://github.com/webpack-contrib/less-loader
136
+ *
137
+ * @example
138
+ * ```ts
139
+ * lessLoader: {
140
+ * lessOptions: {
141
+ * javascriptEnabled: true,
142
+ * modifyVars: { '@primary-color': '#1DA57A' }
143
+ * }
144
+ * }
145
+ * ```
146
+ */
147
+ lessLoader?: Record<string, any>;
148
+
149
+ /**
150
+ * style-resources-loader 配置项
151
+ *
152
+ * 用于自动注入全局的样式资源,完整选项参考:
153
+ * https://github.com/yenshih/style-resources-loader
154
+ *
155
+ * @example
156
+ * ```ts
157
+ * styleResourcesLoader: {
158
+ * patterns: [
159
+ * './src/styles/variables.less',
160
+ * './src/styles/mixins.less'
161
+ * ]
162
+ * }
163
+ * ```
164
+ */
165
+ styleResourcesLoader?: Record<string, any>;
166
+
167
+ /**
168
+ * SWC loader 配置项
169
+ *
170
+ * 用于配置 TypeScript/JavaScript 编译选项,完整选项参考:
171
+ * https://rspack.dev/guide/features/builtin-swc-loader
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * swcLoader: {
176
+ * jsc: {
177
+ * parser: {
178
+ * syntax: 'typescript',
179
+ * decorators: true
180
+ * },
181
+ * transform: {
182
+ * legacyDecorator: true
183
+ * }
184
+ * }
185
+ * }
186
+ * ```
187
+ */
188
+ swcLoader?: SwcLoaderOptions;
189
+
190
+ /**
191
+ * DefinePlugin 配置项
192
+ *
193
+ * 用于定义编译时的全局常量,支持针对不同构建目标设置不同的值
194
+ * 完整说明参考: https://rspack.dev/plugins/webpack/define-plugin
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * // 统一的值
199
+ * definePlugin: {
200
+ * 'process.env.APP_ENV': JSON.stringify('production')
201
+ * }
202
+ *
203
+ * // 针对不同构建目标的值
204
+ * definePlugin: {
205
+ * 'process.env.IS_SERVER': {
206
+ * server: 'true',
207
+ * client: 'false'
208
+ * }
209
+ * }
210
+ * ```
211
+ */
212
+ definePlugin?: Record<
213
+ string,
214
+ string | Partial<Record<BuildTarget, string>>
215
+ >;
216
+
217
+ /**
218
+ * 构建目标配置
219
+ *
220
+ * 用于设置代码的目标运行环境,影响代码的编译降级和 polyfill 注入
221
+ *
222
+ * @example
223
+ * ```ts
224
+ * target: {
225
+ * // 浏览器构建目标
226
+ * web: ['chrome>=87', 'firefox>=78', 'safari>=14'],
227
+ * // Node.js 构建目标
228
+ * node: ['node>=16']
229
+ * }
230
+ * ```
231
+ */
232
+ target?: {
233
+ /**
234
+ * 浏览器构建目标
235
+ *
236
+ * @default ['chrome>=87', 'edge>=88', 'firefox>=78', 'safari>=14']
237
+ */
238
+ web?: string[];
239
+
240
+ /**
241
+ * Node.js 构建目标
242
+ *
243
+ * @default ['node>=22.6']
244
+ */
245
+ node?: string[];
246
+ };
247
+ }
248
+ /**
249
+ * 创建 Rspack HTML 应用实例。
250
+ *
251
+ * 该函数提供了完整的 Web 应用构建配置,支持以下资源类型的处理:
252
+ * - TypeScript/JavaScript
253
+ * - Web Worker
254
+ * - JSON
255
+ * - CSS/Less
256
+ * - 视频/图片
257
+ * - 字体文件
258
+ *
259
+ * @param esmx - Esmx 框架实例
260
+ * @param options - Rspack HTML 应用配置选项
261
+ * @returns 返回应用实例,包含中间件、渲染函数和构建函数
262
+ *
263
+ * @example
264
+ * ```ts
265
+ * // 开发环境配置
266
+ * // entry.node.ts
267
+ * export default {
268
+ * async devApp(esmx) {
269
+ * return import('@esmx/rspack').then((m) =>
270
+ * m.createRspackHtmlApp(esmx, {
271
+ * // 配置 CSS 输出模式
272
+ * css: 'css',
273
+ * // 配置 TypeScript 编译选项
274
+ * swcLoader: {
275
+ * jsc: {
276
+ * parser: {
277
+ * syntax: 'typescript',
278
+ * decorators: true
279
+ * }
280
+ * }
281
+ * },
282
+ * // 配置构建目标
283
+ * target: {
284
+ * web: ['chrome>=87'],
285
+ * node: ['node>=16']
286
+ * },
287
+ * // 自定义 Rspack 配置
288
+ * config({ config }) {
289
+ * // 添加自定义 loader
290
+ * config.module.rules.push({
291
+ * test: /\.vue$/,
292
+ * loader: 'vue-loader'
293
+ * });
294
+ * }
295
+ * })
296
+ * );
297
+ * }
298
+ * };
299
+ *
300
+ * // 生产环境配置
301
+ * // entry.node.ts
302
+ * export default {
303
+ * async buildApp(esmx) {
304
+ * return import('@esmx/rspack').then((m) =>
305
+ * m.createRspackHtmlApp(esmx, {
306
+ * // 启用代码压缩
307
+ * minimize: true,
308
+ * // 配置全局常量
309
+ * definePlugin: {
310
+ * 'process.env.NODE_ENV': JSON.stringify('production'),
311
+ * 'process.env.IS_SERVER': {
312
+ * server: 'true',
313
+ * client: 'false'
314
+ * }
315
+ * }
316
+ * })
317
+ * );
318
+ * }
319
+ * };
320
+ * ```
321
+ */
322
+ export async function createRspackHtmlApp(
323
+ esmx: Esmx,
324
+ options?: RspackHtmlAppOptions
325
+ ) {
326
+ options = {
327
+ ...options,
328
+ target: {
329
+ web: ['chrome>=87', 'edge>=88', 'firefox>=78', 'safari>=14'],
330
+ node: ['node>=22.6'],
331
+ ...options?.target
332
+ },
333
+ css: options?.css ? options.css : esmx.isProd ? 'css' : 'js'
334
+ };
335
+ return createRspackApp(esmx, {
336
+ ...options,
337
+ config(context) {
338
+ const { config, buildTarget } = context;
339
+ config.stats = 'errors-warnings';
340
+ config.module = {
341
+ ...config.module,
342
+ rules: [
343
+ ...(config.module?.rules ?? []),
344
+ {
345
+ test: /\.(jpe?g|png|gif|bmp|webp|svg)$/i,
346
+ type: 'asset/resource',
347
+ generator: {
348
+ filename: filename(esmx, 'images')
349
+ }
350
+ },
351
+ {
352
+ test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)$/i,
353
+ type: 'asset/resource',
354
+ generator: {
355
+ filename: filename(esmx, 'media')
356
+ }
357
+ },
358
+ {
359
+ test: /\.(woff|woff2|eot|ttf|otf)(\?.*)?$/i,
360
+ type: 'asset/resource',
361
+ generator: {
362
+ filename: filename(esmx, 'fonts')
363
+ }
364
+ },
365
+ {
366
+ test: /\.json$/i,
367
+ type: 'json'
368
+ },
369
+ {
370
+ test: /\.worker\.(c|m)?(t|j)s$/i,
371
+ loader:
372
+ options.loaders?.workerRspackLoader ??
373
+ RSPACK_LOADER.workerRspackLoader,
374
+ options: {
375
+ esModule: false,
376
+ filename: `${esmx.name}/workers/[name].[contenthash]${esmx.isProd ? '.final' : ''}.js`
377
+ }
378
+ },
379
+ {
380
+ test: /\.(ts|mts)$/i,
381
+ loader:
382
+ options.loaders?.builtinSwcLoader ??
383
+ RSPACK_LOADER.builtinSwcLoader,
384
+ options: {
385
+ env: {
386
+ targets:
387
+ buildTarget === 'client'
388
+ ? options?.target?.web
389
+ : options?.target?.node,
390
+ ...options?.swcLoader?.env
391
+ },
392
+ jsc: {
393
+ parser: {
394
+ syntax: 'typescript',
395
+ ...options?.swcLoader?.jsc?.parser
396
+ },
397
+ ...options?.swcLoader?.jsc
398
+ },
399
+ ...options?.swcLoader
400
+ } satisfies SwcLoaderOptions,
401
+ type: 'javascript/auto'
402
+ }
403
+ ]
404
+ };
405
+ config.optimization = {
406
+ ...config.optimization,
407
+ minimizer: [
408
+ new rspack.SwcJsMinimizerRspackPlugin({
409
+ minimizerOptions: {
410
+ format: {
411
+ comments: false
412
+ }
413
+ }
414
+ }),
415
+ new rspack.LightningCssMinimizerRspackPlugin({
416
+ minimizerOptions: {
417
+ targets: options.target?.web,
418
+ errorRecovery: false
419
+ }
420
+ })
421
+ ]
422
+ };
423
+ config.plugins = [
424
+ new NodePolyfillPlugin(),
425
+ ...(config.plugins ?? [])
426
+ ];
427
+ config.devtool = false;
428
+ config.cache = false;
429
+ if (options.definePlugin) {
430
+ const defineOptions: Record<string, string> = {};
431
+ Object.entries(options.definePlugin).forEach(
432
+ ([name, value]) => {
433
+ const targetValue =
434
+ typeof value === 'string'
435
+ ? value
436
+ : value[buildTarget];
437
+ if (
438
+ typeof targetValue === 'string' &&
439
+ name !== targetValue
440
+ ) {
441
+ defineOptions[name] = targetValue;
442
+ }
443
+ }
444
+ );
445
+ if (Object.keys(defineOptions).length) {
446
+ config.plugins.push(new rspack.DefinePlugin(defineOptions));
447
+ }
448
+ }
449
+ config.resolve = {
450
+ ...config.resolve,
451
+ extensions: ['...', '.ts']
452
+ };
453
+ addCssConfig(esmx, options, context);
454
+ options?.config?.(context);
455
+ }
456
+ });
457
+ }
458
+
459
+ function filename(esmx: Esmx, name: string, ext = '[ext]') {
460
+ return esmx.isProd
461
+ ? `${name}/[name].[contenthash:8].final${ext}`
462
+ : `${name}/[path][name]${ext}`;
463
+ }
464
+
465
+ function addCssConfig(
466
+ esmx: Esmx,
467
+ options: RspackHtmlAppOptions,
468
+ { config }: RspackAppConfigContext
469
+ ) {
470
+ if (options.css === false) {
471
+ return;
472
+ }
473
+ // 输出在 .js 文件中
474
+ if (options.css === 'js') {
475
+ const cssRule: RuleSetUse = [
476
+ {
477
+ loader:
478
+ options.loaders?.styleLoader ?? RSPACK_LOADER.styleLoader,
479
+ options: options.styleLoader
480
+ },
481
+ {
482
+ loader: options.loaders?.cssLoader ?? RSPACK_LOADER.cssLoader,
483
+ options: options.cssLoader
484
+ },
485
+ {
486
+ loader:
487
+ options.loaders?.lightningcssLoader ??
488
+ RSPACK_LOADER.lightningcssLoader,
489
+ options: {
490
+ targets: options.target?.web ?? [],
491
+ minify: esmx.isProd
492
+ } satisfies LightningcssLoaderOptions
493
+ }
494
+ ];
495
+ const lessRule: RuleSetUse = [
496
+ {
497
+ loader: options.loaders?.lessLoader ?? RSPACK_LOADER.lessLoader,
498
+ options: options.lessLoader
499
+ }
500
+ ];
501
+ if (options.styleResourcesLoader) {
502
+ lessRule.push({
503
+ loader:
504
+ options.loaders?.styleResourcesLoader ??
505
+ RSPACK_LOADER.styleResourcesLoader,
506
+ options: options.styleResourcesLoader
507
+ });
508
+ }
509
+ config.module = {
510
+ ...config.module,
511
+ rules: [
512
+ ...(config.module?.rules ?? []),
513
+ {
514
+ test: /\.less$/,
515
+ use: [...cssRule, ...lessRule],
516
+ type: 'javascript/auto'
517
+ },
518
+ {
519
+ test: /\.css$/,
520
+ use: cssRule,
521
+ type: 'javascript/auto'
522
+ }
523
+ ]
524
+ };
525
+ return;
526
+ }
527
+ // 输出在 .css 文件中
528
+ config.experiments = {
529
+ ...config.experiments,
530
+ css: true
531
+ };
532
+ if (!config.experiments.css) {
533
+ return;
534
+ }
535
+ const lessLoaders: RuleSetUse = [
536
+ {
537
+ loader: options.loaders?.lessLoader ?? RSPACK_LOADER.lessLoader,
538
+ options: options.lessLoader
539
+ }
540
+ ];
541
+ if (options.styleResourcesLoader) {
542
+ lessLoaders.push({
543
+ loader:
544
+ options.loaders?.styleResourcesLoader ??
545
+ RSPACK_LOADER.styleResourcesLoader,
546
+ options: options.styleResourcesLoader
547
+ });
548
+ }
549
+ config.module = {
550
+ ...config.module,
551
+ rules: [
552
+ ...(config.module?.rules ?? []),
553
+ {
554
+ test: /\.less$/,
555
+ use: [...lessLoaders],
556
+ type: 'css'
557
+ }
558
+ ]
559
+ };
560
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export {
2
+ type RspackAppConfigContext,
3
+ type RspackAppOptions,
4
+ createRspackApp
5
+ } from './app';
6
+ export { createRspackHtmlApp, type RspackHtmlAppOptions } from './html-app';
7
+ export type { BuildTarget } from './build-target';
8
+ export { RSPACK_LOADER } from './loader';
9
+
10
+ import * as rspack from '@rspack/core';
11
+
12
+ export { rspack };
package/src/loader.ts ADDED
@@ -0,0 +1,34 @@
1
+ function resolve(name: string) {
2
+ return new URL(import.meta.resolve(name)).pathname;
3
+ }
4
+
5
+ export const RSPACK_LOADER = {
6
+ /**
7
+ * Rspack 内置的 builtin:swc-loader
8
+ */
9
+ builtinSwcLoader: 'builtin:swc-loader',
10
+ /**
11
+ * Rspack 内置的 lightningcss-loader
12
+ */
13
+ lightningcssLoader: 'builtin:lightningcss-loader',
14
+ /**
15
+ * css-loader
16
+ */
17
+ cssLoader: resolve('css-loader'),
18
+ /**
19
+ * style-loader
20
+ */
21
+ styleLoader: resolve('style-loader'),
22
+ /**
23
+ * less-loader
24
+ */
25
+ lessLoader: resolve('less-loader'),
26
+ /**
27
+ * style-resources-loader
28
+ */
29
+ styleResourcesLoader: resolve('style-resources-loader'),
30
+ /**
31
+ * worker-rspack-loader
32
+ */
33
+ workerRspackLoader: resolve('worker-rspack-loader')
34
+ } satisfies Record<string, string>;
package/src/pack.ts ADDED
@@ -0,0 +1,79 @@
1
+ import crypto from 'node:crypto';
2
+ import type { Esmx } from '@esmx/core';
3
+ import Arborist from '@npmcli/arborist';
4
+ import pacote from 'pacote';
5
+
6
+ export async function pack(esmx: Esmx): Promise<boolean> {
7
+ const { packConfig } = esmx;
8
+
9
+ const pkgJson = await packConfig.packageJson(
10
+ esmx,
11
+ await buildPackageJson(esmx)
12
+ );
13
+ esmx.writeSync(
14
+ esmx.resolvePath('dist/package.json'),
15
+ JSON.stringify(pkgJson, null, 4)
16
+ );
17
+
18
+ if (!packConfig.enable) {
19
+ return true;
20
+ }
21
+
22
+ await packConfig.onBefore(esmx, pkgJson);
23
+
24
+ const data = await pacote.tarball(esmx.resolvePath('dist'), {
25
+ Arborist
26
+ });
27
+ const hash = contentHash(data);
28
+ packConfig.outputs.forEach((file) => {
29
+ const tgz = esmx.resolvePath('./', file);
30
+ const txt = tgz.replace(/\.tgz$/, '.txt');
31
+ esmx.writeSync(tgz, data);
32
+ esmx.writeSync(txt, hash);
33
+ });
34
+
35
+ await packConfig.onAfter(esmx, pkgJson, data);
36
+ return true;
37
+ }
38
+
39
+ async function buildPackageJson(esmx: Esmx): Promise<Record<string, any>> {
40
+ const [clientJson, serverJson, curJson] = await Promise.all([
41
+ esmx.readJson(esmx.resolvePath('dist/client/manifest.json')),
42
+ esmx.readJson(esmx.resolvePath('dist/server/manifest.json')),
43
+ esmx.readJson(esmx.resolvePath('package.json'))
44
+ ]);
45
+ const exports: Record<string, any> = {
46
+ ...curJson?.exports
47
+ };
48
+ const set = new Set([
49
+ ...Object.keys(clientJson.exports),
50
+ ...Object.keys(serverJson.exports)
51
+ ]);
52
+ set.forEach((name) => {
53
+ const client = clientJson.exports[name];
54
+ const server = serverJson.exports[name];
55
+ const exportName = `./${name}`;
56
+ if (client && server) {
57
+ exports[exportName] = {
58
+ default: `./server/${server}`,
59
+ browser: `./client/${client}`
60
+ };
61
+ } else if (client) {
62
+ exports[exportName] = `./client/${client}`;
63
+ } else if (server) {
64
+ exports[exportName] = `./server/${server}`;
65
+ }
66
+ });
67
+
68
+ const buildJson: Record<string, any> = {
69
+ ...curJson,
70
+ exports
71
+ };
72
+ return buildJson;
73
+ }
74
+
75
+ function contentHash(buffer: Buffer, algorithm = 'sha256') {
76
+ const hash = crypto.createHash('sha256');
77
+ hash.update(buffer);
78
+ return `${algorithm}-${hash.digest('hex')}`;
79
+ }
@@ -0,0 +1 @@
1
+ export * from './rsbuild';