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

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.
@@ -1,8 +1,11 @@
1
+ import path from "node:path";
2
+ import { buildPkgWrapper } from "@esmx/pkg-wrapper";
1
3
  import { rspack } from "@rspack/core";
2
- import RspackChain from "rspack-chain";
4
+ import { RspackChain } from "rspack-chain";
5
+ import RspackVirtualModulePlugin from "rspack-plugin-virtual-module";
3
6
  import nodeExternals from "webpack-node-externals";
4
7
  import { initModuleLink } from "../module-link/index.mjs";
5
- export function createChainConfig(esmx, buildTarget, options) {
8
+ export async function createChainConfig(esmx, buildTarget, options) {
6
9
  const isHot = buildTarget === "client" && !esmx.isProd;
7
10
  const isClient = buildTarget === "client";
8
11
  const isServer = buildTarget === "server";
@@ -38,8 +41,7 @@ export function createChainConfig(esmx, buildTarget, options) {
38
41
  }
39
42
  chain.module.parser.set("javascript", {
40
43
  url: isClient ? true : "relative",
41
- importMeta: false,
42
- strictExportPresence: true
44
+ importMeta: false
43
45
  });
44
46
  chain.module.generator.set("asset", {
45
47
  emit: isClient
@@ -48,7 +50,7 @@ export function createChainConfig(esmx, buildTarget, options) {
48
50
  emit: isClient
49
51
  });
50
52
  chain.resolve.alias.set(esmx.name, esmx.root);
51
- chain.optimization.minimize(options.minimize ?? esmx.isProd).emitOnErrors(true);
53
+ chain.optimization.minimize(options.minimize ?? esmx.isProd).emitOnErrors(true).runtimeChunk("single");
52
54
  chain.externalsPresets({
53
55
  web: isClient,
54
56
  node: isServer || isNode
@@ -63,31 +65,39 @@ export function createChainConfig(esmx, buildTarget, options) {
63
65
  })
64
66
  ]);
65
67
  }
66
- chain.experiments({
67
- ...chain.get("experiments") || {},
68
- outputModule: true,
69
- nativeWatcher: true,
70
- rspackFuture: {
71
- bundlerInfo: { force: false }
72
- }
73
- });
68
+ chain.output.set("module", true);
69
+ chain.output.bundlerInfo({ force: false });
70
+ chain.set("nativeWatcher", true);
74
71
  chain.set("lazyCompilation", false);
75
- initModuleLink(chain, createModuleLinkConfig(esmx, buildTarget));
72
+ const { linkOpts, virtualModules } = await createModuleLinkConfig(
73
+ esmx,
74
+ buildTarget
75
+ );
76
+ if (Object.keys(virtualModules).length > 0) {
77
+ chain.plugin("esmx-pkg-virtual-modules").use(RspackVirtualModulePlugin, [
78
+ virtualModules,
79
+ `.esmx-virtual-${esmx.name}`
80
+ ]);
81
+ }
82
+ initModuleLink(chain, linkOpts, esmx.isProd);
76
83
  return chain;
77
84
  }
78
- function createModuleLinkConfig(esmx, buildTarget) {
85
+ async function createModuleLinkConfig(esmx, buildTarget) {
79
86
  const isClient = buildTarget === "client";
80
87
  const isServer = buildTarget === "server";
81
88
  const isNode = buildTarget === "node";
82
89
  if (isNode) {
83
90
  return {
84
- name: esmx.name,
85
- exports: {
86
- "src/entry.node": {
87
- pkg: false,
88
- file: "./src/entry.node"
91
+ linkOpts: {
92
+ name: esmx.name,
93
+ exports: {
94
+ "src/entry.node": {
95
+ pkg: false,
96
+ file: "./src/entry.node"
97
+ }
89
98
  }
90
- }
99
+ },
100
+ virtualModules: {}
91
101
  };
92
102
  }
93
103
  const preEntries = [];
@@ -96,16 +106,45 @@ function createModuleLinkConfig(esmx, buildTarget) {
96
106
  `${import.meta.resolve("webpack-hot-middleware/client.js")}?path=/${esmx.name}/hot-middleware`
97
107
  );
98
108
  }
109
+ const env = esmx.moduleConfig.environments[buildTarget];
110
+ const patchedExports = {};
111
+ const virtualModules = {};
112
+ const wrapperFiles = [];
113
+ const virtualBaseDir = path.join(
114
+ esmx.root,
115
+ "node_modules",
116
+ `.esmx-virtual-${esmx.name}`
117
+ );
118
+ for (const [name, exp] of Object.entries(env.exports)) {
119
+ if (exp.pkg && exp.file) {
120
+ const { source } = await buildPkgWrapper({
121
+ root: esmx.root,
122
+ spec: exp.file
123
+ });
124
+ const safeName = `${exp.file.replace(/[^A-Za-z0-9_-]/g, "_")}.mjs`;
125
+ virtualModules[safeName] = source;
126
+ const actualPath = path.join(virtualBaseDir, safeName);
127
+ wrapperFiles.push(actualPath);
128
+ patchedExports[name] = { ...exp, file: actualPath };
129
+ } else {
130
+ patchedExports[name] = exp;
131
+ }
132
+ }
99
133
  return {
100
- ...esmx.moduleConfig.environments[buildTarget],
101
- name: esmx.name,
102
- injectChunkName: isServer,
103
- deps: Object.keys(esmx.moduleConfig.links),
104
- preEntries
134
+ linkOpts: {
135
+ ...env,
136
+ exports: patchedExports,
137
+ name: esmx.name,
138
+ injectChunkName: isServer,
139
+ deps: Object.keys(esmx.moduleConfig.links),
140
+ preEntries,
141
+ wrapperFiles
142
+ },
143
+ virtualModules
105
144
  };
106
145
  }
107
- export function createRspackConfig(esmx, buildTarget, options) {
108
- const chain = createChainConfig(esmx, buildTarget, options);
146
+ export async function createRspackConfig(esmx, buildTarget, options) {
147
+ const chain = await createChainConfig(esmx, buildTarget, options);
109
148
  options.chain?.({ esmx, options, buildTarget, chain });
110
149
  const config = chain.toConfig();
111
150
  options.config?.({ esmx, options, buildTarget, config });
@@ -89,7 +89,7 @@ function configureOptimization(chain, options) {
89
89
  {
90
90
  minimizerOptions: {
91
91
  targets: getTargetSetting(options?.target, "client"),
92
- errorRecovery: false
92
+ errorRecovery: true
93
93
  }
94
94
  }
95
95
  ]);
@@ -139,14 +139,7 @@ function configureCssInJS(chain, esmx, options) {
139
139
  lessRule.type("javascript/auto");
140
140
  }
141
141
  function configureCssExtract(chain, options) {
142
- chain.set("experiments", {
143
- ...chain.get("experiments") ?? {},
144
- css: true
145
- });
146
- const experiments = chain.get("experiments");
147
- if (!experiments || !experiments.css) {
148
- return;
149
- }
142
+ chain.module.rule("css").test(/\.css$/).type("css/auto");
150
143
  const lessRule = chain.module.rule("less").test(/\.less$/).use("less-loader").loader(options.loaders?.lessLoader ?? RSPACK_LOADER.lessLoader).options(options.lessLoader ?? {}).end();
151
144
  if (options.styleResourcesLoader) {
152
145
  lessRule.use("style-resources-loader").loader(
package/package.json CHANGED
@@ -63,35 +63,37 @@
63
63
  }
64
64
  },
65
65
  "dependencies": {
66
- "@esmx/import": "3.0.0-rc.117",
66
+ "@esmx/import": "3.0.0-rc.118",
67
+ "@esmx/pkg-wrapper": "3.0.0-rc.118",
67
68
  "@npmcli/arborist": "^9.1.6",
68
- "@rspack/core": "1.7.5",
69
+ "@rspack/core": "^2.0.6",
69
70
  "css-loader": "^7.1.2",
70
71
  "less-loader": "^12.3.0",
71
72
  "node-polyfill-webpack-plugin": "^4.1.0",
72
73
  "pacote": "^21.0.0",
73
- "rspack-chain": "^1.4.3",
74
+ "rspack-chain": "^2.0.2",
75
+ "rspack-plugin-virtual-module": "^1.0.1",
74
76
  "style-loader": "^4.0.0",
75
77
  "style-resources-loader": "^1.5.0",
76
78
  "upath": "^2.0.1",
77
79
  "webpack-hot-middleware": "^2.26.1",
78
80
  "webpack-node-externals": "~3.0.0",
79
- "worker-rspack-loader": "^3.1.2"
81
+ "worker-rspack-loader": "^3.1.5"
80
82
  },
81
83
  "devDependencies": {
82
84
  "@biomejs/biome": "2.3.7",
83
- "@esmx/core": "3.0.0-rc.117",
85
+ "@esmx/core": "3.0.0-rc.118",
84
86
  "@types/node": "^24.0.0",
85
87
  "@types/npmcli__arborist": "^6.3.1",
86
88
  "@types/pacote": "^11.1.8",
87
89
  "@types/webpack-hot-middleware": "^2.25.12",
88
90
  "@types/webpack-node-externals": "^3.0.4",
89
- "@vitest/coverage-v8": "3.2.4",
91
+ "@vitest/coverage-v8": "3.2.6",
90
92
  "typescript": "5.9.3",
91
93
  "unbuild": "3.6.1",
92
- "vitest": "3.2.4"
94
+ "vitest": "3.2.6"
93
95
  },
94
- "version": "3.0.0-rc.117",
96
+ "version": "3.0.0-rc.118",
95
97
  "type": "module",
96
98
  "private": false,
97
99
  "exports": {
@@ -110,5 +112,8 @@
110
112
  "template",
111
113
  "public"
112
114
  ],
113
- "gitHead": "c8470bb917221494ee36f0c4ec1cd03560b01f4c"
115
+ "gitHead": "2fdbd62470f64e6e45568550941c78cb259e4917",
116
+ "publishConfig": {
117
+ "access": "public"
118
+ }
114
119
  }
@@ -1,5 +1,5 @@
1
1
  import type { ExternalItem, ExternalItemFunctionData } from '@rspack/core';
2
- import type RspackChain from 'rspack-chain';
2
+ import type { RspackChain } from 'rspack-chain';
3
3
  import type { ParsedModuleLinkPluginOptions } from './types';
4
4
 
5
5
  export function applyEntryConfig(
@@ -73,9 +73,11 @@ function createExternalsFunction(
73
73
  importMap.set(identifier, identifier);
74
74
  importMap.set(value.name, identifier);
75
75
 
76
- const resolvedPath = await resolvePath(value.file);
77
- if (resolvedPath) {
78
- importMap.set(resolvedPath, identifier);
76
+ if (value.file) {
77
+ const resolvedPath = await resolvePath(value.file);
78
+ if (resolvedPath) {
79
+ importMap.set(resolvedPath, identifier);
80
+ }
79
81
  }
80
82
  })
81
83
  );
@@ -118,6 +120,8 @@ function createExternalsFunction(
118
120
  const FILE_EXT_REGEX =
119
121
  /\.worker\.(js|mjs|cjs|jsx|mjsx|cjsx|ts|mts|cts|tsx|mtsx|ctsx)$/i;
120
122
 
123
+ const wrapperFiles = new Set(opts.wrapperFiles);
124
+
121
125
  return async (data: ExternalItemFunctionData) => {
122
126
  if (
123
127
  !data.request ||
@@ -127,6 +131,11 @@ function createExternalsFunction(
127
131
  )
128
132
  return;
129
133
 
134
+ // A pkg-export wrapper file IS the federation chunk for its package.
135
+ // Don't externalize its inner `import __m from '<abs-pkg-path>'` —
136
+ // that would route back to the wrapper itself (cyclic).
137
+ if (wrapperFiles.has(data.contextInfo.issuer)) return;
138
+
130
139
  const defaultContext = compilerContext;
131
140
  const resolvePath: ResolvePath = async (
132
141
  request: string,
@@ -1,4 +1,4 @@
1
- import type RspackChain from 'rspack-chain';
1
+ import type { RspackChain } from 'rspack-chain';
2
2
  import {
3
3
  applyEntryConfig,
4
4
  applyExternalsConfig,
@@ -1,19 +1,20 @@
1
- import type RspackChain from 'rspack-chain';
1
+ import type { RspackChain } from 'rspack-chain';
2
2
  import { applyChainConfig1 } from './config1';
3
- // import { applyChainConfig2 } from './config2';
4
3
  import { ManifestPlugin } from './manifest-plugin';
5
4
  import { parseOptions } from './parse';
6
5
  import type { ModuleLinkPluginOptions } from './types';
7
6
 
8
7
  export function initModuleLink(
9
8
  chain: RspackChain,
10
- options: ModuleLinkPluginOptions
9
+ options: ModuleLinkPluginOptions,
10
+ isProduction = false
11
11
  ): void {
12
12
  const opts = parseOptions(options);
13
13
  applyChainConfig1(chain, opts);
14
- // applyChainConfig2(chain, opts);
15
14
 
16
- chain.plugin('module-link-manifest').use(ManifestPlugin, [opts]);
15
+ chain
16
+ .plugin('module-link-manifest')
17
+ .use(ManifestPlugin, [opts, isProduction]);
17
18
  }
18
19
 
19
20
  export type { ModuleLinkPluginOptions } from './types';
@@ -0,0 +1,227 @@
1
+ import type { StatsCompilation } from '@rspack/core';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { generateIdentifier, getChunks, getExports } from './manifest-plugin';
4
+ import type { ParsedModuleLinkPluginOptions } from './types';
5
+
6
+ const baseOpts: ParsedModuleLinkPluginOptions = {
7
+ name: 'remote',
8
+ exports: {},
9
+ imports: {},
10
+ scopes: {},
11
+ ext: '.mjs',
12
+ deps: [],
13
+ preEntries: [],
14
+ wrapperFiles: [],
15
+ injectChunkName: false
16
+ };
17
+
18
+ const ROOT = '/abs/project';
19
+
20
+ function mkChunk(o: {
21
+ files?: string[];
22
+ modules?: Array<{
23
+ nameForCondition?: string;
24
+ moduleType?: string;
25
+ index?: number;
26
+ }>;
27
+ auxiliaryFiles?: string[];
28
+ }): NonNullable<StatsCompilation['chunks']>[number] {
29
+ return {
30
+ files: o.files ?? [],
31
+ auxiliaryFiles: o.auxiliaryFiles ?? [],
32
+ modules: (o.modules ?? []).map((m, i) => ({
33
+ id: i,
34
+ index: m.index ?? i,
35
+ nameForCondition: m.nameForCondition,
36
+ moduleType: m.moduleType ?? 'javascript/auto'
37
+ }))
38
+ } as never;
39
+ }
40
+
41
+ describe('getChunks — CSS extraction', () => {
42
+ it('populates css[] when a chunk emitted .css files', () => {
43
+ const stats: StatsCompilation = {
44
+ chunks: [
45
+ mkChunk({
46
+ files: [
47
+ 'src/entry.client.95f6085b.final.mjs',
48
+ 'src/entry.client.a73d6772.final.css'
49
+ ],
50
+ modules: [
51
+ {
52
+ nameForCondition: `${ROOT}/src/entry.client.ts`,
53
+ moduleType: 'javascript/auto'
54
+ }
55
+ ]
56
+ })
57
+ ]
58
+ };
59
+
60
+ const chunks = getChunks(baseOpts, stats, ROOT);
61
+ const entry = chunks['remote@src/entry.client.ts'];
62
+ expect(entry).toBeDefined();
63
+ expect(entry.js).toBe('src/entry.client.95f6085b.final.mjs');
64
+ expect(entry.css).toEqual(['src/entry.client.a73d6772.final.css']);
65
+ });
66
+
67
+ it('returns empty css[] when chunk has no stylesheet', () => {
68
+ const stats: StatsCompilation = {
69
+ chunks: [
70
+ mkChunk({
71
+ files: ['vue.b8a9c2d3.final.mjs'],
72
+ modules: [
73
+ {
74
+ nameForCondition: `${ROOT}/node_modules/vue/index.mjs`,
75
+ moduleType: 'javascript/esm'
76
+ }
77
+ ]
78
+ })
79
+ ]
80
+ };
81
+
82
+ const entry = getChunks(baseOpts, stats, ROOT)[
83
+ 'remote@node_modules/vue/index.mjs'
84
+ ];
85
+ expect(entry).toBeDefined();
86
+ expect(entry.css).toEqual([]);
87
+ });
88
+
89
+ it('extracts multiple CSS chunks per JS chunk (split stylesheets)', () => {
90
+ const stats: StatsCompilation = {
91
+ chunks: [
92
+ mkChunk({
93
+ files: [
94
+ 'chunks/widget.111.final.mjs',
95
+ 'chunks/widget.111.final.css',
96
+ 'chunks/widget.111.theme.css'
97
+ ],
98
+ modules: [
99
+ {
100
+ nameForCondition: `${ROOT}/src/widget.ts`,
101
+ moduleType: 'javascript/esm'
102
+ }
103
+ ]
104
+ })
105
+ ]
106
+ };
107
+
108
+ const entry = getChunks(baseOpts, stats, ROOT)['remote@src/widget.ts'];
109
+ expect(entry.css).toEqual([
110
+ 'chunks/widget.111.final.css',
111
+ 'chunks/widget.111.theme.css'
112
+ ]);
113
+ });
114
+
115
+ it('ignores chunks without a JS module (CSS-only / asset-only)', () => {
116
+ const stats: StatsCompilation = {
117
+ chunks: [
118
+ mkChunk({
119
+ files: ['orphan.111.final.css'],
120
+ modules: []
121
+ })
122
+ ]
123
+ };
124
+
125
+ expect(getChunks(baseOpts, stats, ROOT)).toEqual({});
126
+ });
127
+
128
+ it('respects opts.ext for the JS file lookup (.cjs / .js / .mjs)', () => {
129
+ const stats: StatsCompilation = {
130
+ chunks: [
131
+ mkChunk({
132
+ files: ['src/x.111.js', 'src/x.111.css'],
133
+ modules: [
134
+ {
135
+ nameForCondition: `${ROOT}/src/x.ts`,
136
+ moduleType: 'javascript/auto'
137
+ }
138
+ ]
139
+ })
140
+ ]
141
+ };
142
+
143
+ const chunks = getChunks({ ...baseOpts, ext: '.js' }, stats, ROOT);
144
+ expect(chunks['remote@src/x.ts'].js).toBe('src/x.111.js');
145
+ expect(chunks['remote@src/x.ts'].css).toEqual(['src/x.111.css']);
146
+ });
147
+
148
+ it('writes auxiliaryFiles (sourcemaps, images) into resources[]', () => {
149
+ const stats: StatsCompilation = {
150
+ chunks: [
151
+ mkChunk({
152
+ files: ['src/a.111.mjs'],
153
+ auxiliaryFiles: ['src/a.111.mjs.map', 'assets/logo.svg'],
154
+ modules: [
155
+ {
156
+ nameForCondition: `${ROOT}/src/a.ts`,
157
+ moduleType: 'javascript/esm'
158
+ }
159
+ ]
160
+ })
161
+ ]
162
+ };
163
+
164
+ const entry = getChunks(baseOpts, stats, ROOT)['remote@src/a.ts'];
165
+ expect(entry.resources).toEqual([
166
+ 'src/a.111.mjs.map',
167
+ 'assets/logo.svg'
168
+ ]);
169
+ });
170
+
171
+ it('returns {} when stats has no chunks at all', () => {
172
+ expect(getChunks(baseOpts, {}, ROOT)).toEqual({});
173
+ });
174
+ });
175
+
176
+ describe('generateIdentifier', () => {
177
+ it('normalises absolute path to root-relative POSIX form', () => {
178
+ expect(
179
+ generateIdentifier({
180
+ root: '/abs/project',
181
+ name: 'remote',
182
+ filePath: '/abs/project/src/entry.client.ts'
183
+ })
184
+ ).toBe('remote@src/entry.client.ts');
185
+ });
186
+
187
+ it('falls back to absolute path when no root provided', () => {
188
+ expect(
189
+ generateIdentifier({
190
+ root: '',
191
+ name: 'remote',
192
+ filePath: '/abs/project/src/x.ts'
193
+ })
194
+ ).toBe('remote@/abs/project/src/x.ts');
195
+ });
196
+ });
197
+
198
+ describe('getExports', () => {
199
+ it('binds export entries to their hashed asset filenames', () => {
200
+ const opts: ParsedModuleLinkPluginOptions = {
201
+ ...baseOpts,
202
+ exports: {
203
+ 'src/entry.client': {
204
+ name: 'src/entry.client',
205
+ file: './src/entry.client',
206
+ pkg: false,
207
+ identifier: 'remote/src/entry.client'
208
+ }
209
+ }
210
+ };
211
+ const stats: StatsCompilation = {
212
+ entrypoints: {
213
+ 'src/entry.client': {
214
+ chunks: [],
215
+ assets: [
216
+ { name: 'src/entry.client.95f6085b.final.mjs' } as never
217
+ ]
218
+ } as never
219
+ }
220
+ };
221
+
222
+ const out = getExports(opts, stats);
223
+ expect(out['src/entry.client'].file).toBe(
224
+ 'src/entry.client.95f6085b.final.mjs'
225
+ );
226
+ });
227
+ });
@@ -1,4 +1,5 @@
1
- import type { Compilation, Compiler, StatsCompilation } from '@rspack/core';
1
+ import { buildManifestProtocolFields } from '@esmx/core';
2
+ import type { Compiler, StatsChunk, StatsCompilation } from '@rspack/core';
2
3
  import upath from 'upath';
3
4
  import type {
4
5
  ManifestJson,
@@ -10,7 +11,10 @@ import type {
10
11
  export const RSPACK_PLUGIN_NAME = 'rspack-module-link-plugin';
11
12
 
12
13
  export class ManifestPlugin {
13
- constructor(private opts: ParsedModuleLinkPluginOptions) {}
14
+ constructor(
15
+ private opts: ParsedModuleLinkPluginOptions,
16
+ isProduction: boolean
17
+ ) {}
14
18
 
15
19
  apply(compiler: Compiler) {
16
20
  const opts = this.opts;
@@ -19,6 +23,10 @@ export class ManifestPlugin {
19
23
  RSPACK_PLUGIN_NAME,
20
24
  (compilation) => {
21
25
  let manifestJson: ManifestJson = {
26
+ ...buildManifestProtocolFields(
27
+ compiler.options.context ?? process.cwd(),
28
+ []
29
+ ),
22
30
  name: opts.name,
23
31
  exports: {},
24
32
  scopes: opts.scopes,
@@ -41,12 +49,29 @@ export class ManifestPlugin {
41
49
  const resources = Object.keys(assets)
42
50
  .map(transFileName)
43
51
  .filter((file) => !file.includes('hot-update'));
52
+ const chunksStats = compilation.getStats().toJson({
53
+ all: false,
54
+ chunks: true,
55
+ modules: true,
56
+ chunkModules: true,
57
+ ids: true,
58
+ entrypoints: true,
59
+ chunkRelations: true
60
+ });
61
+ const root =
62
+ compilation.options.context ?? process.cwd();
44
63
  manifestJson = {
64
+ ...buildManifestProtocolFields(
65
+ root,
66
+ Object.values(exports)
67
+ .filter((exp) => exp.pkg)
68
+ .map((exp) => exp.name)
69
+ ),
45
70
  name: opts.name,
46
71
  exports: exports,
47
72
  scopes: opts.scopes,
48
73
  files: resources,
49
- chunks: getChunks(opts, compilation)
74
+ chunks: getChunks(opts, chunksStats, root)
50
75
  };
51
76
  const { RawSource } = compiler.rspack.sources;
52
77
 
@@ -115,34 +140,40 @@ export function getExports(
115
140
  return exports;
116
141
  }
117
142
 
118
- function getChunks(
143
+ /**
144
+ * Build the `chunks` section of the federation manifest from rspack stats.
145
+ *
146
+ * Each emitted chunk that owns at least one JS module gets a manifest entry
147
+ * keyed by a stable, root-relative identifier (`<remote>@<source-path>`). The
148
+ * entry lists:
149
+ *
150
+ * - `js` — the chunk's primary JS asset
151
+ * - `css` — every `.css` file the chunk emitted; consumed by the host's
152
+ * renderHost to inject `<link rel="stylesheet">` into `<head>`
153
+ * (G5). This is how `import './x.css'` in a remote translates
154
+ * into stylesheet delivery in the browser without the runtime
155
+ * having to evaluate CSS as JS.
156
+ * - `resources` — auxiliary assets (sourcemaps, images, fonts) so the host
157
+ * can preload / fingerprint them.
158
+ *
159
+ * Pure function over stats + root path — kept testable without booting a
160
+ * real rspack build.
161
+ */
162
+ export function getChunks(
119
163
  opts: ParsedModuleLinkPluginOptions,
120
- compilation: Compilation
121
- ) {
122
- const stats = compilation.getStats().toJson({
123
- all: false,
124
- chunks: true,
125
- modules: true,
126
- chunkModules: true,
127
- ids: true
128
- });
164
+ stats: StatsCompilation,
165
+ root: string
166
+ ): ManifestJsonChunks {
129
167
  const buildChunks: ManifestJsonChunks = {};
130
168
  if (!stats.chunks) return buildChunks;
131
169
 
132
170
  for (const chunk of stats.chunks) {
133
- const module = chunk.modules
134
- ?.sort((a, b) => {
135
- return (a.index ?? -1) - (b?.index ?? -1);
136
- })
137
- ?.find((module) => {
138
- return module.moduleType?.includes('javascript/');
139
- });
171
+ const module = pickPrimaryJsModule(chunk);
140
172
  if (!module?.nameForCondition) continue;
141
173
 
142
174
  const js = chunk.files?.find((file) => file.endsWith(opts.ext));
143
175
  if (!js) continue;
144
176
 
145
- const root = compilation.options.context ?? process.cwd();
146
177
  const name = generateIdentifier({
147
178
  root,
148
179
  name: opts.name,
@@ -161,6 +192,12 @@ function getChunks(
161
192
  return buildChunks;
162
193
  }
163
194
 
195
+ function pickPrimaryJsModule(chunk: StatsChunk) {
196
+ return chunk.modules
197
+ ?.sort((a, b) => (a.index ?? -1) - (b?.index ?? -1))
198
+ ?.find((module) => module.moduleType?.includes('javascript/'));
199
+ }
200
+
164
201
  export function generateIdentifier({
165
202
  root,
166
203
  name,
@@ -26,6 +26,7 @@ export function parseOptions(
26
26
  scopes: options.scopes ?? {},
27
27
  injectChunkName: options.injectChunkName ?? false,
28
28
  preEntries: options.preEntries ?? [],
29
- deps
29
+ deps,
30
+ wrapperFiles: options.wrapperFiles ?? []
30
31
  };
31
32
  }