@ecopages/react 0.2.0-alpha.4 → 0.2.0-alpha.41

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.
Files changed (70) hide show
  1. package/README.md +161 -18
  2. package/package.json +16 -12
  3. package/src/eco-embed.d.ts +11 -0
  4. package/src/eco-embed.js +11 -0
  5. package/src/react-hmr-strategy.d.ts +42 -32
  6. package/src/react-hmr-strategy.js +103 -124
  7. package/src/react-renderer.d.ts +169 -42
  8. package/src/react-renderer.js +484 -164
  9. package/src/react.constants.d.ts +1 -0
  10. package/src/react.constants.js +4 -0
  11. package/src/react.plugin.d.ts +38 -111
  12. package/src/react.plugin.js +132 -61
  13. package/src/react.types.d.ts +88 -0
  14. package/src/react.types.js +0 -0
  15. package/src/router-adapter.d.ts +7 -14
  16. package/src/services/react-bundle.service.d.ts +15 -26
  17. package/src/services/react-bundle.service.js +45 -93
  18. package/src/services/react-hmr-page-metadata-cache.d.ts +9 -0
  19. package/src/services/react-hmr-page-metadata-cache.js +18 -2
  20. package/src/services/react-hydration-asset.service.d.ts +26 -19
  21. package/src/services/react-hydration-asset.service.js +72 -66
  22. package/src/services/react-mdx-config-dependency.service.d.ts +36 -0
  23. package/src/services/react-mdx-config-dependency.service.js +122 -0
  24. package/src/services/react-page-module.service.d.ts +10 -2
  25. package/src/services/react-page-module.service.js +47 -39
  26. package/src/services/react-page-payload.service.d.ts +46 -0
  27. package/src/services/react-page-payload.service.js +67 -0
  28. package/src/services/react-runtime-bundle.service.d.ts +15 -13
  29. package/src/services/react-runtime-bundle.service.js +103 -180
  30. package/src/utils/client-graph-boundary-plugin.d.ts +1 -1
  31. package/src/utils/client-graph-boundary-plugin.js +149 -11
  32. package/src/utils/component-config-traversal.d.ts +36 -0
  33. package/src/utils/component-config-traversal.js +54 -0
  34. package/src/utils/declared-modules.d.ts +1 -1
  35. package/src/utils/declared-modules.js +7 -16
  36. package/src/utils/dynamic.test.browser.d.ts +1 -0
  37. package/src/utils/dynamic.test.browser.js +33 -0
  38. package/src/utils/hydration-scripts.d.ts +25 -6
  39. package/src/utils/hydration-scripts.js +150 -44
  40. package/src/utils/hydration-scripts.test.browser.d.ts +1 -0
  41. package/src/utils/hydration-scripts.test.browser.js +198 -0
  42. package/src/utils/reachability-analyzer.d.ts +12 -1
  43. package/src/utils/reachability-analyzer.js +101 -5
  44. package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
  45. package/src/utils/react-dom-runtime-interop-plugin.js +29 -0
  46. package/src/utils/react-mdx-loader-plugin.d.ts +1 -1
  47. package/src/utils/react-mdx-loader-plugin.js +13 -5
  48. package/src/utils/react-runtime-alias-map.d.ts +6 -0
  49. package/src/utils/react-runtime-alias-map.js +33 -0
  50. package/src/utils/use-sync-external-store-shim-plugin.d.ts +5 -0
  51. package/src/utils/use-sync-external-store-shim-plugin.js +41 -0
  52. package/CHANGELOG.md +0 -62
  53. package/src/react-hmr-strategy.ts +0 -444
  54. package/src/react-renderer.ts +0 -403
  55. package/src/react.plugin.ts +0 -241
  56. package/src/router-adapter.ts +0 -95
  57. package/src/services/react-bundle.service.ts +0 -212
  58. package/src/services/react-hmr-page-metadata-cache.ts +0 -24
  59. package/src/services/react-hydration-asset.service.ts +0 -260
  60. package/src/services/react-page-module.service.ts +0 -214
  61. package/src/services/react-runtime-bundle.service.ts +0 -271
  62. package/src/utils/client-graph-boundary-plugin.ts +0 -590
  63. package/src/utils/client-only.ts +0 -27
  64. package/src/utils/declared-modules.ts +0 -99
  65. package/src/utils/dynamic.ts +0 -27
  66. package/src/utils/hmr-scripts.ts +0 -47
  67. package/src/utils/html-boundary.ts +0 -66
  68. package/src/utils/hydration-scripts.ts +0 -338
  69. package/src/utils/reachability-analyzer.ts +0 -440
  70. package/src/utils/react-mdx-loader-plugin.ts +0 -40
@@ -1,214 +0,0 @@
1
- /**
2
- * Page module loading and configuration resolution service for React integration.
3
- *
4
- * Handles MDX compilation, component config metadata resolution,
5
- * and module hydration analysis.
6
- *
7
- * @module
8
- */
9
-
10
- import path from 'node:path';
11
- import { pathToFileURL } from 'node:url';
12
- import type { EcoComponentConfig, EcoPageFile } from '@ecopages/core';
13
- import { rapidhash } from '@ecopages/core/hash';
14
- import { defaultBuildAdapter } from '@ecopages/core/build/build-adapter';
15
- import { fileSystem } from '@ecopages/file-system';
16
- import type { CompileOptions } from '@mdx-js/mdx';
17
- import { collectDeclaredModulesInConfig } from '../utils/declared-modules.ts';
18
-
19
- /**
20
- * Configuration for the ReactPageModuleService.
21
- */
22
- export interface ReactPageModuleServiceConfig {
23
- rootDir: string;
24
- distDir: string;
25
- layoutsDir?: string;
26
- componentsDir?: string;
27
- mdxCompilerOptions?: CompileOptions;
28
- mdxExtensions: string[];
29
- integrationName: string;
30
- hasRouterAdapter: boolean;
31
- }
32
-
33
- /**
34
- * Manages page module loading (including MDX compilation), config metadata
35
- * resolution, and hydration analysis for React pages.
36
- */
37
- export class ReactPageModuleService {
38
- constructor(private readonly config: ReactPageModuleServiceConfig) {}
39
-
40
- /**
41
- * Checks if the given file path corresponds to an MDX file based on configured extensions.
42
- * @param filePath - The file path to check
43
- * @returns True if the file is an MDX file
44
- */
45
- isMdxFile(filePath: string): boolean {
46
- return this.config.mdxExtensions.some((ext) => filePath.endsWith(ext));
47
- }
48
-
49
- /**
50
- * Compiles and imports an MDX file as a page module.
51
- *
52
- * @param filePath - Absolute path to the MDX file
53
- * @returns The imported module
54
- */
55
- async importMdxPageFile(filePath: string): Promise<unknown> {
56
- const { createReactMdxLoaderPlugin } = await import('../utils/react-mdx-loader-plugin.ts');
57
- const mdxPlugin = createReactMdxLoaderPlugin(
58
- this.config.mdxCompilerOptions ?? {
59
- jsxImportSource: 'react',
60
- jsxRuntime: 'automatic',
61
- development: process?.env?.NODE_ENV === 'development',
62
- },
63
- );
64
-
65
- const outdir = path.join(this.config.distDir, '.server-modules-react-mdx');
66
- const fileBaseName = path.basename(filePath, path.extname(filePath));
67
- const fileHash = fileSystem.hash(filePath);
68
- const cacheBuster = process?.env?.NODE_ENV === 'development' ? `-${Date.now()}` : '';
69
- const outputFileName = `${fileBaseName}-${fileHash}${cacheBuster}.js`;
70
-
71
- const buildResult = await defaultBuildAdapter.build({
72
- entrypoints: [filePath],
73
- root: this.config.rootDir,
74
- outdir,
75
- target: 'node',
76
- format: 'esm',
77
- sourcemap: 'none',
78
- splitting: false,
79
- minify: false,
80
- treeshaking: false,
81
- naming: outputFileName,
82
- plugins: [mdxPlugin],
83
- });
84
-
85
- if (!buildResult.success) {
86
- const details = buildResult.logs.map((log) => log.message).join(' | ');
87
- throw new Error(`Failed to compile MDX page module: ${details}`);
88
- }
89
-
90
- const preferredOutputPath = path.join(outdir, outputFileName);
91
- const compiledOutput =
92
- buildResult.outputs.find((output) => output.path === preferredOutputPath)?.path ??
93
- buildResult.outputs.find((output) => output.path.endsWith('.js'))?.path;
94
-
95
- if (!compiledOutput) {
96
- throw new Error(`No compiled MDX output generated for page: ${filePath}`);
97
- }
98
-
99
- return await import(pathToFileURL(compiledOutput).href);
100
- }
101
-
102
- /**
103
- * Ensures that an EcoComponentConfig has proper `__eco` metadata attached.
104
- * Resolves the file path from dependency declarations when not already set.
105
- *
106
- * @param config - The component config to augment
107
- * @param pagePath - Fallback file path if dependency resolution fails
108
- * @returns Config with `__eco` metadata populated
109
- */
110
- ensureConfigFileMetadata(config: EcoComponentConfig, pagePath: string): EcoComponentConfig {
111
- if (config.__eco?.file) {
112
- return config;
113
- }
114
-
115
- const buildEcoMeta = (file: string) => ({
116
- id: config.__eco?.id ?? rapidhash(file).toString(36),
117
- integration: config.__eco?.integration ?? this.config.integrationName,
118
- file,
119
- });
120
-
121
- const resolveDependencyValue = (value: string | { src?: string }) =>
122
- typeof value === 'string' ? value : value.src;
123
-
124
- const dependencyPaths = [
125
- ...(config.dependencies?.stylesheets ?? []).map(resolveDependencyValue),
126
- ...(config.dependencies?.scripts ?? []).map(resolveDependencyValue),
127
- ]
128
- .filter((value): value is string => Boolean(value))
129
- .filter((value) => value.startsWith('./') || value.startsWith('../'));
130
-
131
- const candidateDirs = [this.config.layoutsDir, this.config.componentsDir, path.dirname(pagePath)].filter(
132
- (value): value is string => typeof value === 'string' && value.length > 0,
133
- );
134
-
135
- for (const dependencyPath of dependencyPaths) {
136
- for (const candidateDir of candidateDirs) {
137
- const resolvedDependency = path.resolve(candidateDir, dependencyPath);
138
- if (fileSystem.exists(resolvedDependency)) {
139
- return {
140
- ...config,
141
- __eco: buildEcoMeta(resolvedDependency),
142
- };
143
- }
144
- }
145
- }
146
-
147
- return {
148
- ...config,
149
- __eco: buildEcoMeta(pagePath),
150
- };
151
- }
152
-
153
- /**
154
- * Recursively checks whether a component config tree declares any browser modules.
155
- * Used to determine if a page needs hydration.
156
- */
157
- hasModulesInConfig(config: EcoComponentConfig | undefined, visited = new Set<EcoComponentConfig>()): boolean {
158
- if (!config || visited.has(config)) {
159
- return false;
160
- }
161
-
162
- visited.add(config);
163
-
164
- if (config.dependencies?.modules?.some((entry) => entry.trim().length > 0)) {
165
- return true;
166
- }
167
-
168
- if (config.layout?.config && this.hasModulesInConfig(config.layout.config, visited)) {
169
- return true;
170
- }
171
-
172
- for (const component of config.dependencies?.components ?? []) {
173
- if (this.hasModulesInConfig(component.config, visited)) {
174
- return true;
175
- }
176
- }
177
-
178
- return false;
179
- }
180
-
181
- /**
182
- * Determines whether a page needs client-side hydration.
183
- *
184
- * @param pageModule - The imported page module
185
- * @returns True if the page should be hydrated
186
- */
187
- shouldHydratePage(
188
- pageModule: EcoPageFile<{ config?: EcoComponentConfig }> & { config?: EcoComponentConfig },
189
- ): boolean {
190
- if (this.config.hasRouterAdapter) {
191
- return true;
192
- }
193
-
194
- const pageConfig = pageModule.default?.config;
195
- return this.hasModulesInConfig(pageConfig) || this.hasModulesInConfig(pageModule.config);
196
- }
197
-
198
- /**
199
- * Collects all explicitly declared browser module specifiers from a page module.
200
- *
201
- * @param pageModule - The imported page module
202
- * @returns Deduplicated list of declared module specifiers
203
- */
204
- collectPageDeclaredModules(
205
- pageModule: EcoPageFile<{ config?: EcoComponentConfig }> & { config?: EcoComponentConfig },
206
- ): string[] {
207
- const declarations = [
208
- ...collectDeclaredModulesInConfig(pageModule.default?.config),
209
- ...collectDeclaredModulesInConfig(pageModule.config),
210
- ];
211
-
212
- return Array.from(new Set(declarations));
213
- }
214
- }
@@ -1,271 +0,0 @@
1
- /**
2
- * Runtime bundle service for React integration.
3
- *
4
- * Owns creation of the browser runtime assets for React and React DOM,
5
- * including temporary entry generation, specifier mapping, and React DOM
6
- * interop rewriting.
7
- *
8
- * @module
9
- */
10
-
11
- import fs from 'node:fs';
12
- import path from 'node:path';
13
- import { createRequire } from 'node:module';
14
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
15
- import { type AssetDefinition, AssetFactory } from '@ecopages/core/services/asset-processing-service';
16
- import type { ReactRouterAdapter } from '../router-adapter.ts';
17
-
18
- type RuntimeModuleConfig = {
19
- specifier: string;
20
- defaultExport?: boolean;
21
- };
22
-
23
- export type ReactRuntimeImports = {
24
- react: string;
25
- reactDomClient: string;
26
- reactJsxRuntime: string;
27
- reactJsxDevRuntime: string;
28
- reactDom: string;
29
- router?: string;
30
- };
31
-
32
- export interface ReactRuntimeBundleServiceConfig {
33
- routerAdapter?: ReactRouterAdapter;
34
- }
35
-
36
- export class ReactRuntimeBundleService {
37
- constructor(private readonly config: ReactRuntimeBundleServiceConfig) {}
38
-
39
- getRuntimeImports(): ReactRuntimeImports {
40
- const runtimeImports: ReactRuntimeImports = {
41
- react: this.buildImportMapSourceUrl('react.js'),
42
- reactDomClient: this.buildImportMapSourceUrl('react-dom.js'),
43
- reactJsxRuntime: this.buildImportMapSourceUrl('react.js'),
44
- reactJsxDevRuntime: this.buildImportMapSourceUrl('react.js'),
45
- reactDom: this.buildImportMapSourceUrl('react-dom.js'),
46
- };
47
-
48
- if (this.config.routerAdapter) {
49
- runtimeImports.router = this.buildImportMapSourceUrl(`${this.config.routerAdapter.bundle.outputName}.js`);
50
- }
51
-
52
- return runtimeImports;
53
- }
54
-
55
- getSpecifierMap(): Record<string, string> {
56
- const runtimeImports = this.getRuntimeImports();
57
- const map: Record<string, string> = {
58
- react: runtimeImports.react,
59
- 'react/jsx-runtime': runtimeImports.reactJsxRuntime,
60
- 'react/jsx-dev-runtime': runtimeImports.reactJsxDevRuntime,
61
- 'react-dom': runtimeImports.reactDom,
62
- 'react-dom/client': runtimeImports.reactDomClient,
63
- };
64
-
65
- if (this.config.routerAdapter && runtimeImports.router) {
66
- map[this.config.routerAdapter.importMapKey] = runtimeImports.router;
67
- }
68
-
69
- return map;
70
- }
71
-
72
- getDependencies(): AssetDefinition[] {
73
- const runtimeAttrs = { type: 'module', defer: '' } as const;
74
- const runtimeImports = this.getRuntimeImports();
75
- const reactRuntimeAliasPlugin = this.createRuntimeSpecifierAliasPlugin({
76
- react: runtimeImports.react,
77
- });
78
- const reactDomRuntimeInteropPlugin = this.createReactDomRuntimeInteropPlugin();
79
-
80
- const reactEntry = this.createRuntimeEntry(
81
- [
82
- { specifier: 'react', defaultExport: true },
83
- { specifier: 'react/jsx-runtime' },
84
- { specifier: 'react/jsx-dev-runtime' },
85
- ],
86
- 'react-entry.mjs',
87
- );
88
- const reactDomEntry = this.createRuntimeEntry(
89
- [{ specifier: 'react-dom', defaultExport: true }, { specifier: 'react-dom/client' }],
90
- 'react-dom-entry.mjs',
91
- );
92
-
93
- const dependencies: AssetDefinition[] = [
94
- AssetFactory.createNodeModuleScript({
95
- position: 'head',
96
- importPath: reactEntry,
97
- name: 'react',
98
- excludeFromHtml: true,
99
- bundleOptions: { naming: 'react.js' },
100
- attributes: runtimeAttrs,
101
- }),
102
- AssetFactory.createNodeModuleScript({
103
- position: 'head',
104
- importPath: reactDomEntry,
105
- name: 'react-dom',
106
- excludeFromHtml: true,
107
- bundleOptions: {
108
- naming: 'react-dom.js',
109
- plugins: [reactRuntimeAliasPlugin, reactDomRuntimeInteropPlugin],
110
- },
111
- attributes: runtimeAttrs,
112
- }),
113
- ];
114
-
115
- if (this.config.routerAdapter) {
116
- const runtimeAliasPlugin = this.createRuntimeAliasPlugin();
117
- const mappedSpecifiers = new Set(Object.keys(this.getSpecifierMap()));
118
- const unresolvedExternals = this.config.routerAdapter.bundle.externals.filter(
119
- (external) => !mappedSpecifiers.has(external),
120
- );
121
-
122
- dependencies.push(
123
- AssetFactory.createNodeModuleScript({
124
- position: 'head',
125
- importPath: this.config.routerAdapter.bundle.importPath,
126
- name: this.config.routerAdapter.bundle.outputName,
127
- excludeFromHtml: true,
128
- bundleOptions: {
129
- naming: `${this.config.routerAdapter.bundle.outputName}.js`,
130
- external: unresolvedExternals,
131
- plugins: [runtimeAliasPlugin],
132
- },
133
- attributes: runtimeAttrs,
134
- }),
135
- );
136
- }
137
-
138
- return dependencies;
139
- }
140
-
141
- createRuntimeAliasPlugin(): EcoBuildPlugin {
142
- const specifierMap = this.getSpecifierMap();
143
- const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
144
- const filter = new RegExp(
145
- `^(${Object.keys(specifierMap)
146
- .map((key) => escapeRegExp(key))
147
- .join('|')})$`,
148
- );
149
-
150
- return {
151
- name: 'react-plugin-runtime-alias',
152
- setup(build) {
153
- build.onResolve({ filter }, (args) => {
154
- const mappedPath = specifierMap[args.path];
155
- if (!mappedPath) {
156
- return undefined;
157
- }
158
-
159
- return {
160
- path: mappedPath,
161
- external: true,
162
- };
163
- });
164
- },
165
- };
166
- }
167
-
168
- private buildImportMapSourceUrl(fileName: string): string {
169
- return `/${AssetFactory.RESOLVED_ASSETS_VENDORS_DIR}/${fileName}`;
170
- }
171
-
172
- private createRuntimeSpecifierAliasPlugin(specifierMap: Record<string, string>, external = true): EcoBuildPlugin {
173
- const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
174
- const filter = new RegExp(
175
- `^(${Object.keys(specifierMap)
176
- .map((key) => escapeRegExp(key))
177
- .join('|')})$`,
178
- );
179
-
180
- return {
181
- name: 'react-plugin-runtime-specifier-alias',
182
- setup(build) {
183
- build.onResolve({ filter }, (args) => {
184
- const mappedPath = specifierMap[args.path];
185
- if (!mappedPath) {
186
- return undefined;
187
- }
188
-
189
- return {
190
- path: mappedPath,
191
- external,
192
- };
193
- });
194
- },
195
- };
196
- }
197
-
198
- private createReactDomRuntimeInteropPlugin(): EcoBuildPlugin {
199
- const reactDomFileFilter = /[\\/]react-dom[\\/].*\.js$/;
200
- const reactRequirePattern = /\brequire\((['"])react\1\)/g;
201
-
202
- return {
203
- name: 'react-dom-runtime-interop',
204
- setup(build) {
205
- build.onLoad({ filter: reactDomFileFilter }, (args) => {
206
- const content = fs.readFileSync(args.path, 'utf-8');
207
- if (!reactRequirePattern.test(content)) {
208
- return undefined;
209
- }
210
-
211
- reactRequirePattern.lastIndex = 0;
212
- const rewritten = content.replace(reactRequirePattern, '__ecopages_react_runtime');
213
-
214
- return {
215
- contents: `import * as __ecopages_react_runtime from 'react';\n${rewritten}`,
216
- loader: 'js',
217
- resolveDir: path.dirname(args.path),
218
- };
219
- });
220
- },
221
- };
222
- }
223
-
224
- private getRuntimeArtifactsDir(): string {
225
- const tmpDir = path.join(process.cwd(), 'node_modules', '.cache', 'ecopages-react-runtime');
226
- fs.mkdirSync(tmpDir, { recursive: true });
227
- return tmpDir;
228
- }
229
-
230
- private createRuntimeEntry(modules: RuntimeModuleConfig[], fileName: string): string {
231
- const tmpDir = this.getRuntimeArtifactsDir();
232
- const requireFromRoot = createRequire(path.join(process.cwd(), 'package.json'));
233
- const seenExports = new Set<string>();
234
- const statements: string[] = [];
235
-
236
- for (const module of modules) {
237
- if (module.defaultExport) {
238
- statements.push(`import __ecopages_default_export__ from '${module.specifier}';`);
239
- statements.push('export default __ecopages_default_export__;');
240
- }
241
-
242
- const exportNames = this.getModuleExportNames(module.specifier, requireFromRoot).filter(
243
- (name) => !seenExports.has(name),
244
- );
245
-
246
- if (exportNames.length > 0) {
247
- statements.push(`export { ${exportNames.join(', ')} } from '${module.specifier}';`);
248
- for (const exportName of exportNames) {
249
- seenExports.add(exportName);
250
- }
251
- }
252
- }
253
-
254
- const filePath = path.join(tmpDir, fileName);
255
- fs.writeFileSync(filePath, statements.join('\n'), 'utf-8');
256
- return filePath;
257
- }
258
-
259
- private getModuleExportNames(specifier: string, requireFromRoot: ReturnType<typeof createRequire>): string[] {
260
- const moduleExports = requireFromRoot(specifier);
261
-
262
- return Object.keys(moduleExports)
263
- .filter((name) => name !== '__esModule' && name !== 'default')
264
- .filter((name) => this.isValidExportName(name))
265
- .sort();
266
- }
267
-
268
- private isValidExportName(name: string): boolean {
269
- return /^[$A-Z_a-z][$\w]*$/.test(name);
270
- }
271
- }