@ecopages/react 0.2.0-alpha.9 → 0.2.0-beta.1

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 (77) hide show
  1. package/README.md +30 -13
  2. package/package.json +23 -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 +102 -18
  6. package/src/react-hmr-strategy.js +427 -50
  7. package/src/react-renderer.d.ts +100 -92
  8. package/src/react-renderer.js +356 -340
  9. package/src/react.constants.d.ts +1 -0
  10. package/src/react.constants.js +4 -0
  11. package/src/react.plugin.d.ts +25 -107
  12. package/src/react.plugin.js +109 -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/runtime/use-sync-external-store-with-selector.d.ts +3 -0
  17. package/src/runtime/use-sync-external-store-with-selector.js +56 -0
  18. package/src/services/pages-index.d.ts +64 -0
  19. package/src/services/pages-index.js +73 -0
  20. package/src/services/react-bundle.service.d.ts +24 -9
  21. package/src/services/react-bundle.service.js +35 -24
  22. package/src/services/react-hmr-page-metadata-cache.d.ts +10 -1
  23. package/src/services/react-hmr-page-metadata-cache.js +18 -2
  24. package/src/services/react-hydration-asset.service.d.ts +28 -19
  25. package/src/services/react-hydration-asset.service.js +83 -64
  26. package/src/services/react-mdx-config-dependency.service.d.ts +36 -0
  27. package/src/services/react-mdx-config-dependency.service.js +122 -0
  28. package/src/services/react-page-module.service.d.ts +8 -3
  29. package/src/services/react-page-module.service.js +33 -26
  30. package/src/services/react-page-payload.service.d.ts +46 -0
  31. package/src/services/react-page-payload.service.js +67 -0
  32. package/src/services/react-runtime-bundle.service.d.ts +9 -2
  33. package/src/services/react-runtime-bundle.service.js +77 -16
  34. package/src/utils/client-graph-boundary-cache.d.ts +108 -0
  35. package/src/utils/client-graph-boundary-cache.js +116 -0
  36. package/src/utils/client-graph-boundary-plugin.d.ts +13 -5
  37. package/src/utils/client-graph-boundary-plugin.js +63 -5
  38. package/src/utils/component-config-traversal.d.ts +36 -0
  39. package/src/utils/component-config-traversal.js +54 -0
  40. package/src/utils/declared-modules.d.ts +1 -1
  41. package/src/utils/declared-modules.js +7 -16
  42. package/src/utils/dynamic.test.browser.d.ts +1 -0
  43. package/src/utils/dynamic.test.browser.js +33 -0
  44. package/src/utils/hydration-scripts.d.ts +9 -5
  45. package/src/utils/hydration-scripts.js +119 -34
  46. package/src/utils/hydration-scripts.test.browser.d.ts +1 -0
  47. package/src/utils/hydration-scripts.test.browser.js +198 -0
  48. package/src/utils/react-dom-runtime-interop-plugin.d.ts +1 -1
  49. package/src/utils/react-dom-runtime-interop-plugin.js +9 -0
  50. package/src/utils/react-mdx-loader-plugin.d.ts +1 -1
  51. package/src/utils/{react-runtime-specifier-map.d.ts → react-runtime-alias-map.d.ts} +3 -1
  52. package/src/utils/react-runtime-alias-map.js +90 -0
  53. package/CHANGELOG.md +0 -27
  54. package/src/react-hmr-strategy.ts +0 -386
  55. package/src/react-renderer.ts +0 -803
  56. package/src/react.plugin.ts +0 -276
  57. package/src/router-adapter.ts +0 -95
  58. package/src/services/react-bundle.service.ts +0 -108
  59. package/src/services/react-hmr-page-metadata-cache.ts +0 -24
  60. package/src/services/react-hydration-asset.service.ts +0 -263
  61. package/src/services/react-page-module.service.ts +0 -224
  62. package/src/services/react-runtime-bundle.service.ts +0 -172
  63. package/src/utils/client-graph-boundary-plugin.ts +0 -831
  64. package/src/utils/client-only.ts +0 -27
  65. package/src/utils/declared-modules.ts +0 -99
  66. package/src/utils/dynamic.ts +0 -27
  67. package/src/utils/hmr-scripts.ts +0 -47
  68. package/src/utils/html-boundary.ts +0 -66
  69. package/src/utils/hydration-scripts.ts +0 -459
  70. package/src/utils/reachability-analyzer.ts +0 -593
  71. package/src/utils/react-dom-runtime-interop-plugin.ts +0 -33
  72. package/src/utils/react-mdx-loader-plugin.ts +0 -63
  73. package/src/utils/react-runtime-specifier-map.js +0 -37
  74. package/src/utils/react-runtime-specifier-map.ts +0 -45
  75. package/src/utils/use-sync-external-store-shim-plugin.d.ts +0 -5
  76. package/src/utils/use-sync-external-store-shim-plugin.js +0 -41
  77. package/src/utils/use-sync-external-store-shim-plugin.ts +0 -45
@@ -1,593 +0,0 @@
1
- /**
2
- * @module ReachabilityAnalyzer
3
- *
4
- * This module is responsible for performing static analysis on Ecopages client components
5
- * using the Oxc AST parser. It computes a strict "reachability graph" of all JavaScript/TypeScript
6
- * dependencies (imports, variables, functions, and classes) that begin from explicit client roots.
7
- *
8
- * In Ecopages, "client roots" are defined as the `render`, `errorBoundary`, or `loadingFallback`
9
- * properties passed into `eco.page()` or `eco.component()`. By tracing the execution path from
10
- * these roots, the analyzer determines exactly which modules and bindings are actually needed
11
- * by the browser to hydrate the page, and which imports are unused on the client (and thus can be pruned).
12
- */
13
-
14
- import { parseSync } from 'oxc-parser';
15
- import { extname } from 'node:path';
16
-
17
- type ParserLanguage = 'js' | 'jsx' | 'ts' | 'tsx';
18
-
19
- /**
20
- * Determines the appropriate parser language configuration for a given file name.
21
- *
22
- * @param filename - The absolute or relative path to the file.
23
- * @returns The Oxc parser language dialect to use ('js', 'jsx', 'ts', or 'tsx').
24
- */
25
- export function parserLanguageForFile(filename: string): ParserLanguage {
26
- const extension = extname(filename).toLowerCase();
27
- if (extension === '.tsx') return 'tsx';
28
- if (extension === '.ts') return 'ts';
29
- if (extension === '.jsx') return 'jsx';
30
- return 'js';
31
- }
32
-
33
- /**
34
- * Represents the computed results of a reachability analysis pass.
35
- */
36
- export type ReachabilityResult = {
37
- /**
38
- * Map from import specifier (e.g. 'node:fs', '@/components/Button')
39
- * to a Set of imported bindings, or '*' for namespace imports.
40
- */
41
- reachableImports: Map<string, Set<string> | '*'>;
42
-
43
- /**
44
- * AST nodes of top-level declarations that are reachable.
45
- */
46
- reachableDeclarations: Set<unknown>;
47
-
48
- unreachableSideEffectImports: unknown[];
49
-
50
- /**
51
- * Indicates whether the file had explicit eco client roots, or fell back to treating all exports as roots.
52
- */
53
- isFallbackRoots: boolean;
54
-
55
- /**
56
- * Whether the file was successfully parsed and analyzed.
57
- */
58
- analyzed: boolean;
59
- };
60
-
61
- /**
62
- * Optional export filter supplied by the client graph boundary when a local
63
- * module is imported through a narrower named-export surface.
64
- *
65
- * `'*'` means the whole module namespace is considered reachable, while a
66
- * `Set` restricts analysis to the named exports that are actually requested by
67
- * downstream client-reachable modules.
68
- */
69
- type ExplicitlyRequestedExports = Set<string> | '*';
70
-
71
- /**
72
- * Analyzes a module using Oxc AST and extracts a strict reachability graph
73
- * starting from client roots (`render`, `errorBoundary`, `loadingFallback` of `eco.page` or `eco.component`).
74
- *
75
- * @param source - Raw source string of the module.
76
- * @param filename - Absolute or relative path to the module file.
77
- * @param program - Optional pre-parsed Oxc program AST. When supplied, the
78
- * internal `parseSync` call is skipped entirely (avoids double-parsing).
79
- * @param explicitlyRequestedExports - Optional named export filter propagated
80
- * from a downstream importer when this module is only partially reachable.
81
- */
82
- export function analyzeReachability(
83
- source: string,
84
- filename: string,
85
- program?: ReturnType<typeof parseSync>['program'],
86
- explicitlyRequestedExports?: ExplicitlyRequestedExports,
87
- ): ReachabilityResult {
88
- /**
89
- * AST Resolution
90
- *
91
- * If the caller already has a parsed AST (e.g. from a prior `parseSync` call in the same
92
- * pipeline), we reuse it directly to avoid double-parsing the same source text.
93
- * Otherwise we parse here and return early with an empty "unanalyzed" result on failure.
94
- */
95
- let resolvedProgram: ReturnType<typeof parseSync>['program'];
96
-
97
- if (program) {
98
- resolvedProgram = program;
99
- } else {
100
- let result;
101
- try {
102
- result = parseSync(filename, source, {
103
- sourceType: 'module',
104
- lang: parserLanguageForFile(filename),
105
- });
106
- } catch {
107
- return {
108
- reachableImports: new Map(),
109
- reachableDeclarations: new Set(),
110
- unreachableSideEffectImports: [],
111
- isFallbackRoots: true,
112
- analyzed: false,
113
- };
114
- }
115
- resolvedProgram = result.program;
116
- }
117
-
118
- /**
119
- * Top-level statement scan
120
- *
121
- * Make a single pass over the top-level AST body to build two indexes:
122
- *
123
- * - `topLevelImports`: every `import` declaration found in this module, keyed by specifier.
124
- * Each entry carries a `bindings` map from local alias → imported name so that later
125
- * identifier lookups can resolve `import { readFile as rf } from 'node:fs'` correctly.
126
- *
127
- * - `topLevelDeclarations`: every locally declared variable, function, or class so that
128
- * the BFS traversal can follow identifier references into their definition nodes.
129
- *
130
- * As declarations are catalogued, `checkPotentialClientRoot()` is called to detect whether
131
- * any of them are `eco.page(…)` or `eco.component(…)` calls — the seed nodes for the graph.
132
- */
133
- const topLevelImports: {
134
- node: unknown;
135
- specifier: string;
136
- bindings: Map<string, string>;
137
- isSideEffect: boolean;
138
- }[] = [];
139
- const topLevelDeclarations: Map<string, unknown> = new Map();
140
- const potentialClientRoots: unknown[] = [];
141
-
142
- for (const statement of resolvedProgram.body) {
143
- if (statement.type === 'ImportDeclaration') {
144
- const specifier = statement.source.value as string;
145
- const bindings = new Map<string, string>();
146
-
147
- if (!statement.specifiers || statement.specifiers.length === 0) {
148
- topLevelImports.push({ node: statement, specifier, bindings, isSideEffect: true });
149
- } else {
150
- for (const spec of statement.specifiers) {
151
- if (spec.type === 'ImportDefaultSpecifier') {
152
- bindings.set(spec.local.name, 'default');
153
- } else if (spec.type === 'ImportNamespaceSpecifier') {
154
- bindings.set(spec.local.name, '*');
155
- } else if (spec.type === 'ImportSpecifier') {
156
- const importedName =
157
- spec.imported.type === 'Identifier' ? spec.imported.name : (spec.imported as any).value;
158
- bindings.set(spec.local.name, importedName);
159
- }
160
- }
161
- topLevelImports.push({ node: statement, specifier, bindings, isSideEffect: false });
162
- }
163
- } else if (statement.type === 'VariableDeclaration') {
164
- for (const decl of statement.declarations) {
165
- if (decl.id.type === 'Identifier') {
166
- topLevelDeclarations.set(decl.id.name, statement);
167
- checkPotentialClientRoot(decl.init);
168
- }
169
- }
170
- } else if (statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration') {
171
- if (statement.id && statement.id.type === 'Identifier') {
172
- topLevelDeclarations.set(statement.id.name, statement);
173
- }
174
- } else if (statement.type === 'ExportNamedDeclaration') {
175
- if (statement.declaration) {
176
- const decl = statement.declaration;
177
- if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
178
- if (decl.id && decl.id.type === 'Identifier') {
179
- topLevelDeclarations.set(decl.id.name, statement);
180
- }
181
- } else if (decl.type === 'VariableDeclaration') {
182
- for (const v of decl.declarations) {
183
- if (v.id.type === 'Identifier') {
184
- topLevelDeclarations.set(v.id.name, statement);
185
- checkPotentialClientRoot(v.init);
186
- }
187
- }
188
- }
189
- }
190
- } else if (statement.type === 'ExportDefaultDeclaration') {
191
- checkPotentialClientRoot(statement.declaration);
192
- } else if (statement.type === 'ExpressionStatement') {
193
- checkPotentialClientRoot(statement.expression);
194
- } else if (statement.type === 'ExportAllDeclaration' && (statement as { source?: { value: string } }).source) {
195
- /**
196
- * `export * from '...'` unconditionally re-exports every binding from the
197
- * source module. Because the re-exported bindings do not create local
198
- * identifiers in this file, the BFS traverser would never encounter them
199
- * naturally. We therefore always seed these nodes into `potentialClientRoots`
200
- * so that `traverse` can mark the source specifier as fully reachable ('*').
201
- */
202
- potentialClientRoots.push(statement);
203
- }
204
- }
205
-
206
- /**
207
- * Inspects a node to determine if it represents an Ecopages client root declaration.
208
- *
209
- * @param node - The AST node to inspect.
210
- */
211
- function checkPotentialClientRoot(node: unknown) {
212
- if (!node || typeof node !== 'object') return;
213
- if (
214
- (node as { type: string }).type === 'CallExpression' &&
215
- (node as { callee: { type: string } }).callee.type === 'MemberExpression'
216
- ) {
217
- const obj = (node as { callee: { object: unknown } }).callee.object;
218
- const prop = (node as { callee: { property: unknown } }).callee.property;
219
- if (
220
- (obj as { type: string }).type === 'Identifier' &&
221
- (obj as { name: string }).name === 'eco' &&
222
- (prop as { type: string }).type === 'Identifier' &&
223
- ((prop as { name: string }).name === 'page' || (prop as { name: string }).name === 'component')
224
- ) {
225
- potentialClientRoots.push((node as { callee: unknown }).callee);
226
-
227
- const arg = (node as { arguments: unknown[] }).arguments[0];
228
- if (arg && (arg as { type: string }).type === 'ObjectExpression') {
229
- for (const prop of (arg as { properties: unknown[] }).properties) {
230
- if (
231
- (prop as { type: string }).type === 'Property' &&
232
- (prop as { key: { type: string } }).key.type === 'Identifier'
233
- ) {
234
- if (
235
- [
236
- 'render',
237
- 'errorBoundary',
238
- 'loadingFallback',
239
- 'clientScripts',
240
- 'dependencies',
241
- ].includes((prop as { key: { name: string } }).key.name)
242
- ) {
243
- potentialClientRoots.push((prop as { value: unknown }).value);
244
- }
245
- }
246
- }
247
- }
248
- }
249
- } else if (
250
- (node as { type: string }).type === 'CallExpression' &&
251
- (node as { callee: { type: string } }).callee.type === 'Identifier' &&
252
- (node as { callee: { name: string } }).callee.name === 'dynamic'
253
- ) {
254
- potentialClientRoots.push(node);
255
- }
256
- }
257
-
258
- /**
259
- * Resolves the externally visible export name from an export specifier.
260
- *
261
- * @param specifier - Oxc export specifier node.
262
- * @returns The exported binding name when available.
263
- */
264
- function getExportedName(specifier: any): string | undefined {
265
- if (specifier?.exported?.type === 'Identifier') return specifier.exported.name;
266
- if (typeof specifier?.exported?.value === 'string') return specifier.exported.value;
267
- if (specifier?.local?.type === 'Identifier') return specifier.local.name;
268
- if (typeof specifier?.local?.value === 'string') return specifier.local.value;
269
- return undefined;
270
- }
271
-
272
- /**
273
- * Resolves the imported binding name represented by a re-export specifier.
274
- *
275
- * @param specifier - Oxc export specifier node.
276
- * @returns The source-module binding name that should be marked reachable.
277
- */
278
- function getReexportedImportName(specifier: any): string | undefined {
279
- if (specifier?.local?.type === 'Identifier') return specifier.local.name;
280
- if (typeof specifier?.local?.value === 'string') return specifier.local.value;
281
- if (specifier?.imported?.type === 'Identifier') return specifier.imported.name;
282
- if (typeof specifier?.imported?.value === 'string') return specifier.imported.value;
283
- return getExportedName(specifier);
284
- }
285
-
286
- /**
287
- * Resolves the local identifier used by a local export list entry.
288
- *
289
- * @param specifier - Oxc export specifier node.
290
- * @returns The local symbol name referenced by the export list.
291
- */
292
- function getLocalExportName(specifier: any): string | undefined {
293
- if (specifier?.local?.type === 'Identifier') return specifier.local.name;
294
- if (typeof specifier?.local?.value === 'string') return specifier.local.value;
295
- return undefined;
296
- }
297
-
298
- /**
299
- * Checks whether a named export is part of the explicitly requested subset.
300
- *
301
- * @param name - Export name to test.
302
- * @returns True when the export should seed or continue traversal.
303
- */
304
- function isExplicitlyRequestedExport(name: string): boolean {
305
- if (explicitlyRequestedExports === '*') return true;
306
- return explicitlyRequestedExports?.has(name) ?? false;
307
- }
308
-
309
- /**
310
- * Client root resolution (fallback mode)
311
- *
312
- * If *no* `eco.page`/`eco.component` call was found in the file, we fall back to treating
313
- * every exported declaration as a potential client root. This covers utility modules that
314
- * don't use Ecopages conventions (plain React components, shared helpers, etc.)
315
- *
316
- * In fallback mode the analysis is intentionally permissive — the `isFallbackRoots` flag
317
- * propagates to callers so they know not to hard-fail on reachable forbidden imports
318
- * (since we cannot be 100% certain about the execution boundary).
319
- */
320
- let isFallbackRoots = false;
321
- if (potentialClientRoots.length === 0) {
322
- if (explicitlyRequestedExports) {
323
- for (const node of resolvedProgram.body) {
324
- if ((node as { type: string }).type === 'ExportNamedDeclaration') {
325
- const exportNode = node as any;
326
- if (exportNode.source && exportNode.specifiers?.length) {
327
- const hasRequestedReexport = exportNode.specifiers.some((specifier: any) => {
328
- const exportedName = getExportedName(specifier);
329
- return exportedName ? isExplicitlyRequestedExport(exportedName) : false;
330
- });
331
- if (hasRequestedReexport) {
332
- potentialClientRoots.push(node);
333
- }
334
- continue;
335
- }
336
-
337
- if (
338
- exportNode.declaration?.type === 'FunctionDeclaration' ||
339
- exportNode.declaration?.type === 'ClassDeclaration'
340
- ) {
341
- const declarationName = exportNode.declaration.id?.name;
342
- if (declarationName && isExplicitlyRequestedExport(declarationName)) {
343
- potentialClientRoots.push(node);
344
- }
345
- continue;
346
- }
347
-
348
- if (exportNode.declaration?.type === 'VariableDeclaration') {
349
- const hasRequestedDeclaration = exportNode.declaration.declarations.some(
350
- (declaration: any) =>
351
- declaration.id?.type === 'Identifier' &&
352
- isExplicitlyRequestedExport(declaration.id.name),
353
- );
354
- if (hasRequestedDeclaration) {
355
- potentialClientRoots.push(node);
356
- }
357
- continue;
358
- }
359
-
360
- if (exportNode.specifiers?.length) {
361
- const hasRequestedSpecifier = exportNode.specifiers.some((specifier: any) => {
362
- const exportedName = getExportedName(specifier);
363
- return exportedName ? isExplicitlyRequestedExport(exportedName) : false;
364
- });
365
- if (hasRequestedSpecifier) {
366
- potentialClientRoots.push(node);
367
- }
368
- }
369
- } else if ((node as { type: string }).type === 'ExportDefaultDeclaration') {
370
- if (isExplicitlyRequestedExport('default')) {
371
- potentialClientRoots.push(node);
372
- }
373
- } else if ((node as { type: string }).type === 'ExportAllDeclaration') {
374
- if (explicitlyRequestedExports === '*') {
375
- potentialClientRoots.push(node);
376
- }
377
- }
378
- }
379
- } else {
380
- isFallbackRoots = true;
381
- for (const node of resolvedProgram.body) {
382
- if (
383
- (node as { type: string }).type === 'ExportNamedDeclaration' ||
384
- (node as { type: string }).type === 'ExportDefaultDeclaration' ||
385
- (node as { type: string }).type === 'ExportAllDeclaration'
386
- ) {
387
- potentialClientRoots.push(node);
388
- }
389
- }
390
- }
391
- }
392
-
393
- /**
394
- * BFS reachability traversal
395
- *
396
- * Starting from the seed nodes collected above, we perform a breadth-first walk of the AST.
397
- * Every identifier encountered is checked against `topLevelDeclarations` (to enqueue further
398
- * nodes) and `topLevelImports` (to mark the referenced binding as reachable).
399
- *
400
- * `visitedNodes` guards against infinite cycles in recursive or mutually-recursive declarations.
401
- */
402
- const reachableImports = new Map<string, Set<string> | '*'>();
403
- const reachableDeclarations = new Set<unknown>();
404
- const queue: unknown[] = [...potentialClientRoots];
405
- const visitedNodes = new Set<unknown>();
406
-
407
- /**
408
- * Registers an imported binding as reachable in the client graph.
409
- *
410
- * @param specifier - The module specifier from which the binding is imported.
411
- * @param importedName - The specific named export being imported, or '*' for namespace imports.
412
- */
413
- function markImportReachable(specifier: string, importedName: string) {
414
- let current = reachableImports.get(specifier);
415
- if (current === '*') return;
416
-
417
- if (importedName === '*') {
418
- reachableImports.set(specifier, '*');
419
- } else {
420
- if (!current) {
421
- current = new Set<string>();
422
- reachableImports.set(specifier, current);
423
- }
424
- current.add(importedName);
425
- }
426
- }
427
-
428
- /**
429
- * Traces an identifier to its origin declaration, enqueuing it for deep traversal if it resolves
430
- * to a local module-level declaration, or marking it as a reachable import if it originates from another module.
431
- *
432
- * @param name - The identifier name to check.
433
- */
434
- function checkIdentifier(name: string) {
435
- if (topLevelDeclarations.has(name)) {
436
- const declNode = topLevelDeclarations.get(name);
437
- if (!reachableDeclarations.has(declNode)) {
438
- reachableDeclarations.add(declNode);
439
- queue.push(declNode);
440
- }
441
- }
442
-
443
- for (const imp of topLevelImports) {
444
- if (imp.bindings.has(name)) {
445
- markImportReachable(imp.specifier, imp.bindings.get(name)!);
446
- }
447
- }
448
- }
449
-
450
- /**
451
- * Recursively walks down an AST node to discover referenced variables and function calls,
452
- * building out the reachability graph.
453
- *
454
- * @param node - The Oxc AST node to traverse. Typed as `any` because Oxc lacks a unified iterable node type.
455
- * @param localScope - A set of identifiers that shadow module-level declarations within the current lexical scope.
456
- */
457
- function traverse(node: any, localScope: Set<string>) {
458
- if (!node || typeof node !== 'object') return;
459
- if (visitedNodes.has(node)) return;
460
- visitedNodes.add(node);
461
-
462
- if (Array.isArray(node)) {
463
- for (const child of node) traverse(child, localScope);
464
- return;
465
- }
466
-
467
- const currentScope = localScope;
468
-
469
- /**
470
- * `export * from '...'` nodes are seeded into the BFS queue by the top-level scan.
471
- * When the traverser reaches one here we immediately mark the entire source module
472
- * as reachable ('*'), then stop — there are no local identifiers to follow.
473
- */
474
- if (node.type === 'ExportAllDeclaration' && typeof node.source?.value === 'string') {
475
- markImportReachable(node.source.value as string, '*');
476
- return;
477
- }
478
-
479
- if (node.type === 'ExportNamedDeclaration' && typeof node.source?.value === 'string') {
480
- for (const specifier of node.specifiers ?? []) {
481
- const importedName = getReexportedImportName(specifier);
482
- if (importedName) {
483
- markImportReachable(node.source.value as string, importedName);
484
- }
485
- }
486
- return;
487
- }
488
-
489
- if (
490
- node.type === 'ExportNamedDeclaration' &&
491
- !node.source &&
492
- explicitlyRequestedExports &&
493
- node.specifiers?.length
494
- ) {
495
- for (const specifier of node.specifiers) {
496
- const exportedName = getExportedName(specifier);
497
- if (!exportedName || !isExplicitlyRequestedExport(exportedName)) {
498
- continue;
499
- }
500
-
501
- const localName = getLocalExportName(specifier);
502
- if (localName && !currentScope.has(localName)) {
503
- checkIdentifier(localName);
504
- }
505
- }
506
- return;
507
- }
508
-
509
- if (node.type === 'Identifier' || (node.type === 'JSXIdentifier' && /^[A-Z]/.test(node.name))) {
510
- if (!currentScope.has(node.name)) {
511
- checkIdentifier(node.name);
512
- }
513
- } else if (node.type === 'MemberExpression') {
514
- traverse(node.object, currentScope);
515
- if (node.computed) {
516
- traverse(node.property, currentScope);
517
- }
518
- return;
519
- } else if (node.type === 'Property') {
520
- if (node.computed) traverse(node.key, currentScope);
521
- traverse(node.value, currentScope);
522
- return;
523
- } else if (node.type === 'JSXOpeningElement' || node.type === 'JSXClosingElement') {
524
- traverse(node.name, currentScope);
525
- if (node.attributes) {
526
- for (const attr of node.attributes) traverse(attr, currentScope);
527
- }
528
- return;
529
- } else if (node.type === 'JSXIdentifier') {
530
- if (/^[A-Z]/.test(node.name) && !currentScope.has(node.name)) {
531
- checkIdentifier(node.name);
532
- }
533
- } else if (node.type === 'JSXMemberExpression') {
534
- traverse(node.object, currentScope);
535
- return;
536
- } else if (
537
- node.type === 'CallExpression' &&
538
- node.callee.type === 'Identifier' &&
539
- node.callee.name === 'dynamic'
540
- ) {
541
- const arg = node.arguments[0];
542
- if (arg && (arg.type === 'ArrowFunctionExpression' || arg.type === 'FunctionExpression')) {
543
- const body = arg.body;
544
- if (body.type === 'ImportExpression' && body.source.type === 'Literal') {
545
- markImportReachable(body.source.value as string, '*');
546
- }
547
- }
548
- } else if (node.type === 'ImportExpression' && node.source.type === 'Literal') {
549
- markImportReachable(node.source.value as string, '*');
550
- }
551
-
552
- if (
553
- node.type === 'ArrowFunctionExpression' ||
554
- node.type === 'FunctionExpression' ||
555
- node.type === 'FunctionDeclaration'
556
- ) {
557
- const newScope = new Set(currentScope);
558
- if (node.id && node.id.type === 'Identifier') newScope.add(node.id.name);
559
- if (node.params && node.params.items) {
560
- for (const p of node.params.items) {
561
- if (p.pattern && p.pattern.type === 'Identifier') {
562
- newScope.add(p.pattern.name);
563
- }
564
- }
565
- }
566
- traverse(node.body, newScope);
567
- return;
568
- }
569
-
570
- for (const key in node) {
571
- if (key !== 'type' && key !== 'start' && key !== 'end') {
572
- traverse(node[key], currentScope);
573
- }
574
- }
575
- }
576
-
577
- while (queue.length > 0) {
578
- const root = queue.shift();
579
- traverse(root, new Set());
580
- }
581
-
582
- const unreachableSideEffectImports = topLevelImports
583
- .filter((imp) => imp.isSideEffect && !reachableImports.has(imp.specifier))
584
- .map((imp) => imp.node);
585
-
586
- return {
587
- reachableImports,
588
- reachableDeclarations,
589
- unreachableSideEffectImports,
590
- isFallbackRoots,
591
- analyzed: true,
592
- };
593
- }
@@ -1,33 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
4
-
5
- export function createReactDomRuntimeInteropPlugin(options?: {
6
- name?: string;
7
- reactSpecifier?: string;
8
- }): EcoBuildPlugin {
9
- const reactDomFileFilter = /[\\/]react-dom[\\/].*\.js$/;
10
- const reactRequirePattern = /\brequire\((['"])react\1\)/g;
11
- const reactSpecifier = options?.reactSpecifier ?? 'react';
12
-
13
- return {
14
- name: options?.name ?? 'react-dom-runtime-interop',
15
- setup(build) {
16
- build.onLoad({ filter: reactDomFileFilter }, (args) => {
17
- const content = fs.readFileSync(args.path, 'utf-8');
18
- if (!reactRequirePattern.test(content)) {
19
- return undefined;
20
- }
21
-
22
- reactRequirePattern.lastIndex = 0;
23
- const rewritten = content.replace(reactRequirePattern, '__ecopages_react_runtime');
24
-
25
- return {
26
- contents: `import * as __ecopages_react_runtime from '${reactSpecifier}';\n${rewritten}`,
27
- loader: 'js',
28
- resolveDir: path.dirname(args.path),
29
- };
30
- });
31
- },
32
- };
33
- }
@@ -1,63 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import path from 'node:path';
3
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
4
- import { type CompileOptions, compile } from '@mdx-js/mdx';
5
- import sourceMap from 'source-map';
6
- import { VFile } from 'vfile';
7
-
8
- /**
9
- * Resolves the MDX parser mode for a React-backed MDX source file.
10
- *
11
- * When `.md` files are explicitly opted into the React MDX loader they still
12
- * need to compile as MDX, not plain markdown. Otherwise top-level ESM exports
13
- * like `config` are treated as literal text and the route cannot execute as a
14
- * page module.
15
- *
16
- * @param filePath Absolute or relative source file path.
17
- * @param compilerOptions User-provided MDX compiler options.
18
- * @returns The compile format that should be passed to `@mdx-js/mdx`.
19
- */
20
- function resolveCompileFormat(filePath: string, compilerOptions?: CompileOptions): CompileOptions['format'] {
21
- const configuredFormat = compilerOptions?.format;
22
-
23
- if (configuredFormat && configuredFormat !== 'detect') {
24
- return configuredFormat;
25
- }
26
-
27
- return path.extname(filePath).toLowerCase() === '.md' ? 'mdx' : configuredFormat;
28
- }
29
-
30
- export function createReactMdxLoaderPlugin(compilerOptions?: CompileOptions): EcoBuildPlugin {
31
- const mdxExtensions = compilerOptions?.mdxExtensions ?? ['.mdx'];
32
- const mdExtensions = compilerOptions?.mdExtensions ?? [];
33
- const allExtensions = [...mdxExtensions, ...mdExtensions];
34
- const escapedExts = allExtensions.map((ext) => ext.replace('.', '\\.'));
35
- const filter = new RegExp(`(${escapedExts.join('|')})(\\?.*)?$`);
36
-
37
- return {
38
- name: 'react-mdx-loader',
39
- setup(build) {
40
- build.onLoad({ filter }, async (args) => {
41
- const filePath = args.path.includes('?') ? args.path.split('?')[0] : args.path;
42
- const source = readFileSync(filePath, 'utf-8');
43
- const file = new VFile({ path: filePath, value: source });
44
-
45
- const compiled = await compile(file, {
46
- ...compilerOptions,
47
- format: resolveCompileFormat(filePath, compilerOptions),
48
- SourceMapGenerator: sourceMap.SourceMapGenerator,
49
- });
50
-
51
- const inlineSourceMap = compiled.map
52
- ? `\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(JSON.stringify(compiled.map)).toString('base64')}\n`
53
- : '';
54
-
55
- return {
56
- contents: `${String(compiled.value)}${inlineSourceMap}`,
57
- loader: compilerOptions?.jsx ? 'jsx' : 'js',
58
- resolveDir: path.dirname(args.path),
59
- };
60
- });
61
- },
62
- };
63
- }