@octanejs/rspack-plugin 0.1.6 → 0.1.8

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 CHANGED
@@ -67,6 +67,34 @@ new OctaneRspackPlugin({
67
67
  Options remain serializable data—there are no renderer callbacks—so the same
68
68
  configuration is safe to reuse across compiler environments and caches.
69
69
 
70
+ Rspack layers can compile the same authored module against distinct universal
71
+ renderer graphs. Configure the background graph at the top level, then key
72
+ `layerSpecializations` by the exact value of `module.layer`:
73
+
74
+ ```js
75
+ new OctaneRspackPlugin({
76
+ runtime: '@fixture/native-background-runtime',
77
+ renderers: backgroundRenderers,
78
+ universalRuntime: { runtime: 'native', thread: 'background' },
79
+ layerSpecializations: {
80
+ 'native:main': {
81
+ runtime: '@fixture/native-main-runtime',
82
+ renderers: mainThreadRenderers,
83
+ universalRuntime: { runtime: 'native', thread: 'main-thread' },
84
+ },
85
+ },
86
+ });
87
+ ```
88
+
89
+ The compiler selects `renderers` and `universalRuntime` from each transformed
90
+ module's layer. The plugin also installs `runtime` as an exact `octane` alias
91
+ for requests whose issuer belongs to that layer. Unconfigured and unknown
92
+ layers keep the top-level options. All renderer configurations participate in
93
+ source-dependency discovery and watching, and their normalized signatures and
94
+ runtime identities salt Rspack's persistent cache. The standalone loader
95
+ supports layer-specific compiler options but cannot install runtime aliases,
96
+ so `runtime` specializations require `OctaneRspackPlugin`.
97
+
70
98
  Rspack's dev server enables the loader's hot context. If you run a custom dev
71
99
  server, add Rspack's `HotModuleReplacementPlugin` as usual.
72
100
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/rspack-plugin",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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.11"
48
+ "octane": "0.1.13"
49
49
  },
50
50
  "devDependencies": {
51
- "@rspack/core": "^2.1.3",
52
- "@types/node": "^24.3.0",
53
- "vitest": "^4.1.9",
54
- "octane": "0.1.11"
51
+ "@rspack/core": "^2.1.4",
52
+ "@types/node": "^24.13.3",
53
+ "vitest": "^4.1.10",
54
+ "octane": "0.1.13"
55
55
  },
56
56
  "scripts": {
57
57
  "test": "vitest run --config vitest.config.js"
package/src/loader.js CHANGED
@@ -2,7 +2,11 @@ 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 { inferRspackEnvironment, normalizeLoaderOptions } from './shared.js';
5
+ import {
6
+ inferRspackEnvironment,
7
+ normalizeLoaderOptions,
8
+ selectLayerCompilerOptions,
9
+ } from './shared.js';
6
10
 
7
11
  function realRoot(path) {
8
12
  try {
@@ -99,14 +103,20 @@ export default function octaneLoader(source, inputSourceMap) {
99
103
  environment === 'client' &&
100
104
  (options.dev ?? (this.mode === undefined || this.mode !== 'production'));
101
105
  const profile = environment === 'client' && options.profile === true;
106
+ const compilerOptions =
107
+ options.layerSpecializations === undefined
108
+ ? options
109
+ : selectLayerCompilerOptions(options, this._module);
102
110
  const compiler = createOctaneCompiler({
103
111
  root,
104
112
  profile,
105
113
  ...(options.exclude === undefined ? null : { exclude: options.exclude }),
106
- ...(options.renderers === undefined ? null : { renderers: options.renderers }),
107
- ...(options.universalRuntime === undefined
114
+ ...(compilerOptions.renderers === undefined
108
115
  ? null
109
- : { universalRuntime: options.universalRuntime }),
116
+ : { renderers: compilerOptions.renderers }),
117
+ ...(compilerOptions.universalRuntime === undefined
118
+ ? null
119
+ : { universalRuntime: compilerOptions.universalRuntime }),
110
120
  ...(options.requireDirective === undefined
111
121
  ? null
112
122
  : { requireDirective: options.requireDirective }),
package/src/plugin.js CHANGED
@@ -78,6 +78,54 @@ function addProjectRendererAliases(resolveOptions, renderers, root) {
78
78
  resolveOptions.alias = { ...aliases, ...additions };
79
79
  }
80
80
 
81
+ function layerSpecializationCacheIdentity(layerSpecializations) {
82
+ if (layerSpecializations === undefined) return undefined;
83
+ return Object.fromEntries(
84
+ Object.entries(layerSpecializations).map(([layer, specialization]) => [
85
+ layer,
86
+ {
87
+ ...(specialization.runtime === undefined ? null : { runtime: specialization.runtime }),
88
+ ...(specialization.renderers === undefined
89
+ ? null
90
+ : { renderers: specialization.renderers.signature }),
91
+ ...(specialization.universalRuntime === undefined
92
+ ? null
93
+ : { universalRuntime: specialization.universalRuntime }),
94
+ },
95
+ ]),
96
+ );
97
+ }
98
+
99
+ function createDiscoveryCompiler(options, root, profile, specialization) {
100
+ const renderers = specialization?.renderers ?? options.renderers;
101
+ const universalRuntime = specialization?.universalRuntime ?? options.universalRuntime;
102
+ return createOctaneCompiler({
103
+ root,
104
+ profile,
105
+ ...(options.exclude === undefined ? null : { exclude: options.exclude }),
106
+ ...(renderers === undefined ? null : { renderers }),
107
+ ...(universalRuntime === undefined ? null : { universalRuntime }),
108
+ });
109
+ }
110
+
111
+ function discoverAll(compilers) {
112
+ if (compilers.length === 1) return compilers[0].discoverSourceDependencies();
113
+ const packages = new Set();
114
+ const dependencies = new Set();
115
+ const missingDependencies = new Set();
116
+ for (const compiler of compilers) {
117
+ const discovery = compiler.discoverSourceDependencies();
118
+ for (const value of discovery.packages ?? []) packages.add(value);
119
+ for (const value of discovery.dependencies ?? []) dependencies.add(value);
120
+ for (const value of discovery.missingDependencies ?? []) missingDependencies.add(value);
121
+ }
122
+ return {
123
+ packages: [...packages].sort(),
124
+ dependencies: [...dependencies].sort(),
125
+ missingDependencies: [...missingDependencies].sort(),
126
+ };
127
+ }
128
+
81
129
  function addDependencies(collection, values) {
82
130
  if (!collection?.add) return;
83
131
  for (const value of values ?? []) collection.add(value);
@@ -239,21 +287,18 @@ export class OctaneRspackPlugin {
239
287
  renderers: this.options.renderers?.signature,
240
288
  runtime: this.options.runtime,
241
289
  universalRuntime: this.options.universalRuntime,
290
+ layerSpecializations: layerSpecializationCacheIdentity(this.options.layerSpecializations),
242
291
  // Ownership flips which modules compile vs pass through — cached
243
292
  // transform results must not survive a requireDirective toggle.
244
293
  requireDirective: this.options.requireDirective === true,
245
294
  transpile: this.options.transpile !== false,
246
295
  });
247
296
  installProfilingDefine(compiler, profile);
248
- const neutralCompiler = createOctaneCompiler({
249
- root,
250
- profile,
251
- ...(this.options.exclude === undefined ? null : { exclude: this.options.exclude }),
252
- ...(this.options.renderers === undefined ? null : { renderers: this.options.renderers }),
253
- ...(this.options.universalRuntime === undefined
254
- ? null
255
- : { universalRuntime: this.options.universalRuntime }),
256
- });
297
+ const neutralCompiler = createDiscoveryCompiler(this.options, root, profile);
298
+ const discoveryCompilers = [neutralCompiler];
299
+ for (const specialization of Object.values(this.options.layerSpecializations ?? {})) {
300
+ discoveryCompilers.push(createDiscoveryCompiler(this.options, root, profile, specialization));
301
+ }
257
302
  const runtimeRequest =
258
303
  this.options.runtime ?? neutralCompiler.resolveRuntimeRequest('octane', environment);
259
304
 
@@ -261,6 +306,9 @@ export class OctaneRspackPlugin {
261
306
  addUniqueExtensions(compiler.options.resolve);
262
307
  addRuntimeAlias(compiler.options.resolve, runtimeRequest, root);
263
308
  addProjectRendererAliases(compiler.options.resolve, this.options.renderers, root);
309
+ for (const specialization of Object.values(this.options.layerSpecializations ?? {})) {
310
+ addProjectRendererAliases(compiler.options.resolve, specialization.renderers, root);
311
+ }
264
312
 
265
313
  compiler.options.module ??= {};
266
314
  compiler.options.module.rules ??= [];
@@ -275,6 +323,9 @@ export class OctaneRspackPlugin {
275
323
  ...(this.options.universalRuntime === undefined
276
324
  ? null
277
325
  : { universalRuntime: this.options.universalRuntime }),
326
+ ...(this.options.layerSpecializations === undefined
327
+ ? null
328
+ : { layerSpecializations: this.options.layerSpecializations }),
278
329
  ...(this.options.requireDirective === undefined
279
330
  ? null
280
331
  : { requireDirective: this.options.requireDirective }),
@@ -292,21 +343,32 @@ export class OctaneRspackPlugin {
292
343
  use: [{ loader: 'builtin:swc-loader', options: { detectSyntax: 'auto' } }],
293
344
  });
294
345
  }
346
+ for (const [layer, specialization] of Object.entries(this.options.layerSpecializations ?? {})) {
347
+ if (specialization.runtime === undefined) continue;
348
+ compiler.options.module.rules.push({
349
+ issuerLayer: layer,
350
+ resolve: {
351
+ alias: {
352
+ octane$: resolveRuntimeModule(specialization.runtime, root),
353
+ },
354
+ },
355
+ });
356
+ }
295
357
 
296
358
  let discovery;
297
359
  const discover = () => {
298
360
  if (discovery === undefined) {
299
- discovery = neutralCompiler.discoverSourceDependencies();
361
+ discovery = discoverAll(discoveryCompilers);
300
362
  this.sourceDependencies = Object.freeze([...(discovery.packages ?? [])]);
301
363
  }
302
364
  return discovery;
303
365
  };
304
366
  compiler.hooks.invalid?.tap(PLUGIN_NAME, (filename) => {
305
- neutralCompiler.invalidate(filename);
367
+ for (const current of discoveryCompilers) current.invalidate(filename);
306
368
  discovery = undefined;
307
369
  });
308
370
  compiler.hooks.watchRun?.tap(PLUGIN_NAME, () => {
309
- neutralCompiler.invalidate();
371
+ for (const current of discoveryCompilers) current.invalidate();
310
372
  discovery = undefined;
311
373
  });
312
374
  compiler.hooks.thisCompilation?.tap(PLUGIN_NAME, (compilation) => {
package/src/shared.js CHANGED
@@ -44,8 +44,19 @@ const LOADER_OPTION_KEYS = new Set([
44
44
  'renderers',
45
45
  'requireDirective',
46
46
  'universalRuntime',
47
+ 'layerSpecializations',
47
48
  ]);
48
49
  const PLUGIN_OPTION_KEYS = new Set([...LOADER_OPTION_KEYS, 'runtime', 'transpile']);
50
+ const LAYER_SPECIALIZATION_KEYS = new Set(['runtime', 'renderers', 'universalRuntime']);
51
+
52
+ function normalizeRuntimeRequest(value, label = 'runtime') {
53
+ if (value !== undefined && (typeof value !== 'string' || value.trim() !== value || !value)) {
54
+ throw new TypeError(
55
+ `@octanejs/rspack-plugin: \`${label}\` must be a non-empty module request string.`,
56
+ );
57
+ }
58
+ return value;
59
+ }
49
60
 
50
61
  function normalizeUniversalRuntime(value) {
51
62
  if (value === undefined) return undefined;
@@ -73,6 +84,68 @@ function normalizeUniversalRuntime(value) {
73
84
  return Object.freeze({ runtime: value.runtime, thread: value.thread });
74
85
  }
75
86
 
87
+ function normalizeLayerSpecializations(value) {
88
+ if (value === undefined) return undefined;
89
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
90
+ throw new TypeError('@octanejs/rspack-plugin: `layerSpecializations` must be an object map.');
91
+ }
92
+ const entries = Object.entries(value).sort(([left], [right]) =>
93
+ left < right ? -1 : left > right ? 1 : 0,
94
+ );
95
+ const normalized = [];
96
+ for (const [layer, specialization] of entries) {
97
+ if (!layer || layer.trim() !== layer) {
98
+ throw new TypeError(
99
+ '@octanejs/rspack-plugin: `layerSpecializations` keys must be non-empty layer names without surrounding whitespace.',
100
+ );
101
+ }
102
+ if (
103
+ specialization === null ||
104
+ typeof specialization !== 'object' ||
105
+ Array.isArray(specialization)
106
+ ) {
107
+ throw new TypeError(
108
+ `@octanejs/rspack-plugin: \`layerSpecializations.${layer}\` must be an object.`,
109
+ );
110
+ }
111
+ for (const key of Object.keys(specialization)) {
112
+ if (!LAYER_SPECIALIZATION_KEYS.has(key)) {
113
+ throw new TypeError(
114
+ `@octanejs/rspack-plugin: unknown \`layerSpecializations.${layer}.${key}\` option.`,
115
+ );
116
+ }
117
+ }
118
+ const runtime = normalizeRuntimeRequest(
119
+ specialization.runtime,
120
+ `layerSpecializations.${layer}.runtime`,
121
+ );
122
+ const renderers =
123
+ specialization.renderers === undefined
124
+ ? undefined
125
+ : normalizeRendererConfig(specialization.renderers);
126
+ const universalRuntime = normalizeUniversalRuntime(specialization.universalRuntime);
127
+ normalized.push([
128
+ layer,
129
+ Object.freeze({
130
+ ...(runtime === undefined ? null : { runtime }),
131
+ ...(renderers === undefined ? null : { renderers }),
132
+ ...(universalRuntime === undefined ? null : { universalRuntime }),
133
+ }),
134
+ ]);
135
+ }
136
+ return Object.freeze(Object.fromEntries(normalized));
137
+ }
138
+
139
+ /** Select the compiler-facing options for a module's Rspack layer. */
140
+ export function selectLayerCompilerOptions(options, module) {
141
+ const layer = typeof module?.layer === 'string' ? module.layer : undefined;
142
+ const specialization = layer === undefined ? undefined : options.layerSpecializations?.[layer];
143
+ return {
144
+ renderers: specialization?.renderers ?? options.renderers,
145
+ universalRuntime: specialization?.universalRuntime ?? options.universalRuntime,
146
+ };
147
+ }
148
+
76
149
  function assertBooleanOption(options, key) {
77
150
  if (options[key] !== undefined && typeof options[key] !== 'boolean') {
78
151
  throw new TypeError(`@octanejs/rspack-plugin: \`${key}\` must be a boolean.`);
@@ -93,17 +166,7 @@ function normalizeOptions(value, plugin) {
93
166
  if (options.root !== undefined && typeof options.root !== 'string') {
94
167
  throw new TypeError('@octanejs/rspack-plugin: `root` must be a path string.');
95
168
  }
96
- if (
97
- plugin &&
98
- options.runtime !== undefined &&
99
- (typeof options.runtime !== 'string' ||
100
- options.runtime.trim() !== options.runtime ||
101
- !options.runtime)
102
- ) {
103
- throw new TypeError(
104
- '@octanejs/rspack-plugin: `runtime` must be a non-empty module request string.',
105
- );
106
- }
169
+ if (plugin) normalizeRuntimeRequest(options.runtime);
107
170
  if (
108
171
  options.environment !== undefined &&
109
172
  options.environment !== 'client' &&
@@ -125,6 +188,7 @@ function normalizeOptions(value, plugin) {
125
188
  const renderers =
126
189
  options.renderers === undefined ? undefined : normalizeRendererConfig(options.renderers);
127
190
  const universalRuntime = normalizeUniversalRuntime(options.universalRuntime);
191
+ const layerSpecializations = normalizeLayerSpecializations(options.layerSpecializations);
128
192
 
129
193
  const normalized = {
130
194
  ...(options.root === undefined ? null : { root: options.root }),
@@ -135,6 +199,7 @@ function normalizeOptions(value, plugin) {
135
199
  ...(options.exclude === undefined ? null : { exclude: [...options.exclude] }),
136
200
  ...(renderers === undefined ? null : { renderers }),
137
201
  ...(universalRuntime === undefined ? null : { universalRuntime }),
202
+ ...(layerSpecializations === undefined ? null : { layerSpecializations }),
138
203
  ...(options.requireDirective === undefined
139
204
  ? null
140
205
  : { requireDirective: options.requireDirective }),
package/types/index.d.ts CHANGED
@@ -32,6 +32,8 @@ export type OctaneRendererRegistryEntry =
32
32
  intrinsics?: string;
33
33
  text?: 'reject' | 'ignore' | 'host';
34
34
  capabilities?: readonly string[];
35
+ /** Host event prop names or prefix patterns retained as first-screen listener sentinels. */
36
+ firstScreenEvents?: readonly string[];
35
37
  validation?: OctaneRendererValidationOptions;
36
38
  };
37
39
 
@@ -64,6 +66,7 @@ export interface OctaneResolvedRendererConfig {
64
66
  readonly intrinsics?: string;
65
67
  readonly text: 'reject' | 'ignore' | 'host';
66
68
  readonly capabilities: readonly string[];
69
+ readonly firstScreenEvents?: readonly string[];
67
70
  readonly validation?: Readonly<OctaneRendererValidationOptions>;
68
71
  }
69
72
  >
@@ -80,6 +83,24 @@ export interface OctaneResolvedRendererConfig {
80
83
  readonly signature: string;
81
84
  }
82
85
 
86
+ /** Compile-only host identity attached to a universal renderer graph. */
87
+ export interface OctaneUniversalRuntimeOptions {
88
+ readonly runtime: string;
89
+ readonly thread: 'background' | 'main-thread';
90
+ }
91
+
92
+ /** Compiler options selected for modules issued from one Rspack layer. */
93
+ export interface OctaneRspackLoaderLayerSpecializationOptions {
94
+ renderers?: OctaneRendererConfigOptions | OctaneResolvedRendererConfig;
95
+ universalRuntime?: OctaneUniversalRuntimeOptions;
96
+ }
97
+
98
+ /** Plugin-owned compiler and runtime options selected for one Rspack layer. */
99
+ export interface OctaneRspackLayerSpecializationOptions extends OctaneRspackLoaderLayerSpecializationOptions {
100
+ /** Override exact `octane` imports for modules issued from this layer. */
101
+ runtime?: string;
102
+ }
103
+
83
104
  export interface OctaneRspackLoaderOptions {
84
105
  /** Project root used to canonicalize module IDs and discover package manifests. */
85
106
  root?: string;
@@ -101,24 +122,32 @@ export interface OctaneRspackLoaderOptions {
101
122
  /** @experimental Renderer registry and ordered per-file selection rules. */
102
123
  renderers?: OctaneRendererConfigOptions | OctaneResolvedRendererConfig;
103
124
  /** Compile-only host runtime/thread identity for a universal renderer graph. */
104
- universalRuntime?: {
105
- readonly runtime: string;
106
- readonly thread: 'background' | 'main-thread';
107
- };
125
+ universalRuntime?: OctaneUniversalRuntimeOptions;
126
+ /**
127
+ * Compiler options selected by the current module's Rspack layer. Unknown
128
+ * layers retain the top-level compiler options. Runtime aliases remain a
129
+ * class-plugin concern and cannot be configured by the standalone loader.
130
+ */
131
+ layerSpecializations?: Readonly<Record<string, OctaneRspackLoaderLayerSpecializationOptions>>;
108
132
  /**
109
- * Mixed-toolchain ownership gate: when `true`, Octane compiles only
110
- * project modules that declare `'use octane'` in their directive prologue.
111
- * Undirected project `.tsx`/`.ts`/`.js` pass through to the host
112
- * framework's own pipeline; an undirected project `.tsrx` is a build
113
- * error. Installed and linked packages keep their Octane package-manifest
114
- * decision. The directive is always tolerated and stripped from compiled
115
- * output, even when this option is off.
133
+ * Mixed-toolchain ownership gate: when `true`, a project `.tsrx` is
134
+ * Octane's by extension, and a project `.tsx` (full compile) or plain
135
+ * `.ts`/`.js` (hook slotting) is Octane's only when it opens with a
136
+ * leading `@jsxImportSource octane` pragma comment (any registered
137
+ * renderer's intrinsics module also counts). Unmarked project modules
138
+ * pass through to the host framework's own pipeline. Installed and
139
+ * linked packages keep their Octane package-manifest decision.
116
140
  * @default false
117
141
  */
118
142
  requireDirective?: boolean;
119
143
  }
120
144
 
121
145
  export interface OctaneRspackPluginOptions extends OctaneRspackLoaderOptions {
146
+ /**
147
+ * Compiler and exact-runtime overrides selected by the current module's
148
+ * Rspack layer. Unknown layers retain the top-level plugin options.
149
+ */
150
+ layerSpecializations?: Readonly<Record<string, OctaneRspackLayerSpecializationOptions>>;
122
151
  /**
123
152
  * Override the exact module used for plain `octane` imports in this graph.
124
153
  * Universal host integrations use this to share one hook/context runtime
@@ -138,10 +167,7 @@ export interface OctaneRspackBuildInfo {
138
167
  transformKind: 'compile' | 'slots' | 'client-only-stub';
139
168
  serverRpc: boolean;
140
169
  /** Universal host runtime/thread identity, when this module was specialized. */
141
- universalRuntime?: {
142
- readonly runtime: string;
143
- readonly thread: 'background' | 'main-thread';
144
- };
170
+ universalRuntime?: OctaneUniversalRuntimeOptions;
145
171
  /** Stable identity shared by the client compile and its inert server stub. */
146
172
  clientReference?: {
147
173
  readonly id: string;
@@ -0,0 +1,41 @@
1
+ import type { OctaneRspackLoaderOptions, OctaneRspackPluginOptions } from './index.js';
2
+
3
+ const loaderOptions: OctaneRspackLoaderOptions = {
4
+ layerSpecializations: {
5
+ 'native:main': {
6
+ renderers: {
7
+ registry: {
8
+ native: {
9
+ module: '@fixture/native-main-renderer',
10
+ capabilities: ['main-thread-render-only'],
11
+ firstScreenEvents: ['bind*', 'catch*'],
12
+ },
13
+ },
14
+ default: 'native',
15
+ },
16
+ universalRuntime: { runtime: 'native', thread: 'main-thread' },
17
+ },
18
+ },
19
+ };
20
+
21
+ const pluginOptions: OctaneRspackPluginOptions = {
22
+ layerSpecializations: {
23
+ 'native:main': {
24
+ runtime: '@fixture/native-main-runtime',
25
+ universalRuntime: { runtime: 'native', thread: 'main-thread' },
26
+ },
27
+ },
28
+ };
29
+
30
+ const unsupportedLoaderRuntime: OctaneRspackLoaderOptions = {
31
+ layerSpecializations: {
32
+ 'native:main': {
33
+ // @ts-expect-error The standalone loader cannot install an issuer-layer runtime alias.
34
+ runtime: '@fixture/native-main-runtime',
35
+ },
36
+ },
37
+ };
38
+
39
+ void loaderOptions;
40
+ void pluginOptions;
41
+ void unsupportedLoaderRuntime;