@octanejs/rspack-plugin 0.1.37 → 0.1.38
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.
- package/README.md +13 -1
- package/package.json +6 -3
- package/src/css-module-constants.js +377 -0
- package/src/css-module-data.js +90 -0
- package/src/finalize-loader.js +14 -0
- package/src/loader.js +21 -3
- package/src/parallel-loader.js +5 -0
- package/src/plugin.js +116 -21
- package/src/shared.js +20 -1
- package/types/index.d.ts +34 -0
package/README.md
CHANGED
|
@@ -83,9 +83,21 @@ new OctaneRspackPlugin({
|
|
|
83
83
|
});
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
Renderer options remain serializable data—there are no renderer callbacks—so the same
|
|
87
87
|
configuration is safe to reuse across compiler environments and caches.
|
|
88
88
|
|
|
89
|
+
For one-shot production builds, the experimental class-plugin option
|
|
90
|
+
`cssModuleConstants: true` folds proven named-string exports from JavaScript
|
|
91
|
+
CSS-module providers such as `css-loader` with CSS extraction. An immutable CSS
|
|
92
|
+
provider can instead supply a `cssModuleConstants(module)` callback. It runs on
|
|
93
|
+
the main thread and its facts are checked against the exact completed loader
|
|
94
|
+
source. Ordinary mutable default maps and native `css/module` are left alone.
|
|
95
|
+
Eligible consumers are compiled once more and are not stored in the persistent
|
|
96
|
+
module cache; other modules keep normal caching. The option is disabled by
|
|
97
|
+
default and does not change development, HMR, or watch output. See
|
|
98
|
+
[CSS-module constants](../../docs/compiler-css-module-constants.md) for the
|
|
99
|
+
provider contract and stylesheet-ownership rules.
|
|
100
|
+
|
|
89
101
|
Rspack layers can compile the same authored module against distinct universal
|
|
90
102
|
renderer graphs. Configure the background graph at the top level, then key
|
|
91
103
|
`layerSpecializations` by the exact value of `module.layer`:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/rspack-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -45,13 +45,16 @@
|
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@rspack/core": "^2.0.0",
|
|
48
|
-
"octane": "0.1.
|
|
48
|
+
"octane": "0.1.43"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@rspack/core": "^2.1.4",
|
|
52
|
+
"@types/jsdom": "30.0.0",
|
|
52
53
|
"@types/node": "^24.13.3",
|
|
54
|
+
"css-loader": "6.11.0",
|
|
55
|
+
"jsdom": "^30.0.1",
|
|
53
56
|
"vitest": "^4.1.10",
|
|
54
|
-
"octane": "0.1.
|
|
57
|
+
"octane": "0.1.43"
|
|
55
58
|
},
|
|
56
59
|
"scripts": {
|
|
57
60
|
"test": "vitest run --config vitest.config.js"
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cleanModuleId,
|
|
3
|
+
isPlainCssModuleId,
|
|
4
|
+
readCssModuleExports,
|
|
5
|
+
validateCssModuleConstants,
|
|
6
|
+
} from 'octane/compiler/bundler';
|
|
7
|
+
import {
|
|
8
|
+
CSS_MODULE_BUILD_INFO_KEY,
|
|
9
|
+
CSS_MODULE_CONTEXT_KEY,
|
|
10
|
+
cssModuleSourceHash,
|
|
11
|
+
} from './css-module-data.js';
|
|
12
|
+
|
|
13
|
+
const PLUGIN_NAME = 'OctaneRspackCssModuleConstants';
|
|
14
|
+
const DIAGNOSTIC_OWNER = '@octanejs/rspack-plugin';
|
|
15
|
+
const JAVASCRIPT_TYPES = new Set(['javascript/auto', 'javascript/esm']);
|
|
16
|
+
const HASH = /^[a-f0-9]{64}$/;
|
|
17
|
+
|
|
18
|
+
function iterable(value) {
|
|
19
|
+
return value != null && typeof value[Symbol.iterator] === 'function' ? value : [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function identifier(module) {
|
|
23
|
+
if (typeof module?.identifier !== 'function') return null;
|
|
24
|
+
const id = module.identifier();
|
|
25
|
+
return typeof id === 'string' ? id : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function moduleSource(module) {
|
|
29
|
+
const source = module?.originalSource?.()?.source();
|
|
30
|
+
if (typeof source === 'string') return source;
|
|
31
|
+
return Buffer.isBuffer(source) ? source.toString('utf8') : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Reacquire current-build objects; native-backed Modules never enter our state. */
|
|
35
|
+
function currentModules(modules) {
|
|
36
|
+
const result = new Map();
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
const visit = (module) => {
|
|
39
|
+
if (module == null || seen.has(module)) return;
|
|
40
|
+
seen.add(module);
|
|
41
|
+
// Concatenation can move a provider under another module. Prefer the
|
|
42
|
+
// original child if a wrapper happens to share its identifier.
|
|
43
|
+
for (const child of iterable(module.modules)) visit(child);
|
|
44
|
+
if (module.rootModule != null) visit(module.rootModule);
|
|
45
|
+
const id = identifier(module);
|
|
46
|
+
if (id !== null && !result.has(id)) result.set(id, module);
|
|
47
|
+
};
|
|
48
|
+
for (const module of iterable(modules)) visit(module);
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function candidateInfo(module) {
|
|
53
|
+
const info = module?.buildInfo?.[CSS_MODULE_BUILD_INFO_KEY];
|
|
54
|
+
if (
|
|
55
|
+
info == null ||
|
|
56
|
+
typeof info.sourceHash !== 'string' ||
|
|
57
|
+
!HASH.test(info.sourceHash) ||
|
|
58
|
+
!Array.isArray(info.requests) ||
|
|
59
|
+
!info.requests.every((request) => typeof request === 'string') ||
|
|
60
|
+
!Array.isArray(info.consumed) ||
|
|
61
|
+
!info.consumed.every((request) => typeof request === 'string')
|
|
62
|
+
) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return info;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function changed(importer, request, reason) {
|
|
69
|
+
const location =
|
|
70
|
+
request === undefined
|
|
71
|
+
? JSON.stringify(importer)
|
|
72
|
+
: `${JSON.stringify(importer)} (${JSON.stringify(request)})`;
|
|
73
|
+
throw new Error(`${DIAGNOSTIC_OWNER}: CSS-module proof changed for ${location}: ${reason}.`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A resource path is not an import identity: issuer rules, layers, queries,
|
|
78
|
+
* dependency categories, and replacements can select different modules. Read
|
|
79
|
+
* the effective target of the actual ESM edge after the make phase instead.
|
|
80
|
+
*/
|
|
81
|
+
function targetForRequest(compilation, importer, request) {
|
|
82
|
+
const targets = new Map();
|
|
83
|
+
for (const connection of compilation.moduleGraph.getOutgoingConnections(importer)) {
|
|
84
|
+
const dependency = connection.dependency;
|
|
85
|
+
if (dependency?.request !== request || dependency.category !== 'esm') continue;
|
|
86
|
+
if (dependency.attributes != null && Object.keys(dependency.attributes).length > 0) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const target = connection.module;
|
|
90
|
+
const id = identifier(target);
|
|
91
|
+
if (id === null) return null;
|
|
92
|
+
targets.set(id, target);
|
|
93
|
+
}
|
|
94
|
+
return targets.size === 1 ? targets.values().next().value : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function sortedStrings(values) {
|
|
98
|
+
return [...new Set([...iterable(values)].filter((value) => typeof value === 'string'))].sort();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function moduleDependencies(module) {
|
|
102
|
+
const info = module.buildInfo;
|
|
103
|
+
return {
|
|
104
|
+
files: sortedStrings(info?.fileDependencies),
|
|
105
|
+
contexts: sortedStrings(info?.contextDependencies),
|
|
106
|
+
missing: sortedStrings(info?.missingDependencies),
|
|
107
|
+
build: sortedStrings(info?.buildDependencies),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function providerMeta(module) {
|
|
112
|
+
const info = module.buildInfo;
|
|
113
|
+
return Object.freeze(info != null && typeof info === 'object' ? { ...info } : {});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readTargetProof(module, option, environment, cache) {
|
|
117
|
+
const id = identifier(module);
|
|
118
|
+
if (id === null || !JAVASCRIPT_TYPES.has(module.type) || typeof module.resource !== 'string') {
|
|
119
|
+
// Native css/* has no final JavaScript export map at this stage. Likewise
|
|
120
|
+
// asset/source can contain ESM-looking text without exporting its bindings.
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const code = moduleSource(module);
|
|
124
|
+
if (code === null) return null;
|
|
125
|
+
const fingerprint = cssModuleSourceHash(code);
|
|
126
|
+
const cached = cache.get(id);
|
|
127
|
+
if (cached !== undefined) {
|
|
128
|
+
if (cached.fingerprint !== fingerprint)
|
|
129
|
+
changed(id, undefined, 'provider source changed during collection');
|
|
130
|
+
return cached.proof;
|
|
131
|
+
}
|
|
132
|
+
const exports = readCssModuleExports(code, { allowPureVar: true });
|
|
133
|
+
const supplied =
|
|
134
|
+
typeof option !== 'function'
|
|
135
|
+
? null
|
|
136
|
+
: validateCssModuleConstants(
|
|
137
|
+
option(
|
|
138
|
+
Object.freeze({
|
|
139
|
+
id,
|
|
140
|
+
resource: module.resource,
|
|
141
|
+
code,
|
|
142
|
+
meta: providerMeta(module),
|
|
143
|
+
environment,
|
|
144
|
+
layer: module.layer,
|
|
145
|
+
type: module.type,
|
|
146
|
+
}),
|
|
147
|
+
),
|
|
148
|
+
exports,
|
|
149
|
+
id,
|
|
150
|
+
DIAGNOSTIC_OWNER,
|
|
151
|
+
);
|
|
152
|
+
const named =
|
|
153
|
+
isPlainCssModuleId(cleanModuleId(module.resource)) && exports?.pure === true
|
|
154
|
+
? new Map(exports.named)
|
|
155
|
+
: new Map();
|
|
156
|
+
for (const [name, value] of supplied?.named ?? []) named.set(name, value);
|
|
157
|
+
const defaultMap = supplied?.default ?? new Map();
|
|
158
|
+
const proof =
|
|
159
|
+
named.size === 0 && defaultMap.size === 0
|
|
160
|
+
? null
|
|
161
|
+
: {
|
|
162
|
+
id,
|
|
163
|
+
fingerprint,
|
|
164
|
+
named: [...named].sort(([left], [right]) => left.localeCompare(right)),
|
|
165
|
+
default: [...defaultMap].sort(([left], [right]) => left.localeCompare(right)),
|
|
166
|
+
dependencies: moduleDependencies(module),
|
|
167
|
+
};
|
|
168
|
+
cache.set(id, { fingerprint, proof });
|
|
169
|
+
return proof;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function sameStrings(left, right) {
|
|
173
|
+
const a = [...new Set(left)].sort();
|
|
174
|
+
const b = [...new Set(right)].sort();
|
|
175
|
+
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function verifyTarget(compilation, importer, entry) {
|
|
179
|
+
const target = targetForRequest(compilation, importer, entry.request);
|
|
180
|
+
if (identifier(target) !== entry.id)
|
|
181
|
+
changed(identifier(importer), entry.request, 'the effective module identity differs');
|
|
182
|
+
if (!JAVASCRIPT_TYPES.has(target.type))
|
|
183
|
+
changed(identifier(importer), entry.request, 'the target is no longer JavaScript ESM');
|
|
184
|
+
const source = moduleSource(target);
|
|
185
|
+
if (source === null || cssModuleSourceHash(source) !== entry.fingerprint) {
|
|
186
|
+
changed(identifier(importer), entry.request, 'the final provider source differs');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function verifyGraph(compilation, state) {
|
|
191
|
+
if (state.receipts.size === 0) return;
|
|
192
|
+
const modules = currentModules(compilation.modules);
|
|
193
|
+
for (const [id, receipt] of state.receipts) {
|
|
194
|
+
const importer = modules.get(id);
|
|
195
|
+
const info = candidateInfo(importer);
|
|
196
|
+
if (
|
|
197
|
+
info === null ||
|
|
198
|
+
info.sourceHash !== receipt.sourceHash ||
|
|
199
|
+
!sameStrings(
|
|
200
|
+
info.consumed,
|
|
201
|
+
receipt.imports.map((entry) => entry.request),
|
|
202
|
+
)
|
|
203
|
+
) {
|
|
204
|
+
changed(id, undefined, 'the authored importer or committed-use receipt differs');
|
|
205
|
+
}
|
|
206
|
+
for (const entry of receipt.imports) verifyTarget(compilation, importer, entry);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function verifyFinalSources(compilation, state) {
|
|
211
|
+
if (state.receipts.size === 0) return;
|
|
212
|
+
const modules = currentModules(compilation.modules);
|
|
213
|
+
const checked = new Map();
|
|
214
|
+
for (const receipt of state.receipts.values()) {
|
|
215
|
+
for (const entry of receipt.imports) {
|
|
216
|
+
if (checked.has(entry.id)) {
|
|
217
|
+
if (checked.get(entry.id) !== entry.fingerprint)
|
|
218
|
+
changed(entry.id, undefined, 'conflicting source receipts');
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
checked.set(entry.id, entry.fingerprint);
|
|
222
|
+
const module = modules.get(entry.id);
|
|
223
|
+
// The full graph was checked at seal, before optimization. An unused
|
|
224
|
+
// component and its stylesheet may now be gone, which is intentional.
|
|
225
|
+
if (module === undefined) continue;
|
|
226
|
+
const source = moduleSource(module);
|
|
227
|
+
if (
|
|
228
|
+
!JAVASCRIPT_TYPES.has(module.type) ||
|
|
229
|
+
source === null ||
|
|
230
|
+
cssModuleSourceHash(source) !== entry.fingerprint
|
|
231
|
+
) {
|
|
232
|
+
changed(entry.id, undefined, 'the emitted provider source differs');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function collectAndRebuild(compilation, state, option, environment) {
|
|
239
|
+
if (state.started) return;
|
|
240
|
+
state.started = true;
|
|
241
|
+
const cache = new Map();
|
|
242
|
+
// Collect from the complete first graph before rebuilding anything. This is
|
|
243
|
+
// both deterministic and safe for mutually importing virtual CSS providers.
|
|
244
|
+
{
|
|
245
|
+
const modules = currentModules(compilation.modules);
|
|
246
|
+
const candidates = [...modules]
|
|
247
|
+
.filter(([, module]) => candidateInfo(module)?.requests.length > 0)
|
|
248
|
+
.map(([id]) => id)
|
|
249
|
+
.sort();
|
|
250
|
+
for (const id of candidates) {
|
|
251
|
+
const importer = modules.get(id);
|
|
252
|
+
const info = candidateInfo(importer);
|
|
253
|
+
const imports = [];
|
|
254
|
+
for (const request of [...new Set(info.requests)].sort()) {
|
|
255
|
+
const target = targetForRequest(compilation, importer, request);
|
|
256
|
+
if (target === null) continue;
|
|
257
|
+
const proof = readTargetProof(target, option, environment, cache);
|
|
258
|
+
if (proof !== null) imports.push({ request, ...proof });
|
|
259
|
+
}
|
|
260
|
+
if (imports.length > 0) {
|
|
261
|
+
state.proofs.set(id, { sourceHash: info.sourceHash, imports });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// A derived CSS provider can itself be an Octane importer. Its first-pass
|
|
266
|
+
// source is not a stable proof if this batch will recompile it. Deopt those
|
|
267
|
+
// edges rather than chase a fixed point or rebuild any importer twice. This
|
|
268
|
+
// deliberately bounded pass need not find every possible constant.
|
|
269
|
+
const scheduled = new Set(state.proofs.keys());
|
|
270
|
+
for (const [id, proof] of state.proofs) {
|
|
271
|
+
const imports = proof.imports.filter((entry) => !scheduled.has(entry.id));
|
|
272
|
+
if (imports.length === 0) state.proofs.delete(id);
|
|
273
|
+
else if (imports.length !== proof.imports.length) state.proofs.set(id, { ...proof, imports });
|
|
274
|
+
}
|
|
275
|
+
// Rspack owns graph mutation. Do not write source objects, synthesize entry
|
|
276
|
+
// dependencies, or invoke importModule (which executes application modules).
|
|
277
|
+
// Its public rebuildModule dispatcher batches synchronous requests. Ignore
|
|
278
|
+
// callback Module values: Rspack may return those in a different order, and
|
|
279
|
+
// only the current graph's exact identifiers authenticate the rebuilt inputs.
|
|
280
|
+
const pending = (() => {
|
|
281
|
+
const modules = currentModules(compilation.modules);
|
|
282
|
+
return [...state.proofs.keys()].map((id) => {
|
|
283
|
+
const importer = modules.get(id);
|
|
284
|
+
if (importer === undefined)
|
|
285
|
+
changed(id, undefined, 'the importer disappeared before rebuilding');
|
|
286
|
+
return new Promise((resolve, reject) => {
|
|
287
|
+
compilation.rebuildModule(importer, (error) => (error ? reject(error) : resolve()));
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
})();
|
|
291
|
+
const results = await Promise.allSettled(pending);
|
|
292
|
+
for (const result of results) if (result.status === 'rejected') throw result.reason;
|
|
293
|
+
const rebuiltModules = currentModules(compilation.modules);
|
|
294
|
+
for (const [id, proof] of state.proofs) {
|
|
295
|
+
const rebuilt = rebuiltModules.get(id);
|
|
296
|
+
const info = candidateInfo(rebuilt);
|
|
297
|
+
if (info === null || info.sourceHash !== proof.sourceHash) {
|
|
298
|
+
changed(id, undefined, 'the authored source changed while rebuilding');
|
|
299
|
+
}
|
|
300
|
+
const entries = new Map(proof.imports.map((entry) => [entry.request, entry]));
|
|
301
|
+
const consumed = [...new Set(info.consumed)].sort();
|
|
302
|
+
if (consumed.some((request) => !entries.has(request))) {
|
|
303
|
+
changed(id, undefined, 'the compiler consumed an unprovided import');
|
|
304
|
+
}
|
|
305
|
+
if (consumed.length > 0) {
|
|
306
|
+
state.receipts.set(id, {
|
|
307
|
+
sourceHash: proof.sourceHash,
|
|
308
|
+
imports: consumed.map((request) => entries.get(request)),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
verifyGraph(compilation, state);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function oneShotProduction(compiler) {
|
|
316
|
+
return (
|
|
317
|
+
compiler.options.mode === 'production' && compiler.watchMode !== true && !compiler.options.watch
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Install the opt-in, main-thread, same-compilation CSS proof controller. */
|
|
322
|
+
export function installCssModuleConstants(compiler, { option, environment }) {
|
|
323
|
+
if (option === undefined || option === false) return;
|
|
324
|
+
if (option !== true && typeof option !== 'function') {
|
|
325
|
+
throw new TypeError(`${DIAGNOSTIC_OWNER}: cssModuleConstants must be a boolean or function.`);
|
|
326
|
+
}
|
|
327
|
+
const NormalModule = compiler.webpack?.NormalModule;
|
|
328
|
+
if (
|
|
329
|
+
typeof NormalModule?.getCompilationHooks !== 'function' ||
|
|
330
|
+
typeof compiler.hooks.thisCompilation?.tap !== 'function' ||
|
|
331
|
+
typeof compiler.hooks.finishMake?.tapPromise !== 'function'
|
|
332
|
+
) {
|
|
333
|
+
throw new TypeError(
|
|
334
|
+
`${DIAGNOSTIC_OWNER}: cssModuleConstants requires Rspack's module and finishMake hooks.`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const states = new WeakMap();
|
|
338
|
+
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
|
|
339
|
+
const state = {
|
|
340
|
+
enabled: compiler.options.mode === 'production',
|
|
341
|
+
discoverOnly: !oneShotProduction(compiler),
|
|
342
|
+
started: false,
|
|
343
|
+
proofs: new Map(),
|
|
344
|
+
receipts: new Map(),
|
|
345
|
+
};
|
|
346
|
+
states.set(compilation, state);
|
|
347
|
+
if (!state.enabled) return;
|
|
348
|
+
NormalModule.getCompilationHooks(compilation).loader.tap(PLUGIN_NAME, (context, module) => {
|
|
349
|
+
context[CSS_MODULE_CONTEXT_KEY] = {
|
|
350
|
+
enabled: true,
|
|
351
|
+
...(state.discoverOnly ? { discoverOnly: true } : null),
|
|
352
|
+
proof: state.discoverOnly ? null : (state.proofs.get(identifier(module)) ?? null),
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
// Production watch may cache ordinary candidate metadata for a later
|
|
356
|
+
// one-shot build, but never authenticates or consumes cross-module facts.
|
|
357
|
+
if (state.discoverOnly) return;
|
|
358
|
+
// All finishModules taps have completed, and module concatenation has not
|
|
359
|
+
// yet rewritten the effective graph. This catches later provider rebuilds.
|
|
360
|
+
compilation.hooks.seal.tap({ name: PLUGIN_NAME, stage: Number.MAX_SAFE_INTEGER }, () =>
|
|
361
|
+
verifyGraph(compilation, state),
|
|
362
|
+
);
|
|
363
|
+
compilation.hooks.processAssets.tap(
|
|
364
|
+
{
|
|
365
|
+
name: PLUGIN_NAME,
|
|
366
|
+
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT,
|
|
367
|
+
},
|
|
368
|
+
() => verifyFinalSources(compilation, state),
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
compiler.hooks.finishMake.tapPromise(PLUGIN_NAME, async (compilation) => {
|
|
372
|
+
const state = states.get(compilation);
|
|
373
|
+
if (state?.enabled && !state.discoverOnly && oneShotProduction(compiler)) {
|
|
374
|
+
await collectAndRebuild(compilation, state, option, environment);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
// Only plain data crosses Rspack's worker boundary. The provider callback and
|
|
4
|
+
// native module graph stay on the main thread for the current compilation.
|
|
5
|
+
export const CSS_MODULE_CONTEXT_KEY = '__octaneCssModuleConstants';
|
|
6
|
+
export const CSS_MODULE_BUILD_INFO_KEY = 'octaneCssModuleConstants';
|
|
7
|
+
|
|
8
|
+
export function cssModuleSourceHash(source) {
|
|
9
|
+
return createHash('sha256').update(String(source)).digest('hex');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function clearCssModuleBuildInfo(module) {
|
|
13
|
+
if (module?.buildInfo && typeof module.buildInfo === 'object') {
|
|
14
|
+
delete module.buildInfo[CSS_MODULE_BUILD_INFO_KEY];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Prepare a proof for this exact authored loader input, never a previous build. */
|
|
19
|
+
export function prepareCssModuleConstants(context, compiler, source, id, options) {
|
|
20
|
+
const data = context[CSS_MODULE_CONTEXT_KEY];
|
|
21
|
+
if (
|
|
22
|
+
data?.enabled !== true ||
|
|
23
|
+
context.mode !== 'production' ||
|
|
24
|
+
context.hot === true ||
|
|
25
|
+
options.dev ||
|
|
26
|
+
options.hmr
|
|
27
|
+
) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const requests = compiler.findCssModuleImportRequests(source, id, options.environment);
|
|
31
|
+
if (requests.length === 0) return null;
|
|
32
|
+
// A host callback may close over configuration outside Rspack's cache key.
|
|
33
|
+
// Only eligible CSS consumers pay for the conservative one-shot rebuild.
|
|
34
|
+
if (data.discoverOnly !== true) context.cacheable?.(false);
|
|
35
|
+
const sourceHash = cssModuleSourceHash(source);
|
|
36
|
+
const requested = new Set(requests);
|
|
37
|
+
const imports = new Map();
|
|
38
|
+
if (data.discoverOnly !== true && data.proof?.sourceHash === sourceHash) {
|
|
39
|
+
for (const entry of data.proof.imports) {
|
|
40
|
+
if (!requested.has(entry.request)) continue;
|
|
41
|
+
imports.set(entry.request, {
|
|
42
|
+
...entry,
|
|
43
|
+
named: new Map(entry.named),
|
|
44
|
+
default: new Map(entry.default),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const transformOptions =
|
|
49
|
+
imports.size === 0
|
|
50
|
+
? null
|
|
51
|
+
: {
|
|
52
|
+
resolveCssModuleConstant(request, imported, property) {
|
|
53
|
+
const entry = imports.get(request);
|
|
54
|
+
if (entry === undefined) return undefined;
|
|
55
|
+
if (property === null) return entry.named.get(imported);
|
|
56
|
+
if (imported === 'default') {
|
|
57
|
+
return entry.default.get(property);
|
|
58
|
+
}
|
|
59
|
+
if (imported === '*') {
|
|
60
|
+
return entry.named.get(property);
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
},
|
|
64
|
+
// Keep each retained template's ordinary stylesheet ownership edge.
|
|
65
|
+
preserveCssModuleReferences: [...imports.keys()],
|
|
66
|
+
};
|
|
67
|
+
return { sourceHash, requests, imports, transformOptions };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Publish only committed folds and their real loader dependencies. */
|
|
71
|
+
export function finishCssModuleConstants(context, prepared, result) {
|
|
72
|
+
if (prepared === null || result?.kind !== 'compile' || !context._module) return;
|
|
73
|
+
const consumed = [...new Set(result.cssModuleConstantImports ?? [])].filter((request) =>
|
|
74
|
+
prepared.imports.has(request),
|
|
75
|
+
);
|
|
76
|
+
for (const request of consumed) {
|
|
77
|
+
const dependencies = prepared.imports.get(request).dependencies;
|
|
78
|
+
for (const file of dependencies.files) context.addDependency?.(file);
|
|
79
|
+
for (const directory of dependencies.contexts) context.addContextDependency?.(directory);
|
|
80
|
+
for (const file of dependencies.missing) context.addMissingDependency?.(file);
|
|
81
|
+
for (const file of dependencies.build) context.addBuildDependency?.(file);
|
|
82
|
+
}
|
|
83
|
+
const module = context._module;
|
|
84
|
+
if (!module.buildInfo || typeof module.buildInfo !== 'object') module.buildInfo = {};
|
|
85
|
+
module.buildInfo[CSS_MODULE_BUILD_INFO_KEY] = {
|
|
86
|
+
sourceHash: prepared.sourceHash,
|
|
87
|
+
requests: [...prepared.requests],
|
|
88
|
+
consumed,
|
|
89
|
+
};
|
|
90
|
+
}
|
package/src/finalize-loader.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearCssModuleBuildInfo,
|
|
3
|
+
CSS_MODULE_BUILD_INFO_KEY,
|
|
4
|
+
CSS_MODULE_CONTEXT_KEY,
|
|
5
|
+
} from './css-module-data.js';
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* Rspack does not copy a module's layer or buildInfo into parallel-loader
|
|
3
9
|
* workers. Pitch data is structured-cloned into the worker before compilation.
|
|
@@ -5,6 +11,8 @@
|
|
|
5
11
|
export function pitch() {
|
|
6
12
|
this.data ??= {};
|
|
7
13
|
this.data.octaneLayer = this._module?.layer ?? null;
|
|
14
|
+
this.data[CSS_MODULE_CONTEXT_KEY] = this[CSS_MODULE_CONTEXT_KEY] ?? null;
|
|
15
|
+
clearCssModuleBuildInfo(this._module);
|
|
8
16
|
if (this._module?.buildInfo && typeof this._module.buildInfo === 'object') {
|
|
9
17
|
delete this._module.buildInfo.octane;
|
|
10
18
|
}
|
|
@@ -19,6 +27,12 @@ export default function finalizeOctaneLoader(source, sourceMap, metadata) {
|
|
|
19
27
|
}
|
|
20
28
|
this._module.buildInfo.octane = result.buildInfo;
|
|
21
29
|
}
|
|
30
|
+
if (result?.cssModuleBuildInfo) {
|
|
31
|
+
if (!this._module.buildInfo || typeof this._module.buildInfo !== 'object') {
|
|
32
|
+
this._module.buildInfo = {};
|
|
33
|
+
}
|
|
34
|
+
this._module.buildInfo[CSS_MODULE_BUILD_INFO_KEY] = result.cssModuleBuildInfo;
|
|
35
|
+
}
|
|
22
36
|
for (const dependency of result?.missingDependencies ?? []) {
|
|
23
37
|
this.addMissingDependency(dependency);
|
|
24
38
|
}
|
package/src/loader.js
CHANGED
|
@@ -2,6 +2,12 @@ import { realpathSync } from 'node:fs';
|
|
|
2
2
|
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
3
3
|
import remapping from '@jridgewell/remapping';
|
|
4
4
|
import { canonicalModuleId, cleanModuleId, createOctaneCompiler } from 'octane/compiler/bundler';
|
|
5
|
+
import {
|
|
6
|
+
clearCssModuleBuildInfo,
|
|
7
|
+
CSS_MODULE_CONTEXT_KEY,
|
|
8
|
+
finishCssModuleConstants,
|
|
9
|
+
prepareCssModuleConstants,
|
|
10
|
+
} from './css-module-data.js';
|
|
5
11
|
import {
|
|
6
12
|
inferRspackEnvironment,
|
|
7
13
|
normalizeLoaderOptions,
|
|
@@ -85,6 +91,7 @@ async function resolveClientOnlyImports(context, compiler, source, id) {
|
|
|
85
91
|
export default function octaneLoader(source, inputSourceMap) {
|
|
86
92
|
this.cacheable?.(true);
|
|
87
93
|
clearBuildInfo(this._module);
|
|
94
|
+
clearCssModuleBuildInfo(this._module);
|
|
88
95
|
|
|
89
96
|
try {
|
|
90
97
|
const options = normalizeLoaderOptions(this.getOptions?.() ?? {});
|
|
@@ -125,14 +132,24 @@ export default function octaneLoader(source, inputSourceMap) {
|
|
|
125
132
|
warn: (message) => this.emitWarning?.(new Error(message)),
|
|
126
133
|
});
|
|
127
134
|
const id = realModuleId(this.resource ?? this.resourcePath);
|
|
135
|
+
const authoredSource = String(source);
|
|
136
|
+
const cssModuleConstants =
|
|
137
|
+
this[CSS_MODULE_CONTEXT_KEY]?.enabled === true
|
|
138
|
+
? prepareCssModuleConstants(this, compiler, authoredSource, id, {
|
|
139
|
+
environment,
|
|
140
|
+
hmr,
|
|
141
|
+
dev,
|
|
142
|
+
})
|
|
143
|
+
: null;
|
|
128
144
|
const finish = (clientOnlyImports, callback) => {
|
|
129
145
|
try {
|
|
130
|
-
const result = compiler.transform(
|
|
146
|
+
const result = compiler.transform(authoredSource, id, {
|
|
131
147
|
environment,
|
|
132
148
|
hmr,
|
|
133
149
|
dev,
|
|
134
150
|
profile,
|
|
135
151
|
...(clientOnlyImports.length > 0 ? { clientOnlyImports } : null),
|
|
152
|
+
...cssModuleConstants?.transformOptions,
|
|
136
153
|
});
|
|
137
154
|
|
|
138
155
|
if (result === null) {
|
|
@@ -141,6 +158,7 @@ export default function octaneLoader(source, inputSourceMap) {
|
|
|
141
158
|
}
|
|
142
159
|
|
|
143
160
|
registerDependencies(this, result);
|
|
161
|
+
finishCssModuleConstants(this, cssModuleConstants, result);
|
|
144
162
|
if (result.kind === 'none') {
|
|
145
163
|
callback(null, source, this.sourceMap === false ? undefined : inputSourceMap);
|
|
146
164
|
return;
|
|
@@ -177,10 +195,10 @@ export default function octaneLoader(source, inputSourceMap) {
|
|
|
177
195
|
currentReference === null &&
|
|
178
196
|
typeof this.getResolve === 'function'
|
|
179
197
|
) {
|
|
180
|
-
const requests = compiler.findServerImportRequests(
|
|
198
|
+
const requests = compiler.findServerImportRequests(authoredSource, id);
|
|
181
199
|
if (requests.length > 0) {
|
|
182
200
|
const asyncCallback = this.async?.() ?? callback;
|
|
183
|
-
resolveClientOnlyImports(this, compiler,
|
|
201
|
+
resolveClientOnlyImports(this, compiler, authoredSource, id).then(
|
|
184
202
|
(imports) => finish(imports, asyncCallback),
|
|
185
203
|
(error) => asyncCallback(error instanceof Error ? error : new Error(String(error))),
|
|
186
204
|
);
|
package/src/parallel-loader.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import octaneLoader from './loader.js';
|
|
2
|
+
import { CSS_MODULE_BUILD_INFO_KEY, CSS_MODULE_CONTEXT_KEY } from './css-module-data.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Run the unchanged public Octane loader inside Rspack's worker pool. Rspack
|
|
@@ -10,6 +11,7 @@ export default function parallelOctaneLoader(source, sourceMap) {
|
|
|
10
11
|
const module = this._module;
|
|
11
12
|
const finalizer = this.loaders[this.loaderIndex - 1];
|
|
12
13
|
module.layer = finalizer?.loaderItem?.data?.octaneLayer ?? undefined;
|
|
14
|
+
this[CSS_MODULE_CONTEXT_KEY] = finalizer?.loaderItem?.data?.[CSS_MODULE_CONTEXT_KEY] ?? undefined;
|
|
13
15
|
|
|
14
16
|
const callback = this.callback.bind(this);
|
|
15
17
|
const async = this.async.bind(this);
|
|
@@ -22,6 +24,9 @@ export default function parallelOctaneLoader(source, sourceMap) {
|
|
|
22
24
|
...(metadata ?? {}),
|
|
23
25
|
__octaneParallelLoader: {
|
|
24
26
|
buildInfo: module.buildInfo?.octane ?? null,
|
|
27
|
+
...(module.buildInfo?.[CSS_MODULE_BUILD_INFO_KEY] === undefined
|
|
28
|
+
? null
|
|
29
|
+
: { cssModuleBuildInfo: module.buildInfo[CSS_MODULE_BUILD_INFO_KEY] }),
|
|
25
30
|
missingDependencies,
|
|
26
31
|
},
|
|
27
32
|
});
|
package/src/plugin.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
|
-
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import {
|
|
7
7
|
CLIENT_REFERENCE_MANIFEST_FILENAME,
|
|
8
8
|
createClientReferenceManifest,
|
|
9
9
|
createOctaneCompiler,
|
|
10
10
|
} from 'octane/compiler/bundler';
|
|
11
|
+
import { installCssModuleConstants } from './css-module-constants.js';
|
|
11
12
|
import {
|
|
12
13
|
getOctaneRspackBuildInfo,
|
|
13
14
|
inferRspackEnvironment,
|
|
@@ -23,6 +24,12 @@ const parallelLoaderPath = fileURLToPath(new URL('./parallel-loader.js', import.
|
|
|
23
24
|
const DEFAULT_MAX_WORKERS = 4;
|
|
24
25
|
const OCTANE_RULE = /\.(?:tsrx|tsx|ts|js)$/i;
|
|
25
26
|
const TYPESCRIPT_RULE = /\.(?:tsrx|tsx|ts)$/i;
|
|
27
|
+
const RUNTIME_SUBPATHS = [
|
|
28
|
+
'octane/server',
|
|
29
|
+
'octane/internal/client',
|
|
30
|
+
'octane/internal/server',
|
|
31
|
+
'octane/profiling',
|
|
32
|
+
];
|
|
26
33
|
|
|
27
34
|
function realRoot(path) {
|
|
28
35
|
try {
|
|
@@ -40,10 +47,10 @@ function addUniqueExtensions(resolveOptions) {
|
|
|
40
47
|
];
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
function resolveRuntimeModule(request, root) {
|
|
50
|
+
function resolveRuntimeModule(resolver, request, root) {
|
|
44
51
|
if (isAbsolute(request)) return request;
|
|
45
52
|
try {
|
|
46
|
-
return
|
|
53
|
+
return resolver.resolveSync({}, root, request);
|
|
47
54
|
} catch {
|
|
48
55
|
// Let Rspack produce its normal resolution diagnostic. This fallback also
|
|
49
56
|
// keeps config inspection usable before peer dependencies are installed.
|
|
@@ -51,18 +58,82 @@ function resolveRuntimeModule(request, root) {
|
|
|
51
58
|
}
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
function octanePackageRoot(entry) {
|
|
62
|
+
if (typeof entry !== 'string' || !isAbsolute(entry)) return undefined;
|
|
63
|
+
let directory = dirname(entry);
|
|
64
|
+
while (true) {
|
|
65
|
+
try {
|
|
66
|
+
const { name } = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'));
|
|
67
|
+
if (name === 'octane') return directory;
|
|
68
|
+
if (name !== undefined) return undefined;
|
|
69
|
+
} catch {
|
|
70
|
+
// Entry files may be several directories below their package manifest.
|
|
71
|
+
}
|
|
72
|
+
const parent = dirname(directory);
|
|
73
|
+
if (parent === directory) return undefined;
|
|
74
|
+
directory = parent;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function mergeRuntimeAliases(aliases, additions) {
|
|
79
|
+
return {
|
|
80
|
+
...additions,
|
|
81
|
+
...(aliases === false ? {} : aliases),
|
|
82
|
+
octane$: additions.octane$,
|
|
63
83
|
};
|
|
64
84
|
}
|
|
65
85
|
|
|
86
|
+
function addRuntimeAliases(compiler, request, root, explicitRuntime) {
|
|
87
|
+
const resolveOptions = compiler.options.resolve;
|
|
88
|
+
// The factory is created before Rspack applies mode/target defaults. Merge
|
|
89
|
+
// the finalized ESM options (including `...`) before resolving; require
|
|
90
|
+
// conditions would select Octane's separate, non-tree-shakeable CJS graph.
|
|
91
|
+
const { byDependency, ...base } = resolveOptions;
|
|
92
|
+
const esm = compiler.webpack.util.cleverMerge(base, byDependency?.esm ?? {});
|
|
93
|
+
const resolver = compiler.resolverFactory.get('normal', { ...esm, dependencyType: 'esm' });
|
|
94
|
+
const packageResolver = compiler.resolverFactory.get('normal', {
|
|
95
|
+
...esm,
|
|
96
|
+
alias: false,
|
|
97
|
+
dependencyType: 'esm',
|
|
98
|
+
});
|
|
99
|
+
const selected = resolveRuntimeModule(resolver, 'octane', root);
|
|
100
|
+
const override = explicitRuntime ? resolveRuntimeModule(resolver, request, root) : undefined;
|
|
101
|
+
const packageRoot = octanePackageRoot(override) ?? octanePackageRoot(selected) ?? root;
|
|
102
|
+
const aliases = esm.alias === false ? {} : (esm.alias ?? {});
|
|
103
|
+
const additions = {};
|
|
104
|
+
for (const subpath of RUNTIME_SUBPATHS) {
|
|
105
|
+
// Resolve package self-references without a broad `octane` alias. Linked
|
|
106
|
+
// packages must share the selected runtime's refs, context, and profiler.
|
|
107
|
+
// A consumer's explicit subpath override remains authoritative.
|
|
108
|
+
additions[`${subpath}$`] =
|
|
109
|
+
aliases[`${subpath}$`] ??
|
|
110
|
+
aliases[subpath] ??
|
|
111
|
+
resolveRuntimeModule(packageResolver, subpath, packageRoot);
|
|
112
|
+
}
|
|
113
|
+
const runtime = explicitRuntime
|
|
114
|
+
? override
|
|
115
|
+
: request === 'octane' || selected === false
|
|
116
|
+
? selected
|
|
117
|
+
: (additions[`${request}$`] ?? resolveRuntimeModule(resolver, request, root));
|
|
118
|
+
additions.octane$ = runtime;
|
|
119
|
+
// Exact entries precede any existing prefix alias; unrelated Octane subpaths
|
|
120
|
+
// (including universal renderer entries) keep their normal resolution.
|
|
121
|
+
resolveOptions.alias = mergeRuntimeAliases(resolveOptions.alias, additions);
|
|
122
|
+
if (byDependency?.esm?.alias !== undefined && byDependency.esm.alias !== false) {
|
|
123
|
+
// Rspack applies this map again during module resolution. A package
|
|
124
|
+
// selection here must not restore its client entry in a server graph.
|
|
125
|
+
// Preserve `false`: it explicitly disables all aliases for ESM requests.
|
|
126
|
+
resolveOptions.byDependency = {
|
|
127
|
+
...byDependency,
|
|
128
|
+
esm: {
|
|
129
|
+
...byDependency.esm,
|
|
130
|
+
alias: mergeRuntimeAliases(byDependency.esm.alias, additions),
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return resolver;
|
|
135
|
+
}
|
|
136
|
+
|
|
66
137
|
function projectRendererModule(request, root) {
|
|
67
138
|
// Renderer config uses project-root IDs such as `/src/object-renderer.ts`.
|
|
68
139
|
// They are never host-filesystem absolute paths, even if the same path happens
|
|
@@ -274,12 +345,20 @@ export class OctaneRspackPlugin {
|
|
|
274
345
|
);
|
|
275
346
|
const environment = this.options.environment ?? inferRspackEnvironment(compiler.options.target);
|
|
276
347
|
const profile = environment === 'client' && this.options.profile === true;
|
|
277
|
-
const
|
|
278
|
-
|
|
348
|
+
const hotModuleReplacement = hasHotModuleReplacement(compiler);
|
|
349
|
+
const hmr = environment === 'client' && hotModuleReplacement && this.options.hmr !== false;
|
|
279
350
|
const dev =
|
|
280
351
|
environment === 'client' &&
|
|
281
352
|
(this.options.dev ??
|
|
282
353
|
(compiler.options.mode === undefined || compiler.options.mode !== 'production'));
|
|
354
|
+
// Disabling Octane's own HMR wrapper does not make a hot CSS provider's
|
|
355
|
+
// exports immutable across replacement.
|
|
356
|
+
const cssModuleConstants =
|
|
357
|
+
this.options.cssModuleConstants !== undefined &&
|
|
358
|
+
this.options.cssModuleConstants !== false &&
|
|
359
|
+
compiler.options.mode === 'production' &&
|
|
360
|
+
!dev &&
|
|
361
|
+
!hotModuleReplacement;
|
|
283
362
|
assertProfilingDefineAvailable(compiler, profile);
|
|
284
363
|
saltPersistentCacheVersion(compiler, {
|
|
285
364
|
root,
|
|
@@ -297,8 +376,15 @@ export class OctaneRspackPlugin {
|
|
|
297
376
|
// transform results must not survive a requireDirective toggle.
|
|
298
377
|
requireDirective: this.options.requireDirective === true,
|
|
299
378
|
transpile: this.options.transpile !== false,
|
|
379
|
+
cssModuleConstants,
|
|
300
380
|
});
|
|
301
381
|
installProfilingDefine(compiler, profile);
|
|
382
|
+
if (cssModuleConstants) {
|
|
383
|
+
installCssModuleConstants(compiler, {
|
|
384
|
+
option: this.options.cssModuleConstants,
|
|
385
|
+
environment,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
302
388
|
const neutralCompiler = createDiscoveryCompiler(this.options, root, profile);
|
|
303
389
|
const discoveryCompilers = [neutralCompiler];
|
|
304
390
|
for (const specialization of Object.values(this.options.layerSpecializations ?? {})) {
|
|
@@ -309,7 +395,6 @@ export class OctaneRspackPlugin {
|
|
|
309
395
|
|
|
310
396
|
compiler.options.resolve ??= {};
|
|
311
397
|
addUniqueExtensions(compiler.options.resolve);
|
|
312
|
-
addRuntimeAlias(compiler.options.resolve, runtimeRequest, root);
|
|
313
398
|
addProjectRendererAliases(compiler.options.resolve, this.options.renderers, root);
|
|
314
399
|
for (const specialization of Object.values(this.options.layerSpecializations ?? {})) {
|
|
315
400
|
addProjectRendererAliases(compiler.options.resolve, specialization.renderers, root);
|
|
@@ -364,17 +449,27 @@ export class OctaneRspackPlugin {
|
|
|
364
449
|
use: [{ loader: 'builtin:swc-loader', options: { detectSyntax: 'auto' } }],
|
|
365
450
|
});
|
|
366
451
|
}
|
|
452
|
+
const layerRuntimeAliases = [];
|
|
367
453
|
for (const [layer, specialization] of Object.entries(this.options.layerSpecializations ?? {})) {
|
|
368
454
|
if (specialization.runtime === undefined) continue;
|
|
455
|
+
const alias = { octane$: specialization.runtime };
|
|
456
|
+
layerRuntimeAliases.push([alias, specialization.runtime]);
|
|
369
457
|
compiler.options.module.rules.push({
|
|
370
458
|
issuerLayer: layer,
|
|
371
|
-
resolve: {
|
|
372
|
-
alias: {
|
|
373
|
-
octane$: resolveRuntimeModule(specialization.runtime, root),
|
|
374
|
-
},
|
|
375
|
-
},
|
|
459
|
+
resolve: { alias },
|
|
376
460
|
});
|
|
377
461
|
}
|
|
462
|
+
compiler.hooks.afterResolvers.tap(PLUGIN_NAME, () => {
|
|
463
|
+
const resolver = addRuntimeAliases(
|
|
464
|
+
compiler,
|
|
465
|
+
runtimeRequest,
|
|
466
|
+
root,
|
|
467
|
+
this.options.runtime !== undefined,
|
|
468
|
+
);
|
|
469
|
+
for (const [alias, request] of layerRuntimeAliases) {
|
|
470
|
+
alias.octane$ = resolveRuntimeModule(resolver, request, root);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
378
473
|
|
|
379
474
|
let discovery;
|
|
380
475
|
const discover = () => {
|
package/src/shared.js
CHANGED
|
@@ -47,7 +47,13 @@ const LOADER_OPTION_KEYS = new Set([
|
|
|
47
47
|
'universalRuntime',
|
|
48
48
|
'layerSpecializations',
|
|
49
49
|
]);
|
|
50
|
-
const PLUGIN_OPTION_KEYS = new Set([
|
|
50
|
+
const PLUGIN_OPTION_KEYS = new Set([
|
|
51
|
+
...LOADER_OPTION_KEYS,
|
|
52
|
+
'parallel',
|
|
53
|
+
'runtime',
|
|
54
|
+
'transpile',
|
|
55
|
+
'cssModuleConstants',
|
|
56
|
+
]);
|
|
51
57
|
const LAYER_SPECIALIZATION_KEYS = new Set(['runtime', 'renderers', 'universalRuntime']);
|
|
52
58
|
|
|
53
59
|
function normalizeRuntimeRequest(value, label = 'runtime') {
|
|
@@ -210,6 +216,16 @@ function normalizeOptions(value, plugin) {
|
|
|
210
216
|
throw new TypeError('@octanejs/rspack-plugin: `exclude` must be an array of path strings.');
|
|
211
217
|
}
|
|
212
218
|
if (plugin) assertBooleanOption(options, 'transpile');
|
|
219
|
+
if (
|
|
220
|
+
plugin &&
|
|
221
|
+
options.cssModuleConstants !== undefined &&
|
|
222
|
+
typeof options.cssModuleConstants !== 'boolean' &&
|
|
223
|
+
typeof options.cssModuleConstants !== 'function'
|
|
224
|
+
) {
|
|
225
|
+
throw new TypeError(
|
|
226
|
+
'@octanejs/rspack-plugin: `cssModuleConstants` must be a boolean or a provider function.',
|
|
227
|
+
);
|
|
228
|
+
}
|
|
213
229
|
const parallel = plugin ? normalizeParallelOption(options.parallel) : undefined;
|
|
214
230
|
const renderers =
|
|
215
231
|
options.renderers === undefined ? undefined : normalizeRendererConfig(options.renderers);
|
|
@@ -233,6 +249,9 @@ function normalizeOptions(value, plugin) {
|
|
|
233
249
|
...(plugin && parallel !== undefined ? { parallel } : null),
|
|
234
250
|
...(plugin && options.transpile !== undefined ? { transpile: options.transpile } : null),
|
|
235
251
|
...(plugin && options.runtime !== undefined ? { runtime: options.runtime } : null),
|
|
252
|
+
...(plugin && options.cssModuleConstants !== undefined
|
|
253
|
+
? { cssModuleConstants: options.cssModuleConstants }
|
|
254
|
+
: null),
|
|
236
255
|
};
|
|
237
256
|
if (normalized.exclude) Object.freeze(normalized.exclude);
|
|
238
257
|
return Object.freeze(normalized);
|
package/types/index.d.ts
CHANGED
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
import type { Compiler, RspackPluginInstance } from '@rspack/core';
|
|
2
|
+
import type { OctaneCssModuleConstants } from 'octane/compiler';
|
|
3
|
+
|
|
4
|
+
export type { OctaneCssModuleConstants } from 'octane/compiler';
|
|
2
5
|
|
|
3
6
|
export type OctaneRspackEnvironment = 'client' | 'server';
|
|
4
7
|
|
|
8
|
+
/** Exact JavaScript CSS-provider module from the current Rspack graph. */
|
|
9
|
+
export interface OctaneRspackCssModuleConstantModule {
|
|
10
|
+
/** Full module identity, including its loader chain, resource query, and layer. */
|
|
11
|
+
readonly id: string;
|
|
12
|
+
/** The resolved NormalModule resource, including its query. */
|
|
13
|
+
readonly resource: string;
|
|
14
|
+
/** Completed loader output, inspected without evaluating the module. */
|
|
15
|
+
readonly code: string;
|
|
16
|
+
/** Provider metadata copied from the module's buildInfo. */
|
|
17
|
+
readonly meta: Readonly<Record<string, unknown>>;
|
|
18
|
+
readonly environment: OctaneRspackEnvironment;
|
|
19
|
+
readonly layer?: string;
|
|
20
|
+
readonly type: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type OctaneRspackCssModuleConstantsProvider = (
|
|
24
|
+
module: OctaneRspackCssModuleConstantModule,
|
|
25
|
+
) => OctaneCssModuleConstants | null | undefined;
|
|
26
|
+
|
|
5
27
|
export interface OctaneRendererRuleOptions {
|
|
6
28
|
/** Glob or globs matched against canonical project-relative module IDs. */
|
|
7
29
|
include: string | readonly string[];
|
|
@@ -152,6 +174,18 @@ export interface OctaneRspackLoaderOptions {
|
|
|
152
174
|
}
|
|
153
175
|
|
|
154
176
|
export interface OctaneRspackPluginOptions extends OctaneRspackLoaderOptions {
|
|
177
|
+
/**
|
|
178
|
+
* @experimental Fold proven CSS-module strings in one-shot production builds.
|
|
179
|
+
* `true` accepts only pure, initialized named ESM string exports. A provider
|
|
180
|
+
* may additionally authenticate immutable exports against the exact completed
|
|
181
|
+
* loader source; ordinary mutable default maps are not constants. The provider
|
|
182
|
+
* runs on the main thread, never in a loader worker. Malformed or stale facts
|
|
183
|
+
* fail the build. Native `css/module`, development, HMR, and watch builds keep
|
|
184
|
+
* their ordinary compilation. Eligible consumers are rebuilt once and are not
|
|
185
|
+
* stored in Rspack's persistent module cache. Requires the class plugin.
|
|
186
|
+
* @default false
|
|
187
|
+
*/
|
|
188
|
+
cssModuleConstants?: boolean | OctaneRspackCssModuleConstantsProvider;
|
|
155
189
|
/**
|
|
156
190
|
* Compile Octane modules in Rspack worker threads. Enabled by default with
|
|
157
191
|
* at most four workers; set `false` to keep compilation on the main thread.
|