@ecopages/core 0.2.0-rc.2 → 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 +3 -3
- package/src/services/README.md +2 -0
- package/src/services/assets/asset-processing-service/browser-runtime-entry-resolution.d.ts +30 -9
- package/src/services/assets/asset-processing-service/browser-runtime-entry-resolution.js +101 -35
- package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.d.ts +2 -0
- package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.js +13 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecopages/core",
|
|
3
|
-
"version": "0.2.0-rc.
|
|
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.
|
|
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.
|
|
34
|
+
"@ecopages/dev-toolbar": "0.2.0-rc.3"
|
|
35
35
|
},
|
|
36
36
|
"peerDependenciesMeta": {
|
|
37
37
|
"@ecopages/dev-toolbar": {
|
package/src/services/README.md
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
49
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
76
|
-
if (
|
|
77
|
-
const source = fs.readFileSync(
|
|
78
|
-
if (
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
*
|
|
101
|
-
*
|
|
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
|
|
169
|
+
export function listBrowserRuntimeCjsExportNames(moduleId, requireFromRoot) {
|
|
104
170
|
let moduleExports;
|
|
105
171
|
try {
|
|
106
|
-
moduleExports = requireFromRoot(
|
|
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) =>
|
|
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,
|
|
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
|
-
|
|
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) {
|