@octanejs/rspack-plugin 0.1.47 → 0.1.48
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 +12 -0
- package/package.json +2 -2
- package/src/descriptor-children.js +116 -0
- package/src/loader.js +25 -14
package/README.md
CHANGED
|
@@ -196,6 +196,18 @@ export default {
|
|
|
196
196
|
|
|
197
197
|
The class plugin is recommended unless another integration owns those concerns.
|
|
198
198
|
|
|
199
|
+
## Scoped CSS
|
|
200
|
+
|
|
201
|
+
Scoped `<style>` blocks compile to `injectStyle(hash, css)` calls — one per
|
|
202
|
+
style scope, in lexical order — emitted as module-level statements in client
|
|
203
|
+
compilations and inside the component body per request in server
|
|
204
|
+
compilations, where the render collects them into the response's
|
|
205
|
+
`<style data-octane>` tags. A theme (`export const theme = <style>…</style>`)
|
|
206
|
+
is an ordinary module value, so importing it injects its sheet ahead of the
|
|
207
|
+
importer's own scopes. There is no virtual CSS module and no CSS HMR path:
|
|
208
|
+
editing a block re-evaluates the module like any other source change. Neither
|
|
209
|
+
the plugin nor the loader needs a CSS rule for this.
|
|
210
|
+
|
|
199
211
|
## App-level metadata
|
|
200
212
|
|
|
201
213
|
Transformed Rspack modules receive a serializable `buildInfo.octane` record
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/rspack-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.48",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"css-loader": "6.11.0",
|
|
55
55
|
"jsdom": "^30.0.1",
|
|
56
56
|
"vitest": "^4.1.10",
|
|
57
|
-
"octane": "0.2.
|
|
57
|
+
"octane": "0.2.4"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"test": "vitest run --config vitest.config.js"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, isAbsolute } from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
cleanModuleId,
|
|
5
|
+
findDescriptorChildrenExports,
|
|
6
|
+
findDescriptorChildrenImports,
|
|
7
|
+
} from 'octane/compiler/bundler';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A loader runs before Rspack publishes outgoing module-graph edges. Resolve
|
|
11
|
+
* each JSX import with its ESM resolver, then prove the named export from the
|
|
12
|
+
* resolved authored source. Every file in the proof becomes a loader dependency
|
|
13
|
+
* so changing a marker or any barrel invalidates its compiled consumers.
|
|
14
|
+
*/
|
|
15
|
+
export async function loadDescriptorChildrenImports(context, source, id) {
|
|
16
|
+
if (typeof context.getResolve !== 'function' || !/<\s*[A-Z_$]/.test(source)) return null;
|
|
17
|
+
const imports = findDescriptorChildrenImports(source, id).filter(
|
|
18
|
+
(candidate) => candidate.local !== undefined && candidate.request !== 'octane',
|
|
19
|
+
);
|
|
20
|
+
if (imports.length === 0) return null;
|
|
21
|
+
const resolver = context.getResolve({ dependencyType: 'esm' });
|
|
22
|
+
const analyses = new Map();
|
|
23
|
+
const classifications = new Map();
|
|
24
|
+
const proven = new Set();
|
|
25
|
+
|
|
26
|
+
async function analyze(resolvedId) {
|
|
27
|
+
const filename = cleanModuleId(resolvedId);
|
|
28
|
+
if (!isAbsolute(filename) || !/\.(?:[cm]?[jt]sx?|tsrx)$/.test(filename)) return null;
|
|
29
|
+
let analysis = analyses.get(filename);
|
|
30
|
+
if (analysis === undefined) {
|
|
31
|
+
context.addDependency?.(filename);
|
|
32
|
+
analysis = readFile(filename, 'utf8')
|
|
33
|
+
.then((code) => {
|
|
34
|
+
// An ordinary source with neither the marker's name nor an
|
|
35
|
+
// import/re-export pair cannot prove any binding. Keep that
|
|
36
|
+
// common case out of the parser entirely.
|
|
37
|
+
const mayReexport = code.includes('export') && code.includes('from');
|
|
38
|
+
return {
|
|
39
|
+
exports: code.includes('descriptorChildren')
|
|
40
|
+
? new Set(findDescriptorChildrenExports(code, filename))
|
|
41
|
+
: new Set(),
|
|
42
|
+
code: mayReexport ? code : null,
|
|
43
|
+
reexports: mayReexport ? undefined : [],
|
|
44
|
+
};
|
|
45
|
+
})
|
|
46
|
+
.catch((error) => {
|
|
47
|
+
if (error?.code === 'ENOENT') context.addMissingDependency?.(filename);
|
|
48
|
+
return null;
|
|
49
|
+
});
|
|
50
|
+
analyses.set(filename, analysis);
|
|
51
|
+
}
|
|
52
|
+
return analysis;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function resolveImport(request, importer) {
|
|
56
|
+
try {
|
|
57
|
+
return await resolver(dirname(cleanModuleId(importer)), request);
|
|
58
|
+
} catch {
|
|
59
|
+
// Let Rspack's module factory report unresolved imports with its own
|
|
60
|
+
// issuer trace; no export can be proven from a missing dependency.
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function isMarked(resolvedId, imported, ancestors = new Set()) {
|
|
66
|
+
const key = `${resolvedId}\0${imported}`;
|
|
67
|
+
if (ancestors.has(key)) return false;
|
|
68
|
+
let classification = ancestors.size === 0 ? classifications.get(key) : undefined;
|
|
69
|
+
if (classification === undefined) {
|
|
70
|
+
classification = (async () => {
|
|
71
|
+
const analysis = await analyze(resolvedId);
|
|
72
|
+
if (analysis === null) return false;
|
|
73
|
+
if (analysis.exports.has(imported)) return true;
|
|
74
|
+
const reexports = (analysis.reexports ??=
|
|
75
|
+
analysis.code !== null
|
|
76
|
+
? findDescriptorChildrenImports(analysis.code, cleanModuleId(resolvedId)).filter(
|
|
77
|
+
(candidate) => candidate.exported !== undefined,
|
|
78
|
+
)
|
|
79
|
+
: []);
|
|
80
|
+
const nextAncestors = new Set(ancestors);
|
|
81
|
+
nextAncestors.add(key);
|
|
82
|
+
for (const candidate of reexports) {
|
|
83
|
+
if (candidate.exported !== imported) continue;
|
|
84
|
+
const target = await resolveImport(candidate.request, resolvedId);
|
|
85
|
+
if (typeof target === 'string' && target !== resolvedId) {
|
|
86
|
+
if (await isMarked(target, candidate.imported, nextAncestors)) return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
})();
|
|
91
|
+
// A result inside a cycle can depend on the starting point. Only root
|
|
92
|
+
// classifications are safe to share across independent import paths.
|
|
93
|
+
if (ancestors.size === 0) classifications.set(key, classification);
|
|
94
|
+
}
|
|
95
|
+
return classification;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const byRequest = new Map();
|
|
99
|
+
for (const candidate of imports) {
|
|
100
|
+
const values = byRequest.get(candidate.request) ?? [];
|
|
101
|
+
values.push(candidate);
|
|
102
|
+
byRequest.set(candidate.request, values);
|
|
103
|
+
}
|
|
104
|
+
await Promise.all(
|
|
105
|
+
[...byRequest].map(async ([request, candidates]) => {
|
|
106
|
+
const target = await resolveImport(request, id);
|
|
107
|
+
if (typeof target !== 'string' || target === id) return;
|
|
108
|
+
await Promise.all(
|
|
109
|
+
candidates.map(async ({ imported }) => {
|
|
110
|
+
if (await isMarked(target, imported)) proven.add(`${request}\0${imported}`);
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
return proven.size === 0 ? null : (request, imported) => proven.has(`${request}\0${imported}`);
|
|
116
|
+
}
|
package/src/loader.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
normalizeLoaderOptions,
|
|
14
14
|
selectLayerCompilerOptions,
|
|
15
15
|
} from './shared.js';
|
|
16
|
+
import { loadDescriptorChildrenImports } from './descriptor-children.js';
|
|
16
17
|
|
|
17
18
|
function realRoot(path) {
|
|
18
19
|
try {
|
|
@@ -142,7 +143,7 @@ export default function octaneLoader(source, inputSourceMap) {
|
|
|
142
143
|
dev,
|
|
143
144
|
})
|
|
144
145
|
: null;
|
|
145
|
-
const finish = (clientOnlyImports, callback) => {
|
|
146
|
+
const finish = (clientOnlyImports, isDescriptorChildrenImport, callback) => {
|
|
146
147
|
try {
|
|
147
148
|
const result = compiler.transform(authoredSource, id, {
|
|
148
149
|
environment,
|
|
@@ -150,6 +151,7 @@ export default function octaneLoader(source, inputSourceMap) {
|
|
|
150
151
|
dev,
|
|
151
152
|
profile,
|
|
152
153
|
...(clientOnlyImports.length > 0 ? { clientOnlyImports } : null),
|
|
154
|
+
...(isDescriptorChildrenImport === null ? null : { isDescriptorChildrenImport }),
|
|
153
155
|
...cssModuleConstants?.transformOptions,
|
|
154
156
|
});
|
|
155
157
|
|
|
@@ -191,22 +193,31 @@ export default function octaneLoader(source, inputSourceMap) {
|
|
|
191
193
|
environment === 'server' && typeof compiler.clientReferenceForFile === 'function'
|
|
192
194
|
? compiler.clientReferenceForFile(id)
|
|
193
195
|
: null;
|
|
194
|
-
|
|
196
|
+
const needsServerImports =
|
|
195
197
|
environment === 'server' &&
|
|
196
198
|
currentReference === null &&
|
|
197
|
-
typeof this.getResolve === 'function'
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
199
|
+
typeof this.getResolve === 'function';
|
|
200
|
+
const requests = needsServerImports
|
|
201
|
+
? compiler.findServerImportRequests(authoredSource, id)
|
|
202
|
+
: [];
|
|
203
|
+
const mayUseDescriptorImports =
|
|
204
|
+
typeof this.getResolve === 'function' &&
|
|
205
|
+
(environment !== 'server' || currentReference === null) &&
|
|
206
|
+
/\.(?:tsrx|tsx)$/.test(cleanModuleId(id)) &&
|
|
207
|
+
/<\s*[A-Z_$]/.test(authoredSource) &&
|
|
208
|
+
authoredSource.includes('import');
|
|
209
|
+
if (requests.length > 0 || mayUseDescriptorImports) {
|
|
210
|
+
const asyncCallback = this.async?.() ?? callback;
|
|
211
|
+
Promise.all([
|
|
212
|
+
requests.length > 0 ? resolveClientOnlyImports(this, compiler, authoredSource, id) : [],
|
|
213
|
+
mayUseDescriptorImports ? loadDescriptorChildrenImports(this, authoredSource, id) : null,
|
|
214
|
+
]).then(
|
|
215
|
+
([imports, descriptorImport]) => finish(imports, descriptorImport, asyncCallback),
|
|
216
|
+
(error) => asyncCallback(error instanceof Error ? error : new Error(String(error))),
|
|
217
|
+
);
|
|
218
|
+
return;
|
|
208
219
|
}
|
|
209
|
-
finish([], callback);
|
|
220
|
+
finish([], null, callback);
|
|
210
221
|
} catch (error) {
|
|
211
222
|
this.callback(error instanceof Error ? error : new Error(String(error)));
|
|
212
223
|
}
|