@octanejs/rspack-plugin 0.1.1 → 0.1.4
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 +64 -4
- package/package.json +3 -3
- package/src/loader.js +116 -34
- package/src/plugin.js +211 -13
- package/src/shared.js +32 -5
- package/types/index.d.ts +92 -4
package/README.md
CHANGED
|
@@ -44,8 +44,28 @@ new OctaneRspackPlugin({ environment: 'server' });
|
|
|
44
44
|
|
|
45
45
|
Set `transpile: false` when an existing rule already strips TypeScript. Set
|
|
46
46
|
`hmr: false` to disable Octane HMR codegen even when Rspack HMR is active.
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
Set `profile: true` to produce a client profiling build; server compilations
|
|
48
|
+
always keep profiling disabled.
|
|
49
|
+
The experimental `renderers` option accepts the same declarative registry,
|
|
50
|
+
filename rules, and module/export boundary metadata as `compiler.renderers` in
|
|
51
|
+
Octane app config:
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
new OctaneRspackPlugin({
|
|
55
|
+
renderers: {
|
|
56
|
+
registry: { three: '@octanejs/three/renderer' },
|
|
57
|
+
boundaries: {
|
|
58
|
+
'@octanejs/three': {
|
|
59
|
+
Canvas: { ownerRenderer: 'dom', childRenderer: 'three', prop: 'children' },
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
rules: [{ include: 'src/scenes/**/*.tsrx', renderer: 'three' }],
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Options remain serializable data—there are no renderer callbacks—so the same
|
|
68
|
+
configuration is safe to reuse across compiler environments and caches.
|
|
49
69
|
|
|
50
70
|
Rspack's dev server enables the loader's hot context. If you run a custom dev
|
|
51
71
|
server, add Rspack's `HotModuleReplacementPlugin` as usual.
|
|
@@ -73,8 +93,43 @@ export default {
|
|
|
73
93
|
```
|
|
74
94
|
|
|
75
95
|
With the loader-only form, configure `.tsrx` resolution, TypeScript stripping,
|
|
76
|
-
and
|
|
77
|
-
|
|
96
|
+
and exact `octane$` / `octane/profiling$` aliases to the app's Octane package
|
|
97
|
+
yourself. The profiling alias keeps compiler metadata and runtime recording on
|
|
98
|
+
one module even when transformed raw dependencies carry a nested Octane copy.
|
|
99
|
+
You must also define Octane's
|
|
100
|
+
reserved profiling constant to the same boolean passed to the loader. Defining
|
|
101
|
+
`false` is important too: it lets production optimization erase the inactive
|
|
102
|
+
profiling runtime. The class plugin installs and owns this definition for you,
|
|
103
|
+
so do not configure it separately when using `OctaneRspackPlugin`.
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
import { rspack } from '@rspack/core';
|
|
107
|
+
|
|
108
|
+
const profiling = process.env.OCTANE_PROFILE === '1';
|
|
109
|
+
|
|
110
|
+
export default {
|
|
111
|
+
module: {
|
|
112
|
+
rules: [
|
|
113
|
+
{
|
|
114
|
+
test: /\.(?:tsrx|tsx|ts|js)$/,
|
|
115
|
+
enforce: 'pre',
|
|
116
|
+
type: 'javascript/auto',
|
|
117
|
+
use: {
|
|
118
|
+
loader: '@octanejs/rspack-plugin/loader',
|
|
119
|
+
options: { environment: 'client', profile: profiling },
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
plugins: [
|
|
125
|
+
new rspack.DefinePlugin({
|
|
126
|
+
__OCTANE_PROFILE_ENABLED__: JSON.stringify(profiling),
|
|
127
|
+
}),
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The class plugin is recommended unless another integration owns those concerns.
|
|
78
133
|
|
|
79
134
|
## App-level metadata
|
|
80
135
|
|
|
@@ -82,3 +137,8 @@ Transformed Rspack modules receive a serializable `buildInfo.octane` record
|
|
|
82
137
|
containing `canonicalId`, `transformKind`, and `serverRpc`. App integrations can
|
|
83
138
|
read the validated value with `getOctaneRspackBuildInfo(module)` without
|
|
84
139
|
depending on compiler output parsing for module identity.
|
|
140
|
+
|
|
141
|
+
When a renderer is declared `server: 'client-only'`, client compilations also
|
|
142
|
+
emit `octane-client-references.json`. Its stable reference IDs map each omitted
|
|
143
|
+
server module to the JavaScript chunks that contain its browser implementation;
|
|
144
|
+
server compilations retain the same ID on the export-preserving inert stub.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/rspack-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -45,13 +45,13 @@
|
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@rspack/core": "^2.0.0",
|
|
48
|
-
"octane": "0.1.
|
|
48
|
+
"octane": "0.1.9"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@rspack/core": "^2.1.3",
|
|
52
52
|
"@types/node": "^24.3.0",
|
|
53
53
|
"vitest": "^4.1.9",
|
|
54
|
-
"octane": "0.1.
|
|
54
|
+
"octane": "0.1.9"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"test": "vitest run --config vitest.config.js"
|
package/src/loader.js
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
2
3
|
import remapping from '@jridgewell/remapping';
|
|
3
|
-
import { canonicalModuleId, createOctaneCompiler } from 'octane/compiler/bundler';
|
|
4
|
+
import { canonicalModuleId, cleanModuleId, createOctaneCompiler } from 'octane/compiler/bundler';
|
|
4
5
|
import { inferRspackEnvironment, normalizeLoaderOptions } from './shared.js';
|
|
5
6
|
|
|
7
|
+
function realRoot(path) {
|
|
8
|
+
try {
|
|
9
|
+
return realpathSync(path);
|
|
10
|
+
} catch {
|
|
11
|
+
return path;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function realModuleId(id) {
|
|
16
|
+
const file = cleanModuleId(id);
|
|
17
|
+
return realRoot(file) + id.slice(file.length);
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
function clearBuildInfo(module) {
|
|
7
21
|
if (module?.buildInfo && typeof module.buildInfo === 'object') {
|
|
8
22
|
delete module.buildInfo.octane;
|
|
@@ -31,6 +45,33 @@ function composeSourceMaps(outputMap, inputSourceMap) {
|
|
|
31
45
|
return String(chained.mappings).length > 0 ? chained : outputMap;
|
|
32
46
|
}
|
|
33
47
|
|
|
48
|
+
async function resolveClientOnlyImports(context, compiler, source, id) {
|
|
49
|
+
if (typeof context.getResolve !== 'function') return [];
|
|
50
|
+
const requests = compiler.findServerImportRequests(String(source), id);
|
|
51
|
+
if (requests.length === 0) return [];
|
|
52
|
+
const resolver = context.getResolve({ dependencyType: 'esm' });
|
|
53
|
+
const issuer = dirname(cleanModuleId(id));
|
|
54
|
+
const classified = [];
|
|
55
|
+
await Promise.all(
|
|
56
|
+
requests.map(async (request) => {
|
|
57
|
+
let resolved;
|
|
58
|
+
try {
|
|
59
|
+
resolved = await resolver(issuer, request);
|
|
60
|
+
} catch {
|
|
61
|
+
// Rspack's normal dependency factory reports unresolved imports with its
|
|
62
|
+
// full request/issuer trace. Do not replace that diagnostic here.
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (typeof resolved !== 'string') return;
|
|
66
|
+
const reference = compiler.clientReferenceForFile(resolved);
|
|
67
|
+
if (reference !== null) classified.push({ request, resolvedId: resolved, reference });
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
return classified.sort((left, right) =>
|
|
71
|
+
left.request < right.request ? -1 : left.request > right.request ? 1 : 0,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
34
75
|
/**
|
|
35
76
|
* Rspack's ESM loader entry. A compiler instance is intentionally scoped to
|
|
36
77
|
* one invocation: Rspack owns output caching and invalidates it from the file
|
|
@@ -44,51 +85,92 @@ export default function octaneLoader(source, inputSourceMap) {
|
|
|
44
85
|
try {
|
|
45
86
|
const options = normalizeLoaderOptions(this.getOptions?.() ?? {});
|
|
46
87
|
const loaderRoot = this.rootContext ?? process.cwd();
|
|
47
|
-
const root =
|
|
48
|
-
|
|
49
|
-
? options.root
|
|
50
|
-
|
|
51
|
-
|
|
88
|
+
const root = realRoot(
|
|
89
|
+
options.root
|
|
90
|
+
? isAbsolute(options.root)
|
|
91
|
+
? options.root
|
|
92
|
+
: resolve(loaderRoot, options.root)
|
|
93
|
+
: loaderRoot,
|
|
94
|
+
);
|
|
52
95
|
const environment = options.environment ?? inferRspackEnvironment(this.target);
|
|
53
96
|
const hmr =
|
|
54
97
|
environment === 'client' && this.hot === true && options.hmr !== false ? 'webpack' : false;
|
|
55
98
|
const dev =
|
|
56
99
|
environment === 'client' &&
|
|
57
100
|
(options.dev ?? (this.mode === undefined || this.mode !== 'production'));
|
|
101
|
+
const profile = environment === 'client' && options.profile === true;
|
|
58
102
|
const compiler = createOctaneCompiler({
|
|
59
103
|
root,
|
|
104
|
+
profile,
|
|
60
105
|
...(options.exclude === undefined ? null : { exclude: options.exclude }),
|
|
61
|
-
...(options.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
dev,
|
|
68
|
-
...(options.parallelUse === undefined ? null : { parallelUse: options.parallelUse }),
|
|
106
|
+
...(options.renderers === undefined ? null : { renderers: options.renderers }),
|
|
107
|
+
...(options.requireDirective === undefined
|
|
108
|
+
? null
|
|
109
|
+
: { requireDirective: options.requireDirective }),
|
|
110
|
+
// Ownership diagnostics surface through Rspack's own module warnings.
|
|
111
|
+
warn: (message) => this.emitWarning?.(new Error(message)),
|
|
69
112
|
});
|
|
113
|
+
const id = realModuleId(this.resource ?? this.resourcePath);
|
|
114
|
+
const finish = (clientOnlyImports, callback) => {
|
|
115
|
+
try {
|
|
116
|
+
const result = compiler.transform(String(source), id, {
|
|
117
|
+
environment,
|
|
118
|
+
hmr,
|
|
119
|
+
dev,
|
|
120
|
+
profile,
|
|
121
|
+
...(clientOnlyImports.length > 0 ? { clientOnlyImports } : null),
|
|
122
|
+
});
|
|
70
123
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
124
|
+
if (result === null) {
|
|
125
|
+
callback(null, source, this.sourceMap === false ? undefined : inputSourceMap);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
registerDependencies(this, result);
|
|
130
|
+
if (result.kind === 'none') {
|
|
131
|
+
callback(null, source, this.sourceMap === false ? undefined : inputSourceMap);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
setBuildInfo(this._module, {
|
|
135
|
+
canonicalId: canonicalModuleId(id, root),
|
|
136
|
+
transformKind: result.kind,
|
|
137
|
+
serverRpc:
|
|
138
|
+
result.kind === 'compile' &&
|
|
139
|
+
(result.code.includes('_$__serverRpc(') ||
|
|
140
|
+
result.code.includes('export const _$_server_$_')),
|
|
141
|
+
...(result.clientReference === undefined
|
|
142
|
+
? null
|
|
143
|
+
: { clientReference: { ...result.clientReference } }),
|
|
144
|
+
});
|
|
145
|
+
const sourceMap =
|
|
146
|
+
this.sourceMap === false ? undefined : composeSourceMaps(result.map, inputSourceMap);
|
|
147
|
+
callback(null, result.code, sourceMap);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
callback(error instanceof Error ? error : new Error(String(error)));
|
|
150
|
+
}
|
|
151
|
+
};
|
|
75
152
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
153
|
+
const callback = this.callback.bind(this);
|
|
154
|
+
const currentReference =
|
|
155
|
+
environment === 'server' && typeof compiler.clientReferenceForFile === 'function'
|
|
156
|
+
? compiler.clientReferenceForFile(id)
|
|
157
|
+
: null;
|
|
158
|
+
if (
|
|
159
|
+
environment === 'server' &&
|
|
160
|
+
currentReference === null &&
|
|
161
|
+
typeof this.getResolve === 'function'
|
|
162
|
+
) {
|
|
163
|
+
const requests = compiler.findServerImportRequests(String(source), id);
|
|
164
|
+
if (requests.length > 0) {
|
|
165
|
+
const asyncCallback = this.async?.() ?? callback;
|
|
166
|
+
resolveClientOnlyImports(this, compiler, source, id).then(
|
|
167
|
+
(imports) => finish(imports, asyncCallback),
|
|
168
|
+
(error) => asyncCallback(error instanceof Error ? error : new Error(String(error))),
|
|
169
|
+
);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
80
172
|
}
|
|
81
|
-
|
|
82
|
-
canonicalId: canonicalModuleId(id, root),
|
|
83
|
-
transformKind: result.kind,
|
|
84
|
-
serverRpc:
|
|
85
|
-
result.kind === 'compile' &&
|
|
86
|
-
(result.code.includes('_$__serverRpc(') ||
|
|
87
|
-
result.code.includes('export const _$_server_$_')),
|
|
88
|
-
});
|
|
89
|
-
const sourceMap =
|
|
90
|
-
this.sourceMap === false ? undefined : composeSourceMaps(result.map, inputSourceMap);
|
|
91
|
-
this.callback(null, result.code, sourceMap);
|
|
173
|
+
finish([], callback);
|
|
92
174
|
} catch (error) {
|
|
93
175
|
this.callback(error instanceof Error ? error : new Error(String(error)));
|
|
94
176
|
}
|
package/src/plugin.js
CHANGED
|
@@ -1,14 +1,34 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
1
3
|
import { createRequire } from 'node:module';
|
|
2
4
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
5
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
+
import {
|
|
7
|
+
CLIENT_REFERENCE_MANIFEST_FILENAME,
|
|
8
|
+
createClientReferenceManifest,
|
|
9
|
+
createOctaneCompiler,
|
|
10
|
+
} from 'octane/compiler/bundler';
|
|
11
|
+
import {
|
|
12
|
+
getOctaneRspackBuildInfo,
|
|
13
|
+
inferRspackEnvironment,
|
|
14
|
+
normalizePluginOptions,
|
|
15
|
+
} from './shared.js';
|
|
6
16
|
|
|
7
17
|
const PLUGIN_NAME = 'OctaneRspackPlugin';
|
|
18
|
+
const PROFILE_DEFINE = '__OCTANE_PROFILE_ENABLED__';
|
|
19
|
+
const PLUGIN_VERSION = createRequire(import.meta.url)('../package.json').version;
|
|
8
20
|
const loaderPath = fileURLToPath(new URL('./loader.js', import.meta.url));
|
|
9
21
|
const OCTANE_RULE = /\.(?:tsrx|tsx|ts|js)$/i;
|
|
10
22
|
const TYPESCRIPT_RULE = /\.(?:tsrx|tsx|ts)$/i;
|
|
11
23
|
|
|
24
|
+
function realRoot(path) {
|
|
25
|
+
try {
|
|
26
|
+
return realpathSync(path);
|
|
27
|
+
} catch {
|
|
28
|
+
return path;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
12
32
|
function addUniqueExtensions(resolveOptions) {
|
|
13
33
|
const extensions = resolveOptions.extensions ?? ['.js', '.json', '.wasm'];
|
|
14
34
|
resolveOptions.extensions = [
|
|
@@ -33,14 +53,157 @@ function addRuntimeAlias(resolveOptions, request, root) {
|
|
|
33
53
|
resolveOptions.alias = {
|
|
34
54
|
...aliases,
|
|
35
55
|
octane$: resolveRuntimeModule(request, root),
|
|
56
|
+
// Compiler-emitted metadata imports this public subpath directly. Pin it
|
|
57
|
+
// alongside the runtime entry so raw/linked packages with their own nested
|
|
58
|
+
// Octane copy cannot split metadata registration from runtime recording.
|
|
59
|
+
'octane/profiling$': resolveRuntimeModule('octane/profiling', root),
|
|
36
60
|
};
|
|
37
61
|
}
|
|
38
62
|
|
|
63
|
+
function projectRendererModule(request, root) {
|
|
64
|
+
// Renderer config uses project-root IDs such as `/src/object-renderer.ts`.
|
|
65
|
+
// They are never host-filesystem absolute paths, even if the same path happens
|
|
66
|
+
// to exist on a developer machine or inside a container.
|
|
67
|
+
return resolve(root, request.replace(/^[/\\]+/, ''));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function addProjectRendererAliases(resolveOptions, renderers, root) {
|
|
71
|
+
if (renderers === undefined) return;
|
|
72
|
+
const aliases = resolveOptions.alias === false ? {} : (resolveOptions.alias ?? {});
|
|
73
|
+
const additions = {};
|
|
74
|
+
for (const renderer of Object.values(renderers.registry)) {
|
|
75
|
+
if (!renderer.module.startsWith('/')) continue;
|
|
76
|
+
additions[`${renderer.module}$`] = projectRendererModule(renderer.module, root);
|
|
77
|
+
}
|
|
78
|
+
resolveOptions.alias = { ...aliases, ...additions };
|
|
79
|
+
}
|
|
80
|
+
|
|
39
81
|
function addDependencies(collection, values) {
|
|
40
82
|
if (!collection?.add) return;
|
|
41
83
|
for (const value of values ?? []) collection.add(value);
|
|
42
84
|
}
|
|
43
85
|
|
|
86
|
+
function iterable(value) {
|
|
87
|
+
return value && typeof value === 'object' && Symbol.iterator in value ? value : [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isJavaScriptAsset(filename) {
|
|
91
|
+
return (
|
|
92
|
+
/\.(?:c|m)?js(?:\?|$)/.test(filename) && !/\.hot-update\.(?:c|m)?js(?:\?|$)/.test(filename)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function moduleChunks(compilation, module, inherited) {
|
|
97
|
+
const chunks = new Set(inherited);
|
|
98
|
+
for (const chunk of iterable(compilation.chunkGraph.getModuleChunksIterable(module)))
|
|
99
|
+
chunks.add(chunk);
|
|
100
|
+
return chunks;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function visitClientReferenceModules(
|
|
104
|
+
compilation,
|
|
105
|
+
module,
|
|
106
|
+
inheritedChunks,
|
|
107
|
+
visit,
|
|
108
|
+
seen = new Set(),
|
|
109
|
+
) {
|
|
110
|
+
if (!module || seen.has(module)) return;
|
|
111
|
+
seen.add(module);
|
|
112
|
+
const chunks = moduleChunks(compilation, module, inheritedChunks);
|
|
113
|
+
visit(module, chunks);
|
|
114
|
+
for (const child of iterable(module.modules)) {
|
|
115
|
+
visitClientReferenceModules(compilation, child, chunks, visit, seen);
|
|
116
|
+
}
|
|
117
|
+
if (module.rootModule) {
|
|
118
|
+
visitClientReferenceModules(compilation, module.rootModule, chunks, visit, seen);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Emit the client-only module identity mapped to its concrete browser chunks. */
|
|
123
|
+
function emitClientReferenceManifest(compiler, compilation) {
|
|
124
|
+
const entries = [];
|
|
125
|
+
for (const topLevelModule of iterable(compilation.modules)) {
|
|
126
|
+
visitClientReferenceModules(
|
|
127
|
+
compilation,
|
|
128
|
+
topLevelModule,
|
|
129
|
+
[],
|
|
130
|
+
(module, chunks) => {
|
|
131
|
+
const reference = getOctaneRspackBuildInfo(module)?.clientReference;
|
|
132
|
+
if (reference === undefined) return;
|
|
133
|
+
const files = new Set();
|
|
134
|
+
for (const chunk of chunks) {
|
|
135
|
+
for (const file of iterable(chunk?.files)) {
|
|
136
|
+
const filename = String(file);
|
|
137
|
+
if (isJavaScriptAsset(filename)) files.add(filename);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
entries.push({ reference, chunks: files });
|
|
141
|
+
},
|
|
142
|
+
new Set(),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const manifest = createClientReferenceManifest(entries);
|
|
146
|
+
if (Object.keys(manifest.references).length === 0) return;
|
|
147
|
+
const source = JSON.stringify(manifest, null, 2) + '\n';
|
|
148
|
+
compilation.emitAsset(
|
|
149
|
+
CLIENT_REFERENCE_MANIFEST_FILENAME,
|
|
150
|
+
new compiler.webpack.sources.RawSource(source),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function defineMatchesBoolean(value, expected) {
|
|
155
|
+
return value === expected || value === JSON.stringify(expected);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function assertProfilingDefineAvailable(compiler, enabled) {
|
|
159
|
+
for (const plugin of compiler.options.plugins ?? []) {
|
|
160
|
+
// Rspack's DefinePlugin keeps its constructor argument in `_args`. Inspecting
|
|
161
|
+
// the configured plugin list catches conflicts regardless of apply order;
|
|
162
|
+
// otherwise DefinePlugin keeps the first value and emits only a warning,
|
|
163
|
+
// which can leave compiler metadata and runtime specialization out of sync.
|
|
164
|
+
const definitions = plugin?._args?.[0];
|
|
165
|
+
if (
|
|
166
|
+
definitions === null ||
|
|
167
|
+
typeof definitions !== 'object' ||
|
|
168
|
+
!Object.prototype.hasOwnProperty.call(definitions, PROFILE_DEFINE)
|
|
169
|
+
) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (!defineMatchesBoolean(definitions[PROFILE_DEFINE], enabled)) {
|
|
173
|
+
throw new TypeError(
|
|
174
|
+
`@octanejs/rspack-plugin: ${PROFILE_DEFINE} is reserved by Octane and conflicts with \`profile: ${enabled}\`. Remove the custom DefinePlugin entry and configure profiling through OctaneRspackPlugin.`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function installProfilingDefine(compiler, enabled) {
|
|
181
|
+
const DefinePlugin = compiler.webpack?.DefinePlugin;
|
|
182
|
+
if (typeof DefinePlugin !== 'function') {
|
|
183
|
+
throw new TypeError(
|
|
184
|
+
'@octanejs/rspack-plugin: this Rspack compiler does not expose webpack.DefinePlugin.',
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
new DefinePlugin({ [PROFILE_DEFINE]: JSON.stringify(enabled) }).apply(compiler);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function hasHotModuleReplacement(compiler) {
|
|
191
|
+
return (compiler.options.plugins ?? []).some(
|
|
192
|
+
(plugin) => plugin?.name === 'HotModuleReplacementPlugin',
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function saltPersistentCacheVersion(compiler, inputs) {
|
|
197
|
+
const cache = compiler.options.cache;
|
|
198
|
+
if (cache === null || typeof cache !== 'object' || cache.type !== 'persistent') return;
|
|
199
|
+
const digest = createHash('sha256')
|
|
200
|
+
.update(JSON.stringify({ pluginVersion: PLUGIN_VERSION, ...inputs }))
|
|
201
|
+
.digest('hex')
|
|
202
|
+
.slice(0, 16);
|
|
203
|
+
const octaneVersion = `octane-rspack@${PLUGIN_VERSION}:${digest}`;
|
|
204
|
+
cache.version = cache.version ? `${cache.version}|${octaneVersion}` : octaneVersion;
|
|
205
|
+
}
|
|
206
|
+
|
|
44
207
|
export class OctaneRspackPlugin {
|
|
45
208
|
constructor(options = {}) {
|
|
46
209
|
this.options = normalizePluginOptions(options);
|
|
@@ -50,36 +213,62 @@ export class OctaneRspackPlugin {
|
|
|
50
213
|
apply(compiler) {
|
|
51
214
|
const configuredRoot = this.options.root;
|
|
52
215
|
const compilerRoot = compiler.options.context ?? process.cwd();
|
|
53
|
-
const root =
|
|
54
|
-
|
|
55
|
-
? configuredRoot
|
|
56
|
-
|
|
57
|
-
|
|
216
|
+
const root = realRoot(
|
|
217
|
+
configuredRoot
|
|
218
|
+
? isAbsolute(configuredRoot)
|
|
219
|
+
? configuredRoot
|
|
220
|
+
: resolve(compilerRoot, configuredRoot)
|
|
221
|
+
: compilerRoot,
|
|
222
|
+
);
|
|
58
223
|
const environment = this.options.environment ?? inferRspackEnvironment(compiler.options.target);
|
|
224
|
+
const profile = environment === 'client' && this.options.profile === true;
|
|
225
|
+
const hmr =
|
|
226
|
+
environment === 'client' && hasHotModuleReplacement(compiler) && this.options.hmr !== false;
|
|
227
|
+
const dev =
|
|
228
|
+
environment === 'client' &&
|
|
229
|
+
(this.options.dev ??
|
|
230
|
+
(compiler.options.mode === undefined || compiler.options.mode !== 'production'));
|
|
231
|
+
assertProfilingDefineAvailable(compiler, profile);
|
|
232
|
+
saltPersistentCacheVersion(compiler, {
|
|
233
|
+
root,
|
|
234
|
+
environment,
|
|
235
|
+
hmr,
|
|
236
|
+
dev,
|
|
237
|
+
profile,
|
|
238
|
+
exclude: this.options.exclude ?? [],
|
|
239
|
+
renderers: this.options.renderers?.signature,
|
|
240
|
+
// Ownership flips which modules compile vs pass through — cached
|
|
241
|
+
// transform results must not survive a requireDirective toggle.
|
|
242
|
+
requireDirective: this.options.requireDirective === true,
|
|
243
|
+
transpile: this.options.transpile !== false,
|
|
244
|
+
});
|
|
245
|
+
installProfilingDefine(compiler, profile);
|
|
59
246
|
const neutralCompiler = createOctaneCompiler({
|
|
60
247
|
root,
|
|
248
|
+
profile,
|
|
61
249
|
...(this.options.exclude === undefined ? null : { exclude: this.options.exclude }),
|
|
62
|
-
...(this.options.
|
|
63
|
-
? null
|
|
64
|
-
: { parallelUse: this.options.parallelUse }),
|
|
250
|
+
...(this.options.renderers === undefined ? null : { renderers: this.options.renderers }),
|
|
65
251
|
});
|
|
66
252
|
const runtimeRequest = neutralCompiler.resolveRuntimeRequest('octane', environment);
|
|
67
253
|
|
|
68
254
|
compiler.options.resolve ??= {};
|
|
69
255
|
addUniqueExtensions(compiler.options.resolve);
|
|
70
256
|
addRuntimeAlias(compiler.options.resolve, runtimeRequest, root);
|
|
257
|
+
addProjectRendererAliases(compiler.options.resolve, this.options.renderers, root);
|
|
71
258
|
|
|
72
259
|
compiler.options.module ??= {};
|
|
73
260
|
compiler.options.module.rules ??= [];
|
|
74
261
|
const loaderOptions = {
|
|
75
262
|
root,
|
|
76
263
|
environment,
|
|
264
|
+
profile,
|
|
77
265
|
...(this.options.hmr === undefined ? null : { hmr: this.options.hmr }),
|
|
78
266
|
...(this.options.dev === undefined ? null : { dev: this.options.dev }),
|
|
79
|
-
...(this.options.parallelUse === undefined
|
|
80
|
-
? null
|
|
81
|
-
: { parallelUse: this.options.parallelUse }),
|
|
82
267
|
...(this.options.exclude === undefined ? null : { exclude: this.options.exclude }),
|
|
268
|
+
...(this.options.renderers === undefined ? null : { renderers: this.options.renderers }),
|
|
269
|
+
...(this.options.requireDirective === undefined
|
|
270
|
+
? null
|
|
271
|
+
: { requireDirective: this.options.requireDirective }),
|
|
83
272
|
};
|
|
84
273
|
compiler.options.module.rules.push({
|
|
85
274
|
test: OCTANE_RULE,
|
|
@@ -115,6 +304,15 @@ export class OctaneRspackPlugin {
|
|
|
115
304
|
const current = discover();
|
|
116
305
|
addDependencies(compilation.fileDependencies, current.dependencies);
|
|
117
306
|
addDependencies(compilation.missingDependencies, current.missingDependencies);
|
|
307
|
+
if (environment === 'client') {
|
|
308
|
+
compilation.hooks.processAssets.tap(
|
|
309
|
+
{
|
|
310
|
+
name: PLUGIN_NAME,
|
|
311
|
+
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT,
|
|
312
|
+
},
|
|
313
|
+
() => emitClientReferenceManifest(compiler, compilation),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
118
316
|
});
|
|
119
317
|
}
|
|
120
318
|
}
|
package/src/shared.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeRendererConfig } from 'octane/compiler/renderers';
|
|
2
|
+
|
|
1
3
|
const CLIENT_TARGETS = new Set(['web', 'webworker', 'electron-renderer', 'browserslist']);
|
|
2
4
|
|
|
3
5
|
function targetValues(target) {
|
|
@@ -32,7 +34,16 @@ export function inferRspackEnvironment(target) {
|
|
|
32
34
|
return 'client';
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
const LOADER_OPTION_KEYS = new Set([
|
|
37
|
+
const LOADER_OPTION_KEYS = new Set([
|
|
38
|
+
'root',
|
|
39
|
+
'environment',
|
|
40
|
+
'hmr',
|
|
41
|
+
'dev',
|
|
42
|
+
'profile',
|
|
43
|
+
'exclude',
|
|
44
|
+
'renderers',
|
|
45
|
+
'requireDirective',
|
|
46
|
+
]);
|
|
36
47
|
const PLUGIN_OPTION_KEYS = new Set([...LOADER_OPTION_KEYS, 'transpile']);
|
|
37
48
|
|
|
38
49
|
function assertBooleanOption(options, key) {
|
|
@@ -64,7 +75,8 @@ function normalizeOptions(value, plugin) {
|
|
|
64
75
|
}
|
|
65
76
|
assertBooleanOption(options, 'hmr');
|
|
66
77
|
assertBooleanOption(options, 'dev');
|
|
67
|
-
assertBooleanOption(options, '
|
|
78
|
+
assertBooleanOption(options, 'profile');
|
|
79
|
+
assertBooleanOption(options, 'requireDirective');
|
|
68
80
|
if (
|
|
69
81
|
options.exclude !== undefined &&
|
|
70
82
|
(!Array.isArray(options.exclude) || options.exclude.some((entry) => typeof entry !== 'string'))
|
|
@@ -72,14 +84,20 @@ function normalizeOptions(value, plugin) {
|
|
|
72
84
|
throw new TypeError('@octanejs/rspack-plugin: `exclude` must be an array of path strings.');
|
|
73
85
|
}
|
|
74
86
|
if (plugin) assertBooleanOption(options, 'transpile');
|
|
87
|
+
const renderers =
|
|
88
|
+
options.renderers === undefined ? undefined : normalizeRendererConfig(options.renderers);
|
|
75
89
|
|
|
76
90
|
const normalized = {
|
|
77
91
|
...(options.root === undefined ? null : { root: options.root }),
|
|
78
92
|
...(options.environment === undefined ? null : { environment: options.environment }),
|
|
79
93
|
...(options.hmr === undefined ? null : { hmr: options.hmr }),
|
|
80
94
|
...(options.dev === undefined ? null : { dev: options.dev }),
|
|
81
|
-
...(options.
|
|
95
|
+
...(options.profile === undefined ? null : { profile: options.profile }),
|
|
82
96
|
...(options.exclude === undefined ? null : { exclude: [...options.exclude] }),
|
|
97
|
+
...(renderers === undefined ? null : { renderers }),
|
|
98
|
+
...(options.requireDirective === undefined
|
|
99
|
+
? null
|
|
100
|
+
: { requireDirective: options.requireDirective }),
|
|
83
101
|
...(plugin && options.transpile !== undefined ? { transpile: options.transpile } : null),
|
|
84
102
|
};
|
|
85
103
|
if (normalized.exclude) Object.freeze(normalized.exclude);
|
|
@@ -97,12 +115,21 @@ export function normalizePluginOptions(value) {
|
|
|
97
115
|
/** Read the serializable metadata attached to an Octane-transformed module. */
|
|
98
116
|
export function getOctaneRspackBuildInfo(module) {
|
|
99
117
|
const value = module?.buildInfo?.octane;
|
|
118
|
+
const nestedReferenceValid =
|
|
119
|
+
value?.clientReference !== null &&
|
|
120
|
+
typeof value?.clientReference === 'object' &&
|
|
121
|
+
typeof value.clientReference.id === 'string' &&
|
|
122
|
+
typeof value.clientReference.moduleId === 'string' &&
|
|
123
|
+
typeof value.clientReference.renderer === 'string';
|
|
100
124
|
if (
|
|
101
125
|
value &&
|
|
102
126
|
typeof value === 'object' &&
|
|
103
127
|
typeof value.canonicalId === 'string' &&
|
|
104
|
-
(value.transformKind === 'compile' ||
|
|
105
|
-
|
|
128
|
+
(value.transformKind === 'compile' ||
|
|
129
|
+
value.transformKind === 'slots' ||
|
|
130
|
+
value.transformKind === 'client-only-stub') &&
|
|
131
|
+
typeof value.serverRpc === 'boolean' &&
|
|
132
|
+
(value.clientReference === undefined || nestedReferenceValid)
|
|
106
133
|
) {
|
|
107
134
|
return value;
|
|
108
135
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -2,6 +2,70 @@ import type { Compiler, RspackPluginInstance } from '@rspack/core';
|
|
|
2
2
|
|
|
3
3
|
export type OctaneRspackEnvironment = 'client' | 'server';
|
|
4
4
|
|
|
5
|
+
export interface OctaneRendererRuleOptions {
|
|
6
|
+
/** Glob or globs matched against canonical project-relative module IDs. */
|
|
7
|
+
include: string | readonly string[];
|
|
8
|
+
/** Optional glob or globs that remove files from this rule. */
|
|
9
|
+
exclude?: string | readonly string[];
|
|
10
|
+
/** Renderer alias declared in `registry`, or the built-in `dom` alias. */
|
|
11
|
+
renderer: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type OctaneRendererRegistryEntry =
|
|
15
|
+
| string
|
|
16
|
+
| {
|
|
17
|
+
module: string;
|
|
18
|
+
target?: 'dom' | 'universal';
|
|
19
|
+
server?: 'render' | 'client-only' | 'unsupported';
|
|
20
|
+
intrinsics?: string;
|
|
21
|
+
text?: 'reject' | 'ignore' | 'host';
|
|
22
|
+
capabilities?: readonly string[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Static metadata for a component prop lowered for another renderer. */
|
|
26
|
+
export interface OctaneRendererBoundaryOptions {
|
|
27
|
+
ownerRenderer: string;
|
|
28
|
+
childRenderer: string;
|
|
29
|
+
prop: string;
|
|
30
|
+
server?: 'omit-child';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @experimental Declarative renderer selection shared with other Octane compilers. */
|
|
34
|
+
export interface OctaneRendererConfigOptions {
|
|
35
|
+
registry?: Readonly<Record<string, OctaneRendererRegistryEntry>>;
|
|
36
|
+
/** Boundary metadata keyed by stable module ID and export name. */
|
|
37
|
+
boundaries?: Readonly<Record<string, Readonly<Record<string, OctaneRendererBoundaryOptions>>>>;
|
|
38
|
+
default?: string;
|
|
39
|
+
rules?: readonly OctaneRendererRuleOptions[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Canonical renderer configuration accepted when another Octane integration resolved it. */
|
|
43
|
+
export interface OctaneResolvedRendererConfig {
|
|
44
|
+
readonly registry: Readonly<
|
|
45
|
+
Record<
|
|
46
|
+
string,
|
|
47
|
+
{
|
|
48
|
+
readonly module: string;
|
|
49
|
+
readonly target: 'dom' | 'universal';
|
|
50
|
+
readonly server: 'render' | 'client-only' | 'unsupported';
|
|
51
|
+
readonly intrinsics?: string;
|
|
52
|
+
readonly text: 'reject' | 'ignore' | 'host';
|
|
53
|
+
readonly capabilities: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
>
|
|
56
|
+
>;
|
|
57
|
+
readonly boundaries: Readonly<
|
|
58
|
+
Record<string, Readonly<Record<string, Readonly<OctaneRendererBoundaryOptions>>>>
|
|
59
|
+
>;
|
|
60
|
+
readonly default: string;
|
|
61
|
+
readonly rules: readonly {
|
|
62
|
+
readonly include: readonly string[];
|
|
63
|
+
readonly exclude: readonly string[];
|
|
64
|
+
readonly renderer: string;
|
|
65
|
+
}[];
|
|
66
|
+
readonly signature: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
5
69
|
export interface OctaneRspackLoaderOptions {
|
|
6
70
|
/** Project root used to canonicalize module IDs and discover package manifests. */
|
|
7
71
|
root?: string;
|
|
@@ -11,10 +75,28 @@ export interface OctaneRspackLoaderOptions {
|
|
|
11
75
|
hmr?: boolean;
|
|
12
76
|
/** Emit client development metadata. Defaults to Rspack's non-production mode. */
|
|
13
77
|
dev?: boolean;
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
/**
|
|
78
|
+
/** Emit client profiling metadata and enable the profiling runtime. Default `false`. */
|
|
79
|
+
profile?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Path fragments excluded from the plain `.ts`/`.js` hook-slot pass. With
|
|
82
|
+
* `requireDirective`, excluded paths are exempt from Octane ownership
|
|
83
|
+
* entirely — including `.tsrx`/`.tsx` — for projects routing those paths
|
|
84
|
+
* through a different tsrx compiler (e.g. `@tsrx/react`).
|
|
85
|
+
*/
|
|
17
86
|
exclude?: string[];
|
|
87
|
+
/** @experimental Renderer registry and ordered per-file selection rules. */
|
|
88
|
+
renderers?: OctaneRendererConfigOptions | OctaneResolvedRendererConfig;
|
|
89
|
+
/**
|
|
90
|
+
* Mixed-toolchain ownership gate: when `true`, Octane compiles only
|
|
91
|
+
* project modules that declare `'use octane'` in their directive prologue.
|
|
92
|
+
* Undirected project `.tsx`/`.ts`/`.js` pass through to the host
|
|
93
|
+
* framework's own pipeline; an undirected project `.tsrx` is a build
|
|
94
|
+
* error. Installed and linked packages keep their Octane package-manifest
|
|
95
|
+
* decision. The directive is always tolerated and stripped from compiled
|
|
96
|
+
* output, even when this option is off.
|
|
97
|
+
* @default false
|
|
98
|
+
*/
|
|
99
|
+
requireDirective?: boolean;
|
|
18
100
|
}
|
|
19
101
|
|
|
20
102
|
export interface OctaneRspackPluginOptions extends OctaneRspackLoaderOptions {
|
|
@@ -28,8 +110,14 @@ export interface OctaneRspackPluginOptions extends OctaneRspackLoaderOptions {
|
|
|
28
110
|
|
|
29
111
|
export interface OctaneRspackBuildInfo {
|
|
30
112
|
canonicalId: string;
|
|
31
|
-
transformKind: 'compile' | 'slots';
|
|
113
|
+
transformKind: 'compile' | 'slots' | 'client-only-stub';
|
|
32
114
|
serverRpc: boolean;
|
|
115
|
+
/** Stable identity shared by the client compile and its inert server stub. */
|
|
116
|
+
clientReference?: {
|
|
117
|
+
readonly id: string;
|
|
118
|
+
readonly moduleId: string;
|
|
119
|
+
readonly renderer: string;
|
|
120
|
+
};
|
|
33
121
|
}
|
|
34
122
|
|
|
35
123
|
export declare class OctaneRspackPlugin implements RspackPluginInstance {
|