@ecopages/core 0.2.0-rc.1 → 0.2.0-rc.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/core",
3
- "version": "0.2.0-rc.1",
3
+ "version": "0.2.0-rc.3",
4
4
  "description": "Core package for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -17,7 +17,7 @@
17
17
  "directory": "packages/core"
18
18
  },
19
19
  "dependencies": {
20
- "@ecopages/file-system": "0.2.0-rc.1",
20
+ "@ecopages/file-system": "0.2.0-rc.3",
21
21
  "@ecopages/logger": "^0.2.3",
22
22
  "@ecopages/scripts-injector": "^0.1.5",
23
23
  "@oxc-project/runtime": "0.141.0",
@@ -31,7 +31,7 @@
31
31
  "@standard-schema/utils": "^0.3.0"
32
32
  },
33
33
  "peerDependencies": {
34
- "@ecopages/dev-toolbar": "0.2.0-rc.1"
34
+ "@ecopages/dev-toolbar": "0.2.0-rc.3"
35
35
  },
36
36
  "peerDependenciesMeta": {
37
37
  "@ecopages/dev-toolbar": {
@@ -28,6 +28,8 @@ The asset-processing service caches emitted file assets by source identity in de
28
28
  When a source file changes, its source hash or explicit invalidation removes the cached asset before the next render,
29
29
  so shared layout styles do not need to be rebuilt for every navigated page while HMR remains fresh.
30
30
 
31
+ Browser runtime module assets resolve bare package roots through their ESM import target. For legacy packages without an `exports` map, they prefer `package.json#module` over CJS `main`; CJS resolution is only a compatibility fallback. Generated entries use `export *` for ESM files and explicit named re-exports for CJS files (so bindings such as React `jsx` exist on the vendor). A default binding is added only when the selected entry exposes one. Runtime vendors are package-root contracts: subpath imports need their own vendor declaration or remain in the consuming bundle.
32
+
31
33
  ## Design Rule
32
34
 
33
35
  If a concern affects more than one integration or more than one runtime adapter, it usually belongs here instead of in a package-specific implementation.
@@ -2,9 +2,21 @@ import type { createRequire } from 'node:module';
2
2
  export type BrowserRuntimeDefaultExportPolicy = 'emit-default' | 'skip-default';
3
3
  type RequireFromRoot = ReturnType<typeof createRequire>;
4
4
  /**
5
- * Resolves a package specifier to its ESM entry file path from the app root.
5
+ * Resolves a package specifier through Node's ESM import conditions from the app root.
6
6
  */
7
7
  export declare function resolvePackageEsmEntryPath(specifier: string, rootDir: string): string | undefined;
8
+ /**
9
+ * Resolves a browser runtime specifier to the absolute file that the generated
10
+ * vendor entry will import, when that import is a path.
11
+ *
12
+ * @remarks
13
+ * `node:` specifiers have no file path. `file:` URLs are converted to paths.
14
+ */
15
+ export declare function resolveBrowserRuntimeEntryPath(options: {
16
+ specifier: string;
17
+ requireFromRoot: RequireFromRoot;
18
+ rootDir: string;
19
+ }): string | undefined;
8
20
  /**
9
21
  * Builds a relative import path from a generated runtime entry file to a resolved module path.
10
22
  */
@@ -13,10 +25,9 @@ export declare function toRelativeEntryImport(entryDir: string, resolvedPath: st
13
25
  * Resolves a browser runtime entry import to an ESM file path when possible.
14
26
  *
15
27
  * @remarks
16
- * `createRequire().resolve()` follows the `require` export condition and can
17
- * land on `.cjs` entrypoints. Browser vendor bundles then emit runtime
18
- * `require()` calls for React externals. Prefer Node's ESM resolver first, then
19
- * a `.cjs` → `.js` sibling fallback.
28
+ * Browser vendor entries resolve through ESM import conditions first. A package
29
+ * root without `exports` may instead select its legacy `module` field; CJS
30
+ * resolution is the final compatibility fallback.
20
31
  */
21
32
  export declare function resolveBrowserRuntimeEntryImport(options: {
22
33
  specifier: string;
@@ -42,11 +53,21 @@ export declare function inferBrowserRuntimeDefaultExportPolicy(options: {
42
53
  rootDir: string;
43
54
  }): BrowserRuntimeDefaultExportPolicy;
44
55
  /**
45
- * Reads the named runtime exports that should be re-exported from a generated runtime entry module.
56
+ * Returns true when the resolved vendor file is an ESM module that Rolldown can
57
+ * re-export with `export *`.
58
+ *
59
+ * @remarks
60
+ * React's published "import" files are still CJS wrappers. `export *` from those
61
+ * files does not emit named ESM bindings such as `jsx`.
62
+ */
63
+ export declare function browserRuntimeEntryHasEsmExports(entryPath: string | undefined): boolean;
64
+ /**
65
+ * Reads enumerable CJS exports from the same file the generated entry imports.
46
66
  *
47
67
  * @remarks
48
- * Default exports are handled separately because generated runtime entry files need to emit a
49
- * synthetic default binding only when the caller explicitly asks for it.
68
+ * Used only when {@link browserRuntimeEntryHasEsmExports} is false. ESM entries
69
+ * must not harvest names from `require()`, which copies class statics that are
70
+ * not ESM named exports.
50
71
  */
51
- export declare function listBrowserRuntimeModuleExportNames(specifier: string, requireFromRoot: RequireFromRoot): string[];
72
+ export declare function listBrowserRuntimeCjsExportNames(moduleId: string, requireFromRoot: RequireFromRoot): string[];
52
73
  export {};
@@ -2,8 +2,9 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { pathToFileURL, fileURLToPath } from 'node:url';
4
4
  import { isBarePackageImportSpecifier } from '../../../plugins/tsconfig-import-resolver.js';
5
+ import { toPackageRootSpecifier } from '../../../plugins/package-specifier.js';
5
6
  /**
6
- * Resolves a package specifier to its ESM entry file path from the app root.
7
+ * Resolves a package specifier through Node's ESM import conditions from the app root.
7
8
  */
8
9
  export function resolvePackageEsmEntryPath(specifier, rootDir) {
9
10
  try {
@@ -13,6 +14,71 @@ export function resolvePackageEsmEntryPath(specifier, rootDir) {
13
14
  return undefined;
14
15
  }
15
16
  }
17
+ function resolveLegacyPackageModuleEntry(options) {
18
+ if (!isBarePackageImportSpecifier(options.specifier, options.rootDir)) {
19
+ return undefined;
20
+ }
21
+ const packageRoot = toPackageRootSpecifier(options.specifier);
22
+ if (options.specifier !== packageRoot) {
23
+ return undefined;
24
+ }
25
+ try {
26
+ const packageJsonPath = options.requireFromRoot.resolve(`${packageRoot}/package.json`);
27
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
28
+ if (packageJson.exports !== undefined || typeof packageJson.module !== 'string') {
29
+ return undefined;
30
+ }
31
+ const modulePath = path.resolve(path.dirname(packageJsonPath), packageJson.module);
32
+ return fs.existsSync(modulePath) ? modulePath : undefined;
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
38
+ const ESM_DEFAULT_EXPORT = /\bexport\s+default\b/;
39
+ const ESM_NAMED_EXPORT = /\bexport\s+(?:[\w*{]|const|let|var|function|class)/;
40
+ const EXPORTABLE_BINDING_NAME = /^[$A-Z_a-z][$\w]*$/;
41
+ /**
42
+ * Resolves a browser runtime specifier to the absolute file that the generated
43
+ * vendor entry will import, when that import is a path.
44
+ *
45
+ * @remarks
46
+ * `node:` specifiers have no file path. `file:` URLs are converted to paths.
47
+ */
48
+ export function resolveBrowserRuntimeEntryPath(options) {
49
+ const { specifier, requireFromRoot, rootDir } = options;
50
+ if (specifier.startsWith('node:')) {
51
+ return undefined;
52
+ }
53
+ if (specifier.startsWith('file:')) {
54
+ return fileURLToPath(specifier);
55
+ }
56
+ if (specifier.startsWith('.')) {
57
+ return requireFromRoot.resolve(specifier);
58
+ }
59
+ const esmResolvedPath = isBarePackageImportSpecifier(specifier, rootDir)
60
+ ? resolvePackageEsmEntryPath(specifier, rootDir)
61
+ : undefined;
62
+ const legacyModulePath = resolveLegacyPackageModuleEntry({ specifier, requireFromRoot, rootDir });
63
+ if (legacyModulePath) {
64
+ return legacyModulePath;
65
+ }
66
+ if (esmResolvedPath) {
67
+ return esmResolvedPath;
68
+ }
69
+ try {
70
+ const resolvedPath = requireFromRoot.resolve(specifier);
71
+ const esmSibling = resolvedPath.endsWith('.cjs')
72
+ ? `${resolvedPath.slice(0, -4)}.js`
73
+ : resolvedPath.endsWith('.cts')
74
+ ? `${resolvedPath.slice(0, -4)}.ts`
75
+ : undefined;
76
+ return esmSibling && fs.existsSync(esmSibling) ? esmSibling : resolvedPath;
77
+ }
78
+ catch {
79
+ return undefined;
80
+ }
81
+ }
16
82
  /**
17
83
  * Builds a relative import path from a generated runtime entry file to a resolved module path.
18
84
  */
@@ -27,36 +93,20 @@ export function toRelativeEntryImport(entryDir, resolvedPath) {
27
93
  * Resolves a browser runtime entry import to an ESM file path when possible.
28
94
  *
29
95
  * @remarks
30
- * `createRequire().resolve()` follows the `require` export condition and can
31
- * land on `.cjs` entrypoints. Browser vendor bundles then emit runtime
32
- * `require()` calls for React externals. Prefer Node's ESM resolver first, then
33
- * a `.cjs` → `.js` sibling fallback.
96
+ * Browser vendor entries resolve through ESM import conditions first. A package
97
+ * root without `exports` may instead select its legacy `module` field; CJS
98
+ * resolution is the final compatibility fallback.
34
99
  */
35
100
  export function resolveBrowserRuntimeEntryImport(options) {
36
101
  const { specifier, requireFromRoot, entryDir, rootDir } = options;
37
102
  if (specifier.startsWith('node:') || specifier.startsWith('file:')) {
38
103
  return specifier;
39
104
  }
40
- if (specifier.startsWith('.')) {
41
- const resolvedPath = requireFromRoot.resolve(specifier);
42
- return toRelativeEntryImport(entryDir, resolvedPath);
43
- }
44
- if (isBarePackageImportSpecifier(specifier, rootDir)) {
45
- const esmResolvedPath = resolvePackageEsmEntryPath(specifier, rootDir);
46
- if (esmResolvedPath) {
47
- return toRelativeEntryImport(entryDir, esmResolvedPath);
48
- }
105
+ const entryPath = resolveBrowserRuntimeEntryPath({ specifier, requireFromRoot, rootDir });
106
+ if (!entryPath) {
107
+ throw new Error(`Unable to resolve browser runtime entry for "${specifier}" from "${rootDir}"`);
49
108
  }
50
- const resolvedPath = requireFromRoot.resolve(specifier);
51
- const esmSibling = resolvedPath.endsWith('.cjs')
52
- ? `${resolvedPath.slice(0, -4)}.js`
53
- : resolvedPath.endsWith('.cts')
54
- ? `${resolvedPath.slice(0, -4)}.ts`
55
- : undefined;
56
- if (esmSibling && fs.existsSync(esmSibling)) {
57
- return toRelativeEntryImport(entryDir, esmSibling);
58
- }
59
- return toRelativeEntryImport(entryDir, resolvedPath);
109
+ return toRelativeEntryImport(entryDir, entryPath);
60
110
  }
61
111
  /**
62
112
  * Decides whether a generated runtime entry should re-export a default binding.
@@ -72,13 +122,13 @@ export function resolveBrowserRuntimeEntryImport(options) {
72
122
  */
73
123
  export function inferBrowserRuntimeDefaultExportPolicy(options) {
74
124
  const { specifier, requireFromRoot, rootDir } = options;
75
- const esmPath = resolvePackageEsmEntryPath(specifier, rootDir);
76
- if (esmPath && fs.existsSync(esmPath)) {
77
- const source = fs.readFileSync(esmPath, 'utf8');
78
- if (/\bexport\s+default\b/.test(source)) {
125
+ const entryPath = resolveBrowserRuntimeEntryPath({ specifier, requireFromRoot, rootDir });
126
+ if (entryPath && fs.existsSync(entryPath)) {
127
+ const source = fs.readFileSync(entryPath, 'utf8');
128
+ if (ESM_DEFAULT_EXPORT.test(source)) {
79
129
  return 'emit-default';
80
130
  }
81
- if (/\bexport\s+(?:[\w*{]|const|let|var|function|class)/.test(source)) {
131
+ if (ESM_NAMED_EXPORT.test(source)) {
82
132
  return 'skip-default';
83
133
  }
84
134
  }
@@ -94,22 +144,38 @@ export function inferBrowserRuntimeDefaultExportPolicy(options) {
94
144
  }
95
145
  }
96
146
  /**
97
- * Reads the named runtime exports that should be re-exported from a generated runtime entry module.
147
+ * Returns true when the resolved vendor file is an ESM module that Rolldown can
148
+ * re-export with `export *`.
149
+ *
150
+ * @remarks
151
+ * React's published "import" files are still CJS wrappers. `export *` from those
152
+ * files does not emit named ESM bindings such as `jsx`.
153
+ */
154
+ export function browserRuntimeEntryHasEsmExports(entryPath) {
155
+ if (!entryPath || !fs.existsSync(entryPath)) {
156
+ return true;
157
+ }
158
+ const source = fs.readFileSync(entryPath, 'utf8');
159
+ return ESM_DEFAULT_EXPORT.test(source) || ESM_NAMED_EXPORT.test(source);
160
+ }
161
+ /**
162
+ * Reads enumerable CJS exports from the same file the generated entry imports.
98
163
  *
99
164
  * @remarks
100
- * Default exports are handled separately because generated runtime entry files need to emit a
101
- * synthetic default binding only when the caller explicitly asks for it.
165
+ * Used only when {@link browserRuntimeEntryHasEsmExports} is false. ESM entries
166
+ * must not harvest names from `require()`, which copies class statics that are
167
+ * not ESM named exports.
102
168
  */
103
- export function listBrowserRuntimeModuleExportNames(specifier, requireFromRoot) {
169
+ export function listBrowserRuntimeCjsExportNames(moduleId, requireFromRoot) {
104
170
  let moduleExports;
105
171
  try {
106
- moduleExports = requireFromRoot(specifier);
172
+ moduleExports = requireFromRoot(moduleId);
107
173
  }
108
174
  catch {
109
175
  return [];
110
176
  }
111
177
  return Object.keys(moduleExports)
112
178
  .filter((name) => name !== '__esModule' && name !== 'default')
113
- .filter((name) => /^[$A-Z_a-z][$\w]*$/.test(name))
179
+ .filter((name) => EXPORTABLE_BINDING_NAME.test(name))
114
180
  .sort();
115
181
  }
@@ -12,6 +12,8 @@ export type BrowserRuntimeEntryModuleConfig = {
12
12
  * logic themselves. The generated file lives under the app work directory so
13
13
  * repeated runs can reuse the same location without placing sources inside
14
14
  * `node_modules`, which can cause bundlers to externalize bare imports.
15
+ * ESM entries use `export *`. CJS entries emit explicit named re-exports from
16
+ * that same file so Rolldown can synthesize ESM bindings such as `jsx`.
15
17
  */
16
18
  export declare function createBrowserRuntimeEntryModule(options: {
17
19
  modules: BrowserRuntimeEntryModuleConfig[];
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { createRequire } from 'node:module';
4
4
  import { DEFAULT_ECOPAGES_WORK_DIR } from '../../../config/constants.js';
5
- import { inferBrowserRuntimeDefaultExportPolicy, listBrowserRuntimeModuleExportNames, resolveBrowserRuntimeEntryImport, } from './browser-runtime-entry-resolution.js';
5
+ import { browserRuntimeEntryHasEsmExports, inferBrowserRuntimeDefaultExportPolicy, listBrowserRuntimeCjsExportNames, resolveBrowserRuntimeEntryImport, resolveBrowserRuntimeEntryPath, } from './browser-runtime-entry-resolution.js';
6
6
  /**
7
7
  * Creates a generated ESM entry module that re-exports runtime modules through
8
8
  * one stable file.
@@ -13,6 +13,8 @@ import { inferBrowserRuntimeDefaultExportPolicy, listBrowserRuntimeModuleExportN
13
13
  * logic themselves. The generated file lives under the app work directory so
14
14
  * repeated runs can reuse the same location without placing sources inside
15
15
  * `node_modules`, which can cause bundlers to externalize bare imports.
16
+ * ESM entries use `export *`. CJS entries emit explicit named re-exports from
17
+ * that same file so Rolldown can synthesize ESM bindings such as `jsx`.
16
18
  */
17
19
  export function createBrowserRuntimeEntryModule(options) {
18
20
  if (options.modules.some((module) => !module.specifier.startsWith('node:')) && !options.rootDir) {
@@ -34,13 +36,22 @@ export function createBrowserRuntimeEntryModule(options) {
34
36
  entryDir,
35
37
  rootDir,
36
38
  });
39
+ const entryPath = resolveBrowserRuntimeEntryPath({
40
+ specifier: module.specifier,
41
+ requireFromRoot,
42
+ rootDir,
43
+ });
37
44
  if (module.defaultExport &&
38
45
  inferBrowserRuntimeDefaultExportPolicy({ specifier: module.specifier, requireFromRoot, rootDir }) ===
39
46
  'emit-default') {
40
47
  statements.push(`import __ecopages_default_export__ from '${importSpecifier}';`);
41
48
  statements.push('export default __ecopages_default_export__;');
42
49
  }
43
- const exportNames = listBrowserRuntimeModuleExportNames(module.specifier, requireFromRoot).filter((name) => !seenExports.has(name));
50
+ if (browserRuntimeEntryHasEsmExports(entryPath)) {
51
+ statements.push(`export * from '${importSpecifier}';`);
52
+ continue;
53
+ }
54
+ const exportNames = listBrowserRuntimeCjsExportNames(entryPath ?? module.specifier, requireFromRoot).filter((name) => !seenExports.has(name));
44
55
  if (exportNames.length > 0) {
45
56
  statements.push(`export { ${exportNames.join(', ')} } from '${importSpecifier}';`);
46
57
  for (const exportName of exportNames) {