@octanejs/rsbuild-plugin 0.1.0

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/src/html.js ADDED
@@ -0,0 +1,79 @@
1
+ // @ts-check
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ import { validateSsrTemplate } from '@octanejs/app-core/html';
6
+
7
+ /** @param {string} value */
8
+ function escapeRegExp(value) {
9
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
10
+ }
11
+
12
+ /**
13
+ * Rsbuild has already injected the web entry by the time `modifyHTML` runs.
14
+ * Mark the concrete generated tag so the shared production runtime can add a
15
+ * per-request CSP nonce without knowing its hashed filename.
16
+ *
17
+ * @param {string} html
18
+ * @param {Iterable<string>} entryFiles
19
+ */
20
+ export function markHydrationEntry(html, entryFiles) {
21
+ validateSsrTemplate(html);
22
+ const javascriptFiles = [...entryFiles].filter((file) => /\.m?js(?:\?.*)?$/.test(file));
23
+ const scriptTags =
24
+ html.match(/<script\b[^>]*\bsrc\s*=\s*(?:"[^"]+"|'[^']+'|[^\s>]+)[^>]*>/gi) ?? [];
25
+ const matches = scriptTags.filter((tag) =>
26
+ javascriptFiles.some((file) =>
27
+ new RegExp(`(?:^|[/"'])${escapeRegExp(file)}(?:[?"']|$)`).test(tag),
28
+ ),
29
+ );
30
+ if (matches.length !== 1) {
31
+ throw new Error(
32
+ `[@octanejs/rsbuild-plugin] Expected one generated hydration entry script; found ${matches.length}.`,
33
+ );
34
+ }
35
+ const oldTag = matches[0];
36
+ const nextTag = /\bdata-octane-hydrate\b/i.test(oldTag)
37
+ ? oldTag
38
+ : oldTag.replace(/^<script\b/i, '<script data-octane-hydrate');
39
+ return html.replace(oldTag, nextTag);
40
+ }
41
+
42
+ /**
43
+ * Avoid sending app navigations to Rspack's asset middleware, while ensuring
44
+ * Octane's catch-all route never consumes Rsbuild internals or emitted files.
45
+ *
46
+ * @param {URL} url
47
+ * @param {Set<string>} emittedPaths
48
+ * @param {string[]} [publicRoots]
49
+ */
50
+ export function isRsbuildOwnedUrl(url, emittedPaths = new Set(), publicRoots = []) {
51
+ const pathname = url.pathname;
52
+ if (
53
+ pathname.startsWith('/__') ||
54
+ pathname.startsWith('/@') ||
55
+ pathname.startsWith('/rsbuild-dev-server') ||
56
+ pathname.includes('/node_modules/')
57
+ ) {
58
+ return true;
59
+ }
60
+ const normalized = pathname.replace(/^\/+/, '');
61
+ if (emittedPaths.has(normalized) || emittedPaths.has(pathname)) return true;
62
+ let decoded = normalized;
63
+ try {
64
+ decoded = decodeURIComponent(normalized);
65
+ } catch {
66
+ // Treat malformed percent escapes as an app URL; the router can reject it.
67
+ }
68
+ for (const publicRoot of publicRoots) {
69
+ const root = path.resolve(publicRoot);
70
+ const candidate = path.resolve(root, decoded);
71
+ if (candidate !== root && !candidate.startsWith(root + path.sep)) continue;
72
+ try {
73
+ if (fs.statSync(candidate).isFile()) return true;
74
+ } catch {
75
+ // Missing public files remain available to application routing.
76
+ }
77
+ }
78
+ return false;
79
+ }
package/src/index.js ADDED
@@ -0,0 +1,592 @@
1
+ // @ts-check
2
+ import fs from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import path from 'node:path';
5
+
6
+ import {
7
+ RenderRoute,
8
+ ServerRoute,
9
+ defineConfig,
10
+ createRouter,
11
+ is_rpc_request,
12
+ } from '@octanejs/app-core';
13
+ import {
14
+ getOctaneConfigPath,
15
+ loadOctaneConfig,
16
+ loadOctaneConfigWithMetadata,
17
+ octaneConfigExists,
18
+ } from '@octanejs/app-core/config-loader';
19
+ import {
20
+ SERVER_ONLY_ADAPTER_IDS,
21
+ create_adapter_browser_stub_source,
22
+ create_client_entry_source,
23
+ generateServerEntry,
24
+ generateServerManifestEntry,
25
+ write_project_generated_file,
26
+ } from '@octanejs/app-core/codegen';
27
+ import { OctaneRspackPlugin } from '@octanejs/rspack-plugin';
28
+ import { createOctaneCompiler } from 'octane/compiler/bundler';
29
+
30
+ import { finalizeOctaneRsbuildOutput } from './build.js';
31
+ import { OctaneClientAssetsPlugin } from './client-assets-plugin.js';
32
+ import { createOctaneDevMiddleware } from './dev-server.js';
33
+ import { markHydrationEntry } from './html.js';
34
+ import {
35
+ collectClientEntries,
36
+ discoverServerModules,
37
+ resolveOctaneSourceRoots,
38
+ resolveProjectModule,
39
+ } from './project.js';
40
+
41
+ export * from '@octanejs/app-core';
42
+ export { getOctaneConfigPath, loadOctaneConfig, loadOctaneConfigWithMetadata, octaneConfigExists };
43
+
44
+ const PLUGIN_NAME = '@octanejs/rsbuild-plugin';
45
+ const CLIENT_ENTRY_NAME = 'index';
46
+ const SERVER_ENTRY_NAME = 'entry';
47
+ const CLIENT_ASSET_MAP = 'octane-client-assets.json';
48
+ const localRequire = createRequire(import.meta.url);
49
+ const SWC_ES_TARGETS = new Set([
50
+ 'es3',
51
+ 'es5',
52
+ 'es2015',
53
+ 'es2016',
54
+ 'es2017',
55
+ 'es2018',
56
+ 'es2019',
57
+ 'es2020',
58
+ 'es2021',
59
+ 'es2022',
60
+ 'es2023',
61
+ 'es2024',
62
+ 'esnext',
63
+ ]);
64
+ const MODULE_BROWSER_TARGETS = [
65
+ 'chrome >= 87',
66
+ 'edge >= 88',
67
+ 'firefox >= 78',
68
+ 'ios_saf >= 14',
69
+ 'safari >= 14',
70
+ ];
71
+
72
+ /** @param {string} target */
73
+ function esTargetRank(target) {
74
+ if (target === 'es3') return 3;
75
+ if (target === 'es5') return 5;
76
+ if (target === 'esnext') return Number.POSITIVE_INFINITY;
77
+ return Number(target.slice(2));
78
+ }
79
+
80
+ /** @param {string} target */
81
+ function toBrowserslistTarget(target) {
82
+ const match = /^(android|chrome|edge|firefox|ie|ios|opera|safari|node)(\d+(?:\.\d+)*)$/.exec(
83
+ target,
84
+ );
85
+ if (!match) {
86
+ throw new Error(
87
+ `[@octanejs/rsbuild-plugin] Unsupported build.target ${JSON.stringify(target)}. Use an ES target, "modules", or an esbuild-style browser target such as "chrome100".`,
88
+ );
89
+ }
90
+ const browser = match[1] === 'ios' ? 'ios_saf' : match[1];
91
+ return `${browser} >= ${match[2]}`;
92
+ }
93
+
94
+ /** @param {import('@octanejs/app-core').BuildTarget} target */
95
+ function createBuildTargetPlan(target) {
96
+ if (target === false) {
97
+ return { swcTarget: 'esnext', rspackTarget: 'es2024', browserslist: null };
98
+ }
99
+ const targets = (Array.isArray(target) ? target : [target]).map((entry) =>
100
+ entry === 'es6' ? 'es2015' : entry,
101
+ );
102
+ if (targets.length === 0) {
103
+ throw new Error('[@octanejs/rsbuild-plugin] build.target must not be an empty array.');
104
+ }
105
+ if (targets.every((entry) => SWC_ES_TARGETS.has(entry))) {
106
+ const swcTarget = [...targets].sort(
107
+ (left, right) => esTargetRank(left) - esTargetRank(right),
108
+ )[0];
109
+ return {
110
+ swcTarget,
111
+ rspackTarget: swcTarget === 'esnext' ? 'es2024' : swcTarget,
112
+ browserslist: null,
113
+ };
114
+ }
115
+ if (targets.some((entry) => SWC_ES_TARGETS.has(entry))) {
116
+ throw new Error(
117
+ '[@octanejs/rsbuild-plugin] build.target cannot mix ES levels and browser targets. Use one ES level or browser targets only.',
118
+ );
119
+ }
120
+ const browserslist = targets.flatMap((entry) =>
121
+ entry === 'modules' ? MODULE_BROWSER_TARGETS : [toBrowserslistTarget(entry)],
122
+ );
123
+ return {
124
+ swcTarget: browserslist,
125
+ rspackTarget: `browserslist:${browserslist.join(',')}`,
126
+ browserslist,
127
+ };
128
+ }
129
+
130
+ /** @param {import('@octanejs/app-core').ResolvedOctaneConfig | null} config */
131
+ function hasRoutes(config) {
132
+ return (config?.router.routes.length ?? 0) > 0;
133
+ }
134
+
135
+ /** @param {import('@octanejs/app-core').ResolvedOctaneConfig} config */
136
+ function configSignature(config) {
137
+ return JSON.stringify({
138
+ build: config.build,
139
+ routes: config.router.routes.map((route) =>
140
+ route.type === 'render'
141
+ ? {
142
+ type: route.type,
143
+ path: route.path,
144
+ entry: route.entry,
145
+ layout: route.layout,
146
+ status: route.status,
147
+ }
148
+ : { type: route.type, path: route.path, methods: route.methods },
149
+ ),
150
+ preHydrate: config.router.preHydrate,
151
+ rootBoundary: config.rootBoundary,
152
+ server: config.server,
153
+ });
154
+ }
155
+
156
+ /** @param {string} value */
157
+ function escapeRegExp(value) {
158
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
159
+ }
160
+
161
+ /** @param {string} file Match an exact absolute resource path on POSIX or Windows. */
162
+ function exactResourceRegExp(file) {
163
+ const logical = path.resolve(file);
164
+ let physical = logical;
165
+ try {
166
+ physical = fs.realpathSync(logical);
167
+ } catch {
168
+ // The generated entry is normally present already; retain the logical path
169
+ // if a custom output filesystem makes it unavailable during setup.
170
+ }
171
+ const patterns = [...new Set([logical, physical])].map((candidate) =>
172
+ candidate.split(/[/\\]/).map(escapeRegExp).join('[/\\\\]'),
173
+ );
174
+ return new RegExp(`^(?:${patterns.join('|')})$`);
175
+ }
176
+
177
+ /** @param {ReturnType<typeof createBuildTargetPlan>} plan */
178
+ function createSwcTargetConfig(plan) {
179
+ return (/** @type {import('@rspack/core').SwcLoaderOptions} */ options) => {
180
+ if (typeof plan.swcTarget === 'string') {
181
+ options.env = undefined;
182
+ options.jsc = {
183
+ ...options.jsc,
184
+ target: /** @type {any} */ (plan.swcTarget),
185
+ };
186
+ return;
187
+ }
188
+ if (options.jsc) delete options.jsc.target;
189
+ options.env = { ...options.env, targets: plan.swcTarget };
190
+ };
191
+ }
192
+
193
+ /** @param {any} resolveOptions @param {string} key @param {string | false} value */
194
+ function addExactAlias(resolveOptions, key, value) {
195
+ const aliases = resolveOptions.alias === false ? {} : (resolveOptions.alias ?? {});
196
+ resolveOptions.alias = { ...aliases, [`${key}$`]: value };
197
+ }
198
+
199
+ /** @param {import('@rsbuild/core').RsbuildPluginAPI['logger']} logger */
200
+ function createLog(logger) {
201
+ return (/** @type {string} */ message) => logger.info(`[octane] ${message}`);
202
+ }
203
+
204
+ /** @param {any} config @param {string} clientEnvironment */
205
+ function assertRootPublicPaths(config, clientEnvironment) {
206
+ const base = config.server?.base;
207
+ if (base !== undefined && base !== '/') {
208
+ throw new Error(
209
+ '[@octanejs/rsbuild-plugin] App mode currently requires server.base to be "/". Rewrite a deployment subpath to the app root at your proxy.',
210
+ );
211
+ }
212
+ const assetPrefixes = [
213
+ config.output?.assetPrefix,
214
+ config.environments?.[clientEnvironment]?.output?.assetPrefix,
215
+ ];
216
+ if (assetPrefixes.some((prefix) => prefix !== undefined && prefix !== 'auto' && prefix !== '/')) {
217
+ throw new Error(
218
+ '[@octanejs/rsbuild-plugin] App mode currently supports only the default output.assetPrefix ("auto" or "/").',
219
+ );
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Full Octane app integration for Rsbuild 2.x. Without route config it remains
225
+ * a thin compiler integration and leaves the user's environments/entries alone.
226
+ *
227
+ * @param {{
228
+ * hmr?: boolean,
229
+ * parallelUse?: boolean,
230
+ * exclude?: string[],
231
+ * clientEnvironment?: string,
232
+ * serverEnvironment?: string,
233
+ * }} [inlineOptions]
234
+ * @returns {import('@rsbuild/core').RsbuildPlugin}
235
+ */
236
+ export function pluginOctane(inlineOptions = {}) {
237
+ return {
238
+ name: PLUGIN_NAME,
239
+ enforce: 'pre',
240
+ async setup(api) {
241
+ const root = path.resolve(api.context.rootPath);
242
+ const isProductionBuild = () => api.context.action === 'build';
243
+ const clientEnvironment = inlineOptions.clientEnvironment ?? 'web';
244
+ const serverEnvironment = inlineOptions.serverEnvironment ?? 'node';
245
+ const generatedDir = path.join(api.context.cachePath, 'octane');
246
+ const generatedOptions = { root, generatedDir };
247
+ const configPath = getOctaneConfigPath(root);
248
+ const initialLoaded = octaneConfigExists(root)
249
+ ? await loadOctaneConfigWithMetadata(root, {
250
+ cacheDir: path.join(generatedDir, 'config'),
251
+ })
252
+ : null;
253
+ const initialConfig = initialLoaded?.config ?? null;
254
+ const appEnabled = hasRoutes(initialConfig);
255
+ const buildTargetPlan =
256
+ initialConfig?.build.target === undefined
257
+ ? null
258
+ : createBuildTargetPlan(initialConfig.build.target);
259
+ const neutralCompiler = appEnabled ? createOctaneCompiler({ root }) : null;
260
+ let currentConfig = initialConfig;
261
+ let lastConfigSignature = initialLoaded ? configSignature(initialLoaded.config) : '';
262
+ let configNeedsReload = false;
263
+ /** @type {import('@rsbuild/core').RsbuildDevServer | null} */
264
+ let devServer = null;
265
+
266
+ const clientEntryFile = write_project_generated_file(
267
+ generatedOptions,
268
+ 'rsbuild-client-entry.js',
269
+ '// Replaced by @octanejs/rsbuild-plugin during compilation.\n',
270
+ );
271
+ const serverEntryFile = write_project_generated_file(
272
+ generatedOptions,
273
+ 'rsbuild-server-entry.js',
274
+ '// Replaced by @octanejs/rsbuild-plugin during compilation.\n',
275
+ );
276
+ const adapterBrowserStub = write_project_generated_file(
277
+ generatedOptions,
278
+ 'adapter-browser-stub.js',
279
+ create_adapter_browser_stub_source(),
280
+ );
281
+
282
+ /** @param {any} context @param {any} metadata */
283
+ function registerConfigDependencies(context, metadata) {
284
+ if (!context || !metadata || typeof metadata !== 'object') return;
285
+ for (const dependency of metadata.dependencies ?? []) {
286
+ if (typeof dependency === 'string') context.addDependency(dependency);
287
+ }
288
+ for (const dependency of metadata.missingDependencies ?? []) {
289
+ if (typeof dependency === 'string') context.addMissingDependency(dependency);
290
+ }
291
+ }
292
+
293
+ /** @param {any} [context] */
294
+ async function loadCurrentConfig(context) {
295
+ let loaded;
296
+ try {
297
+ loaded = await loadOctaneConfigWithMetadata(root, {
298
+ cacheDir: path.join(generatedDir, 'config'),
299
+ });
300
+ } catch (error) {
301
+ // Failed config evaluation still reports every consulted dependency.
302
+ // Register it before rethrowing so editing the broken helper retriggers
303
+ // compilation without restarting the dev server.
304
+ registerConfigDependencies(context, error);
305
+ throw error;
306
+ }
307
+ registerConfigDependencies(context, loaded);
308
+ const signature = configSignature(loaded.config);
309
+ if (lastConfigSignature && signature !== lastConfigSignature) configNeedsReload = true;
310
+ lastConfigSignature = signature;
311
+ currentConfig = loaded.config;
312
+ return loaded;
313
+ }
314
+
315
+ function discoverCompilerSources() {
316
+ if (!neutralCompiler) {
317
+ return {
318
+ packages: [],
319
+ dependencies: [],
320
+ missingDependencies: [],
321
+ sourceRoots: [root],
322
+ };
323
+ }
324
+ // Entry transforms run after watched invalidations. Re-evaluate package
325
+ // manifests so adding/removing a linked raw Octane dependency does not
326
+ // require restarting the Rsbuild dev server.
327
+ neutralCompiler.invalidate();
328
+ const metadata = neutralCompiler.discoverSourceDependencies();
329
+ return {
330
+ ...metadata,
331
+ sourceRoots: resolveOctaneSourceRoots(root, metadata.packages),
332
+ };
333
+ }
334
+
335
+ api.modifyRsbuildConfig((config, { mergeRsbuildConfig }) => {
336
+ if (!appEnabled) return config;
337
+ assertRootPublicPaths(config, clientEnvironment);
338
+ const productionBuild = isProductionBuild();
339
+ const octaneConfig = /** @type {import('@octanejs/app-core').ResolvedOctaneConfig} */ (
340
+ initialConfig
341
+ );
342
+ const template = path.join(root, 'index.html');
343
+ if (!fs.existsSync(template)) {
344
+ throw new Error(
345
+ '[@octanejs/rsbuild-plugin] index.html not found — required when octane.config.ts defines routes.',
346
+ );
347
+ }
348
+ const outputMinify =
349
+ octaneConfig.build.minify === undefined ? {} : { minify: octaneConfig.build.minify };
350
+ const targetConfig = buildTargetPlan
351
+ ? { tools: { swc: createSwcTargetConfig(buildTargetPlan) } }
352
+ : {};
353
+ const targetOutput = buildTargetPlan?.browserslist
354
+ ? { overrideBrowserslist: buildTargetPlan.browserslist }
355
+ : {};
356
+ return mergeRsbuildConfig(config, {
357
+ server: { htmlFallback: false, historyApiFallback: false },
358
+ environments: {
359
+ [clientEnvironment]: {
360
+ ...targetConfig,
361
+ source: { entry: { [CLIENT_ENTRY_NAME]: clientEntryFile } },
362
+ html: { template },
363
+ output: {
364
+ ...targetOutput,
365
+ target: 'web',
366
+ distPath: { root: path.resolve(root, octaneConfig.build.outDir, 'client') },
367
+ filename: { html: 'index.html' },
368
+ ...outputMinify,
369
+ },
370
+ },
371
+ [serverEnvironment]: {
372
+ ...targetConfig,
373
+ source: {
374
+ entry: {
375
+ [SERVER_ENTRY_NAME]: { import: [serverEntryFile], html: false },
376
+ },
377
+ },
378
+ output: {
379
+ ...targetOutput,
380
+ target: 'node',
381
+ autoExternal: false,
382
+ distPath: {
383
+ root: path.resolve(root, octaneConfig.build.outDir, 'server'),
384
+ js: '',
385
+ jsAsync: '',
386
+ },
387
+ filename: { js: '[name].js' },
388
+ filenameHash: false,
389
+ module: productionBuild,
390
+ emitAssets: false,
391
+ ...outputMinify,
392
+ },
393
+ },
394
+ },
395
+ });
396
+ });
397
+
398
+ if (appEnabled) {
399
+ api.transform(
400
+ {
401
+ test: exactResourceRegExp(clientEntryFile),
402
+ environments: [clientEnvironment],
403
+ order: 'pre',
404
+ },
405
+ async (context) => {
406
+ const loaded = await loadCurrentConfig(context);
407
+ const compilerDiscovery = discoverCompilerSources();
408
+ for (const dependency of compilerDiscovery.dependencies) {
409
+ context.addDependency(dependency);
410
+ }
411
+ for (const dependency of compilerDiscovery.missingDependencies) {
412
+ context.addMissingDependency(dependency);
413
+ }
414
+ const entries = collectClientEntries(loaded.config).map((id) => ({
415
+ id,
416
+ specifier: resolveProjectModule(id, root),
417
+ }));
418
+ return create_client_entry_source({
419
+ staticEntries: entries,
420
+ generatedBy: PLUGIN_NAME,
421
+ });
422
+ },
423
+ );
424
+
425
+ api.transform(
426
+ {
427
+ test: exactResourceRegExp(serverEntryFile),
428
+ environments: [serverEnvironment],
429
+ order: 'pre',
430
+ },
431
+ async (context) => {
432
+ const loaded = await loadCurrentConfig(context);
433
+ const compilerDiscovery = discoverCompilerSources();
434
+ for (const dependency of compilerDiscovery.dependencies) {
435
+ context.addDependency(dependency);
436
+ }
437
+ for (const dependency of compilerDiscovery.missingDependencies) {
438
+ context.addMissingDependency(dependency);
439
+ }
440
+ const serverModules = discoverServerModules(root, compilerDiscovery.sourceRoots);
441
+ for (const directory of serverModules.directories) {
442
+ context.addContextDependency(directory);
443
+ }
444
+ for (const file of serverModules.files) context.addDependency(file);
445
+ // Build intent, not optimization mode, selects the bootable entry. A
446
+ // programmatic `rsbuild.build()` may intentionally keep development
447
+ // transforms while still requiring production output/finalization.
448
+ const production = isProductionBuild();
449
+ const generator = production ? generateServerEntry : generateServerManifestEntry;
450
+ return generator({
451
+ routes: loaded.config.router.routes,
452
+ octaneConfigPath: loaded.configPath,
453
+ configImportPath: loaded.configPath,
454
+ rootBoundary: loaded.config.rootBoundary,
455
+ rpcModulePaths: serverModules.ids,
456
+ ...(production ? { clientAssetMapFile: CLIENT_ASSET_MAP } : { clientAssetMap: {} }),
457
+ resolveImport: (id) => resolveProjectModule(id, root),
458
+ configModuleId: localRequire.resolve('@octanejs/app-core/config'),
459
+ productionModuleId: localRequire.resolve('@octanejs/app-core/production'),
460
+ nodeModuleId: localRequire.resolve('@octanejs/app-core/node'),
461
+ generatedBy: PLUGIN_NAME,
462
+ });
463
+ },
464
+ );
465
+ }
466
+
467
+ api.modifyRspackConfig((config, utils) => {
468
+ const productionBuild = isProductionBuild();
469
+ const environment =
470
+ utils.environment.name === serverEnvironment || utils.isServer ? 'server' : 'client';
471
+ if (buildTargetPlan) {
472
+ config.target = /** @type {any} */ ([
473
+ environment === 'server' ? 'node' : 'web',
474
+ buildTargetPlan.rspackTarget,
475
+ ]);
476
+ }
477
+ config.plugins ??= [];
478
+ config.plugins.push(
479
+ new OctaneRspackPlugin({
480
+ root,
481
+ environment,
482
+ transpile: false,
483
+ ...(inlineOptions.hmr === undefined ? null : { hmr: inlineOptions.hmr }),
484
+ ...(inlineOptions.parallelUse === undefined
485
+ ? null
486
+ : { parallelUse: inlineOptions.parallelUse }),
487
+ ...(inlineOptions.exclude === undefined ? null : { exclude: inlineOptions.exclude }),
488
+ }),
489
+ );
490
+ config.resolve ??= {};
491
+ addExactAlias(
492
+ config.resolve,
493
+ '@octanejs/rsbuild-plugin',
494
+ localRequire.resolve('./config-entry.js'),
495
+ );
496
+ addExactAlias(
497
+ config.resolve,
498
+ '@octanejs/vite-plugin',
499
+ localRequire.resolve('./config-entry.js'),
500
+ );
501
+ if (appEnabled && environment === 'server') {
502
+ config.output ??= {};
503
+ config.output.library = { type: productionBuild ? 'module' : 'commonjs2' };
504
+ if (productionBuild) {
505
+ config.module ??= {};
506
+ config.module.rules ??= [];
507
+ // The generated entry must retain native import.meta.url for its
508
+ // colocated HTML/asset-map lookup. Scope that exception exactly so
509
+ // application modules keep Rspack's import-meta/new-URL handling.
510
+ config.module.rules.push({
511
+ test: exactResourceRegExp(serverEntryFile),
512
+ parser: { importMeta: false },
513
+ });
514
+ }
515
+ }
516
+
517
+ if (environment === 'client') {
518
+ for (const adapterId of SERVER_ONLY_ADAPTER_IDS) {
519
+ addExactAlias(config.resolve, adapterId, adapterBrowserStub);
520
+ }
521
+ if (appEnabled) {
522
+ config.plugins.push(
523
+ new OctaneClientAssetsPlugin({
524
+ root,
525
+ clientEntry: clientEntryFile,
526
+ entries: () =>
527
+ collectClientEntries(
528
+ /** @type {import('@octanejs/app-core').ResolvedOctaneConfig} */ (
529
+ currentConfig
530
+ ),
531
+ ),
532
+ filename: CLIENT_ASSET_MAP,
533
+ }),
534
+ );
535
+ }
536
+ }
537
+ return config;
538
+ });
539
+
540
+ if (!appEnabled) return;
541
+
542
+ api.modifyHTML((html, context) => {
543
+ if (context.environment.name !== clientEnvironment) return html;
544
+ const entrypoint = context.compilation.entrypoints.get(CLIENT_ENTRY_NAME);
545
+ const entryFiles = entrypoint?.getEntrypointChunk().files ?? [];
546
+ return markHydrationEntry(html, entryFiles);
547
+ });
548
+
549
+ api.onBeforeStartDevServer(({ server }) => {
550
+ devServer = server;
551
+ const publicRoots = api
552
+ .getNormalizedConfig()
553
+ .server.publicDir.map((entry) => path.resolve(root, entry.name));
554
+ const middleware = createOctaneDevMiddleware({
555
+ server,
556
+ clientEnvironment,
557
+ serverEnvironment,
558
+ clientEntry: CLIENT_ENTRY_NAME,
559
+ serverEntry: SERVER_ENTRY_NAME,
560
+ publicRoots,
561
+ logError(message, error) {
562
+ api.logger.error(`${message}: ${error instanceof Error ? error.stack : String(error)}`);
563
+ },
564
+ });
565
+ // Register before Rsbuild's HTML completion middleware so application
566
+ // routes reach SSR. This middleware yields emitted, internal, and public
567
+ // URLs back to Rsbuild.
568
+ server.middlewares.use(middleware);
569
+ });
570
+
571
+ api.onAfterDevCompile(() => {
572
+ if (!configNeedsReload || !devServer) return;
573
+ configNeedsReload = false;
574
+ devServer.environments[clientEnvironment]?.hot.send('full-reload');
575
+ });
576
+
577
+ api.onAfterBuild(async ({ stats }) => {
578
+ if (stats?.hasErrors()) return;
579
+ const loaded = await loadCurrentConfig();
580
+ await finalizeOctaneRsbuildOutput({
581
+ root,
582
+ config: loaded.config,
583
+ assetMapFilename: CLIENT_ASSET_MAP,
584
+ log: createLog(api.logger),
585
+ });
586
+ });
587
+ },
588
+ };
589
+ }
590
+
591
+ /** Alias matching the Vite metaframework package. */
592
+ export const octane = pluginOctane;
package/src/node.js ADDED
@@ -0,0 +1 @@
1
+ export * from '@octanejs/app-core/node';
@@ -0,0 +1 @@
1
+ export * from '@octanejs/app-core/production';