@esmx/rspack 3.0.0-rc.11 → 3.0.0-rc.111

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.
Files changed (76) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +48 -20
  3. package/README.zh-CN.md +57 -0
  4. package/dist/index.d.ts +2 -4
  5. package/dist/index.mjs +4 -4
  6. package/dist/module-link/config.d.ts +5 -0
  7. package/dist/module-link/config.mjs +100 -0
  8. package/dist/module-link/config1.d.ts +3 -0
  9. package/dist/module-link/config1.mjs +16 -0
  10. package/dist/module-link/config2.d.ts +3 -0
  11. package/dist/module-link/config2.mjs +17 -0
  12. package/dist/module-link/index.d.ts +4 -0
  13. package/dist/module-link/index.mjs +8 -0
  14. package/dist/module-link/manifest-plugin.d.ts +14 -0
  15. package/dist/module-link/manifest-plugin.mjs +141 -0
  16. package/dist/module-link/parse.d.ts +2 -0
  17. package/dist/module-link/parse.mjs +24 -0
  18. package/dist/module-link/types.d.ts +25 -0
  19. package/dist/rspack/app.d.ts +183 -0
  20. package/dist/{app.mjs → rspack/app.mjs} +18 -46
  21. package/dist/rspack/build-target.d.ts +7 -0
  22. package/dist/rspack/build-target.mjs +0 -0
  23. package/dist/{config.d.ts → rspack/chain-config.d.ts} +3 -4
  24. package/dist/rspack/chain-config.mjs +113 -0
  25. package/dist/rspack/index.d.ts +3 -0
  26. package/dist/rspack/index.mjs +4 -0
  27. package/dist/rspack/loader.d.ts +9 -0
  28. package/dist/{loader.mjs → rspack/loader.mjs} +2 -22
  29. package/dist/rspack/pack.d.ts +9 -0
  30. package/dist/{pack.mjs → rspack/pack.mjs} +33 -25
  31. package/dist/rspack/pack.test.d.ts +1 -0
  32. package/dist/rspack/pack.test.mjs +180 -0
  33. package/dist/rspack/utils/rsbuild.d.ts +6 -0
  34. package/dist/{utils → rspack/utils}/rsbuild.mjs +7 -37
  35. package/dist/rspack-html/index.d.ts +168 -0
  36. package/dist/rspack-html/index.mjs +160 -0
  37. package/dist/rspack-html/target-setting.d.ts +17 -0
  38. package/dist/rspack-html/target-setting.mjs +31 -0
  39. package/dist/rspack-html/target-setting.test.d.ts +1 -0
  40. package/dist/rspack-html/target-setting.test.mjs +105 -0
  41. package/package.json +23 -21
  42. package/src/index.ts +7 -6
  43. package/src/module-link/config.ts +157 -0
  44. package/src/module-link/config1.ts +24 -0
  45. package/src/module-link/config2.ts +28 -0
  46. package/src/module-link/index.ts +19 -0
  47. package/src/module-link/manifest-plugin.ts +179 -0
  48. package/src/module-link/parse.ts +31 -0
  49. package/src/module-link/types.ts +31 -0
  50. package/src/{app.ts → rspack/app.ts} +104 -107
  51. package/src/rspack/build-target.ts +7 -0
  52. package/src/rspack/chain-config.ts +165 -0
  53. package/src/rspack/index.ts +8 -0
  54. package/src/{loader.ts → rspack/loader.ts} +3 -22
  55. package/src/rspack/pack.test.ts +215 -0
  56. package/src/rspack/pack.ts +101 -0
  57. package/src/{utils → rspack/utils}/rsbuild.ts +11 -40
  58. package/src/rspack-html/index.ts +495 -0
  59. package/src/rspack-html/target-setting.test.ts +123 -0
  60. package/src/rspack-html/target-setting.ts +52 -0
  61. package/dist/app.d.ts +0 -160
  62. package/dist/build-target.d.ts +0 -8
  63. package/dist/config.mjs +0 -142
  64. package/dist/html-app.d.ts +0 -299
  65. package/dist/html-app.mjs +0 -214
  66. package/dist/loader.d.ts +0 -30
  67. package/dist/pack.d.ts +0 -2
  68. package/dist/utils/rsbuild.d.ts +0 -12
  69. package/src/build-target.ts +0 -8
  70. package/src/config.ts +0 -171
  71. package/src/html-app.ts +0 -560
  72. package/src/pack.ts +0 -79
  73. /package/dist/{build-target.mjs → module-link/types.mjs} +0 -0
  74. /package/dist/{utils → rspack/utils}/index.d.ts +0 -0
  75. /package/dist/{utils → rspack/utils}/index.mjs +0 -0
  76. /package/src/{utils → rspack/utils}/index.ts +0 -0
@@ -0,0 +1,495 @@
1
+ import type { Esmx } from '@esmx/core';
2
+ import {
3
+ type LightningcssLoaderOptions,
4
+ rspack,
5
+ type SwcLoaderOptions
6
+ } from '@rspack/core';
7
+ import NodePolyfillPlugin from 'node-polyfill-webpack-plugin';
8
+ import type RspackChain from 'rspack-chain';
9
+ import {
10
+ type BuildTarget,
11
+ createRspackApp,
12
+ RSPACK_LOADER,
13
+ type RspackAppOptions
14
+ } from '../rspack';
15
+ import type { TargetSetting } from './target-setting';
16
+ import { getTargetSetting } from './target-setting';
17
+
18
+ export type { TargetSetting };
19
+ export interface RspackHtmlAppOptions extends RspackAppOptions {
20
+ /**
21
+ * CSS output mode configuration
22
+ *
23
+ * @default Automatically selected based on environment:
24
+ * - Production: 'css', outputs CSS to separate files for better caching and parallel loading
25
+ * - Development: 'js', bundles CSS into JS to support hot module replacement (HMR) for instant style updates
26
+ *
27
+ * - 'css': Output CSS to separate CSS files
28
+ * - 'js': Bundle CSS into JS files and dynamically inject styles at runtime
29
+ * - false: Disable default CSS processing configuration, requires manual loader rule configuration
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * // Use environment default configuration
34
+ * css: undefined
35
+ *
36
+ * // Force output to separate CSS files
37
+ * css: 'css'
38
+ *
39
+ * // Force bundle into JS
40
+ * css: 'js'
41
+ *
42
+ * // Custom CSS processing
43
+ * css: false
44
+ * ```
45
+ */
46
+ css?: 'css' | 'js' | false;
47
+
48
+ /**
49
+ * Custom loader configuration
50
+ *
51
+ * Allows replacing default loader implementations, useful for switching to framework-specific loaders
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * // Use Vue's style-loader
56
+ * loaders: {
57
+ * styleLoader: 'vue-style-loader'
58
+ * }
59
+ * ```
60
+ */
61
+ loaders?: Partial<Record<keyof typeof RSPACK_LOADER, string>>;
62
+
63
+ /**
64
+ * Configure style injection method. For complete options, see:
65
+ * https://github.com/webpack-contrib/style-loader
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * styleLoader: {
70
+ * injectType: 'singletonStyleTag',
71
+ * attributes: { id: 'app-styles' }
72
+ * }
73
+ * ```
74
+ */
75
+ styleLoader?: Record<string, any>;
76
+
77
+ /**
78
+ * Configure CSS modules, URL resolution, etc. For complete options, see:
79
+ * https://github.com/webpack-contrib/css-loader
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * cssLoader: {
84
+ * modules: true,
85
+ * url: false
86
+ * }
87
+ * ```
88
+ */
89
+ cssLoader?: Record<string, any>;
90
+
91
+ /**
92
+ * Configure Less compilation options. For complete options, see:
93
+ * https://github.com/webpack-contrib/less-loader
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * lessLoader: {
98
+ * lessOptions: {
99
+ * javascriptEnabled: true,
100
+ * modifyVars: { '@primary-color': '#1DA57A' }
101
+ * }
102
+ * }
103
+ * ```
104
+ */
105
+ lessLoader?: Record<string, any>;
106
+
107
+ /**
108
+ * Automatically inject global style resources. For complete options, see:
109
+ * https://github.com/yenshih/style-resources-loader
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * styleResourcesLoader: {
114
+ * patterns: [
115
+ * './src/styles/variables.less',
116
+ * './src/styles/mixins.less'
117
+ * ]
118
+ * }
119
+ * ```
120
+ */
121
+ styleResourcesLoader?: Record<string, any>;
122
+
123
+ /**
124
+ * Configure TypeScript/JavaScript compilation options. For complete options, see:
125
+ * https://rspack.dev/guide/features/builtin-swc-loader
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * swcLoader: {
130
+ * jsc: {
131
+ * parser: {
132
+ * syntax: 'typescript',
133
+ * decorators: true
134
+ * },
135
+ * transform: {
136
+ * legacyDecorator: true
137
+ * }
138
+ * }
139
+ * }
140
+ * ```
141
+ */
142
+ swcLoader?: SwcLoaderOptions;
143
+
144
+ /**
145
+ * Define compile-time global constants, supports setting different values for different build targets
146
+ * For complete documentation, see: https://rspack.dev/plugins/webpack/define-plugin
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * // Unified value
151
+ * definePlugin: {
152
+ * 'process.env.APP_ENV': JSON.stringify('production')
153
+ * }
154
+ *
155
+ * // Values for different build targets
156
+ * definePlugin: {
157
+ * 'process.env.IS_SERVER': {
158
+ * server: 'true',
159
+ * client: 'false'
160
+ * }
161
+ * }
162
+ * ```
163
+ */
164
+ definePlugin?: Record<
165
+ string,
166
+ string | Partial<Record<BuildTarget, string>>
167
+ >;
168
+
169
+ /**
170
+ * Set the target runtime environment for the code, affecting code compilation downgrading and polyfill injection
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * // Global compatible mode
175
+ * target: 'compatible'
176
+ *
177
+ * // Global modern mode
178
+ * target: 'modern'
179
+ *
180
+ * // Global custom targets
181
+ * target: ['chrome>=89', 'edge>=89', 'firefox>=108', 'safari>=16.4', 'node>=24']
182
+ *
183
+ * // Per-build-target configuration
184
+ * target: {
185
+ * client: 'modern',
186
+ * server: ['node>=24']
187
+ * }
188
+ * ```
189
+ */
190
+ target?: TargetSetting;
191
+ }
192
+
193
+ export async function createRspackHtmlApp(
194
+ esmx: Esmx,
195
+ options?: RspackHtmlAppOptions
196
+ ) {
197
+ options = {
198
+ ...options,
199
+ css: options?.css ? options.css : esmx.isProd ? 'css' : 'js'
200
+ };
201
+ return createRspackApp(esmx, {
202
+ ...options,
203
+ chain(context) {
204
+ const { chain, buildTarget, esmx } = context;
205
+
206
+ chain.stats('errors-warnings');
207
+ chain.devtool(false);
208
+ chain.cache(false);
209
+
210
+ configureAssetRules(chain, esmx);
211
+
212
+ chain.module
213
+ .rule('json')
214
+ .test(/\.json$/i)
215
+ .type('json');
216
+
217
+ configureWorkerRule(chain, esmx, options);
218
+
219
+ configureTypeScriptRule(chain, buildTarget, options);
220
+
221
+ configureOptimization(chain, options);
222
+
223
+ chain.plugin('node-polyfill').use(NodePolyfillPlugin);
224
+
225
+ configureDefinePlugin(chain, buildTarget, options);
226
+
227
+ chain.resolve.extensions.clear().add('...').add('.ts');
228
+
229
+ configureCssRules(chain, esmx, options);
230
+
231
+ options?.chain?.(context);
232
+ }
233
+ });
234
+ }
235
+
236
+ function configureAssetRules(chain: RspackChain, esmx: Esmx): void {
237
+ chain.module
238
+ .rule('images')
239
+ .test(
240
+ /\.(png|jpg|jpeg|gif|svg|bmp|webp|ico|apng|avif|tif|tiff|jfif|pjpeg|pjp|cur)$/i
241
+ )
242
+ .type('asset/resource')
243
+ .set('generator', {
244
+ filename: filename(esmx, 'images')
245
+ });
246
+
247
+ chain.module
248
+ .rule('media')
249
+ .test(/\.(mp4|webm|ogg|mov)$/i)
250
+ .type('asset/resource')
251
+ .set('generator', {
252
+ filename: filename(esmx, 'media')
253
+ });
254
+
255
+ chain.module
256
+ .rule('audio')
257
+ .test(/\.(mp3|wav|flac|aac|m4a|opus)$/i)
258
+ .type('asset/resource')
259
+ .set('generator', {
260
+ filename: filename(esmx, 'audio')
261
+ });
262
+
263
+ chain.module
264
+ .rule('fonts')
265
+ .test(/\.(woff|woff2|eot|ttf|otf|ttc)(\?.*)?$/i)
266
+ .type('asset/resource')
267
+ .set('generator', {
268
+ filename: filename(esmx, 'fonts')
269
+ });
270
+ }
271
+
272
+ function configureWorkerRule(
273
+ chain: RspackChain,
274
+ esmx: Esmx,
275
+ options: RspackHtmlAppOptions
276
+ ): void {
277
+ chain.module
278
+ .rule('worker')
279
+ .test(/\.worker\.(c|m)?(t|j)s$/i)
280
+ .use('worker-loader')
281
+ .loader(
282
+ options.loaders?.workerRspackLoader ??
283
+ RSPACK_LOADER.workerRspackLoader
284
+ )
285
+ .options({
286
+ esModule: false,
287
+ filename: `${esmx.name}/workers/[name].[contenthash]${esmx.isProd ? '.final' : ''}.js`
288
+ });
289
+ }
290
+
291
+ function configureTypeScriptRule(
292
+ chain: RspackChain,
293
+ buildTarget: BuildTarget,
294
+ options: RspackHtmlAppOptions
295
+ ): void {
296
+ const targets = getTargetSetting(options?.target, buildTarget);
297
+ chain.module
298
+ .rule('typescript')
299
+ .test(/\.(ts|mts)$/i)
300
+ .use('swc-loader')
301
+ .loader(
302
+ options.loaders?.builtinSwcLoader ?? RSPACK_LOADER.builtinSwcLoader
303
+ )
304
+ .options({
305
+ env: {
306
+ targets,
307
+ ...options?.swcLoader?.env
308
+ },
309
+ jsc: {
310
+ parser: {
311
+ syntax: 'typescript',
312
+ ...options?.swcLoader?.jsc?.parser
313
+ },
314
+ ...options?.swcLoader?.jsc
315
+ },
316
+ ...options?.swcLoader
317
+ } as SwcLoaderOptions)
318
+ .end()
319
+ .type('javascript/auto');
320
+ }
321
+
322
+ function configureOptimization(
323
+ chain: RspackChain,
324
+ options: RspackHtmlAppOptions
325
+ ): void {
326
+ chain.optimization
327
+ .minimizer('swc-js-minimizer')
328
+ .use(rspack.SwcJsMinimizerRspackPlugin, [
329
+ {
330
+ minimizerOptions: {
331
+ format: {
332
+ comments: false
333
+ }
334
+ }
335
+ }
336
+ ]);
337
+
338
+ chain.optimization
339
+ .minimizer('lightningcss-minimizer')
340
+ .use(rspack.LightningCssMinimizerRspackPlugin, [
341
+ {
342
+ minimizerOptions: {
343
+ targets: getTargetSetting(options?.target, 'client'),
344
+ errorRecovery: false
345
+ }
346
+ }
347
+ ]);
348
+ }
349
+
350
+ function configureDefinePlugin(
351
+ chain: RspackChain,
352
+ buildTarget: BuildTarget,
353
+ options: RspackHtmlAppOptions
354
+ ): void {
355
+ if (options.definePlugin) {
356
+ const defineOptions: Record<string, string> = {};
357
+ Object.entries(options.definePlugin).forEach(([name, value]) => {
358
+ const targetValue =
359
+ typeof value === 'string'
360
+ ? value
361
+ : value[buildTarget as keyof typeof value];
362
+ if (typeof targetValue === 'string' && name !== targetValue) {
363
+ defineOptions[name] = targetValue;
364
+ }
365
+ });
366
+
367
+ if (Object.keys(defineOptions).length) {
368
+ chain.plugin('define').use(rspack.DefinePlugin, [defineOptions]);
369
+ }
370
+ }
371
+ }
372
+
373
+ function configureCssRules(
374
+ chain: RspackChain,
375
+ esmx: Esmx,
376
+ options: RspackHtmlAppOptions
377
+ ): void {
378
+ if (options.css === false) {
379
+ return;
380
+ }
381
+
382
+ if (options.css === 'js') {
383
+ configureCssInJS(chain, esmx, options);
384
+ return;
385
+ }
386
+ configureCssExtract(chain, options);
387
+ }
388
+
389
+ function configureCssInJS(
390
+ chain: RspackChain,
391
+ esmx: Esmx,
392
+ options: RspackHtmlAppOptions
393
+ ): void {
394
+ chain.module
395
+ .rule('css')
396
+ .test(/\.css$/)
397
+ .use('style-loader')
398
+ .loader(options.loaders?.styleLoader ?? RSPACK_LOADER.styleLoader)
399
+ .options(options.styleLoader ?? {})
400
+ .end()
401
+ .use('css-loader')
402
+ .loader(options.loaders?.cssLoader ?? RSPACK_LOADER.cssLoader)
403
+ .options(options.cssLoader ?? {})
404
+ .end()
405
+ .use('lightning-css-loader')
406
+ .loader(
407
+ options.loaders?.lightningcssLoader ??
408
+ RSPACK_LOADER.lightningcssLoader
409
+ )
410
+ .options({
411
+ targets: getTargetSetting(options?.target, 'client'),
412
+ minify: esmx.isProd
413
+ } as LightningcssLoaderOptions)
414
+ .end()
415
+ .type('javascript/auto');
416
+
417
+ const lessRule = chain.module
418
+ .rule('less')
419
+ .test(/\.less$/)
420
+ .use('style-loader')
421
+ .loader(options.loaders?.styleLoader ?? RSPACK_LOADER.styleLoader)
422
+ .options(options.styleLoader ?? {})
423
+ .end()
424
+ .use('css-loader')
425
+ .loader(options.loaders?.cssLoader ?? RSPACK_LOADER.cssLoader)
426
+ .options(options.cssLoader ?? {})
427
+ .end()
428
+ .use('lightning-css-loader')
429
+ .loader(
430
+ options.loaders?.lightningcssLoader ??
431
+ RSPACK_LOADER.lightningcssLoader
432
+ )
433
+ .options({
434
+ targets: getTargetSetting(options?.target, 'client'),
435
+ minify: esmx.isProd
436
+ } as LightningcssLoaderOptions)
437
+ .end()
438
+ .use('less-loader')
439
+ .loader(options.loaders?.lessLoader ?? RSPACK_LOADER.lessLoader)
440
+ .options(options.lessLoader ?? {})
441
+ .end();
442
+
443
+ if (options.styleResourcesLoader) {
444
+ lessRule
445
+ .use('style-resources-loader')
446
+ .loader(
447
+ options.loaders?.styleResourcesLoader ??
448
+ RSPACK_LOADER.styleResourcesLoader
449
+ )
450
+ .options(options.styleResourcesLoader);
451
+ }
452
+
453
+ lessRule.type('javascript/auto');
454
+ }
455
+
456
+ function configureCssExtract(
457
+ chain: RspackChain,
458
+ options: RspackHtmlAppOptions
459
+ ): void {
460
+ chain.set('experiments', {
461
+ ...(chain.get('experiments') ?? {}),
462
+ css: true
463
+ });
464
+
465
+ const experiments = chain.get('experiments');
466
+ if (!experiments || !experiments.css) {
467
+ return;
468
+ }
469
+
470
+ const lessRule = chain.module
471
+ .rule('less')
472
+ .test(/\.less$/)
473
+ .use('less-loader')
474
+ .loader(options.loaders?.lessLoader ?? RSPACK_LOADER.lessLoader)
475
+ .options(options.lessLoader ?? {})
476
+ .end();
477
+
478
+ if (options.styleResourcesLoader) {
479
+ lessRule
480
+ .use('style-resources-loader')
481
+ .loader(
482
+ options.loaders?.styleResourcesLoader ??
483
+ RSPACK_LOADER.styleResourcesLoader
484
+ )
485
+ .options(options.styleResourcesLoader);
486
+ }
487
+
488
+ lessRule.type('css');
489
+ }
490
+
491
+ function filename(esmx: Esmx, name: string, ext = '[ext]') {
492
+ return esmx.isProd
493
+ ? `${name}/[name].[contenthash:8].final${ext}`
494
+ : `${name}/[path][name]${ext}`;
495
+ }
@@ -0,0 +1,123 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { BuildTarget } from '../rspack';
3
+ import type { TargetSetting } from './target-setting';
4
+ import { getTargetSetting, PRESET_TARGETS } from './target-setting';
5
+
6
+ describe('getTargetSetting', () => {
7
+ const buildTargets: BuildTarget[] = ['client', 'server', 'node'];
8
+
9
+ describe('when setting is undefined', () => {
10
+ it('should return compatible preset for all build targets', () => {
11
+ buildTargets.forEach((buildTarget) => {
12
+ const result = getTargetSetting(undefined, buildTarget);
13
+ expect(result).toEqual(PRESET_TARGETS.compatible[buildTarget]);
14
+ });
15
+ });
16
+ });
17
+
18
+ describe('when setting is a string preset', () => {
19
+ it('should return compatible preset for all build targets', () => {
20
+ buildTargets.forEach((buildTarget) => {
21
+ const result = getTargetSetting('compatible', buildTarget);
22
+ expect(result).toEqual(PRESET_TARGETS.compatible[buildTarget]);
23
+ });
24
+ });
25
+
26
+ it('should return modern preset for all build targets', () => {
27
+ buildTargets.forEach((buildTarget) => {
28
+ const result = getTargetSetting('modern', buildTarget);
29
+ expect(result).toEqual(PRESET_TARGETS.modern[buildTarget]);
30
+ });
31
+ });
32
+ });
33
+
34
+ describe('when setting is a custom array', () => {
35
+ const customTargets = ['chrome>=90', 'firefox>=80', 'safari>=14'];
36
+
37
+ it('should return the custom array for all build targets', () => {
38
+ buildTargets.forEach((buildTarget) => {
39
+ const result = getTargetSetting(customTargets, buildTarget);
40
+ expect(result).toEqual(customTargets);
41
+ });
42
+ });
43
+ });
44
+
45
+ describe('when setting is an object with specific build targets', () => {
46
+ it('should return specified preset for configured build targets', () => {
47
+ const setting: TargetSetting = {
48
+ client: 'modern',
49
+ server: 'compatible'
50
+ };
51
+
52
+ expect(getTargetSetting(setting, 'client')).toEqual(
53
+ PRESET_TARGETS.modern.client
54
+ );
55
+ expect(getTargetSetting(setting, 'server')).toEqual(
56
+ PRESET_TARGETS.compatible.server
57
+ );
58
+ });
59
+
60
+ it('should return compatible preset for unconfigured build targets', () => {
61
+ const setting: TargetSetting = {
62
+ client: 'modern'
63
+ };
64
+
65
+ expect(getTargetSetting(setting, 'client')).toEqual(
66
+ PRESET_TARGETS.modern.client
67
+ );
68
+ expect(getTargetSetting(setting, 'server')).toEqual(
69
+ PRESET_TARGETS.compatible.server
70
+ );
71
+ expect(getTargetSetting(setting, 'node')).toEqual(
72
+ PRESET_TARGETS.compatible.node
73
+ );
74
+ });
75
+
76
+ it('should return custom array for configured build targets', () => {
77
+ const customClientTargets = ['chrome>=90', 'firefox>=80'];
78
+ const customServerTargets = ['node>=18'];
79
+ const setting: TargetSetting = {
80
+ client: customClientTargets,
81
+ server: customServerTargets
82
+ };
83
+
84
+ expect(getTargetSetting(setting, 'client')).toEqual(
85
+ customClientTargets
86
+ );
87
+ expect(getTargetSetting(setting, 'server')).toEqual(
88
+ customServerTargets
89
+ );
90
+ expect(getTargetSetting(setting, 'node')).toEqual(
91
+ PRESET_TARGETS.compatible.node
92
+ );
93
+ });
94
+
95
+ it('should handle mixed preset and custom configurations', () => {
96
+ const setting: TargetSetting = {
97
+ client: 'modern',
98
+ server: ['node>=18'],
99
+ node: 'compatible'
100
+ };
101
+
102
+ expect(getTargetSetting(setting, 'client')).toEqual(
103
+ PRESET_TARGETS.modern.client
104
+ );
105
+ expect(getTargetSetting(setting, 'server')).toEqual(['node>=18']);
106
+ expect(getTargetSetting(setting, 'node')).toEqual(
107
+ PRESET_TARGETS.compatible.node
108
+ );
109
+ });
110
+ });
111
+
112
+ describe('edge cases', () => {
113
+ it('should handle empty custom array', () => {
114
+ const result = getTargetSetting([], 'client');
115
+ expect(result).toEqual([]);
116
+ });
117
+
118
+ it('should handle single item custom array', () => {
119
+ const result = getTargetSetting(['chrome>=90'], 'client');
120
+ expect(result).toEqual(['chrome>=90']);
121
+ });
122
+ });
123
+ });
@@ -0,0 +1,52 @@
1
+ import type { BuildTarget } from '../rspack/build-target';
2
+
3
+ export type TargetPreset = 'compatible' | 'modern';
4
+
5
+ export type TargetSpec = TargetPreset | string[];
6
+
7
+ export type TargetSetting =
8
+ | TargetSpec
9
+ | Partial<Record<BuildTarget, TargetSpec>>;
10
+
11
+ export const PRESET_TARGETS = {
12
+ compatible: {
13
+ client: ['chrome>=64', 'edge>=79', 'firefox>=67', 'safari>=11.1'],
14
+ server: ['node>=24'],
15
+ node: ['node>=24']
16
+ },
17
+ modern: {
18
+ client: ['chrome>=89', 'edge>=89', 'firefox>=108', 'safari>=16.4'],
19
+ server: ['node>=24'],
20
+ node: ['node>=24']
21
+ }
22
+ } as const;
23
+
24
+ function resolveTargetSpec(
25
+ spec: TargetSpec,
26
+ buildTarget: BuildTarget
27
+ ): string[] {
28
+ if (typeof spec === 'string') {
29
+ return [...PRESET_TARGETS[spec][buildTarget]];
30
+ }
31
+ return spec;
32
+ }
33
+
34
+ export function getTargetSetting(
35
+ setting: TargetSetting | undefined,
36
+ buildTarget: BuildTarget
37
+ ): string[] {
38
+ if (!setting) {
39
+ return [...PRESET_TARGETS.compatible[buildTarget]];
40
+ }
41
+
42
+ if (typeof setting === 'string' || Array.isArray(setting)) {
43
+ return resolveTargetSpec(setting, buildTarget);
44
+ }
45
+
46
+ const targetSpec = setting[buildTarget];
47
+ if (!targetSpec) {
48
+ return [...PRESET_TARGETS.compatible[buildTarget]];
49
+ }
50
+
51
+ return resolveTargetSpec(targetSpec, buildTarget);
52
+ }