@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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -3
  3. package/README.zh-CN.md +20 -6
  4. package/dist/app.mjs +4 -2
  5. package/dist/cli/cli.d.ts +1 -0
  6. package/dist/cli/cli.mjs +28 -1
  7. package/dist/cli/validate.d.ts +15 -0
  8. package/dist/cli/validate.mjs +139 -0
  9. package/dist/cli/validate.test.d.ts +1 -0
  10. package/dist/cli/validate.test.mjs +216 -0
  11. package/dist/core.d.ts +31 -0
  12. package/dist/core.mjs +71 -5
  13. package/dist/declaration/index.d.ts +47 -0
  14. package/dist/declaration/index.mjs +63 -0
  15. package/dist/declaration/index.test.d.ts +1 -0
  16. package/dist/declaration/index.test.mjs +202 -0
  17. package/dist/declaration/lower.d.ts +11 -0
  18. package/dist/declaration/lower.mjs +78 -0
  19. package/dist/declaration/lower.test.d.ts +1 -0
  20. package/dist/declaration/lower.test.mjs +333 -0
  21. package/dist/declaration/reader.d.ts +28 -0
  22. package/dist/declaration/reader.mjs +55 -0
  23. package/dist/declaration/reinit.e2e.test.d.ts +1 -0
  24. package/dist/declaration/reinit.e2e.test.mjs +117 -0
  25. package/dist/declaration/resolver.d.ts +59 -0
  26. package/dist/declaration/resolver.mjs +430 -0
  27. package/dist/declaration/resolver.test.d.ts +1 -0
  28. package/dist/declaration/resolver.test.mjs +1005 -0
  29. package/dist/declaration/schema.d.ts +89 -0
  30. package/dist/declaration/schema.mjs +282 -0
  31. package/dist/declaration/schema.test.d.ts +1 -0
  32. package/dist/declaration/schema.test.mjs +101 -0
  33. package/dist/declaration/semver.d.ts +22 -0
  34. package/dist/declaration/semver.mjs +229 -0
  35. package/dist/declaration/semver.test.d.ts +1 -0
  36. package/dist/declaration/semver.test.mjs +87 -0
  37. package/dist/declaration/test-fixtures.d.ts +18 -0
  38. package/dist/declaration/test-fixtures.mjs +69 -0
  39. package/dist/declaration/types.d.ts +58 -0
  40. package/dist/declaration/types.mjs +21 -0
  41. package/dist/index.d.ts +4 -1
  42. package/dist/index.mjs +10 -0
  43. package/dist/manifest-json.d.ts +61 -0
  44. package/dist/manifest-json.mjs +68 -5
  45. package/dist/manifest-json.test.d.ts +1 -0
  46. package/dist/manifest-json.test.mjs +196 -0
  47. package/dist/module-config.d.ts +45 -5
  48. package/dist/module-config.mjs +60 -49
  49. package/dist/module-config.test.mjs +227 -91
  50. package/dist/pack-config.d.ts +2 -9
  51. package/dist/render-context.mjs +22 -5
  52. package/dist/utils/import-map.d.ts +23 -3
  53. package/dist/utils/import-map.mjs +38 -1
  54. package/dist/utils/import-map.test.mjs +228 -0
  55. package/package.json +9 -6
  56. package/src/app.ts +5 -2
  57. package/src/cli/cli.ts +44 -1
  58. package/src/cli/validate.test.ts +251 -0
  59. package/src/cli/validate.ts +196 -0
  60. package/src/core.ts +84 -5
  61. package/src/declaration/index.test.ts +223 -0
  62. package/src/declaration/index.ts +135 -0
  63. package/src/declaration/lower.test.ts +372 -0
  64. package/src/declaration/lower.ts +135 -0
  65. package/src/declaration/reader.ts +93 -0
  66. package/src/declaration/reinit.e2e.test.ts +148 -0
  67. package/src/declaration/resolver.test.ts +1111 -0
  68. package/src/declaration/resolver.ts +638 -0
  69. package/src/declaration/schema.test.ts +118 -0
  70. package/src/declaration/schema.ts +339 -0
  71. package/src/declaration/semver.test.ts +101 -0
  72. package/src/declaration/semver.ts +278 -0
  73. package/src/declaration/test-fixtures.ts +96 -0
  74. package/src/declaration/types.ts +71 -0
  75. package/src/index.ts +28 -1
  76. package/src/manifest-json.test.ts +236 -0
  77. package/src/manifest-json.ts +166 -5
  78. package/src/module-config.test.ts +266 -105
  79. package/src/module-config.ts +130 -58
  80. package/src/pack-config.ts +2 -9
  81. package/src/render-context.ts +34 -6
  82. package/src/utils/import-map.test.ts +261 -0
  83. 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
+ }