@octanejs/rsbuild-plugin 0.1.0 → 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 CHANGED
@@ -70,17 +70,20 @@ and browser targets cannot be mixed in the same array.
70
70
  Options are declarative and cache-stable:
71
71
 
72
72
  - `hmr` controls browser component handoff;
73
- - `parallelUse` controls the compiler's parallel `use()` transform;
73
+ - `profile` enables component profiling in the browser environment;
74
74
  - `exclude` skips path fragments in the plain `.ts`/`.js` hook-slot pass; and
75
75
  - `clientEnvironment` / `serverEnvironment` rename the generated environments.
76
76
 
77
77
  App mode currently serves from the root path and uses Rsbuild's default asset
78
78
  prefix. Keep `server.base` at `/` and `output.assetPrefix` at `auto` or `/`; for
79
79
  a subpath deployment, rewrite that prefix to the app root in the hosting proxy.
80
- Changing routes or server-render settings in `octane.config.ts` triggers a full
81
- browser reload within an already enabled app. Changing `build.target`,
82
- `build.outDir`, `build.minify`, or adding the first route reshapes Rsbuild
83
- environments and requires a dev-server restart.
84
-
85
- The package follows the Rsbuild plugin model consumed by Rspeedy, but this
86
- release targets Octane's DOM renderer. Lynx renderer selection is future work.
80
+ When `octane.config.ts` or one of its imported helpers changes, `rsbuild dev`
81
+ restarts the dev server and applies the complete config atomically. This is
82
+ required for `compiler.renderers`, because renderer selection is part of each
83
+ Rspack compiler's cache and loader identity. Source-module edits continue to
84
+ use the normal HMR or browser-reload path.
85
+
86
+ The package forwards normalized `compiler.renderers` registry, filename-rule,
87
+ and renderer-boundary metadata through the same Rspack compiler path used by
88
+ Vite and direct compilation. This enables the experimental universal client
89
+ target; a concrete Lynx runtime and cross-thread transport remain future work.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/rsbuild-plugin",
3
- "version": "0.1.0",
3
+ "version": "0.1.4",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -48,19 +48,20 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@octanejs/app-core": "0.0.1",
52
- "@octanejs/rspack-plugin": "0.1.0"
51
+ "@octanejs/app-core": "0.0.5",
52
+ "@octanejs/rspack-plugin": "0.1.4"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@rsbuild/core": "^2.0.0",
56
- "octane": "0.1.5"
56
+ "octane": "0.1.9"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@rsbuild/core": "^2.1.5",
60
60
  "@rspack/core": "^2.1.3",
61
61
  "@types/node": "^24.3.0",
62
+ "playwright": "^1.61.0",
62
63
  "type-fest": "^5.6.0",
63
64
  "vitest": "^4.1.9",
64
- "octane": "0.1.5"
65
+ "octane": "0.1.9"
65
66
  }
66
67
  }
@@ -3,11 +3,23 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
 
5
5
  import { rspack } from '@rsbuild/core';
6
+ import { HYDRATE_QUERY_PARAM } from 'octane/compiler/bundler';
6
7
 
7
8
  import { resolveProjectModule } from './project.js';
8
9
 
9
10
  const PLUGIN_NAME = 'OctaneClientAssetsPlugin';
10
11
 
12
+ /** @param {unknown} request */
13
+ function isDeferredHydrationRequest(request) {
14
+ if (typeof request !== 'string' || request.length === 0) return false;
15
+ const resource = request.slice(request.lastIndexOf('!') + 1);
16
+ const queryStart = resource.indexOf('?');
17
+ if (queryStart === -1) return false;
18
+ const hashStart = resource.indexOf('#', queryStart);
19
+ const query = resource.slice(queryStart + 1, hashStart === -1 ? undefined : hashStart);
20
+ return new URLSearchParams(query).has(HYDRATE_QUERY_PARAM);
21
+ }
22
+
11
23
  /** @param {string} file */
12
24
  function canonicalResource(file) {
13
25
  if (!file) return '';
@@ -76,6 +88,37 @@ function collectCss(files, chunk) {
76
88
  }
77
89
  }
78
90
 
91
+ /**
92
+ * Collect CSS from compiler-generated deferred hydration groups and every
93
+ * async descendant below them. Their JavaScript deliberately stays out of the
94
+ * route asset entry so the production server does not modulepreload it.
95
+ *
96
+ * @param {Set<string>} files
97
+ * @param {any} group
98
+ * @param {boolean} [insideDeferredHydration]
99
+ * @param {{ eager: Set<any>, deferred: Set<any> }} [seen]
100
+ */
101
+ function collectDeferredHydrationCss(
102
+ files,
103
+ group,
104
+ insideDeferredHydration = false,
105
+ seen = { eager: new Set(), deferred: new Set() },
106
+ ) {
107
+ if (!group) return;
108
+ const deferredHydration =
109
+ insideDeferredHydration ||
110
+ [...iterable(group.origins)].some((origin) => isDeferredHydrationRequest(origin?.request));
111
+ const visited = deferredHydration ? seen.deferred : seen.eager;
112
+ if (visited.has(group)) return;
113
+ visited.add(group);
114
+ if (deferredHydration) {
115
+ for (const chunk of iterable(group.chunks)) collectCss(files, chunk);
116
+ }
117
+ for (const child of iterable(group.childrenIterable)) {
118
+ collectDeferredHydrationCss(files, child, deferredHydration, seen);
119
+ }
120
+ }
121
+
79
122
  /** @param {any} compilation @param {string} message */
80
123
  function addCompilationError(compilation, message) {
81
124
  const error = new Error(`[octane] ${message}`);
@@ -172,6 +215,7 @@ export class OctaneClientAssetsPlugin {
172
215
  for (const group of routeGroups.get(id) ?? []) {
173
216
  const chunks = [...iterable(group?.chunks)];
174
217
  for (const chunk of chunks) collectCss(css, chunk);
218
+ collectDeferredHydrationCss(css, group);
175
219
  // SplitChunks inserts shared chunks before the original async chunk,
176
220
  // so scan from the end for the route script. Do not call
177
221
  // getEntrypointChunk(): normal Rspack async groups are not entrypoints.
package/src/index.js CHANGED
@@ -150,6 +150,7 @@ function configSignature(config) {
150
150
  preHydrate: config.router.preHydrate,
151
151
  rootBoundary: config.rootBoundary,
152
152
  server: config.server,
153
+ compiler: { renderers: config.compiler.renderers.signature },
153
154
  });
154
155
  }
155
156
 
@@ -226,8 +227,9 @@ function assertRootPublicPaths(config, clientEnvironment) {
226
227
  *
227
228
  * @param {{
228
229
  * hmr?: boolean,
229
- * parallelUse?: boolean,
230
+ * profile?: boolean,
230
231
  * exclude?: string[],
232
+ * requireDirective?: boolean,
231
233
  * clientEnvironment?: string,
232
234
  * serverEnvironment?: string,
233
235
  * }} [inlineOptions]
@@ -333,8 +335,23 @@ export function pluginOctane(inlineOptions = {}) {
333
335
  }
334
336
 
335
337
  api.modifyRsbuildConfig((config, { mergeRsbuildConfig }) => {
336
- if (!appEnabled) return config;
337
- assertRootPublicPaths(config, clientEnvironment);
338
+ // Renderer selection is serialized into each Rspack compiler's loader
339
+ // options, so a browser reload cannot apply config changes safely. Ask
340
+ // Rsbuild to reconstruct the dev server (and therefore every compiler)
341
+ // whenever the Octane config or one of its imported helpers changes.
342
+ // This also covers compiler-only projects without application routes.
343
+ const watchedConfig = initialLoaded
344
+ ? mergeRsbuildConfig(config, {
345
+ dev: {
346
+ watchFiles: {
347
+ paths: [...initialLoaded.dependencies],
348
+ type: 'reload-server',
349
+ },
350
+ },
351
+ })
352
+ : config;
353
+ if (!appEnabled) return watchedConfig;
354
+ assertRootPublicPaths(watchedConfig, clientEnvironment);
338
355
  const productionBuild = isProductionBuild();
339
356
  const octaneConfig = /** @type {import('@octanejs/app-core').ResolvedOctaneConfig} */ (
340
357
  initialConfig
@@ -353,7 +370,7 @@ export function pluginOctane(inlineOptions = {}) {
353
370
  const targetOutput = buildTargetPlan?.browserslist
354
371
  ? { overrideBrowserslist: buildTargetPlan.browserslist }
355
372
  : {};
356
- return mergeRsbuildConfig(config, {
373
+ return mergeRsbuildConfig(watchedConfig, {
357
374
  server: { htmlFallback: false, historyApiFallback: false },
358
375
  environments: {
359
376
  [clientEnvironment]: {
@@ -395,6 +412,23 @@ export function pluginOctane(inlineOptions = {}) {
395
412
  });
396
413
  });
397
414
 
415
+ api.modifyEnvironmentConfig((config, { mergeEnvironmentConfig }) => {
416
+ // Octane and several framework bindings use this conventional guard for
417
+ // diagnostics and production-only branches. Rsbuild does not define it
418
+ // when a programmatic build inherits mode "none", so scope the define to
419
+ // browser environments where the Node `process` global is unavailable.
420
+ if (config.output.target === 'node') return config;
421
+ return mergeEnvironmentConfig(config, {
422
+ source: {
423
+ define: {
424
+ 'process.env.NODE_ENV': JSON.stringify(
425
+ isProductionBuild() ? 'production' : 'development',
426
+ ),
427
+ },
428
+ },
429
+ });
430
+ });
431
+
398
432
  if (appEnabled) {
399
433
  api.transform(
400
434
  {
@@ -481,10 +515,14 @@ export function pluginOctane(inlineOptions = {}) {
481
515
  environment,
482
516
  transpile: false,
483
517
  ...(inlineOptions.hmr === undefined ? null : { hmr: inlineOptions.hmr }),
484
- ...(inlineOptions.parallelUse === undefined
518
+ ...(inlineOptions.profile === undefined
485
519
  ? null
486
- : { parallelUse: inlineOptions.parallelUse }),
520
+ : { profile: environment === 'client' && inlineOptions.profile }),
487
521
  ...(inlineOptions.exclude === undefined ? null : { exclude: inlineOptions.exclude }),
522
+ ...(inlineOptions.requireDirective === undefined
523
+ ? null
524
+ : { requireDirective: inlineOptions.requireDirective }),
525
+ renderers: initialConfig?.compiler.renderers,
488
526
  }),
489
527
  );
490
528
  config.resolve ??= {};
package/types/index.d.ts CHANGED
@@ -11,10 +11,23 @@ export {
11
11
  export interface OctaneRsbuildPluginOptions {
12
12
  /** Override component HMR in the browser environment. */
13
13
  hmr?: boolean;
14
- /** Disable the compiler's parallel `use()` transform. */
15
- parallelUse?: boolean;
16
- /** Ad-hoc path fragments skipped by the plain TypeScript/JavaScript hook-slot pass. */
14
+ /** Enable component profiling in the browser environment. */
15
+ profile?: boolean;
16
+ /**
17
+ * Ad-hoc path fragments skipped by the plain TypeScript/JavaScript
18
+ * hook-slot pass. With `requireDirective`, excluded paths are exempt from
19
+ * Octane ownership entirely — including `.tsrx`/`.tsx` — for projects
20
+ * routing those paths through a different tsrx compiler (e.g.
21
+ * `@tsrx/react`).
22
+ */
17
23
  exclude?: string[];
24
+ /**
25
+ * Mixed-toolchain ownership gate: compile only project modules declaring
26
+ * `'use octane'`; undirected project `.tsx`/`.ts` pass through to the host
27
+ * framework's own pipeline. See `@octanejs/rspack-plugin` for details.
28
+ * @default false
29
+ */
30
+ requireDirective?: boolean;
18
31
  /** Rsbuild environment name used for the browser bundle. @default 'web' */
19
32
  clientEnvironment?: string;
20
33
  /** Rsbuild environment name used for the Node SSR bundle. @default 'node' */