@esmx/rspack 3.0.0-rc.117 → 3.0.0-rc.119

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.
@@ -17,6 +17,15 @@ export interface ModuleLinkPluginOptions {
17
17
  injectChunkName?: boolean;
18
18
  preEntries?: string[];
19
19
  deps?: string[];
20
+ /**
21
+ * Absolute paths of pkg-export wrapper files (one per pkg export). When
22
+ * the externals function sees an import whose issuer is one of these
23
+ * wrappers, it skips externalization — the wrapper IS the federation
24
+ * chunk for that package, so externalizing its inner
25
+ * `import __m from '<pkg-path>'` would route back to the wrapper itself
26
+ * (cyclic).
27
+ */
28
+ wrapperFiles?: string[];
20
29
  }
21
30
 
22
31
  export interface ParsedModuleLinkPluginOptions {
@@ -28,4 +37,5 @@ export interface ParsedModuleLinkPluginOptions {
28
37
  injectChunkName: boolean;
29
38
  preEntries: string[];
30
39
  deps: string[];
40
+ wrapperFiles: string[];
31
41
  }
package/src/rspack/app.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
1
4
  import { pathToFileURL } from 'node:url';
2
5
  import {
3
6
  type App,
@@ -98,7 +101,7 @@ export interface RspackAppChainContext {
98
101
  * rspack-chain configuration object.
99
102
  * You can use the chain API in chain hooks to modify the configuration.
100
103
  */
101
- chain: import('rspack-chain').default;
104
+ chain: import('rspack-chain').RspackChain;
102
105
 
103
106
  /**
104
107
  * Options object passed when creating the application.
@@ -153,7 +156,7 @@ export interface RspackAppOptions {
153
156
 
154
157
  /**
155
158
  * Uses rspack-chain to provide chained configuration method, allowing more flexible modification of Rspack configuration.
156
- * Called after the config hook, if chain hook exists, chained configuration is preferred.
159
+ * Called before the config hook: the chain is applied first, then `chain.toConfig()` produces the final RspackOptions which the config hook may still mutate.
157
160
  *
158
161
  * @param context - Configuration context, containing framework instance, build target, and chain configuration object
159
162
  */
@@ -233,10 +236,12 @@ async function createMiddleware(
233
236
  if (esmx.command !== esmx.COMMAND.dev) {
234
237
  return [];
235
238
  }
236
- const rsBuild = createRsBuild([
237
- generateBuildConfig(esmx, options, 'client'),
238
- generateBuildConfig(esmx, options, 'server')
239
- ]);
239
+ const rsBuild = createRsBuild(
240
+ await Promise.all([
241
+ generateBuildConfig(esmx, options, 'client'),
242
+ generateBuildConfig(esmx, options, 'server')
243
+ ])
244
+ );
240
245
  rsBuild.watch();
241
246
 
242
247
  // @ts-expect-error
@@ -254,11 +259,11 @@ async function createMiddleware(
254
259
  ];
255
260
  }
256
261
 
257
- function generateBuildConfig(
262
+ async function generateBuildConfig(
258
263
  esmx: Esmx,
259
264
  options: RspackAppOptions,
260
265
  buildTarget: BuildTarget
261
- ): RspackOptions {
266
+ ): Promise<RspackOptions> {
262
267
  return createRspackConfig(esmx, buildTarget, options);
263
268
  }
264
269
 
@@ -288,11 +293,21 @@ function rewriteBuild(esmx: Esmx, options: RspackAppOptions = {}) {
288
293
  }
289
294
  return async (): Promise<boolean> => {
290
295
  const ok = await createRsBuild(
291
- targets.map((target) => generateBuildConfig(esmx, options, target))
296
+ await Promise.all(
297
+ targets.map((target) =>
298
+ generateBuildConfig(esmx, options, target)
299
+ )
300
+ )
292
301
  ).build();
293
302
  if (!ok) {
294
303
  return false;
295
304
  }
305
+
306
+ // Update manifest with integrity hashes after all optimizations
307
+ if (esmx.isProd) {
308
+ await updateManifestIntegrity(esmx);
309
+ }
310
+
296
311
  esmx.writeSync(
297
312
  esmx.resolvePath('dist/index.mjs'),
298
313
  `
@@ -314,3 +329,67 @@ start();
314
329
  return pack(esmx);
315
330
  };
316
331
  }
332
+
333
+ /**
334
+ * Computes SHA-384 Subresource Integrity hashes for every JS chunk under
335
+ * `dist/client` and records them in the client manifest's `integrity` field.
336
+ *
337
+ * Only invoked for production builds. If the client manifest is absent (e.g. a
338
+ * build that produced no client output) the step is skipped with a warning;
339
+ * any other failure (read/hash/write error) is propagated so the build fails
340
+ * loudly rather than silently shipping resources without integrity hashes.
341
+ */
342
+ async function updateManifestIntegrity(esmx: Esmx): Promise<void> {
343
+ const clientDir = esmx.resolvePath('dist/client');
344
+ const manifestPath = path.join(clientDir, 'manifest.json');
345
+
346
+ const manifestExists = await fs
347
+ .access(manifestPath)
348
+ .then(() => true)
349
+ .catch(() => false);
350
+ if (!manifestExists) {
351
+ console.warn(
352
+ `SRI generation skipped: client manifest not found at ${manifestPath}`
353
+ );
354
+ return;
355
+ }
356
+
357
+ const manifestContent = await fs.readFile(manifestPath, 'utf-8');
358
+ const manifest = JSON.parse(manifestContent);
359
+
360
+ const integrity: Record<string, string> = {};
361
+
362
+ async function walkDir(dir: string, relativeDir = ''): Promise<void> {
363
+ const entries = await fs.readdir(dir, { withFileTypes: true });
364
+
365
+ for (const entry of entries) {
366
+ const fullPath = path.join(dir, entry.name);
367
+ const relativePath = relativeDir
368
+ ? `${relativeDir}/${entry.name}`
369
+ : entry.name;
370
+
371
+ if (entry.isDirectory()) {
372
+ await walkDir(fullPath, relativePath);
373
+ } else if (
374
+ entry.isFile() &&
375
+ (entry.name.endsWith('.mjs') || entry.name.endsWith('.js')) &&
376
+ !entry.name.includes('hot-update') &&
377
+ entry.name !== 'manifest.json'
378
+ ) {
379
+ const content = await fs.readFile(fullPath);
380
+ const hash = crypto
381
+ .createHash('sha384')
382
+ .update(content)
383
+ .digest('base64');
384
+ integrity[relativePath] = `sha384-${hash}`;
385
+ }
386
+ }
387
+ }
388
+
389
+ await walkDir(clientDir);
390
+
391
+ if (Object.keys(integrity).length > 0) {
392
+ manifest.integrity = integrity;
393
+ await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 4));
394
+ }
395
+ }
@@ -1,18 +1,21 @@
1
+ import path from 'node:path';
1
2
  import type { Esmx } from '@esmx/core';
3
+ import { buildPkgWrapper } from '@esmx/pkg-wrapper';
2
4
  import type { RspackOptions } from '@rspack/core';
3
5
  import { rspack } from '@rspack/core';
4
- import RspackChain from 'rspack-chain';
6
+ import { RspackChain } from 'rspack-chain';
7
+ import RspackVirtualModulePlugin from 'rspack-plugin-virtual-module';
5
8
  import nodeExternals from 'webpack-node-externals';
6
9
  import type { ModuleLinkPluginOptions } from '../module-link';
7
10
  import { initModuleLink } from '../module-link';
8
11
  import type { RspackAppOptions } from './app';
9
12
  import type { BuildTarget } from './build-target';
10
13
 
11
- export function createChainConfig(
14
+ export async function createChainConfig(
12
15
  esmx: Esmx,
13
16
  buildTarget: BuildTarget,
14
17
  options: RspackAppOptions
15
- ): RspackChain {
18
+ ): Promise<RspackChain> {
16
19
  const isHot = buildTarget === 'client' && !esmx.isProd;
17
20
  const isClient = buildTarget === 'client';
18
21
  const isServer = buildTarget === 'server';
@@ -65,8 +68,7 @@ export function createChainConfig(
65
68
 
66
69
  chain.module.parser.set('javascript', {
67
70
  url: isClient ? true : 'relative',
68
- importMeta: false,
69
- strictExportPresence: true
71
+ importMeta: false
70
72
  });
71
73
 
72
74
  chain.module.generator.set('asset', {
@@ -81,7 +83,8 @@ export function createChainConfig(
81
83
 
82
84
  chain.optimization
83
85
  .minimize(options.minimize ?? esmx.isProd)
84
- .emitOnErrors(true);
86
+ .emitOnErrors(true)
87
+ .runtimeChunk('single');
85
88
 
86
89
  chain.externalsPresets({
87
90
  web: isClient,
@@ -100,39 +103,65 @@ export function createChainConfig(
100
103
  ]);
101
104
  }
102
105
 
103
- chain.experiments({
104
- ...(chain.get('experiments') || {}),
105
- outputModule: true,
106
- nativeWatcher: true,
107
- rspackFuture: {
108
- bundlerInfo: { force: false }
109
- }
110
- });
106
+ chain.output.set('module', true);
107
+ chain.output.bundlerInfo({ force: false });
108
+ chain.set('nativeWatcher', true);
111
109
 
112
110
  chain.set('lazyCompilation', false);
113
111
 
114
- initModuleLink(chain, createModuleLinkConfig(esmx, buildTarget));
112
+ const { linkOpts, virtualModules } = await createModuleLinkConfig(
113
+ esmx,
114
+ buildTarget
115
+ );
116
+
117
+ // Install pkg-export wrappers as VIRTUAL MODULES. Each `pkg:react`
118
+ // becomes a virtual file under `<cwd>/node_modules/.esmx-virtual/<remote>/`
119
+ // whose source is the generated re-export wrapper. The federation entry
120
+ // points at this virtual file; the wrapper internally does
121
+ // `import * as __ns from 'react'` so the bundler's resolver / aliases
122
+ // still apply to the real package. Same mental model as Vite's
123
+ // `esmx://react` virtual id pattern, realized through
124
+ // `rspack-plugin-virtual-module` here.
125
+ if (Object.keys(virtualModules).length > 0) {
126
+ chain
127
+ .plugin('esmx-pkg-virtual-modules')
128
+ .use(RspackVirtualModulePlugin, [
129
+ virtualModules,
130
+ `.esmx-virtual-${esmx.name}`
131
+ ]);
132
+ }
133
+
134
+ initModuleLink(chain, linkOpts, esmx.isProd);
115
135
 
116
136
  return chain;
117
137
  }
118
138
 
119
- function createModuleLinkConfig(
139
+ interface ModuleLinkBundle {
140
+ linkOpts: ModuleLinkPluginOptions;
141
+ /** Virtual module map: pseudo-path → wrapper source (empty for node). */
142
+ virtualModules: Record<string, string>;
143
+ }
144
+
145
+ async function createModuleLinkConfig(
120
146
  esmx: Esmx,
121
147
  buildTarget: BuildTarget
122
- ): ModuleLinkPluginOptions {
148
+ ): Promise<ModuleLinkBundle> {
123
149
  const isClient = buildTarget === 'client';
124
150
  const isServer = buildTarget === 'server';
125
151
  const isNode = buildTarget === 'node';
126
152
 
127
153
  if (isNode) {
128
154
  return {
129
- name: esmx.name,
130
- exports: {
131
- 'src/entry.node': {
132
- pkg: false,
133
- file: './src/entry.node'
155
+ linkOpts: {
156
+ name: esmx.name,
157
+ exports: {
158
+ 'src/entry.node': {
159
+ pkg: false,
160
+ file: './src/entry.node'
161
+ }
134
162
  }
135
- }
163
+ },
164
+ virtualModules: {}
136
165
  };
137
166
  }
138
167
 
@@ -143,21 +172,63 @@ function createModuleLinkConfig(
143
172
  );
144
173
  }
145
174
 
175
+ // Compute pkg-export wrappers as VIRTUAL MODULES (see installation site
176
+ // in createChainConfig). The wrapper imports the real package by its
177
+ // ORIGINAL bare specifier so the bundler's resolver / aliases still
178
+ // apply; we only add static named-export plumbing on top.
179
+ const env = esmx.moduleConfig.environments[buildTarget];
180
+ const patchedExports: typeof env.exports = {};
181
+ const virtualModules: Record<string, string> = {};
182
+ const wrapperFiles: string[] = [];
183
+ // The virtual modules plugin writes its files into a temp dir under
184
+ // node_modules (see RspackVirtualModulePlugin instantiation above) — we
185
+ // anchor on the same path so the federation entry (and externals
186
+ // wrapper-skip check) sees the real on-disk path the bundler will report
187
+ // as the chunk's issuer.
188
+ const virtualBaseDir = path.join(
189
+ esmx.root,
190
+ 'node_modules',
191
+ `.esmx-virtual-${esmx.name}`
192
+ );
193
+ for (const [name, exp] of Object.entries(env.exports)) {
194
+ if (exp.pkg && exp.file) {
195
+ const { source } = await buildPkgWrapper({
196
+ root: esmx.root,
197
+ spec: exp.file
198
+ });
199
+ const safeName = `${exp.file.replace(/[^A-Za-z0-9_-]/g, '_')}.mjs`;
200
+ // Key passed to the plugin is a SIMPLE filename — it gets joined
201
+ // under the plugin's tempDir. The resulting actual on-disk path
202
+ // is what rspack reports as the chunk's issuer.
203
+ virtualModules[safeName] = source;
204
+ const actualPath = path.join(virtualBaseDir, safeName);
205
+ wrapperFiles.push(actualPath);
206
+ patchedExports[name] = { ...exp, file: actualPath };
207
+ } else {
208
+ patchedExports[name] = exp;
209
+ }
210
+ }
211
+
146
212
  return {
147
- ...esmx.moduleConfig.environments[buildTarget],
148
- name: esmx.name,
149
- injectChunkName: isServer,
150
- deps: Object.keys(esmx.moduleConfig.links),
151
- preEntries
213
+ linkOpts: {
214
+ ...env,
215
+ exports: patchedExports,
216
+ name: esmx.name,
217
+ injectChunkName: isServer,
218
+ deps: Object.keys(esmx.moduleConfig.links),
219
+ preEntries,
220
+ wrapperFiles
221
+ },
222
+ virtualModules
152
223
  };
153
224
  }
154
225
 
155
- export function createRspackConfig(
226
+ export async function createRspackConfig(
156
227
  esmx: Esmx,
157
228
  buildTarget: BuildTarget,
158
229
  options: RspackAppOptions
159
- ): RspackOptions {
160
- const chain = createChainConfig(esmx, buildTarget, options);
230
+ ): Promise<RspackOptions> {
231
+ const chain = await createChainConfig(esmx, buildTarget, options);
161
232
  options.chain?.({ esmx, options, buildTarget, chain });
162
233
  const config = chain.toConfig();
163
234
  options.config?.({ esmx, options, buildTarget, config });
@@ -5,7 +5,7 @@ import {
5
5
  type SwcLoaderOptions
6
6
  } from '@rspack/core';
7
7
  import NodePolyfillPlugin from 'node-polyfill-webpack-plugin';
8
- import type RspackChain from 'rspack-chain';
8
+ import type { RspackChain } from 'rspack-chain';
9
9
  import {
10
10
  type BuildTarget,
11
11
  createRspackApp,
@@ -341,7 +341,7 @@ function configureOptimization(
341
341
  {
342
342
  minimizerOptions: {
343
343
  targets: getTargetSetting(options?.target, 'client'),
344
- errorRecovery: false
344
+ errorRecovery: true
345
345
  }
346
346
  }
347
347
  ]);
@@ -457,15 +457,10 @@ function configureCssExtract(
457
457
  chain: RspackChain,
458
458
  options: RspackHtmlAppOptions
459
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
- }
460
+ chain.module
461
+ .rule('css')
462
+ .test(/\.css$/)
463
+ .type('css/auto');
469
464
 
470
465
  const lessRule = chain.module
471
466
  .rule('less')
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2020 Esmx Team
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,3 +0,0 @@
1
- import type RspackChain from 'rspack-chain';
2
- import type { ParsedModuleLinkPluginOptions } from './types';
3
- export declare function applyChainConfig2(chain: RspackChain, opts: ParsedModuleLinkPluginOptions): void;
@@ -1,17 +0,0 @@
1
- import { rspack } from "@rspack/core";
2
- import {
3
- applyEntryConfig,
4
- applyExternalsConfig,
5
- applyModuleConfig
6
- } from "./config.mjs";
7
- export function applyChainConfig2(chain, opts) {
8
- applyEntryConfig(chain, opts);
9
- applyExternalsConfig(chain, opts);
10
- if (chain.get("mode") === "production") {
11
- chain.output.set("module", true);
12
- chain.plugin("esm-library").use(new rspack.experiments.EsmLibraryPlugin());
13
- chain.optimization.set("runtimeChunk", "single");
14
- } else {
15
- applyModuleConfig(chain);
16
- }
17
- }
@@ -1,28 +0,0 @@
1
- import { rspack } from '@rspack/core';
2
- import type RspackChain from 'rspack-chain';
3
- import {
4
- applyEntryConfig,
5
- applyExternalsConfig,
6
- applyModuleConfig
7
- } from './config';
8
- import type { ParsedModuleLinkPluginOptions } from './types';
9
-
10
- export function applyChainConfig2(
11
- chain: RspackChain,
12
- opts: ParsedModuleLinkPluginOptions
13
- ): void {
14
- applyEntryConfig(chain, opts);
15
- applyExternalsConfig(chain, opts);
16
-
17
- // Set module compilation configuration
18
- if (chain.get('mode') === 'production') {
19
- chain.output.set('module', true);
20
-
21
- chain
22
- .plugin('esm-library')
23
- .use(new rspack.experiments.EsmLibraryPlugin());
24
- chain.optimization.set('runtimeChunk', 'single');
25
- } else {
26
- applyModuleConfig(chain);
27
- }
28
- }