@esmx/core 3.0.0-rc.116 → 3.0.0-rc.118
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/LICENSE +21 -0
- package/README.md +17 -3
- package/README.zh-CN.md +20 -6
- package/dist/app.mjs +4 -2
- package/dist/cli/cli.d.ts +1 -0
- package/dist/cli/cli.mjs +28 -1
- package/dist/cli/validate.d.ts +15 -0
- package/dist/cli/validate.mjs +139 -0
- package/dist/cli/validate.test.d.ts +1 -0
- package/dist/cli/validate.test.mjs +216 -0
- package/dist/core.d.ts +31 -0
- package/dist/core.mjs +71 -5
- package/dist/declaration/index.d.ts +47 -0
- package/dist/declaration/index.mjs +63 -0
- package/dist/declaration/index.test.d.ts +1 -0
- package/dist/declaration/index.test.mjs +202 -0
- package/dist/declaration/lower.d.ts +11 -0
- package/dist/declaration/lower.mjs +78 -0
- package/dist/declaration/lower.test.d.ts +1 -0
- package/dist/declaration/lower.test.mjs +333 -0
- package/dist/declaration/reader.d.ts +28 -0
- package/dist/declaration/reader.mjs +55 -0
- package/dist/declaration/reinit.e2e.test.d.ts +1 -0
- package/dist/declaration/reinit.e2e.test.mjs +117 -0
- package/dist/declaration/resolver.d.ts +59 -0
- package/dist/declaration/resolver.mjs +430 -0
- package/dist/declaration/resolver.test.d.ts +1 -0
- package/dist/declaration/resolver.test.mjs +1005 -0
- package/dist/declaration/schema.d.ts +89 -0
- package/dist/declaration/schema.mjs +282 -0
- package/dist/declaration/schema.test.d.ts +1 -0
- package/dist/declaration/schema.test.mjs +101 -0
- package/dist/declaration/semver.d.ts +22 -0
- package/dist/declaration/semver.mjs +229 -0
- package/dist/declaration/semver.test.d.ts +1 -0
- package/dist/declaration/semver.test.mjs +87 -0
- package/dist/declaration/test-fixtures.d.ts +18 -0
- package/dist/declaration/test-fixtures.mjs +69 -0
- package/dist/declaration/types.d.ts +58 -0
- package/dist/declaration/types.mjs +21 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.mjs +10 -0
- package/dist/manifest-json.d.ts +61 -0
- package/dist/manifest-json.mjs +68 -5
- package/dist/manifest-json.test.d.ts +1 -0
- package/dist/manifest-json.test.mjs +196 -0
- package/dist/module-config.d.ts +45 -5
- package/dist/module-config.mjs +60 -49
- package/dist/module-config.test.mjs +227 -91
- package/dist/pack-config.d.ts +2 -9
- package/dist/render-context.mjs +22 -5
- package/dist/utils/import-map.d.ts +23 -3
- package/dist/utils/import-map.mjs +38 -1
- package/dist/utils/import-map.test.mjs +228 -0
- package/package.json +9 -6
- package/src/app.ts +5 -2
- package/src/cli/cli.ts +44 -1
- package/src/cli/validate.test.ts +251 -0
- package/src/cli/validate.ts +196 -0
- package/src/core.ts +84 -5
- package/src/declaration/index.test.ts +223 -0
- package/src/declaration/index.ts +135 -0
- package/src/declaration/lower.test.ts +372 -0
- package/src/declaration/lower.ts +135 -0
- package/src/declaration/reader.ts +93 -0
- package/src/declaration/reinit.e2e.test.ts +148 -0
- package/src/declaration/resolver.test.ts +1111 -0
- package/src/declaration/resolver.ts +638 -0
- package/src/declaration/schema.test.ts +118 -0
- package/src/declaration/schema.ts +339 -0
- package/src/declaration/semver.test.ts +101 -0
- package/src/declaration/semver.ts +278 -0
- package/src/declaration/test-fixtures.ts +96 -0
- package/src/declaration/types.ts +71 -0
- package/src/index.ts +28 -1
- package/src/manifest-json.test.ts +236 -0
- package/src/manifest-json.ts +166 -5
- package/src/module-config.test.ts +266 -105
- package/src/module-config.ts +130 -58
- package/src/pack-config.ts +2 -9
- package/src/render-context.ts +34 -6
- package/src/utils/import-map.test.ts +261 -0
- package/src/utils/import-map.ts +92 -6
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { ModuleConfig } from '../module-config';
|
|
2
|
+
import { lowerDeclaration } from './lower';
|
|
3
|
+
import { readDeclaration } from './reader';
|
|
4
|
+
import type { ResolvedMount, ResolverEnvLinks, SupplyEntry } from './resolver';
|
|
5
|
+
import { resolveMounts } from './resolver';
|
|
6
|
+
import type { Diagnostic } from './types';
|
|
7
|
+
|
|
8
|
+
export { lowerDeclaration } from './lower';
|
|
9
|
+
export type { PackageRecord, ReadDeclarationResult } from './reader';
|
|
10
|
+
export { readDeclaration, readPackageRecord } from './reader';
|
|
11
|
+
export type {
|
|
12
|
+
ResolvedMount,
|
|
13
|
+
ResolveMountsResult,
|
|
14
|
+
ResolverEnvLinks,
|
|
15
|
+
SupplyEntry,
|
|
16
|
+
SupplyGroup
|
|
17
|
+
} from './resolver';
|
|
18
|
+
export { resolveMounts, selectSupplyGroup } from './resolver';
|
|
19
|
+
export type { ValidateDeclarationResult } from './schema';
|
|
20
|
+
export { esmxDeclarationSchema, validateDeclaration } from './schema';
|
|
21
|
+
export { parseSemver, satisfiesRange } from './semver';
|
|
22
|
+
export type {
|
|
23
|
+
Diagnostic,
|
|
24
|
+
DiagnosticCheck,
|
|
25
|
+
DiagnosticCodeValue,
|
|
26
|
+
DiagnosticSeverity,
|
|
27
|
+
EsmxDeclaration,
|
|
28
|
+
EsmxDeclarationEntry,
|
|
29
|
+
EsmxDeclarationExportFork,
|
|
30
|
+
EsmxDeclarationExportValue
|
|
31
|
+
} from './types';
|
|
32
|
+
export { DiagnosticCode } from './types';
|
|
33
|
+
|
|
34
|
+
export interface ResolveDeclarationOptions {
|
|
35
|
+
/**
|
|
36
|
+
* Explicit mount overrides (environment fact): module name → artifact
|
|
37
|
+
* directory, same semantics as today's `ModuleConfig.links` values,
|
|
38
|
+
* resolved relative to `rootDir`.
|
|
39
|
+
*/
|
|
40
|
+
envLinks?: ResolverEnvLinks;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ResolveDeclarationResult {
|
|
44
|
+
/** The declaration lowered to today's internal ModuleConfig IR. */
|
|
45
|
+
config: ModuleConfig;
|
|
46
|
+
/** Merged supply table: bare package → per-major group winners. */
|
|
47
|
+
supply: Record<string, SupplyEntry>;
|
|
48
|
+
/** Resolved mount table: module name → mounted artifact location. */
|
|
49
|
+
mounts: Record<string, ResolvedMount>;
|
|
50
|
+
diagnostics: Diagnostic[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* One-call helper composing reader + resolver + lowering (RFC Phase 1).
|
|
55
|
+
* Returns null when the package at `rootDir` has no `esmx` declaration.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveDeclaration(
|
|
58
|
+
rootDir: string,
|
|
59
|
+
options: ResolveDeclarationOptions = {}
|
|
60
|
+
): ResolveDeclarationResult | null {
|
|
61
|
+
const pkg = readDeclaration(rootDir);
|
|
62
|
+
if (!pkg) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const resolution = resolveMounts(rootDir, pkg, options.envLinks);
|
|
66
|
+
return {
|
|
67
|
+
config: lowerDeclaration(pkg, resolution),
|
|
68
|
+
supply: resolution.supply,
|
|
69
|
+
mounts: resolution.mounts,
|
|
70
|
+
diagnostics: resolution.diagnostics
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const PROTOCOL_MODULE_KEYS = ['lib', 'imports', 'exports'] as const;
|
|
75
|
+
|
|
76
|
+
function formatDiagnostic(diagnostic: Diagnostic): string {
|
|
77
|
+
const location = diagnostic.package
|
|
78
|
+
? `${diagnostic.module} → ${diagnostic.package}`
|
|
79
|
+
: diagnostic.module;
|
|
80
|
+
return `[${diagnostic.code}] (${location}) ${diagnostic.message} Fix: ${diagnostic.fix}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Integration point for `Esmx.init`: decides between the legacy
|
|
85
|
+
* `options.modules` path and the declaration path.
|
|
86
|
+
*
|
|
87
|
+
* - No `esmx` field in package.json → legacy path, untouched.
|
|
88
|
+
* - `esmx` field present and `options.modules` carries protocol facts
|
|
89
|
+
* (lib/imports/exports) → E_PROTOCOL_IN_BEHAVIOR (protocol facts
|
|
90
|
+
* must live in package.json only).
|
|
91
|
+
* - `links` alone is an environment fact and is passed to the resolver as
|
|
92
|
+
* explicit mount overrides.
|
|
93
|
+
*/
|
|
94
|
+
export function resolveModuleOptions(
|
|
95
|
+
rootDir: string,
|
|
96
|
+
packageJson: Record<string, unknown>,
|
|
97
|
+
modules?: ModuleConfig
|
|
98
|
+
): ModuleConfig | undefined {
|
|
99
|
+
if (packageJson.esmx === undefined) {
|
|
100
|
+
return modules;
|
|
101
|
+
}
|
|
102
|
+
if (modules) {
|
|
103
|
+
const protocolKeys = PROTOCOL_MODULE_KEYS.filter(
|
|
104
|
+
(key) => modules[key] !== undefined
|
|
105
|
+
);
|
|
106
|
+
if (protocolKeys.length > 0) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`[E_PROTOCOL_IN_BEHAVIOR] package.json declares "esmx" but options.modules contains protocol field(s): ${protocolKeys.join(', ')}. ` +
|
|
109
|
+
`Protocol facts must live in the package.json "esmx" field only; entry.node.ts keeps behavior (devApp, server, postBuild) and environment links. ` +
|
|
110
|
+
`Fix: move ${protocolKeys.join(', ')} into the "esmx" declaration and delete them from options.modules.`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const resolved = resolveDeclaration(rootDir, {
|
|
115
|
+
envLinks: modules?.links
|
|
116
|
+
});
|
|
117
|
+
if (!resolved) {
|
|
118
|
+
return modules;
|
|
119
|
+
}
|
|
120
|
+
const errors = resolved.diagnostics.filter(
|
|
121
|
+
(diagnostic) => diagnostic.severity === 'error'
|
|
122
|
+
);
|
|
123
|
+
if (errors.length > 0) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Esmx declaration resolution failed with ${errors.length} error(s):\n` +
|
|
126
|
+
errors.map((error) => ` ${formatDiagnostic(error)}`).join('\n')
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
for (const warning of resolved.diagnostics) {
|
|
130
|
+
if (warning.severity === 'warning') {
|
|
131
|
+
console.warn(formatDiagnostic(warning));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return resolved.config;
|
|
135
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import type { ModuleConfig } from '../module-config';
|
|
4
|
+
import { parseModuleConfig } from '../module-config';
|
|
5
|
+
import { lowerDeclaration } from './lower';
|
|
6
|
+
import type { ReadDeclarationResult } from './reader';
|
|
7
|
+
import { readDeclaration } from './reader';
|
|
8
|
+
import { resolveMounts } from './resolver';
|
|
9
|
+
import {
|
|
10
|
+
createFixtureRoot,
|
|
11
|
+
removeFixtureRoot,
|
|
12
|
+
writeFixturePackage
|
|
13
|
+
} from './test-fixtures';
|
|
14
|
+
|
|
15
|
+
const fixtureRoots: string[] = [];
|
|
16
|
+
|
|
17
|
+
async function fixtureRoot(): Promise<string> {
|
|
18
|
+
const root = await createFixtureRoot();
|
|
19
|
+
fixtureRoots.push(root);
|
|
20
|
+
return root;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await Promise.all(fixtureRoots.splice(0).map(removeFixtureRoot));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function readRootDeclaration(appDir: string): ReadDeclarationResult {
|
|
28
|
+
const pkg = readDeclaration(appDir);
|
|
29
|
+
if (!pkg) {
|
|
30
|
+
throw new Error(`fixture app at ${appDir} must declare esmx`);
|
|
31
|
+
}
|
|
32
|
+
return pkg;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('lowerDeclaration', () => {
|
|
36
|
+
it('should lower the ssr-micro-react-equivalent declaration to parity with the legacy config', async () => {
|
|
37
|
+
const root = await fixtureRoot();
|
|
38
|
+
writeFixturePackage(root, {
|
|
39
|
+
dir: 'shared',
|
|
40
|
+
packageJson: {
|
|
41
|
+
name: 'ssr-micro-shared',
|
|
42
|
+
version: '1.0.0',
|
|
43
|
+
esmx: { provides: ['@esmx/router'] }
|
|
44
|
+
},
|
|
45
|
+
built: true
|
|
46
|
+
});
|
|
47
|
+
writeFixturePackage(root, {
|
|
48
|
+
dir: 'shared/node_modules/@esmx/router',
|
|
49
|
+
packageJson: { name: '@esmx/router', version: '1.2.3' }
|
|
50
|
+
});
|
|
51
|
+
const appDir = writeFixturePackage(root, {
|
|
52
|
+
dir: 'app',
|
|
53
|
+
packageJson: {
|
|
54
|
+
name: 'ssr-micro-react',
|
|
55
|
+
version: '1.0.0',
|
|
56
|
+
devDependencies: {
|
|
57
|
+
'@esmx/router': '*',
|
|
58
|
+
react: '*',
|
|
59
|
+
'react-dom': '*'
|
|
60
|
+
},
|
|
61
|
+
esmx: {
|
|
62
|
+
entry: {
|
|
63
|
+
client: './src/entry.client.ts',
|
|
64
|
+
server: './src/entry.server.ts'
|
|
65
|
+
},
|
|
66
|
+
exports: { './src/routes': './src/routes.ts' },
|
|
67
|
+
provides: ['react', 'react-dom'],
|
|
68
|
+
uses: ['ssr-micro-shared']
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
const pkg = readRootDeclaration(appDir);
|
|
73
|
+
const resolution = resolveMounts(appDir, pkg, {
|
|
74
|
+
'ssr-micro-shared': '../shared/dist'
|
|
75
|
+
});
|
|
76
|
+
const legacy: ModuleConfig = {
|
|
77
|
+
links: {
|
|
78
|
+
'ssr-micro-shared': path.join(root, 'shared/dist')
|
|
79
|
+
},
|
|
80
|
+
imports: {
|
|
81
|
+
'@esmx/router': 'ssr-micro-shared/@esmx/router'
|
|
82
|
+
},
|
|
83
|
+
exports: ['pkg:react', 'pkg:react-dom', 'root:src/routes.ts']
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const lowered = lowerDeclaration(pkg, resolution);
|
|
87
|
+
|
|
88
|
+
expect(lowered.imports).toEqual({
|
|
89
|
+
'@esmx/router': 'ssr-micro-shared/@esmx/router'
|
|
90
|
+
});
|
|
91
|
+
expect(parseModuleConfig('ssr-micro-react', appDir, lowered)).toEqual(
|
|
92
|
+
parseModuleConfig('ssr-micro-react', appDir, legacy)
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should set lib:true when no entry is declared', async () => {
|
|
97
|
+
const root = await fixtureRoot();
|
|
98
|
+
const appDir = writeFixturePackage(root, {
|
|
99
|
+
dir: 'lib-module',
|
|
100
|
+
packageJson: {
|
|
101
|
+
name: 'lib-module',
|
|
102
|
+
version: '1.0.0',
|
|
103
|
+
esmx: { exports: { './widget': './src/widget.ts' } }
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
const pkg = readRootDeclaration(appDir);
|
|
107
|
+
const resolution = resolveMounts(appDir, pkg);
|
|
108
|
+
|
|
109
|
+
const lowered = lowerDeclaration(pkg, resolution);
|
|
110
|
+
|
|
111
|
+
expect(lowered.lib).toBe(true);
|
|
112
|
+
expect(lowered.exports).toEqual([{ widget: 'root:src/widget.ts' }]);
|
|
113
|
+
expect(lowered.links).toBeUndefined();
|
|
114
|
+
expect(lowered.imports).toBeUndefined();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should lower custom entries through config.entry with path-derived names', async () => {
|
|
118
|
+
const root = await fixtureRoot();
|
|
119
|
+
const appDir = writeFixturePackage(root, {
|
|
120
|
+
dir: 'custom-entry',
|
|
121
|
+
packageJson: {
|
|
122
|
+
name: 'custom-entry',
|
|
123
|
+
version: '1.0.0',
|
|
124
|
+
esmx: {
|
|
125
|
+
entry: {
|
|
126
|
+
client: './src/main.client.ts',
|
|
127
|
+
server: './src/entry.server.ts'
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
const pkg = readRootDeclaration(appDir);
|
|
133
|
+
const resolution = resolveMounts(appDir, pkg);
|
|
134
|
+
|
|
135
|
+
const lowered = lowerDeclaration(pkg, resolution);
|
|
136
|
+
|
|
137
|
+
expect(lowered.lib).toBeUndefined();
|
|
138
|
+
expect(lowered.entry).toEqual({ client: './src/main.client.ts' });
|
|
139
|
+
expect(lowered.exports).toBeUndefined();
|
|
140
|
+
const parsed = parseModuleConfig('custom-entry', appDir, lowered);
|
|
141
|
+
expect(parsed.entry).toEqual({
|
|
142
|
+
client: { name: 'src/main.client', file: './src/main.client.ts' },
|
|
143
|
+
server: { name: 'src/entry.server', file: './src/entry.server' }
|
|
144
|
+
});
|
|
145
|
+
expect(parsed.environments.client.exports['src/main.client']).toEqual({
|
|
146
|
+
name: 'src/main.client',
|
|
147
|
+
file: './src/main.client.ts',
|
|
148
|
+
pkg: false
|
|
149
|
+
});
|
|
150
|
+
expect(parsed.environments.server.exports['src/entry.server']).toEqual({
|
|
151
|
+
name: 'src/entry.server',
|
|
152
|
+
file: './src/entry.server',
|
|
153
|
+
pkg: false
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should disable an undeclared entry side', async () => {
|
|
158
|
+
const root = await fixtureRoot();
|
|
159
|
+
const appDir = writeFixturePackage(root, {
|
|
160
|
+
dir: 'client-only',
|
|
161
|
+
packageJson: {
|
|
162
|
+
name: 'client-only',
|
|
163
|
+
version: '1.0.0',
|
|
164
|
+
esmx: { entry: { client: './src/entry.client.ts' } }
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
const pkg = readRootDeclaration(appDir);
|
|
168
|
+
const resolution = resolveMounts(appDir, pkg);
|
|
169
|
+
|
|
170
|
+
const lowered = lowerDeclaration(pkg, resolution);
|
|
171
|
+
|
|
172
|
+
expect(lowered.entry).toEqual({ server: false });
|
|
173
|
+
const parsed = parseModuleConfig('client-only', appDir, lowered);
|
|
174
|
+
expect(parsed.entry.server).toBeNull();
|
|
175
|
+
expect(
|
|
176
|
+
parsed.environments.server.exports['src/entry.server']
|
|
177
|
+
).toBeUndefined();
|
|
178
|
+
expect(
|
|
179
|
+
parsed.environments.client.exports['src/entry.client'].file
|
|
180
|
+
).toBe('./src/entry.client');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should preserve env forks including false in exports', async () => {
|
|
184
|
+
const root = await fixtureRoot();
|
|
185
|
+
const appDir = writeFixturePackage(root, {
|
|
186
|
+
dir: 'forked',
|
|
187
|
+
packageJson: {
|
|
188
|
+
name: 'forked',
|
|
189
|
+
version: '1.0.0',
|
|
190
|
+
esmx: {
|
|
191
|
+
exports: {
|
|
192
|
+
'./store': {
|
|
193
|
+
client: './src/store.client.ts',
|
|
194
|
+
server: false
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
const pkg = readRootDeclaration(appDir);
|
|
201
|
+
const resolution = resolveMounts(appDir, pkg);
|
|
202
|
+
|
|
203
|
+
const lowered = lowerDeclaration(pkg, resolution);
|
|
204
|
+
|
|
205
|
+
expect(lowered.exports).toEqual([
|
|
206
|
+
{
|
|
207
|
+
store: {
|
|
208
|
+
client: 'root:src/store.client.ts',
|
|
209
|
+
server: false
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
]);
|
|
213
|
+
const parsed = parseModuleConfig('forked', appDir, lowered);
|
|
214
|
+
expect(parsed.environments.client.exports.store.file).toBe(
|
|
215
|
+
'./src/store.client.ts'
|
|
216
|
+
);
|
|
217
|
+
expect(parsed.environments.server.exports.store.file).toBe('');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should wire a single-owner supply group into config.imports', async () => {
|
|
221
|
+
const root = await fixtureRoot();
|
|
222
|
+
writeFixturePackage(root, {
|
|
223
|
+
dir: 'node_modules/shared',
|
|
224
|
+
packageJson: {
|
|
225
|
+
name: 'shared',
|
|
226
|
+
version: '1.0.0',
|
|
227
|
+
dependencies: { vue: '^3.4.0' },
|
|
228
|
+
esmx: { provides: ['vue'] }
|
|
229
|
+
},
|
|
230
|
+
built: true
|
|
231
|
+
});
|
|
232
|
+
writeFixturePackage(root, {
|
|
233
|
+
dir: 'node_modules/shared/node_modules/vue',
|
|
234
|
+
packageJson: { name: 'vue', version: '3.5.2' }
|
|
235
|
+
});
|
|
236
|
+
const appDir = writeFixturePackage(root, {
|
|
237
|
+
dir: 'app',
|
|
238
|
+
packageJson: {
|
|
239
|
+
name: 'app',
|
|
240
|
+
version: '1.0.0',
|
|
241
|
+
dependencies: { vue: '^3.4.0' },
|
|
242
|
+
esmx: { uses: ['shared'] }
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const pkg = readRootDeclaration(appDir);
|
|
247
|
+
const lowered = lowerDeclaration(pkg, resolveMounts(appDir, pkg));
|
|
248
|
+
|
|
249
|
+
expect(lowered.imports).toEqual({ vue: 'shared/vue' });
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe('per-major group wiring', () => {
|
|
253
|
+
async function multiMajorFixture(): Promise<string> {
|
|
254
|
+
const root = await fixtureRoot();
|
|
255
|
+
writeFixturePackage(root, {
|
|
256
|
+
dir: 'node_modules/vue2-app',
|
|
257
|
+
packageJson: {
|
|
258
|
+
name: 'vue2-app',
|
|
259
|
+
version: '1.0.0',
|
|
260
|
+
dependencies: { vue: '2.7.16' },
|
|
261
|
+
esmx: { provides: ['vue'] }
|
|
262
|
+
},
|
|
263
|
+
built: true
|
|
264
|
+
});
|
|
265
|
+
writeFixturePackage(root, {
|
|
266
|
+
dir: 'node_modules/vue2-app/node_modules/vue',
|
|
267
|
+
packageJson: { name: 'vue', version: '2.7.16' }
|
|
268
|
+
});
|
|
269
|
+
writeFixturePackage(root, {
|
|
270
|
+
dir: 'node_modules/vue3-base',
|
|
271
|
+
packageJson: {
|
|
272
|
+
name: 'vue3-base',
|
|
273
|
+
version: '1.0.0',
|
|
274
|
+
dependencies: { vue: '^3.5.0' },
|
|
275
|
+
esmx: { provides: ['vue'] }
|
|
276
|
+
},
|
|
277
|
+
built: true
|
|
278
|
+
});
|
|
279
|
+
writeFixturePackage(root, {
|
|
280
|
+
dir: 'node_modules/vue3-base/node_modules/vue',
|
|
281
|
+
packageJson: { name: 'vue', version: '3.5.13' }
|
|
282
|
+
});
|
|
283
|
+
return root;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function lowerApp(appDir: string) {
|
|
287
|
+
const pkg = readRootDeclaration(appDir);
|
|
288
|
+
const resolution = resolveMounts(appDir, pkg);
|
|
289
|
+
return { resolution, lowered: lowerDeclaration(pkg, resolution) };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
it('should wire a ^3 consumer to the vue3 group winner', async () => {
|
|
293
|
+
const root = await multiMajorFixture();
|
|
294
|
+
const appDir = writeFixturePackage(root, {
|
|
295
|
+
dir: 'app',
|
|
296
|
+
packageJson: {
|
|
297
|
+
name: 'app',
|
|
298
|
+
version: '1.0.0',
|
|
299
|
+
peerDependencies: { vue: '^3.5.0' },
|
|
300
|
+
esmx: { uses: ['vue2-app', 'vue3-base'] }
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const { lowered } = lowerApp(appDir);
|
|
305
|
+
|
|
306
|
+
expect(lowered.imports?.vue).toBe('vue3-base/vue');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should wire a ^2 consumer to the vue2 group winner', async () => {
|
|
310
|
+
const root = await multiMajorFixture();
|
|
311
|
+
const appDir = writeFixturePackage(root, {
|
|
312
|
+
dir: 'app',
|
|
313
|
+
packageJson: {
|
|
314
|
+
name: 'app',
|
|
315
|
+
version: '1.0.0',
|
|
316
|
+
dependencies: { vue: '^2.7.0' },
|
|
317
|
+
esmx: { uses: ['vue2-app', 'vue3-base'] }
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const { lowered } = lowerApp(appDir);
|
|
322
|
+
|
|
323
|
+
expect(lowered.imports?.vue).toBe('vue2-app/vue');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should wire a range-less consumer to the highest major with W_NO_RANGE', async () => {
|
|
327
|
+
const root = await multiMajorFixture();
|
|
328
|
+
const appDir = writeFixturePackage(root, {
|
|
329
|
+
dir: 'app',
|
|
330
|
+
packageJson: {
|
|
331
|
+
name: 'app',
|
|
332
|
+
version: '1.0.0',
|
|
333
|
+
esmx: { uses: ['vue2-app', 'vue3-base'] }
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const { resolution, lowered } = lowerApp(appDir);
|
|
338
|
+
|
|
339
|
+
expect(lowered.imports?.vue).toBe('vue3-base/vue');
|
|
340
|
+
expect(
|
|
341
|
+
resolution.diagnostics.some(
|
|
342
|
+
(d) => d.code === 'W_NO_RANGE' && d.package === 'vue'
|
|
343
|
+
)
|
|
344
|
+
).toBe(true);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should let a 2.x provider wire to its own copy despite a 3.x group existing', async () => {
|
|
348
|
+
const root = await multiMajorFixture();
|
|
349
|
+
const appDir = writeFixturePackage(root, {
|
|
350
|
+
dir: 'app',
|
|
351
|
+
packageJson: {
|
|
352
|
+
name: 'app',
|
|
353
|
+
version: '1.0.0',
|
|
354
|
+
dependencies: { vue: '2.7.16' },
|
|
355
|
+
esmx: { uses: ['vue3-base'], provides: ['vue'] }
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
writeFixturePackage(root, {
|
|
359
|
+
dir: 'app/node_modules/vue',
|
|
360
|
+
packageJson: { name: 'vue', version: '2.7.16' }
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const { resolution, lowered } = lowerApp(appDir);
|
|
364
|
+
|
|
365
|
+
// Self-provided package: never imported from another module.
|
|
366
|
+
expect(lowered.imports?.vue).toBeUndefined();
|
|
367
|
+
expect(
|
|
368
|
+
resolution.diagnostics.filter((d) => d.severity === 'error')
|
|
369
|
+
).toEqual([]);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ModuleConfig,
|
|
3
|
+
ModuleConfigEntry,
|
|
4
|
+
ModuleConfigExportExports,
|
|
5
|
+
ModuleConfigExportObject,
|
|
6
|
+
ModuleConfigExportObjectValue
|
|
7
|
+
} from '../module-config';
|
|
8
|
+
import type { ReadDeclarationResult } from './reader';
|
|
9
|
+
import type { ResolveMountsResult } from './resolver';
|
|
10
|
+
import { selectSupplyGroup } from './resolver';
|
|
11
|
+
import type { EsmxDeclarationExportValue } from './types';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_CLIENT_ENTRY = './src/entry.client.ts';
|
|
14
|
+
const DEFAULT_SERVER_ENTRY = './src/entry.server.ts';
|
|
15
|
+
|
|
16
|
+
/** './src/foo.ts' → 'root:src/foo.ts' */
|
|
17
|
+
function toRootExport(relativePath: string): string {
|
|
18
|
+
return `root:${relativePath.slice(2)}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function lowerExportValue(
|
|
22
|
+
value: EsmxDeclarationExportValue
|
|
23
|
+
): ModuleConfigExportObjectValue {
|
|
24
|
+
if (typeof value === 'string') {
|
|
25
|
+
return toRootExport(value);
|
|
26
|
+
}
|
|
27
|
+
// An absent fork side means the side is disabled, same as false.
|
|
28
|
+
return {
|
|
29
|
+
client:
|
|
30
|
+
typeof value.client === 'string'
|
|
31
|
+
? toRootExport(value.client)
|
|
32
|
+
: false,
|
|
33
|
+
server:
|
|
34
|
+
typeof value.server === 'string'
|
|
35
|
+
? toRootExport(value.server)
|
|
36
|
+
: false
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Lowers a declared entry to the `ModuleConfig.entry` IR: an absent side
|
|
42
|
+
* is disabled (`false`), the default path is omitted (rides on the legacy
|
|
43
|
+
* default, keeping migration parity byte-identical), and a custom path
|
|
44
|
+
* passes through (its export name is path-derived by `parseEntryConfig`,
|
|
45
|
+
* RFC 0001 §4.1: no reserved names).
|
|
46
|
+
*/
|
|
47
|
+
function lowerEntry(
|
|
48
|
+
entry: NonNullable<ReadDeclarationResult['declaration']['entry']>
|
|
49
|
+
): ModuleConfigEntry | null {
|
|
50
|
+
const lowered: ModuleConfigEntry = {};
|
|
51
|
+
if (!entry.client) {
|
|
52
|
+
lowered.client = false;
|
|
53
|
+
} else if (entry.client !== DEFAULT_CLIENT_ENTRY) {
|
|
54
|
+
lowered.client = entry.client;
|
|
55
|
+
}
|
|
56
|
+
if (!entry.server) {
|
|
57
|
+
lowered.server = false;
|
|
58
|
+
} else if (entry.server !== DEFAULT_SERVER_ENTRY) {
|
|
59
|
+
lowered.server = entry.server;
|
|
60
|
+
}
|
|
61
|
+
return Object.keys(lowered).length > 0 ? lowered : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Lowers a declaration + resolution result to today's internal
|
|
66
|
+
* `ModuleConfig` IR (RFC Phase 1+2): default entry paths ride on the
|
|
67
|
+
* legacy defaults, custom/disabled entries flow through `config.entry`,
|
|
68
|
+
* `provides` become `pkg:` exports, the merged supply table becomes
|
|
69
|
+
* `imports`, and mounts become `links`.
|
|
70
|
+
*/
|
|
71
|
+
export function lowerDeclaration(
|
|
72
|
+
pkg: ReadDeclarationResult,
|
|
73
|
+
resolution: ResolveMountsResult
|
|
74
|
+
): ModuleConfig {
|
|
75
|
+
const { declaration } = pkg;
|
|
76
|
+
const config: ModuleConfig = {};
|
|
77
|
+
|
|
78
|
+
if (!declaration.entry) {
|
|
79
|
+
config.lib = true;
|
|
80
|
+
} else {
|
|
81
|
+
const entry = lowerEntry(declaration.entry);
|
|
82
|
+
if (entry) {
|
|
83
|
+
config.entry = entry;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const links: Record<string, string> = {};
|
|
88
|
+
for (const mount of Object.values(resolution.mounts)) {
|
|
89
|
+
links[mount.name] = mount.artifactDir;
|
|
90
|
+
}
|
|
91
|
+
if (Object.keys(links).length > 0) {
|
|
92
|
+
config.links = links;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// The static supply table: every merged entry is wired, whether or not
|
|
96
|
+
// the consumer ever imports it — the externalization predicates treat
|
|
97
|
+
// it as a pure membership test, so extra entries are harmless. With
|
|
98
|
+
// per-major groups each module picks its own group locally: a module
|
|
99
|
+
// that provides the package itself wires to its own copy (self wins
|
|
100
|
+
// its group in its own merge); otherwise the dependencies ∪
|
|
101
|
+
// peerDependencies range selects the satisfying major group.
|
|
102
|
+
const imports: Record<string, string> = {};
|
|
103
|
+
for (const [packageName, entry] of Object.entries(resolution.supply)) {
|
|
104
|
+
if (entry.groups.some((group) => group.provider === pkg.name)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const range =
|
|
108
|
+
pkg.dependencies[packageName] ?? pkg.peerDependencies[packageName];
|
|
109
|
+
const group = selectSupplyGroup(entry, range);
|
|
110
|
+
if (!group) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
imports[packageName] = `${group.provider}/${packageName}`;
|
|
114
|
+
}
|
|
115
|
+
if (Object.keys(imports).length > 0) {
|
|
116
|
+
config.imports = imports;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const exports: ModuleConfigExportExports = [];
|
|
120
|
+
for (const provided of declaration.provides ?? []) {
|
|
121
|
+
exports.push(`pkg:${provided}`);
|
|
122
|
+
}
|
|
123
|
+
const exportObject: ModuleConfigExportObject = {};
|
|
124
|
+
for (const [subpath, value] of Object.entries(declaration.exports ?? {})) {
|
|
125
|
+
exportObject[subpath.slice(2)] = lowerExportValue(value);
|
|
126
|
+
}
|
|
127
|
+
if (Object.keys(exportObject).length > 0) {
|
|
128
|
+
exports.push(exportObject);
|
|
129
|
+
}
|
|
130
|
+
if (exports.length > 0) {
|
|
131
|
+
config.exports = exports;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return config;
|
|
135
|
+
}
|