@octanejs/rsbuild-plugin 0.1.49 → 0.1.51
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/package.json +5 -5
- package/src/build.js +42 -0
- package/src/dev-server.js +49 -6
- package/src/html.js +36 -0
- package/src/index.js +28 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/rsbuild-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.51",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@octanejs/app-core": "0.0.
|
|
52
|
-
"@octanejs/rspack-plugin": "0.1.
|
|
51
|
+
"@octanejs/app-core": "0.0.52",
|
|
52
|
+
"@octanejs/rspack-plugin": "0.1.51"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@rsbuild/core": "^2.0.0",
|
|
56
|
-
"octane": "^0.
|
|
56
|
+
"octane": "^0.3.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@rsbuild/core": "^2.1.6",
|
|
@@ -62,6 +62,6 @@
|
|
|
62
62
|
"@types/node": "^24.13.3",
|
|
63
63
|
"playwright": "^1.61.1",
|
|
64
64
|
"vitest": "^4.1.10",
|
|
65
|
-
"octane": "0.
|
|
65
|
+
"octane": "0.3.0"
|
|
66
66
|
}
|
|
67
67
|
}
|
package/src/build.js
CHANGED
|
@@ -10,6 +10,8 @@ import path from 'node:path';
|
|
|
10
10
|
* root: string,
|
|
11
11
|
* config: import('@octanejs/app-core').ResolvedOctaneConfig,
|
|
12
12
|
* assetMapFilename?: string,
|
|
13
|
+
* clientBuildFilename?: string,
|
|
14
|
+
* independentHydrationManifestFilename?: string,
|
|
13
15
|
* log?: (message: string) => void,
|
|
14
16
|
* }} options
|
|
15
17
|
*/
|
|
@@ -19,11 +21,18 @@ export async function finalizeOctaneRsbuildOutput(options) {
|
|
|
19
21
|
const clientDir = path.resolve(root, outDir, 'client');
|
|
20
22
|
const serverDir = path.resolve(root, outDir, 'server');
|
|
21
23
|
const assetMapFilename = options.assetMapFilename ?? 'octane-client-assets.json';
|
|
24
|
+
const clientBuildFilename = options.clientBuildFilename ?? 'octane-client-build.json';
|
|
25
|
+
const independentHydrationManifestFilename =
|
|
26
|
+
options.independentHydrationManifestFilename ?? 'octane-independent-hydration.json';
|
|
22
27
|
const log = options.log ?? (() => {});
|
|
23
28
|
const clientHtml = path.join(clientDir, 'index.html');
|
|
24
29
|
const serverHtml = path.join(serverDir, 'index.html');
|
|
25
30
|
const clientAssetMap = path.join(clientDir, assetMapFilename);
|
|
26
31
|
const serverAssetMap = path.join(serverDir, assetMapFilename);
|
|
32
|
+
const clientBuildPath = path.join(clientDir, clientBuildFilename);
|
|
33
|
+
const serverBuildPath = path.join(serverDir, clientBuildFilename);
|
|
34
|
+
const clientIndependentHydration = path.join(clientDir, independentHydrationManifestFilename);
|
|
35
|
+
const serverIndependentHydration = path.join(serverDir, independentHydrationManifestFilename);
|
|
27
36
|
const serverEntry = path.join(serverDir, 'entry.js');
|
|
28
37
|
|
|
29
38
|
if (!fs.existsSync(clientHtml)) {
|
|
@@ -37,12 +46,45 @@ export async function finalizeOctaneRsbuildOutput(options) {
|
|
|
37
46
|
if (!fs.existsSync(serverEntry)) {
|
|
38
47
|
throw new Error(`[@octanejs/rsbuild-plugin] Server entry was not emitted at ${serverEntry}.`);
|
|
39
48
|
}
|
|
49
|
+
if (!fs.existsSync(clientBuildPath)) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`[@octanejs/rsbuild-plugin] Client build metadata was not emitted at ${clientBuildPath}.`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const clientBuild = JSON.parse(fs.readFileSync(clientBuildPath, 'utf8'));
|
|
55
|
+
if (
|
|
56
|
+
clientBuild?.version !== 1 ||
|
|
57
|
+
typeof clientBuild.buildId !== 'string' ||
|
|
58
|
+
!clientBuild.buildId ||
|
|
59
|
+
clientBuild.mode !== 'production' ||
|
|
60
|
+
typeof clientBuild.capabilities?.independentHydration !== 'boolean'
|
|
61
|
+
) {
|
|
62
|
+
throw new Error('[@octanejs/rsbuild-plugin] Invalid production client build metadata.');
|
|
63
|
+
}
|
|
64
|
+
if (fs.existsSync(clientIndependentHydration)) {
|
|
65
|
+
const widgets = JSON.parse(fs.readFileSync(clientIndependentHydration, 'utf8'));
|
|
66
|
+
if (widgets.buildId !== clientBuild.buildId) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
'[@octanejs/rsbuild-plugin] Independent hydration and client build metadata disagree.',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
} else if (clientBuild.capabilities.independentHydration) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
'[@octanejs/rsbuild-plugin] Client build is missing independent hydration metadata.',
|
|
74
|
+
);
|
|
75
|
+
}
|
|
40
76
|
|
|
41
77
|
fs.mkdirSync(serverDir, { recursive: true });
|
|
42
78
|
fs.rmSync(serverHtml, { force: true });
|
|
43
79
|
fs.rmSync(serverAssetMap, { force: true });
|
|
80
|
+
fs.rmSync(serverBuildPath, { force: true });
|
|
44
81
|
fs.renameSync(clientHtml, serverHtml);
|
|
45
82
|
fs.renameSync(clientAssetMap, serverAssetMap);
|
|
83
|
+
fs.renameSync(clientBuildPath, serverBuildPath);
|
|
84
|
+
fs.rmSync(serverIndependentHydration, { force: true });
|
|
85
|
+
if (fs.existsSync(clientIndependentHydration)) {
|
|
86
|
+
fs.renameSync(clientIndependentHydration, serverIndependentHydration);
|
|
87
|
+
}
|
|
46
88
|
|
|
47
89
|
log(`Server build complete: ${path.relative(root, serverEntry)}`);
|
|
48
90
|
if (options.config.adapter?.serverTarget !== 'webworker') {
|
package/src/dev-server.js
CHANGED
|
@@ -5,6 +5,8 @@ import { nodeRequestToWebRequest, sendWebResponse } from '@octanejs/app-core/nod
|
|
|
5
5
|
|
|
6
6
|
import { isRsbuildOwnedUrl } from './html.js';
|
|
7
7
|
|
|
8
|
+
const INDEPENDENT_HYDRATION_MANIFEST_FILENAME = 'octane-independent-hydration.json';
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* @typedef {{
|
|
10
12
|
* manifest: import('@octanejs/app-core/production').ServerManifest,
|
|
@@ -27,6 +29,35 @@ function collectAssetPaths(stats) {
|
|
|
27
29
|
return paths;
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
/** @param {import('@rspack/core').Stats} stats */
|
|
33
|
+
function readIndependentHydrationManifest(stats) {
|
|
34
|
+
const asset = stats.compilation.getAsset?.(INDEPENDENT_HYDRATION_MANIFEST_FILENAME);
|
|
35
|
+
if (!asset) return null;
|
|
36
|
+
const source = asset.source.source();
|
|
37
|
+
return JSON.parse(typeof source === 'string' ? source : Buffer.from(source).toString('utf8'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @param {import('@rspack/core').Stats} stats */
|
|
41
|
+
function readClientBuild(stats) {
|
|
42
|
+
const asset = stats.compilation.getAsset?.('octane-client-build.json');
|
|
43
|
+
if (!asset) throw new Error('The Octane client build metadata is missing.');
|
|
44
|
+
const source = asset.source.source();
|
|
45
|
+
const metadata = JSON.parse(
|
|
46
|
+
typeof source === 'string' ? source : Buffer.from(source).toString('utf8'),
|
|
47
|
+
);
|
|
48
|
+
if (
|
|
49
|
+
metadata?.version !== 1 ||
|
|
50
|
+
metadata.buildId !== stats.hash ||
|
|
51
|
+
metadata.mode !== 'development' ||
|
|
52
|
+
typeof metadata.capabilities?.independentHydration !== 'boolean'
|
|
53
|
+
) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
'The Octane client build metadata is incompatible with this development compilation.',
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return metadata;
|
|
59
|
+
}
|
|
60
|
+
|
|
30
61
|
/**
|
|
31
62
|
* Create the early Connect middleware used by the Rsbuild Environment API.
|
|
32
63
|
* Asset/internal requests fall through to Rsbuild; matched app/RPC requests
|
|
@@ -54,7 +85,7 @@ export function createOctaneDevMiddleware(options) {
|
|
|
54
85
|
|
|
55
86
|
let assetHash = '';
|
|
56
87
|
let assetPaths = new Set();
|
|
57
|
-
/** @type {WeakMap<object, { html: string, handler: (request: Request) => Promise<Response> }>} */
|
|
88
|
+
/** @type {WeakMap<object, { html: string, assetHash: string, handler: (request: Request) => Promise<Response> }>} */
|
|
58
89
|
const handlerCache = new WeakMap();
|
|
59
90
|
|
|
60
91
|
return async function octaneDevMiddleware(request, response, next) {
|
|
@@ -69,6 +100,7 @@ export function createOctaneDevMiddleware(options) {
|
|
|
69
100
|
return;
|
|
70
101
|
}
|
|
71
102
|
const clientStats = await clientApi.getStats();
|
|
103
|
+
if (clientStats.hasErrors()) throw new Error('The Octane client compilation failed.');
|
|
72
104
|
if (clientStats.hash !== assetHash) {
|
|
73
105
|
assetHash = clientStats.hash ?? '';
|
|
74
106
|
assetPaths = collectAssetPaths(clientStats);
|
|
@@ -92,13 +124,24 @@ export function createOctaneDevMiddleware(options) {
|
|
|
92
124
|
|
|
93
125
|
const html = await clientApi.getTransformedHtml(options.clientEntry);
|
|
94
126
|
let cached = handlerCache.get(typedBundle.manifest);
|
|
95
|
-
if (!cached || cached.html !== html) {
|
|
127
|
+
if (!cached || cached.html !== html || cached.assetHash !== assetHash) {
|
|
128
|
+
const independentHydration = readIndependentHydrationManifest(clientStats);
|
|
129
|
+
const clientBuild = readClientBuild(clientStats);
|
|
130
|
+
if (independentHydration !== null && independentHydration.buildId !== clientBuild.buildId) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
'The Octane independent widget metadata belongs to another client build.',
|
|
133
|
+
);
|
|
134
|
+
}
|
|
96
135
|
cached = {
|
|
97
136
|
html,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
137
|
+
assetHash,
|
|
138
|
+
handler: createHandler(
|
|
139
|
+
{ ...typedBundle.manifest, independentHydration, clientBuild },
|
|
140
|
+
{
|
|
141
|
+
...typedBundle.rendererDeps,
|
|
142
|
+
htmlTemplate: html,
|
|
143
|
+
},
|
|
144
|
+
),
|
|
102
145
|
};
|
|
103
146
|
handlerCache.set(typedBundle.manifest, cached);
|
|
104
147
|
}
|
package/src/html.js
CHANGED
|
@@ -9,6 +9,42 @@ function escapeRegExp(value) {
|
|
|
9
9
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* A classic bootstrap can run after the streamed shell only if its native
|
|
14
|
+
* entrypoint needs no deferred prerequisite and starts no other entry module.
|
|
15
|
+
* Keep the bundler's runtime/chunk ownership intact; reject unsafe overrides.
|
|
16
|
+
*
|
|
17
|
+
* @param {import('@rspack/core').Compilation} compilation
|
|
18
|
+
* @param {string} name
|
|
19
|
+
* @param {string} clientEntry
|
|
20
|
+
*/
|
|
21
|
+
export function assertEarlyHydrationEntry(compilation, name, clientEntry) {
|
|
22
|
+
const entrypoint = compilation.entrypoints.get(name);
|
|
23
|
+
const chunk = entrypoint?.getEntrypointChunk();
|
|
24
|
+
const files = entrypoint?.getFiles().filter((file) => /\.m?js(?:\?.*)?$/.test(file)) ?? [];
|
|
25
|
+
if (!chunk || files.length !== 1 || !chunk.files.has(files[0])) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
'Octane early hydration requires one self-contained entry script. ' +
|
|
28
|
+
'Do not extract its runtime or initial JavaScript dependencies into deferred chunks.',
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const entries = [...compilation.chunkGraph.getChunkEntryModulesIterable(chunk)];
|
|
32
|
+
const entry = entries[0];
|
|
33
|
+
const root = entry && 'rootModule' in entry ? (entry.rootModule ?? entry) : entry;
|
|
34
|
+
const resource =
|
|
35
|
+
root && typeof root === 'object' && 'resource' in root ? root.resource : undefined;
|
|
36
|
+
if (
|
|
37
|
+
entries.length !== 1 ||
|
|
38
|
+
typeof resource !== 'string' ||
|
|
39
|
+
fs.realpathSync(resource.split('?', 1)[0]) !== fs.realpathSync(clientEntry)
|
|
40
|
+
) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
'Octane early hydration must own its entry module exclusively. ' +
|
|
43
|
+
'Keep user entries and pre-entry scripts separate from the generated hydration entry.',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
12
48
|
/**
|
|
13
49
|
* Rsbuild has already injected the web entry by the time `modifyHTML` runs.
|
|
14
50
|
* Mark the concrete generated tag so the shared production runtime can add a
|
package/src/index.js
CHANGED
|
@@ -25,12 +25,15 @@ import {
|
|
|
25
25
|
write_project_generated_file,
|
|
26
26
|
} from '@octanejs/app-core/codegen';
|
|
27
27
|
import { OctaneRspackPlugin } from '@octanejs/rspack-plugin';
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
INDEPENDENT_HYDRATION_MANIFEST_FILENAME,
|
|
30
|
+
createOctaneCompiler,
|
|
31
|
+
} from 'octane/compiler/bundler';
|
|
29
32
|
|
|
30
33
|
import { finalizeOctaneRsbuildOutput } from './build.js';
|
|
31
34
|
import { OctaneClientAssetsPlugin } from './client-assets-plugin.js';
|
|
32
35
|
import { createOctaneDevMiddleware } from './dev-server.js';
|
|
33
|
-
import { markHydrationEntry } from './html.js';
|
|
36
|
+
import { assertEarlyHydrationEntry, markHydrationEntry } from './html.js';
|
|
34
37
|
import {
|
|
35
38
|
collectClientEntries,
|
|
36
39
|
discoverServerModules,
|
|
@@ -397,6 +400,18 @@ export function pluginOctane(inlineOptions = {}) {
|
|
|
397
400
|
server: { htmlFallback: false, historyApiFallback: false },
|
|
398
401
|
environments: {
|
|
399
402
|
[clientEnvironment]: {
|
|
403
|
+
// Keep the generated bootstrap self-contained by default, including
|
|
404
|
+
// downlevel helpers. Explicit splitting remains subject to the
|
|
405
|
+
// emitted-entry ownership check instead of being silently replaced.
|
|
406
|
+
...(watchedConfig.splitChunks !== false &&
|
|
407
|
+
Object.keys(watchedConfig.splitChunks ?? {}).length === 0 &&
|
|
408
|
+
watchedConfig.environments?.[clientEnvironment]?.splitChunks !== false &&
|
|
409
|
+
Object.keys(watchedConfig.environments?.[clientEnvironment]?.splitChunks ?? {})
|
|
410
|
+
.length === 0 &&
|
|
411
|
+
watchedConfig.performance?.chunkSplit === undefined &&
|
|
412
|
+
watchedConfig.environments?.[clientEnvironment]?.performance?.chunkSplit === undefined
|
|
413
|
+
? { splitChunks: { chunks: 'async' } }
|
|
414
|
+
: {}),
|
|
400
415
|
...targetConfig,
|
|
401
416
|
source: { entry: { [CLIENT_ENTRY_NAME]: clientEntryFile } },
|
|
402
417
|
html: { template },
|
|
@@ -480,6 +495,7 @@ export function pluginOctane(inlineOptions = {}) {
|
|
|
480
495
|
}));
|
|
481
496
|
return create_client_entry_source({
|
|
482
497
|
staticEntries: entries,
|
|
498
|
+
clientBuildIdExpression: '__webpack_hash__',
|
|
483
499
|
generatedBy: PLUGIN_NAME,
|
|
484
500
|
});
|
|
485
501
|
},
|
|
@@ -520,7 +536,11 @@ export function pluginOctane(inlineOptions = {}) {
|
|
|
520
536
|
rpcModulePaths: serverModules.ids,
|
|
521
537
|
...(webWorkerServer ? { mode: 'webworker' } : null),
|
|
522
538
|
...(production && !webWorkerServer
|
|
523
|
-
? {
|
|
539
|
+
? {
|
|
540
|
+
clientAssetMapFile: CLIENT_ASSET_MAP,
|
|
541
|
+
clientBuildFile: 'octane-client-build.json',
|
|
542
|
+
independentHydrationManifestFile: INDEPENDENT_HYDRATION_MANIFEST_FILENAME,
|
|
543
|
+
}
|
|
524
544
|
: { clientAssetMap: {} }),
|
|
525
545
|
resolveImport: (id) => resolveProjectModule(id, root),
|
|
526
546
|
configModuleId: localRequire.resolve('@octanejs/app-core/config'),
|
|
@@ -551,6 +571,7 @@ export function pluginOctane(inlineOptions = {}) {
|
|
|
551
571
|
new OctaneRspackPlugin({
|
|
552
572
|
root,
|
|
553
573
|
environment,
|
|
574
|
+
clientBuildMode: productionBuild ? 'production' : 'development',
|
|
554
575
|
transpile: false,
|
|
555
576
|
...(inlineOptions.parallel === undefined ? null : { parallel: inlineOptions.parallel }),
|
|
556
577
|
...(inlineOptions.cssModuleConstants === undefined
|
|
@@ -645,6 +666,9 @@ export function pluginOctane(inlineOptions = {}) {
|
|
|
645
666
|
|
|
646
667
|
api.modifyHTML((html, context) => {
|
|
647
668
|
if (context.environment.name !== clientEnvironment) return html;
|
|
669
|
+
if (isProductionBuild()) {
|
|
670
|
+
assertEarlyHydrationEntry(context.compilation, CLIENT_ENTRY_NAME, clientEntryFile);
|
|
671
|
+
}
|
|
648
672
|
const entrypoint = context.compilation.entrypoints.get(CLIENT_ENTRY_NAME);
|
|
649
673
|
const entryFiles = entrypoint?.getEntrypointChunk().files ?? [];
|
|
650
674
|
return markHydrationEntry(html, entryFiles);
|
|
@@ -685,6 +709,7 @@ export function pluginOctane(inlineOptions = {}) {
|
|
|
685
709
|
root,
|
|
686
710
|
config: loaded.config,
|
|
687
711
|
assetMapFilename: CLIENT_ASSET_MAP,
|
|
712
|
+
independentHydrationManifestFilename: INDEPENDENT_HYDRATION_MANIFEST_FILENAME,
|
|
688
713
|
log: createLog(api.logger),
|
|
689
714
|
});
|
|
690
715
|
});
|