@esmx/core 3.0.0-rc.117 → 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 (82) hide show
  1. package/README.md +17 -3
  2. package/README.zh-CN.md +20 -6
  3. package/dist/app.mjs +4 -2
  4. package/dist/cli/cli.d.ts +1 -0
  5. package/dist/cli/cli.mjs +28 -1
  6. package/dist/cli/validate.d.ts +15 -0
  7. package/dist/cli/validate.mjs +139 -0
  8. package/dist/cli/validate.test.d.ts +1 -0
  9. package/dist/cli/validate.test.mjs +216 -0
  10. package/dist/core.d.ts +31 -0
  11. package/dist/core.mjs +71 -5
  12. package/dist/declaration/index.d.ts +47 -0
  13. package/dist/declaration/index.mjs +63 -0
  14. package/dist/declaration/index.test.d.ts +1 -0
  15. package/dist/declaration/index.test.mjs +202 -0
  16. package/dist/declaration/lower.d.ts +11 -0
  17. package/dist/declaration/lower.mjs +78 -0
  18. package/dist/declaration/lower.test.d.ts +1 -0
  19. package/dist/declaration/lower.test.mjs +333 -0
  20. package/dist/declaration/reader.d.ts +28 -0
  21. package/dist/declaration/reader.mjs +55 -0
  22. package/dist/declaration/reinit.e2e.test.d.ts +1 -0
  23. package/dist/declaration/reinit.e2e.test.mjs +117 -0
  24. package/dist/declaration/resolver.d.ts +59 -0
  25. package/dist/declaration/resolver.mjs +430 -0
  26. package/dist/declaration/resolver.test.d.ts +1 -0
  27. package/dist/declaration/resolver.test.mjs +1005 -0
  28. package/dist/declaration/schema.d.ts +89 -0
  29. package/dist/declaration/schema.mjs +282 -0
  30. package/dist/declaration/schema.test.d.ts +1 -0
  31. package/dist/declaration/schema.test.mjs +101 -0
  32. package/dist/declaration/semver.d.ts +22 -0
  33. package/dist/declaration/semver.mjs +229 -0
  34. package/dist/declaration/semver.test.d.ts +1 -0
  35. package/dist/declaration/semver.test.mjs +87 -0
  36. package/dist/declaration/test-fixtures.d.ts +18 -0
  37. package/dist/declaration/test-fixtures.mjs +69 -0
  38. package/dist/declaration/types.d.ts +58 -0
  39. package/dist/declaration/types.mjs +21 -0
  40. package/dist/index.d.ts +4 -1
  41. package/dist/index.mjs +10 -0
  42. package/dist/manifest-json.d.ts +61 -0
  43. package/dist/manifest-json.mjs +68 -5
  44. package/dist/manifest-json.test.d.ts +1 -0
  45. package/dist/manifest-json.test.mjs +196 -0
  46. package/dist/module-config.d.ts +45 -5
  47. package/dist/module-config.mjs +60 -49
  48. package/dist/module-config.test.mjs +227 -91
  49. package/dist/pack-config.d.ts +2 -9
  50. package/dist/render-context.mjs +22 -5
  51. package/dist/utils/import-map.d.ts +23 -3
  52. package/dist/utils/import-map.mjs +38 -1
  53. package/dist/utils/import-map.test.mjs +228 -0
  54. package/package.json +9 -6
  55. package/src/app.ts +5 -2
  56. package/src/cli/cli.ts +44 -1
  57. package/src/cli/validate.test.ts +251 -0
  58. package/src/cli/validate.ts +196 -0
  59. package/src/core.ts +84 -5
  60. package/src/declaration/index.test.ts +223 -0
  61. package/src/declaration/index.ts +135 -0
  62. package/src/declaration/lower.test.ts +372 -0
  63. package/src/declaration/lower.ts +135 -0
  64. package/src/declaration/reader.ts +93 -0
  65. package/src/declaration/reinit.e2e.test.ts +148 -0
  66. package/src/declaration/resolver.test.ts +1111 -0
  67. package/src/declaration/resolver.ts +638 -0
  68. package/src/declaration/schema.test.ts +118 -0
  69. package/src/declaration/schema.ts +339 -0
  70. package/src/declaration/semver.test.ts +101 -0
  71. package/src/declaration/semver.ts +278 -0
  72. package/src/declaration/test-fixtures.ts +96 -0
  73. package/src/declaration/types.ts +71 -0
  74. package/src/index.ts +28 -1
  75. package/src/manifest-json.test.ts +236 -0
  76. package/src/manifest-json.ts +166 -5
  77. package/src/module-config.test.ts +266 -105
  78. package/src/module-config.ts +130 -58
  79. package/src/pack-config.ts +2 -9
  80. package/src/render-context.ts +34 -6
  81. package/src/utils/import-map.test.ts +261 -0
  82. package/src/utils/import-map.ts +92 -6
@@ -1,15 +1,18 @@
1
+ import fs from 'node:fs';
1
2
  import path from 'node:path';
2
3
  import { describe, expect, it } from 'vitest';
3
4
  import {
4
5
  addPackageExportsToScopes,
5
- createDefaultExports,
6
+ createEntryExports,
7
+ DEFAULT_MODULE_ENTRY,
8
+ getEntryChunkId,
6
9
  getEnvironmentExports,
7
10
  getEnvironmentImports,
8
- getEnvironmentScopes,
9
11
  getEnvironments,
10
12
  getLinks,
11
13
  type ModuleConfig,
12
14
  parsedExportValue,
15
+ parseEntryConfig,
13
16
  parseModuleConfig,
14
17
  processExportArray,
15
18
  processObjectExport,
@@ -234,43 +237,18 @@ describe('Module Config Parser', () => {
234
237
  });
235
238
  });
236
239
 
237
- describe('getEnvironmentScopes', () => {
238
- it('should process scoped imports per environment', () => {
239
- const scopes = {
240
- utils: {
241
- lodash: 'lodash',
242
- moment: {
243
- client: 'moment',
244
- server: 'moment/server'
245
- }
246
- }
247
- };
248
- const result = getEnvironmentScopes('client', scopes);
249
- expect(result.utils.lodash).toBe('lodash');
250
- expect(result.utils.moment).toBe('moment');
251
- });
252
-
253
- it('should handle empty scopes object', () => {
254
- const result = getEnvironmentScopes('client', {});
255
- expect(Object.keys(result)).toHaveLength(0);
256
- });
257
- });
258
-
259
240
  describe('getEnvironments', () => {
260
- it('should combine imports, exports and scopes', () => {
241
+ it('should combine imports and exports into the root scope', () => {
261
242
  const config: ModuleConfig = {
262
243
  imports: {
263
244
  react: 'react'
264
- },
265
- scopes: {
266
- utils: {
267
- lodash: 'lodash'
268
- }
269
245
  }
270
246
  };
271
247
  const result = getEnvironments(config, 'client', 'test-module');
272
248
  expect(result.imports.react).toBe('react');
273
- expect(result.scopes.utils.lodash).toBe('lodash');
249
+ // The only scope is the derived root scope, carrying imports.
250
+ expect(Object.keys(result.scopes)).toEqual(['']);
251
+ expect(result.scopes[''].react).toBe('react');
274
252
  expect(result.exports).toBeDefined();
275
253
  });
276
254
 
@@ -298,49 +276,22 @@ describe('Module Config Parser', () => {
298
276
  expect(emptyScope['./src/component']).toBeUndefined();
299
277
  });
300
278
 
301
- it('should not override existing empty string scope', () => {
302
- const config: ModuleConfig = {
303
- exports: ['pkg:lodash'],
304
- scopes: {
305
- '': {
306
- existing: 'existing-value'
307
- }
308
- }
309
- };
310
- const result = getEnvironments(config, 'client', 'test-module');
311
- const emptyScope = result.scopes[''];
312
- expect(emptyScope).toBeDefined();
313
- expect(emptyScope.existing).toBe('existing-value');
314
- expect(emptyScope.lodash).toBe('test-module/lodash');
315
- });
316
-
317
- it('should verify the specific scopes merging logic with imports', () => {
279
+ it('should merge imports and pkg exports into the single root scope', () => {
318
280
  const config: ModuleConfig = {
319
281
  imports: {
320
282
  react: 'react',
321
283
  vue: 'vue'
322
284
  },
323
- scopes: {
324
- utils: {
325
- lodash: 'lodash'
326
- },
327
- '': {
328
- existing: 'existing-value'
329
- }
330
- }
285
+ exports: ['pkg:lodash']
331
286
  };
332
287
  const result = getEnvironments(config, 'client', 'test-module');
333
288
 
334
- // 验证核心逻辑:imports 被合并到空字符串 scope
335
- expect(result.scopes['']).toBeDefined();
336
- expect(result.scopes[''].existing).toBe('existing-value'); // 保留现有内容
337
- expect(result.scopes[''].react).toBe('react'); // 合并 imports
338
- expect(result.scopes[''].vue).toBe('vue'); // 合并 imports
339
-
340
- // 验证其他 scopes 不受影响
341
- expect(result.scopes.utils.lodash).toBe('lodash');
342
-
343
- // 验证 result.imports 也包含相同的 imports
289
+ // The derived root scope carries both supply imports and the
290
+ // module's own pkg-exports — no user-authored scopes exist.
291
+ expect(Object.keys(result.scopes)).toEqual(['']);
292
+ expect(result.scopes[''].react).toBe('react');
293
+ expect(result.scopes[''].vue).toBe('vue');
294
+ expect(result.scopes[''].lodash).toBe('test-module/lodash');
344
295
  expect(result.imports.react).toBe('react');
345
296
  expect(result.imports.vue).toBe('vue');
346
297
  });
@@ -403,40 +354,6 @@ describe('Module Config Parser', () => {
403
354
  });
404
355
 
405
356
  describe('Export Processing Functions', () => {
406
- describe('createDefaultExports', () => {
407
- it('should generate client default exports', () => {
408
- const result = createDefaultExports('client');
409
- expect(result['src/entry.client'].file).toBe(
410
- './src/entry.client'
411
- );
412
- expect(result['src/entry.server'].file).toBe('');
413
- });
414
-
415
- it('should generate server default exports', () => {
416
- const result = createDefaultExports('server');
417
- expect(result['src/entry.client'].file).toBe('');
418
- expect(result['src/entry.server'].file).toBe(
419
- './src/entry.server'
420
- );
421
- });
422
-
423
- it('should handle client environment switch case', () => {
424
- const result = createDefaultExports('client');
425
- expect(result['src/entry.client'].file).toBe(
426
- './src/entry.client'
427
- );
428
- expect(result['src/entry.server'].file).toBe('');
429
- });
430
-
431
- it('should handle server environment switch case', () => {
432
- const result = createDefaultExports('server');
433
- expect(result['src/entry.client'].file).toBe('');
434
- expect(result['src/entry.server'].file).toBe(
435
- './src/entry.server'
436
- );
437
- });
438
- });
439
-
440
357
  describe('processStringExport', () => {
441
358
  it('should parse simple string export', () => {
442
359
  const result = processStringExport('./src/component');
@@ -725,11 +642,6 @@ describe('Module Config Parser', () => {
725
642
  server: 'vue/server'
726
643
  }
727
644
  },
728
- scopes: {
729
- utils: {
730
- lodash: 'lodash'
731
- }
732
- },
733
645
  exports: [
734
646
  './src/component',
735
647
  {
@@ -826,3 +738,252 @@ describe('Module Config Parser', () => {
826
738
  });
827
739
  });
828
740
  });
741
+
742
+ describe('Framework entry threading (RFC 0001 Phase 2)', () => {
743
+ describe('parseEntryConfig', () => {
744
+ it('should default to legacy entries for an empty config', () => {
745
+ const entry = parseEntryConfig({});
746
+
747
+ expect(entry).toEqual({
748
+ client: {
749
+ name: 'src/entry.client',
750
+ file: './src/entry.client'
751
+ },
752
+ server: {
753
+ name: 'src/entry.server',
754
+ file: './src/entry.server'
755
+ }
756
+ });
757
+ });
758
+
759
+ it('should resolve both entries to null for lib modules', () => {
760
+ const entry = parseEntryConfig({ lib: true });
761
+
762
+ expect(entry).toEqual({ client: null, server: null });
763
+ });
764
+
765
+ it('should derive the export name from a custom entry file path', () => {
766
+ const entry = parseEntryConfig({
767
+ entry: { client: './custom/main.ts' }
768
+ });
769
+
770
+ expect(entry.client).toEqual({
771
+ name: 'custom/main',
772
+ file: './custom/main.ts'
773
+ });
774
+ expect(entry.server).toEqual(DEFAULT_MODULE_ENTRY.server);
775
+ });
776
+
777
+ it('should keep the path-derived default name for a standard declaration path', () => {
778
+ const entry = parseEntryConfig({
779
+ entry: { client: './src/entry.client.ts' }
780
+ });
781
+
782
+ expect(entry.client).toEqual({
783
+ name: 'src/entry.client',
784
+ file: './src/entry.client.ts'
785
+ });
786
+ });
787
+
788
+ it('should disable a side declared as false', () => {
789
+ const entry = parseEntryConfig({ entry: { server: false } });
790
+
791
+ expect(entry.server).toBeNull();
792
+ expect(entry.client).toEqual(DEFAULT_MODULE_ENTRY.client);
793
+ });
794
+ });
795
+
796
+ describe('parseModuleConfig entry population', () => {
797
+ it('should populate entry for legacy configs', () => {
798
+ const result = parseModuleConfig('test-module', '/test/root');
799
+
800
+ expect(result.entry.client).toEqual(DEFAULT_MODULE_ENTRY.client);
801
+ expect(result.entry.server).toEqual(DEFAULT_MODULE_ENTRY.server);
802
+ });
803
+
804
+ it('should populate null entries for lib configs and omit entry exports', () => {
805
+ const result = parseModuleConfig('test-module', '/test/root', {
806
+ lib: true
807
+ });
808
+
809
+ expect(result.entry).toEqual({ client: null, server: null });
810
+ expect(result.environments.client.exports).toEqual({});
811
+ expect(result.environments.server.exports).toEqual({});
812
+ });
813
+
814
+ it('should thread a custom entry into the environment exports', () => {
815
+ const result = parseModuleConfig('test-module', '/test/root', {
816
+ entry: { client: './custom/main.ts' }
817
+ });
818
+
819
+ expect(result.environments.client.exports['custom/main']).toEqual({
820
+ name: 'custom/main',
821
+ file: './custom/main.ts',
822
+ pkg: false
823
+ });
824
+ expect(result.environments.server.exports['custom/main']).toEqual({
825
+ name: 'custom/main',
826
+ file: '',
827
+ pkg: false
828
+ });
829
+ expect(
830
+ result.environments.server.exports['src/entry.server']
831
+ ).toEqual({
832
+ name: 'src/entry.server',
833
+ file: './src/entry.server',
834
+ pkg: false
835
+ });
836
+ expect(
837
+ result.environments.client.exports['src/entry.client']
838
+ ).toBeUndefined();
839
+ });
840
+ });
841
+
842
+ describe('default entry exports single source of truth', () => {
843
+ it('should derive byte-identical defaults from DEFAULT_MODULE_ENTRY', () => {
844
+ const entry = parseEntryConfig({});
845
+
846
+ expect(createEntryExports(entry, 'client')).toEqual({
847
+ 'src/entry.client': {
848
+ name: 'src/entry.client',
849
+ file: './src/entry.client',
850
+ pkg: false
851
+ },
852
+ 'src/entry.server': {
853
+ name: 'src/entry.server',
854
+ file: '',
855
+ pkg: false
856
+ }
857
+ });
858
+ expect(createEntryExports(entry, 'server')).toEqual({
859
+ 'src/entry.client': {
860
+ name: 'src/entry.client',
861
+ file: '',
862
+ pkg: false
863
+ },
864
+ 'src/entry.server': {
865
+ name: 'src/entry.server',
866
+ file: './src/entry.server',
867
+ pkg: false
868
+ }
869
+ });
870
+ });
871
+ });
872
+
873
+ describe('getEntryChunkId', () => {
874
+ it('should append .ts to the legacy extensionless default', () => {
875
+ const id = getEntryChunkId(
876
+ 'test-module',
877
+ DEFAULT_MODULE_ENTRY.client
878
+ );
879
+
880
+ expect(id).toBe('test-module@src/entry.client.ts');
881
+ });
882
+
883
+ it('should preserve an explicit extension on custom entries', () => {
884
+ expect(
885
+ getEntryChunkId('m', {
886
+ name: 'custom/main',
887
+ file: './custom/main.tsx'
888
+ })
889
+ ).toBe('m@custom/main.tsx');
890
+ expect(
891
+ getEntryChunkId('m', {
892
+ name: 'src/entry.client',
893
+ file: './src/entry.client.ts'
894
+ })
895
+ ).toBe('m@src/entry.client.ts');
896
+ });
897
+ });
898
+
899
+ describe('fleet invariant: built example manifests', () => {
900
+ const repoRoot = path.resolve(import.meta.dirname, '../../..');
901
+ const examplesDir = path.join(repoRoot, 'examples');
902
+
903
+ const findClientManifests = (dir: string): string[] => {
904
+ if (!fs.existsSync(dir)) {
905
+ return [];
906
+ }
907
+ const found: string[] = [];
908
+ for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
909
+ if (!item.isDirectory() || item.name === 'node_modules') {
910
+ continue;
911
+ }
912
+ const child = path.join(dir, item.name);
913
+ const manifest = path.join(child, 'dist/client/manifest.json');
914
+ if (fs.existsSync(manifest)) {
915
+ found.push(manifest);
916
+ }
917
+ found.push(...findClientManifests(child));
918
+ }
919
+ return found;
920
+ };
921
+
922
+ const manifests = findClientManifests(examplesDir);
923
+
924
+ it.skipIf(manifests.length === 0)(
925
+ 'should compute the exact historical chunk seed for every built example',
926
+ () => {
927
+ expect(manifests.length).toBeGreaterThan(0);
928
+ for (const manifestPath of manifests) {
929
+ const manifest = JSON.parse(
930
+ fs.readFileSync(manifestPath, 'utf-8')
931
+ ) as {
932
+ name: string;
933
+ chunks: Record<string, unknown>;
934
+ exports: Record<string, unknown>;
935
+ };
936
+ const parsed = parseModuleConfig(
937
+ manifest.name,
938
+ path.resolve(manifestPath, '../../..')
939
+ );
940
+ if (!parsed.entry.client) {
941
+ continue;
942
+ }
943
+
944
+ const seed = getEntryChunkId(
945
+ manifest.name,
946
+ parsed.entry.client
947
+ );
948
+
949
+ expect(seed).toBe(`${manifest.name}@src/entry.client.ts`);
950
+ if (manifest.exports['src/entry.client']) {
951
+ expect(Object.keys(manifest.chunks)).toContain(seed);
952
+ }
953
+ }
954
+ }
955
+ );
956
+
957
+ it.skipIf(
958
+ !fs.existsSync(
959
+ path.join(
960
+ examplesDir,
961
+ 'micro-app/ssr-micro-shared/dist/client/manifest.json'
962
+ )
963
+ )
964
+ )(
965
+ 'should match the ssr-micro-shared manifest chunk key exactly',
966
+ () => {
967
+ const manifestPath = path.join(
968
+ examplesDir,
969
+ 'micro-app/ssr-micro-shared/dist/client/manifest.json'
970
+ );
971
+ const manifest = JSON.parse(
972
+ fs.readFileSync(manifestPath, 'utf-8')
973
+ ) as { name: string; chunks: Record<string, unknown> };
974
+ const parsed = parseModuleConfig(
975
+ manifest.name,
976
+ path.resolve(manifestPath, '../../..')
977
+ );
978
+
979
+ const seed = getEntryChunkId(
980
+ manifest.name,
981
+ parsed.entry.client!
982
+ );
983
+
984
+ expect(seed).toBe('ssr-micro-shared@src/entry.client.ts');
985
+ expect(Object.keys(manifest.chunks)).toContain(seed);
986
+ }
987
+ );
988
+ });
989
+ });
@@ -3,12 +3,24 @@ import type { BuildEnvironment } from './core';
3
3
 
4
4
  export interface ModuleConfig {
5
5
  lib?: boolean;
6
+ entry?: ModuleConfigEntry;
6
7
  links?: Record<string, string>;
7
8
  imports?: ModuleConfigImportMapping;
8
- scopes?: Record<string, ModuleConfigImportMapping>;
9
9
  exports?: ModuleConfigExportExports;
10
10
  }
11
11
 
12
+ /**
13
+ * Framework entry declaration (RFC 0001 Phase 2).
14
+ *
15
+ * Per side: a `./`-relative source file path declares a custom entry,
16
+ * `false` disables the side, and `undefined` keeps the legacy default
17
+ * (`./src/entry.client` / `./src/entry.server`).
18
+ */
19
+ export interface ModuleConfigEntry {
20
+ client?: string | false;
21
+ server?: string | false;
22
+ }
23
+
12
24
  export type ModuleConfigImportMapping = Record<
13
25
  string,
14
26
  string | Record<BuildEnvironment, string>
@@ -30,6 +42,7 @@ export interface ParsedModuleConfig {
30
42
  name: string;
31
43
  root: string;
32
44
  lib: boolean;
45
+ entry: ParsedModuleConfigEntry;
33
46
  links: Record<string, ParsedModuleConfigLink>;
34
47
  environments: {
35
48
  client: ParsedModuleConfigEnvironment;
@@ -37,6 +50,19 @@ export interface ParsedModuleConfig {
37
50
  };
38
51
  }
39
52
 
53
+ /** A resolved framework entry: export name + source file path. */
54
+ export interface ParsedModuleConfigEntryTarget {
55
+ /** Export name (import specifier suffix), e.g. 'src/entry.client'. */
56
+ name: string;
57
+ /** Source file path, e.g. './src/entry.client' or './custom/main.ts'. */
58
+ file: string;
59
+ }
60
+
61
+ export interface ParsedModuleConfigEntry {
62
+ client: ParsedModuleConfigEntryTarget | null;
63
+ server: ParsedModuleConfigEntryTarget | null;
64
+ }
65
+
40
66
  export type ParsedModuleConfigExports = Record<
41
67
  string,
42
68
  ParsedModuleConfigExport
@@ -63,19 +89,88 @@ export interface ParsedModuleConfigLink {
63
89
  serverManifestJson: string;
64
90
  }
65
91
 
92
+ /**
93
+ * Single source of truth for the default framework entries, used as the
94
+ * fallback when a module declares no explicit `entry` (RFC 0001 Phase 2
95
+ * threads resolved entries through the config instead of hard-coding them).
96
+ */
97
+ export const DEFAULT_MODULE_ENTRY: Record<
98
+ 'client' | 'server',
99
+ ParsedModuleConfigEntryTarget
100
+ > = {
101
+ client: { name: 'src/entry.client', file: './src/entry.client' },
102
+ server: { name: 'src/entry.server', file: './src/entry.server' }
103
+ };
104
+
105
+ const FILE_EXT_REGEX =
106
+ /\.(js|mjs|cjs|jsx|mjsx|cjsx|ts|mts|cts|tsx|mtsx|ctsx)$/i;
107
+
108
+ function parseEntryTarget(
109
+ value: string | false | undefined,
110
+ fallback: ParsedModuleConfigEntryTarget
111
+ ): ParsedModuleConfigEntryTarget | null {
112
+ if (value === false) {
113
+ return null;
114
+ }
115
+ if (value === undefined) {
116
+ return { ...fallback };
117
+ }
118
+ const relativePath = value.startsWith('./') ? value.slice(2) : value;
119
+ const parsed = parsedExportValue(`root:${relativePath}`);
120
+ return { name: parsed.name, file: parsed.file };
121
+ }
122
+
123
+ export function parseEntryConfig(
124
+ config: ModuleConfig
125
+ ): ParsedModuleConfigEntry {
126
+ if (config.lib) {
127
+ return { client: null, server: null };
128
+ }
129
+ return {
130
+ client: parseEntryTarget(
131
+ config.entry?.client,
132
+ DEFAULT_MODULE_ENTRY.client
133
+ ),
134
+ server: parseEntryTarget(
135
+ config.entry?.server,
136
+ DEFAULT_MODULE_ENTRY.server
137
+ )
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Manifest chunk identifier for an entry target: `<module>@<srcRelPath>`
143
+ * including the file extension (bundlers key chunks by the resolved source
144
+ * file, so the legacy extensionless default maps to `.ts`).
145
+ */
146
+ export function getEntryChunkId(
147
+ moduleName: string,
148
+ target: ParsedModuleConfigEntryTarget
149
+ ): string {
150
+ const relativePath = target.file.startsWith('./')
151
+ ? target.file.slice(2)
152
+ : target.file;
153
+ const withExtension = FILE_EXT_REGEX.test(relativePath)
154
+ ? relativePath
155
+ : `${relativePath}.ts`;
156
+ return `${moduleName}@${withExtension}`;
157
+ }
158
+
66
159
  export function parseModuleConfig(
67
160
  name: string,
68
161
  root: string,
69
162
  config: ModuleConfig = {}
70
163
  ): ParsedModuleConfig {
164
+ const entry = parseEntryConfig(config);
71
165
  return {
72
166
  name,
73
167
  root,
74
168
  lib: config.lib ?? false,
169
+ entry,
75
170
  links: getLinks(name, root, config),
76
171
  environments: {
77
- client: getEnvironments(config, 'client', name),
78
- server: getEnvironments(config, 'server', name)
172
+ client: getEnvironments(config, 'client', name, entry),
173
+ server: getEnvironments(config, 'server', name, entry)
79
174
  }
80
175
  };
81
176
  }
@@ -128,33 +223,21 @@ export function getEnvironmentImports(
128
223
  return result;
129
224
  }
130
225
 
131
- export function getEnvironmentScopes(
132
- environment: BuildEnvironment,
133
- scopes: Record<string, ModuleConfigImportMapping> = {}
134
- ): Record<string, Record<string, string>> {
135
- const result: Record<string, Record<string, string>> = {};
136
-
137
- for (const [scopeName, scopeImports] of Object.entries(scopes)) {
138
- result[scopeName] = getEnvironmentImports(environment, scopeImports);
139
- }
140
-
141
- return result;
142
- }
143
-
144
226
  export function getEnvironments(
145
227
  config: ModuleConfig,
146
228
  env: BuildEnvironment,
147
- moduleName: string
229
+ moduleName: string,
230
+ entry: ParsedModuleConfigEntry = parseEntryConfig(config)
148
231
  ): ParsedModuleConfigEnvironment {
149
232
  const imports = getEnvironmentImports(env, config.imports);
150
- const exports = getEnvironmentExports(config, env);
151
- const scopes = getEnvironmentScopes(env, {
152
- ...config.scopes,
153
- '': {
154
- ...config.scopes?.[''],
155
- ...imports
156
- }
157
- });
233
+ const exports = getEnvironmentExports(config, env, entry);
234
+ // The single root scope (`''`) is derived from the supply wiring and the
235
+ // module's own pkg-exports. There is no user-authored directory-scope
236
+ // remapping (RFC 0001 §4): per-module isolation and multi-major
237
+ // coexistence are derived from declarations, never hand-mapped.
238
+ const scopes: Record<string, Record<string, string>> = {
239
+ '': { ...imports }
240
+ };
158
241
  addPackageExportsToScopes(exports, scopes, moduleName);
159
242
  return {
160
243
  imports,
@@ -163,37 +246,28 @@ export function getEnvironments(
163
246
  };
164
247
  }
165
248
 
166
- export function createDefaultExports(
249
+ /**
250
+ * Builds the per-environment exports for the resolved framework entries.
251
+ * Each entry exports its source file in its own environment and an empty
252
+ * file on the other side (the inactive side is skipped by adapters).
253
+ */
254
+ export function createEntryExports(
255
+ entry: ParsedModuleConfigEntry,
167
256
  env: BuildEnvironment
168
257
  ): ParsedModuleConfigExports {
169
- switch (env) {
170
- case 'client':
171
- return {
172
- 'src/entry.client': {
173
- name: 'src/entry.client',
174
- file: './src/entry.client',
175
- pkg: false
176
- },
177
- 'src/entry.server': {
178
- name: 'src/entry.server',
179
- file: '',
180
- pkg: false
181
- }
182
- };
183
- case 'server':
184
- return {
185
- 'src/entry.client': {
186
- name: 'src/entry.client',
187
- file: '',
188
- pkg: false
189
- },
190
- 'src/entry.server': {
191
- name: 'src/entry.server',
192
- file: './src/entry.server',
193
- pkg: false
194
- }
195
- };
258
+ const exports: ParsedModuleConfigExports = {};
259
+ for (const side of ['client', 'server'] as const) {
260
+ const target = entry[side];
261
+ if (!target) {
262
+ continue;
263
+ }
264
+ exports[target.name] = {
265
+ name: target.name,
266
+ file: side === env ? target.file : '',
267
+ pkg: false
268
+ };
196
269
  }
270
+ return exports;
197
271
  }
198
272
 
199
273
  export function processStringExport(
@@ -263,9 +337,10 @@ export function processExportArray(
263
337
 
264
338
  export function getEnvironmentExports(
265
339
  config: ModuleConfig,
266
- env: BuildEnvironment
340
+ env: BuildEnvironment,
341
+ entry: ParsedModuleConfigEntry = parseEntryConfig(config)
267
342
  ): ParsedModuleConfigExports {
268
- const exports = config.lib ? {} : createDefaultExports(env);
343
+ const exports = createEntryExports(entry, env);
269
344
 
270
345
  if (config.exports) {
271
346
  const userExports = processExportArray(config.exports, env);
@@ -293,9 +368,6 @@ export function addPackageExportsToScopes(
293
368
  }
294
369
 
295
370
  export function parsedExportValue(value: string): ParsedModuleConfigExport {
296
- const FILE_EXT_REGEX =
297
- /\.(js|mjs|cjs|jsx|mjsx|cjsx|ts|mts|cts|tsx|mtsx|ctsx)$/i;
298
-
299
371
  if (value.startsWith('pkg:')) {
300
372
  const item = value.substring('pkg:'.length);
301
373
  return {