@ecopages/react 0.2.0-alpha.48 → 0.2.0-alpha.49
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 +1 -1
- package/src/react-hmr-strategy.d.ts +18 -0
- package/src/react-hmr-strategy.js +197 -12
- package/src/services/react-bundle.service.d.ts +4 -0
- package/src/services/react-bundle.service.js +8 -4
- package/src/services/react-hydration-asset.service.d.ts +2 -0
- package/src/services/react-hydration-asset.service.js +15 -3
package/package.json
CHANGED
|
@@ -114,6 +114,21 @@ export declare class ReactHmrStrategy extends HmrStrategy {
|
|
|
114
114
|
* @returns True if the file is in the layouts directory
|
|
115
115
|
*/
|
|
116
116
|
private isLayoutFile;
|
|
117
|
+
private isPageEntrypoint;
|
|
118
|
+
private getEntrypointOutput;
|
|
119
|
+
private getGroupedTempOutputPattern;
|
|
120
|
+
private collectReactPageBuildTargets;
|
|
121
|
+
private getRequestedTargets;
|
|
122
|
+
/**
|
|
123
|
+
* Expands one HMR request into the full React page build cohort when needed.
|
|
124
|
+
*
|
|
125
|
+
* @remarks
|
|
126
|
+
* Page and layout changes need one shared rebuild pass so sibling routes keep
|
|
127
|
+
* a consistent client module graph. Non-page changes that do not touch a page
|
|
128
|
+
* cohort can stay scoped to the originally requested targets.
|
|
129
|
+
*/
|
|
130
|
+
private resolveBuildTargets;
|
|
131
|
+
private partitionBuildTargets;
|
|
117
132
|
/**
|
|
118
133
|
* Processes a React file change by rebuilding all React entrypoints.
|
|
119
134
|
*
|
|
@@ -134,12 +149,15 @@ export declare class ReactHmrStrategy extends HmrStrategy {
|
|
|
134
149
|
* @returns True if bundling was successful
|
|
135
150
|
*/
|
|
136
151
|
private bundleReactEntrypoint;
|
|
152
|
+
private bundleReactEntrypoints;
|
|
137
153
|
private resolveTempOutputPath;
|
|
138
154
|
/**
|
|
139
155
|
* Encodes dynamic route segments (brackets) in file paths.
|
|
140
156
|
* Converts `[slug]` to `_slug_` to avoid filesystem issues.
|
|
141
157
|
*/
|
|
142
158
|
private encodeDynamicSegments;
|
|
159
|
+
private rewriteChunkImportUrls;
|
|
160
|
+
private isMissingTempOutputError;
|
|
143
161
|
/**
|
|
144
162
|
* Processes bundled output and injects the React HMR handler.
|
|
145
163
|
* Writes to temp file first, then renames atomically to avoid conflicts.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { HmrStrategy, HmrStrategyType } from "@ecopages/core/hmr/hmr-strategy";
|
|
3
|
+
import { RESOLVED_ASSETS_DIR } from "@ecopages/core/constants";
|
|
3
4
|
import { rewriteRuntimeSpecifierAliases } from "@ecopages/core/build/runtime-specifier-aliases";
|
|
4
5
|
import { createRuntimeSpecifierAliasPlugin } from "@ecopages/core/build/runtime-specifier-alias-plugin";
|
|
5
6
|
import { FileNotFoundError, fileSystem } from "@ecopages/file-system";
|
|
@@ -132,6 +133,96 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
132
133
|
isLayoutFile(filePath) {
|
|
133
134
|
return filePath.startsWith(this.context.getLayoutsDir());
|
|
134
135
|
}
|
|
136
|
+
isPageEntrypoint(filePath) {
|
|
137
|
+
return filePath.startsWith(this.context.getPagesDir()) && this.isReactEntrypoint(filePath);
|
|
138
|
+
}
|
|
139
|
+
getEntrypointOutput(entrypointPath) {
|
|
140
|
+
const srcDir = this.context.getSrcDir();
|
|
141
|
+
const relativePath = path.relative(srcDir, entrypointPath);
|
|
142
|
+
const relativePathJs = relativePath.replace(/\.(tsx?|jsx?|mdx)$/, ".js");
|
|
143
|
+
const encodedPathJs = this.encodeDynamicSegments(relativePathJs);
|
|
144
|
+
const outputPath = path.join(this.context.getDistDir(), encodedPathJs);
|
|
145
|
+
const outputUrl = `/${path.join(RESOLVED_ASSETS_DIR, "_hmr", encodedPathJs).split(path.sep).join("/")}`;
|
|
146
|
+
return { outputPath, outputUrl };
|
|
147
|
+
}
|
|
148
|
+
getGroupedTempOutputPattern(entrypointPath) {
|
|
149
|
+
const srcDir = this.context.getSrcDir();
|
|
150
|
+
const relativePath = path.relative(srcDir, entrypointPath);
|
|
151
|
+
const relativePathJs = relativePath.replace(/\.(tsx?|jsx?|mdx)$/, ".js");
|
|
152
|
+
return {
|
|
153
|
+
outputDir: path.join(this.context.getDistDir(), path.dirname(relativePathJs)),
|
|
154
|
+
outputBaseName: path.basename(relativePathJs, ".js")
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async collectReactPageBuildTargets() {
|
|
158
|
+
const pagesDir = this.context.getPagesDir();
|
|
159
|
+
const scannedFiles = await fileSystem.glob(
|
|
160
|
+
this.allTemplateExtensions.map((extension) => `**/*${extension}`),
|
|
161
|
+
{ cwd: pagesDir }
|
|
162
|
+
);
|
|
163
|
+
const targets = /* @__PURE__ */ new Map();
|
|
164
|
+
for (const file of scannedFiles) {
|
|
165
|
+
if (file.includes(".ecopages-node.")) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const entrypointPath = path.join(pagesDir, file);
|
|
169
|
+
if (!this.isPageEntrypoint(entrypointPath)) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
this.pageMetadataCache.markOwnedEntrypoint(entrypointPath);
|
|
173
|
+
targets.set(entrypointPath, {
|
|
174
|
+
entrypointPath,
|
|
175
|
+
outputUrl: this.getEntrypointOutput(entrypointPath).outputUrl
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return Array.from(targets.values()).sort(
|
|
179
|
+
(left, right) => left.entrypointPath.localeCompare(right.entrypointPath)
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
getRequestedTargets(changedFilePath, changedEntrypointOutput, watchedFiles) {
|
|
183
|
+
const requestedEntries = changedEntrypointOutput ? [[changedFilePath, changedEntrypointOutput]] : Array.from(watchedFiles.entries());
|
|
184
|
+
return requestedEntries.map(([entrypointPath, outputUrl]) => ({
|
|
185
|
+
entrypointPath,
|
|
186
|
+
outputUrl
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Expands one HMR request into the full React page build cohort when needed.
|
|
191
|
+
*
|
|
192
|
+
* @remarks
|
|
193
|
+
* Page and layout changes need one shared rebuild pass so sibling routes keep
|
|
194
|
+
* a consistent client module graph. Non-page changes that do not touch a page
|
|
195
|
+
* cohort can stay scoped to the originally requested targets.
|
|
196
|
+
*/
|
|
197
|
+
async resolveBuildTargets(requestedTargets, changedFilePath) {
|
|
198
|
+
const requestedPageTargets = requestedTargets.filter((target) => this.isPageEntrypoint(target.entrypointPath));
|
|
199
|
+
const shouldGroupPageBuilds = this.isLayoutFile(changedFilePath) || requestedPageTargets.length > 0;
|
|
200
|
+
if (!shouldGroupPageBuilds) {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
const groupedTargets = new Map(requestedPageTargets.map((target) => [target.entrypointPath, target]));
|
|
204
|
+
for (const target of await this.collectReactPageBuildTargets()) {
|
|
205
|
+
groupedTargets.set(target.entrypointPath, target);
|
|
206
|
+
}
|
|
207
|
+
return Array.from(groupedTargets.values()).sort(
|
|
208
|
+
(left, right) => left.entrypointPath.localeCompare(right.entrypointPath)
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
partitionBuildTargets(requestedTargets, groupedPageTargets) {
|
|
212
|
+
if (groupedPageTargets.length === 0) {
|
|
213
|
+
return {
|
|
214
|
+
pageTargets: [],
|
|
215
|
+
nonPageTargets: requestedTargets
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const groupedPageEntrypoints = new Set(groupedPageTargets.map((target) => target.entrypointPath));
|
|
219
|
+
return {
|
|
220
|
+
pageTargets: groupedPageTargets,
|
|
221
|
+
nonPageTargets: requestedTargets.filter(
|
|
222
|
+
(target) => !groupedPageEntrypoints.has(target.entrypointPath) && !this.isPageEntrypoint(target.entrypointPath)
|
|
223
|
+
)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
135
226
|
/**
|
|
136
227
|
* Processes a React file change by rebuilding all React entrypoints.
|
|
137
228
|
*
|
|
@@ -159,15 +250,35 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
159
250
|
appLogger.debug(`Skipping non-React watched entrypoint: ${_filePath}`);
|
|
160
251
|
return { type: "none" };
|
|
161
252
|
}
|
|
162
|
-
const
|
|
253
|
+
const requestedTargets = this.getRequestedTargets(_filePath, changedEntrypointOutput, watchedFiles);
|
|
254
|
+
const groupedPageTargets = await this.resolveBuildTargets(requestedTargets, _filePath);
|
|
255
|
+
const { pageTargets, nonPageTargets } = this.partitionBuildTargets(requestedTargets, groupedPageTargets);
|
|
163
256
|
const updates = [];
|
|
164
|
-
|
|
165
|
-
|
|
257
|
+
const requestedOutputUrls = new Set(requestedTargets.map((target) => target.outputUrl));
|
|
258
|
+
if (pageTargets.length > 1) {
|
|
259
|
+
appLogger.debug(`Bundling ${pageTargets.length} React page entrypoints together`);
|
|
260
|
+
const rebuiltOutputs = await this.bundleReactEntrypoints(pageTargets);
|
|
261
|
+
for (const outputUrl of rebuiltOutputs) {
|
|
262
|
+
if (requestedOutputUrls.has(outputUrl)) {
|
|
263
|
+
updates.push(outputUrl);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
for (const { entrypointPath, outputUrl } of pageTargets) {
|
|
268
|
+
appLogger.debug(`Bundling ${entrypointPath}`);
|
|
269
|
+
const success = await this.bundleReactEntrypoint(entrypointPath, outputUrl);
|
|
270
|
+
if (success && requestedOutputUrls.has(outputUrl)) {
|
|
271
|
+
updates.push(outputUrl);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (const { entrypointPath, outputUrl } of nonPageTargets) {
|
|
276
|
+
if (!this.isReactEntrypoint(entrypointPath)) {
|
|
166
277
|
continue;
|
|
167
278
|
}
|
|
168
|
-
appLogger.debug(`Bundling ${
|
|
169
|
-
const success = await this.bundleReactEntrypoint(
|
|
170
|
-
if (success) {
|
|
279
|
+
appLogger.debug(`Bundling ${entrypointPath}`);
|
|
280
|
+
const success = await this.bundleReactEntrypoint(entrypointPath, outputUrl);
|
|
281
|
+
if (success && requestedOutputUrls.has(outputUrl)) {
|
|
171
282
|
updates.push(outputUrl);
|
|
172
283
|
}
|
|
173
284
|
}
|
|
@@ -206,11 +317,7 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
206
317
|
async bundleReactEntrypoint(entrypointPath, outputUrl) {
|
|
207
318
|
try {
|
|
208
319
|
const isMdx = entrypointPath.endsWith(".mdx");
|
|
209
|
-
const
|
|
210
|
-
const relativePath = path.relative(srcDir, entrypointPath);
|
|
211
|
-
const relativePathJs = relativePath.replace(/\.(tsx?|jsx?|mdx)$/, ".js");
|
|
212
|
-
const encodedPathJs = this.encodeDynamicSegments(relativePathJs);
|
|
213
|
-
const outputPath = path.join(this.context.getDistDir(), encodedPathJs);
|
|
320
|
+
const { outputPath } = this.getEntrypointOutput(entrypointPath);
|
|
214
321
|
const tempDir = path.dirname(outputPath);
|
|
215
322
|
const declaredModules = this.pageMetadataCache.getDeclaredModules(entrypointPath) ? this.pageMetadataCache.getDeclaredModules(entrypointPath) : isMdx ? await collectPageDeclaredModules(entrypointPath) : collectPageDeclaredModulesFromModule(await this.importNodePageModule(entrypointPath));
|
|
216
323
|
const plugins = this.getBuildPlugins(declaredModules);
|
|
@@ -247,6 +354,61 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
247
354
|
return false;
|
|
248
355
|
}
|
|
249
356
|
}
|
|
357
|
+
async bundleReactEntrypoints(entrypoints) {
|
|
358
|
+
try {
|
|
359
|
+
const declaredModules = /* @__PURE__ */ new Set();
|
|
360
|
+
let shouldEnableMdx = false;
|
|
361
|
+
for (const { entrypointPath } of entrypoints) {
|
|
362
|
+
const entrypointDeclaredModules = this.pageMetadataCache.getDeclaredModules(entrypointPath) ? this.pageMetadataCache.getDeclaredModules(entrypointPath) : entrypointPath.endsWith(".mdx") ? await collectPageDeclaredModules(entrypointPath) : collectPageDeclaredModulesFromModule(await this.importNodePageModule(entrypointPath));
|
|
363
|
+
this.pageMetadataCache.setDeclaredModules(entrypointPath, entrypointDeclaredModules);
|
|
364
|
+
for (const declaredModule of entrypointDeclaredModules) {
|
|
365
|
+
declaredModules.add(declaredModule);
|
|
366
|
+
}
|
|
367
|
+
if (entrypointPath.endsWith(".mdx")) {
|
|
368
|
+
shouldEnableMdx = true;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const plugins = this.getBuildPlugins([...declaredModules]);
|
|
372
|
+
if (shouldEnableMdx && this.mdxCompilerOptions) {
|
|
373
|
+
plugins.unshift(createReactMdxLoaderPlugin(this.mdxCompilerOptions));
|
|
374
|
+
}
|
|
375
|
+
const result = await this.context.getBrowserBundleService().bundle({
|
|
376
|
+
profile: "hmr-entrypoint",
|
|
377
|
+
entrypoints: entrypoints.map(({ entrypointPath }) => entrypointPath),
|
|
378
|
+
outdir: this.context.getDistDir(),
|
|
379
|
+
outbase: this.context.getSrcDir(),
|
|
380
|
+
naming: "[dir]/[name].[hash].tmp",
|
|
381
|
+
splitting: true,
|
|
382
|
+
plugins,
|
|
383
|
+
minify: false
|
|
384
|
+
});
|
|
385
|
+
if (!result.success) {
|
|
386
|
+
appLogger.error(`Failed to build grouped React entrypoints:`, result.logs);
|
|
387
|
+
return [];
|
|
388
|
+
}
|
|
389
|
+
const updatedOutputs = [];
|
|
390
|
+
for (const { entrypointPath, outputUrl } of entrypoints) {
|
|
391
|
+
const { outputPath } = this.getEntrypointOutput(entrypointPath);
|
|
392
|
+
const { outputDir, outputBaseName } = this.getGroupedTempOutputPattern(entrypointPath);
|
|
393
|
+
const tempOutput = result.outputs.find((output) => {
|
|
394
|
+
return path.dirname(output.path) === outputDir && path.basename(output.path).startsWith(`${outputBaseName}.`) && path.basename(output.path).includes(".tmp");
|
|
395
|
+
})?.path;
|
|
396
|
+
const resolvedTempOutput = tempOutput ? await this.resolveTempOutputPath(tempOutput) : await this.resolveTempOutputPath(path.join(outputDir, `${outputBaseName}.[hash].tmp.js`));
|
|
397
|
+
if (!resolvedTempOutput) {
|
|
398
|
+
appLogger.debug(`Missing grouped temp output for ${outputUrl}`);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const processed = await this.processOutput(resolvedTempOutput, outputPath, outputUrl);
|
|
402
|
+
if (processed) {
|
|
403
|
+
updatedOutputs.push(outputUrl);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return updatedOutputs;
|
|
407
|
+
} catch (error) {
|
|
408
|
+
appLogger.error(`Error bundling grouped React entrypoints:`, error);
|
|
409
|
+
return [];
|
|
410
|
+
}
|
|
411
|
+
}
|
|
250
412
|
async resolveTempOutputPath(tempPath) {
|
|
251
413
|
if (fileSystem.exists(tempPath)) {
|
|
252
414
|
return tempPath;
|
|
@@ -269,6 +431,28 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
269
431
|
encodeDynamicSegments(filepath) {
|
|
270
432
|
return filepath.replace(/\[([^\]]+)\]/g, "_$1_");
|
|
271
433
|
}
|
|
434
|
+
rewriteChunkImportUrls(code) {
|
|
435
|
+
const hmrChunkBaseUrl = `/${path.join(RESOLVED_ASSETS_DIR, "_hmr").split(path.sep).join("/")}`;
|
|
436
|
+
return code.replace(/(['"])(?:\.\.\/)+(chunk-[^'"]+\.js)\1/g, (_match, quote, chunkFile) => {
|
|
437
|
+
return `${quote}${hmrChunkBaseUrl}/${chunkFile}${quote}`;
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
isMissingTempOutputError(error) {
|
|
441
|
+
if (error instanceof FileNotFoundError) {
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
if (!(error instanceof Error)) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
if (error.message.includes("not found") || error.message.includes("ENOENT")) {
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
const errorCause = error.cause;
|
|
451
|
+
if (errorCause instanceof FileNotFoundError) {
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
return typeof errorCause === "object" && errorCause !== null && "code" in errorCause && errorCause.code === "ENOENT";
|
|
455
|
+
}
|
|
272
456
|
/**
|
|
273
457
|
* Processes bundled output and injects the React HMR handler.
|
|
274
458
|
* Writes to temp file first, then renames atomically to avoid conflicts.
|
|
@@ -286,6 +470,7 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
286
470
|
try {
|
|
287
471
|
let code = await fileSystem.readFile(tempPath);
|
|
288
472
|
code = rewriteRuntimeSpecifierAliases(code, this.runtimeAliasMap);
|
|
473
|
+
code = this.rewriteChunkImportUrls(code);
|
|
289
474
|
code = injectHmrHandler(code);
|
|
290
475
|
await fileSystem.writeAsync(finalPath, code);
|
|
291
476
|
await fileSystem.removeAsync(tempPath).catch(() => {
|
|
@@ -293,7 +478,7 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
293
478
|
appLogger.debug(`Processed ${url} with HMR handler`);
|
|
294
479
|
return true;
|
|
295
480
|
} catch (error) {
|
|
296
|
-
if (
|
|
481
|
+
if (this.isMissingTempOutputError(error)) {
|
|
297
482
|
appLogger.debug(`Skipping stale temp output for ${url}: ${tempPath}`);
|
|
298
483
|
await fileSystem.removeAsync(tempPath).catch(() => {
|
|
299
484
|
});
|
|
@@ -28,6 +28,10 @@ export interface ReactClientBundleOptions {
|
|
|
28
28
|
* rewriting them to external runtime specifiers.
|
|
29
29
|
*/
|
|
30
30
|
includeRuntime?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* When set, overrides the build adapter chunk splitting mode for this entry.
|
|
33
|
+
*/
|
|
34
|
+
splitting?: boolean;
|
|
31
35
|
}
|
|
32
36
|
/**
|
|
33
37
|
* Manages esbuild bundle configuration and plugin creation for React page/component builds.
|
|
@@ -40,14 +40,18 @@ class ReactBundleService {
|
|
|
40
40
|
naming: `${componentName}.[ext]`,
|
|
41
41
|
...import.meta.env?.NODE_ENV === "production" && {
|
|
42
42
|
minify: true,
|
|
43
|
-
splitting: false,
|
|
44
43
|
treeshaking: true
|
|
45
|
-
}
|
|
44
|
+
},
|
|
45
|
+
...bundleOptions.splitting === void 0 ? {} : { splitting: bundleOptions.splitting }
|
|
46
46
|
};
|
|
47
47
|
if (!bundleOptions.includeRuntime) {
|
|
48
|
+
const reactRuntimeSpecifiers = new Set(getReactRuntimeExternalSpecifiers());
|
|
48
49
|
options.external = [
|
|
49
|
-
...
|
|
50
|
-
|
|
50
|
+
...Object.values(runtimeImports).filter(
|
|
51
|
+
(specifier) => Boolean(specifier) && !reactRuntimeSpecifiers.has(
|
|
52
|
+
specifier
|
|
53
|
+
)
|
|
54
|
+
)
|
|
51
55
|
];
|
|
52
56
|
}
|
|
53
57
|
const graphBoundaryPlugin = createClientGraphBoundaryPlugin({
|
|
@@ -28,9 +28,11 @@ export declare function getReactIslandComponentKey(componentFile: string, config
|
|
|
28
28
|
*/
|
|
29
29
|
export declare class ReactHydrationAssetService {
|
|
30
30
|
private readonly config;
|
|
31
|
+
private static readonly ROUTER_PAGE_GROUPED_BUNDLE_ID;
|
|
31
32
|
constructor(config: ReactHydrationAssetServiceConfig);
|
|
32
33
|
private getIslandBundleName;
|
|
33
34
|
private getIslandHydrationName;
|
|
35
|
+
private getRouterPageGroupedEntryName;
|
|
34
36
|
/**
|
|
35
37
|
* Resolves the browser import path used for a React-owned page or island module.
|
|
36
38
|
* Uses HMR manager for development or constructs static path for production.
|
|
@@ -11,6 +11,7 @@ function getReactIslandComponentKey(componentFile, config) {
|
|
|
11
11
|
}
|
|
12
12
|
class ReactHydrationAssetService {
|
|
13
13
|
config;
|
|
14
|
+
static ROUTER_PAGE_GROUPED_BUNDLE_ID = "ecopages-react-router-pages";
|
|
14
15
|
constructor(config) {
|
|
15
16
|
this.config = config;
|
|
16
17
|
}
|
|
@@ -20,6 +21,10 @@ class ReactHydrationAssetService {
|
|
|
20
21
|
getIslandHydrationName(bundleName, componentKey) {
|
|
21
22
|
return `${bundleName}-hydration-${componentKey}`;
|
|
22
23
|
}
|
|
24
|
+
getRouterPageGroupedEntryName(pagePath) {
|
|
25
|
+
const relativePath = path.relative(this.config.srcDir, pagePath);
|
|
26
|
+
return relativePath.replace(/\.(tsx?|jsx?|mdx?)$/, "").replace(/[\\/]+/g, "__").replace(/\[([^\]]+)\]/g, "_$1_");
|
|
27
|
+
}
|
|
23
28
|
/**
|
|
24
29
|
* Resolves the browser import path used for a React-owned page or island module.
|
|
25
30
|
* Uses HMR manager for development or constructs static path for production.
|
|
@@ -48,6 +53,10 @@ class ReactHydrationAssetService {
|
|
|
48
53
|
*/
|
|
49
54
|
createPageDependencies(pagePath, componentName, importPath, pageModuleUrlExpression, bundleOptions, isDevelopment, useBrowserRuntimeImports, isMdx) {
|
|
50
55
|
const runtimeImports = this.config.bundleService.getRuntimeImports();
|
|
56
|
+
const groupedBundle = this.config.routerAdapter ? {
|
|
57
|
+
id: ReactHydrationAssetService.ROUTER_PAGE_GROUPED_BUNDLE_ID,
|
|
58
|
+
entryName: this.getRouterPageGroupedEntryName(pagePath)
|
|
59
|
+
} : void 0;
|
|
51
60
|
return [
|
|
52
61
|
AssetFactory.createContentScript({
|
|
53
62
|
position: "head",
|
|
@@ -65,12 +74,14 @@ class ReactHydrationAssetService {
|
|
|
65
74
|
name: componentName,
|
|
66
75
|
packageRole: "page-script",
|
|
67
76
|
bundle: !isDevelopment,
|
|
77
|
+
groupedBundle,
|
|
68
78
|
bundleOptions,
|
|
69
79
|
attributes: {
|
|
70
80
|
type: "module",
|
|
71
81
|
defer: "",
|
|
72
82
|
"data-eco-rerun": "true",
|
|
73
83
|
"data-eco-script-id": componentName,
|
|
84
|
+
...this.config.routerAdapter ? { "data-eco-page-bootstrap": "react-router" } : {},
|
|
74
85
|
"data-eco-persist": "true"
|
|
75
86
|
}
|
|
76
87
|
})
|
|
@@ -159,17 +170,18 @@ class ReactHydrationAssetService {
|
|
|
159
170
|
const hmrManager = this.config.assetProcessingService?.getHmrManager();
|
|
160
171
|
const isDevelopment = hmrManager?.isEnabled() ?? false;
|
|
161
172
|
const isHostedDevelopment = !isDevelopment && process.env.NODE_ENV !== "production";
|
|
162
|
-
const
|
|
173
|
+
const usesRouterRuntime = Boolean(this.config.routerAdapter);
|
|
174
|
+
const useBrowserRuntimeImports = isDevelopment || isHostedDevelopment || usesRouterRuntime;
|
|
163
175
|
if (isDevelopment) {
|
|
164
176
|
this.config.hmrPageMetadataCache?.setDeclaredModules(pagePath, declaredModules);
|
|
165
177
|
}
|
|
166
178
|
const importPath = await this.resolveAssetImportPath(pagePath, componentName);
|
|
167
|
-
const pageModuleUrlExpression = "import.meta.url";
|
|
179
|
+
const pageModuleUrlExpression = isDevelopment ? JSON.stringify(importPath) : "import.meta.url";
|
|
168
180
|
const bundleOptions = await this.config.bundleService.createBundleOptions(
|
|
169
181
|
componentName,
|
|
170
182
|
isMdx,
|
|
171
183
|
declaredModules,
|
|
172
|
-
{ includeRuntime: !useBrowserRuntimeImports }
|
|
184
|
+
{ includeRuntime: !useBrowserRuntimeImports, splitting: usesRouterRuntime }
|
|
173
185
|
);
|
|
174
186
|
const dependencies = this.createPageDependencies(
|
|
175
187
|
pagePath,
|