@esmx/core 3.0.0-rc.70 → 3.0.0-rc.71

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/README.md CHANGED
@@ -20,7 +20,7 @@
20
20
  </a>
21
21
  </div>
22
22
 
23
- <p>A high-performance microfrontend framework supporting Vue, React, Preact, Solid, and Svelte with SSR and Module Federation capabilities</p>
23
+ <p>A high-performance microfrontend framework supporting Vue, React, Preact, Solid, and Svelte with SSR and Module Linking capabilities</p>
24
24
 
25
25
  <p>
26
26
  English | <a href="https://github.com/esmnext/esmx/blob/master/packages/core/README.zh-CN.md">中文</a>
@@ -38,16 +38,19 @@
38
38
  ## 📦 Installation
39
39
 
40
40
  ```bash
41
+ # npm
41
42
  npm install @esmx/core
42
- ```
43
43
 
44
- ## 🚀 Quick Start
44
+ # pnpm
45
+ pnpm add @esmx/core
45
46
 
46
- ```bash
47
- npm install @esmx/core
47
+ # yarn
48
+ yarn add @esmx/core
48
49
  ```
49
50
 
50
- For detailed usage examples and configuration options, please visit the [official documentation](https://esmx.dev).
51
+ ## 🚀 Quick Start
52
+
53
+ See Quick Start guide: https://esmx.dev/guide/start/getting-started.html
51
54
 
52
55
  ## 📚 Documentation
53
56
 
@@ -55,4 +58,4 @@ Visit the [official documentation](https://esmx.dev) for detailed usage guides a
55
58
 
56
59
  ## 📄 License
57
60
 
58
- MIT © [Esmx Team](https://github.com/esmnext/esmx)
61
+ MIT © [Esmx Team](https://github.com/esmnext/esmx)
package/README.zh-CN.md CHANGED
@@ -20,7 +20,7 @@
20
20
  </a>
21
21
  </div>
22
22
 
23
- <p>支持 Vue、React、Preact、Solid、Svelte 的高性能微前端框架,具备 SSR 和模块联邦能力</p>
23
+ <p>支持 Vue、React、Preact、Solid、Svelte 的高性能微前端框架,具备 SSR 和模块链接能力</p>
24
24
 
25
25
  <p>
26
26
  <a href="https://github.com/esmnext/esmx/blob/master/packages/core/README.md">English</a> | 中文
@@ -38,16 +38,19 @@
38
38
  ## 📦 安装
39
39
 
40
40
  ```bash
41
+ # npm
41
42
  npm install @esmx/core
42
- ```
43
43
 
44
- ## 🚀 快速开始
44
+ # pnpm
45
+ pnpm add @esmx/core
45
46
 
46
- ```bash
47
- npm install @esmx/core
47
+ # yarn
48
+ yarn add @esmx/core
48
49
  ```
49
50
 
50
- 详细的使用示例和配置选项,请访问[官方文档](https://esmx.dev)。
51
+ ## 🚀 快速开始
52
+
53
+ 查看快速开始指南:https://esmx.dev/zh-CN/guide/start/getting-started.html
51
54
 
52
55
  ## 📚 文档
53
56
 
@@ -55,4 +58,4 @@ npm install @esmx/core
55
58
 
56
59
  ## 📄 许可证
57
60
 
58
- MIT © [Esmx Team](https://github.com/esmnext/esmx)
61
+ MIT © [Esmx Team](https://github.com/esmnext/esmx)
package/dist/cli/cli.mjs CHANGED
@@ -10,12 +10,17 @@ async function getSrcOptions() {
10
10
  }
11
11
  async function getDistOptions() {
12
12
  try {
13
- return import(resolveImportPath(process.cwd(), "./dist/index.mjs")).then((m) => m.default);
13
+ const m = await import(resolveImportPath(process.cwd(), "./dist/node/src/entry.node.mjs"));
14
+ return m.default;
14
15
  } catch (e) {
15
- throw new Error(
16
- `Failed to load configuration from dist/index.mjs. Please make sure you have run 'esmx build' first.
17
- ${e}`
16
+ console.error(
17
+ styleText(
18
+ "red",
19
+ "Failed to load dist entry: dist/node/src/entry.node.mjs"
20
+ )
18
21
  );
22
+ console.error(styleText("yellow", "Run `esmx build` and try again."));
23
+ process.exit(17);
19
24
  }
20
25
  }
21
26
  export async function cli(command) {
package/dist/core.mjs CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  parsePackConfig
15
15
  } from "./pack-config.mjs";
16
16
  import { createCache } from "./utils/cache.mjs";
17
- import { createImportMap } from "./utils/import-map.mjs";
17
+ import { createClientImportMap, createImportMap } from "./utils/import-map.mjs";
18
18
  import { resolvePath } from "./utils/resolve-path.mjs";
19
19
  import { getImportPreloadInfo as getStaticImportPaths } from "./utils/static-import-lexer.mjs";
20
20
  export var COMMAND = /* @__PURE__ */ ((COMMAND2) => {
@@ -677,7 +677,7 @@ export class Esmx {
677
677
  let json = {};
678
678
  switch (env) {
679
679
  case "client": {
680
- json = createImportMap({
680
+ json = createClientImportMap({
681
681
  manifests,
682
682
  getScope(name, scope) {
683
683
  return `/${name}${scope}`;
@@ -47,4 +47,6 @@ export declare function createScopesMap(imports: SpecifierMap, manifests: readon
47
47
  * @see https://issues.chromium.org/issues/453147451
48
48
  */
49
49
  export declare function fixImportMapNestedScopes(importMap: Required<ImportMap>): Required<ImportMap>;
50
+ export declare function compressImportMap(importMap: Required<ImportMap>): ImportMap;
50
51
  export declare function createImportMap({ manifests, getFile, getScope }: GetImportMapOptions): Required<ImportMap>;
52
+ export declare function createClientImportMap(options: GetImportMapOptions): ImportMap;
@@ -48,6 +48,51 @@ export function fixImportMapNestedScopes(importMap) {
48
48
  });
49
49
  return importMap;
50
50
  }
51
+ export function compressImportMap(importMap) {
52
+ const compressed = {
53
+ imports: { ...importMap.imports },
54
+ scopes: {}
55
+ };
56
+ const counts = {};
57
+ Object.values(importMap.scopes).forEach((scopeMappings) => {
58
+ Object.entries(scopeMappings).forEach(([specifier, target]) => {
59
+ if (Object.hasOwn(importMap.imports, specifier)) return;
60
+ counts[specifier] ??= {};
61
+ counts[specifier][target] = (counts[specifier][target] ?? 0) + 1;
62
+ });
63
+ });
64
+ Object.entries(counts).forEach(([specifier, targetCounts]) => {
65
+ const entries = Object.entries(targetCounts);
66
+ let best = null;
67
+ let secondBestCount = 0;
68
+ for (const [t, c] of entries) {
69
+ if (!best || c > best[1]) {
70
+ secondBestCount = best ? Math.max(secondBestCount, best[1]) : secondBestCount;
71
+ best = [t, c];
72
+ } else {
73
+ secondBestCount = Math.max(secondBestCount, c);
74
+ }
75
+ }
76
+ if (best && best[1] > secondBestCount) {
77
+ compressed.imports[specifier] = best[0];
78
+ }
79
+ });
80
+ Object.entries(importMap.scopes).forEach(([scopePath, scopeMappings]) => {
81
+ const filtered = {};
82
+ Object.entries(scopeMappings).forEach(([specifier, target]) => {
83
+ const globalTarget = compressed.imports[specifier];
84
+ if (globalTarget === target) {
85
+ return;
86
+ }
87
+ filtered[specifier] = target;
88
+ });
89
+ if (Object.keys(filtered).length > 0) {
90
+ compressed.scopes[scopePath] = filtered;
91
+ }
92
+ });
93
+ const hasScopes = Object.keys(compressed.scopes).length > 0;
94
+ return hasScopes ? compressed : { imports: compressed.imports };
95
+ }
51
96
  export function createImportMap({
52
97
  manifests,
53
98
  getFile,
@@ -60,3 +105,8 @@ export function createImportMap({
60
105
  scopes
61
106
  };
62
107
  }
108
+ export function createClientImportMap(options) {
109
+ const base = createImportMap(options);
110
+ const fixed = fixImportMapNestedScopes(base);
111
+ return compressImportMap(fixed);
112
+ }
@@ -1,5 +1,6 @@
1
1
  import { assert, describe, test } from "vitest";
2
2
  import {
3
+ compressImportMap,
3
4
  createImportMap,
4
5
  createImportsMap,
5
6
  createScopesMap,
@@ -391,18 +392,16 @@ describe("fixImportMapNestedScopes", () => {
391
392
  }
392
393
  };
393
394
  const result = fixImportMapNestedScopes(importMap);
394
- assert.deepEqual(result.imports, importMap.imports);
395
- assert.deepEqual(
396
- result.scopes["/shared-modules/vue2/"],
397
- importMap.scopes["/shared-modules/vue2/"]
398
- );
399
- assert.deepEqual(
400
- result.scopes["/shared-modules/vue2/component.u5v4w3x2.final.mjs"],
401
- {
402
- vue: "/shared-modules/vue2.q9r8s7t6.final.mjs",
403
- "vue-router": "/shared-modules/vue2/router.y1z0a9b8.final.mjs"
395
+ const expected = {
396
+ imports: importMap.imports,
397
+ scopes: {
398
+ "/shared-modules/vue2/component.u5v4w3x2.final.mjs": {
399
+ vue: "/shared-modules/vue2.q9r8s7t6.final.mjs",
400
+ "vue-router": "/shared-modules/vue2/router.y1z0a9b8.final.mjs"
401
+ }
404
402
  }
405
- );
403
+ };
404
+ assert.deepEqual(result, expected);
406
405
  });
407
406
  test("should handle complex priority scenarios with multiple nested levels", () => {
408
407
  const importMap = {
@@ -609,8 +608,11 @@ describe("fixImportMapNestedScopes", () => {
609
608
  }
610
609
  };
611
610
  const result = fixImportMapNestedScopes(importMap);
612
- assert.deepEqual(result.imports, importMap.imports);
613
- assert.isUndefined(result.scopes["/shared/modules/vue2/"]);
611
+ const expected = {
612
+ imports: importMap.imports,
613
+ scopes: {}
614
+ };
615
+ assert.deepEqual(result, expected);
614
616
  assert.doesNotThrow(() => {
615
617
  fixImportMapNestedScopes(importMap);
616
618
  });
@@ -1438,3 +1440,147 @@ describe("createImportMap", () => {
1438
1440
  });
1439
1441
  });
1440
1442
  });
1443
+ describe("compressImportMap", () => {
1444
+ test("does not promote when no global exists; keeps scopes intact", () => {
1445
+ const importMap = {
1446
+ imports: {},
1447
+ scopes: {
1448
+ "/a/": {
1449
+ vue: "/a/vue.final.mjs"
1450
+ },
1451
+ "/b/": {
1452
+ vue: "/a/vue.final.mjs"
1453
+ }
1454
+ }
1455
+ };
1456
+ const result = compressImportMap(importMap);
1457
+ assert.deepEqual(result, {
1458
+ imports: { vue: "/a/vue.final.mjs" }
1459
+ });
1460
+ });
1461
+ test("does not promote when scoped mappings conflict across scopes", () => {
1462
+ const importMap = {
1463
+ imports: {},
1464
+ scopes: {
1465
+ "/a/": { vue: "/a/vue.final.mjs" },
1466
+ "/b/": { vue: "/b/vue.final.mjs" }
1467
+ }
1468
+ };
1469
+ const result = compressImportMap(importMap);
1470
+ assert.deepEqual(result, importMap);
1471
+ });
1472
+ test("removes scoped entries that equal global mapping", () => {
1473
+ const importMap = {
1474
+ imports: { vue: "/shared/vue.final.mjs" },
1475
+ scopes: {
1476
+ "/a/": {
1477
+ vue: "/shared/vue.final.mjs",
1478
+ lodash: "/a/lodash.final.mjs"
1479
+ }
1480
+ }
1481
+ };
1482
+ const result = compressImportMap(importMap);
1483
+ const expected = {
1484
+ imports: {
1485
+ vue: "/shared/vue.final.mjs",
1486
+ lodash: "/a/lodash.final.mjs"
1487
+ }
1488
+ };
1489
+ assert.deepEqual(result, expected);
1490
+ });
1491
+ test("promotes to global when global matches single unique target across scopes", () => {
1492
+ const importMap = {
1493
+ imports: { vue: "/shared/vue.final.mjs" },
1494
+ scopes: {
1495
+ "/a/": { vue: "/shared/vue.final.mjs" },
1496
+ "/b/": { vue: "/shared/vue.final.mjs" }
1497
+ }
1498
+ };
1499
+ const result = compressImportMap(importMap);
1500
+ assert.deepEqual(result, {
1501
+ imports: { vue: "/shared/vue.final.mjs" }
1502
+ });
1503
+ });
1504
+ test("promotes to global when no global exists and targets are consistent across scopes (promote mode)", () => {
1505
+ const importMap = {
1506
+ imports: {},
1507
+ scopes: {
1508
+ "/a/": { vue: "/x/vue.final.mjs" },
1509
+ "/b/": { vue: "/x/vue.final.mjs" }
1510
+ }
1511
+ };
1512
+ const result = compressImportMap(importMap);
1513
+ assert.deepEqual(result, {
1514
+ imports: { vue: "/x/vue.final.mjs" }
1515
+ });
1516
+ });
1517
+ test("promotes dominant target and keeps exceptions in scopes (promote mode)", () => {
1518
+ const importMap = {
1519
+ imports: {},
1520
+ scopes: {
1521
+ "/a/": { vue: "/x/vue.final.mjs" },
1522
+ "/b/": { vue: "/x/vue.final.mjs" },
1523
+ "/c/": { vue: "/y/vue2.final.mjs" }
1524
+ }
1525
+ };
1526
+ const result = compressImportMap(importMap);
1527
+ assert.deepEqual(result, {
1528
+ imports: { vue: "/x/vue.final.mjs" },
1529
+ scopes: {
1530
+ "/c/": { vue: "/y/vue2.final.mjs" }
1531
+ }
1532
+ });
1533
+ });
1534
+ test("does not promote when no global exists; keeps scopes intact", () => {
1535
+ const importMap = {
1536
+ imports: {},
1537
+ scopes: {
1538
+ "/a/": { vue: "/shared/vue.final.mjs" },
1539
+ "/b/": { vue: "/shared/vue.final.mjs" },
1540
+ "/c/": { vue: "/c/vue2.final.mjs" }
1541
+ }
1542
+ };
1543
+ const result = compressImportMap(importMap);
1544
+ assert.deepEqual(result, {
1545
+ imports: { vue: "/shared/vue.final.mjs" },
1546
+ scopes: { "/c/": { vue: "/c/vue2.final.mjs" } }
1547
+ });
1548
+ });
1549
+ test("aggressive default does not override different existing global", () => {
1550
+ const importMap = {
1551
+ imports: { vue: "/global/vue.final.mjs" },
1552
+ scopes: {
1553
+ "/a/": { vue: "/shared/vue.final.mjs" },
1554
+ "/b/": { vue: "/shared/vue.final.mjs" }
1555
+ }
1556
+ };
1557
+ const result = compressImportMap(importMap);
1558
+ assert.deepEqual(result, {
1559
+ imports: { vue: "/global/vue.final.mjs" },
1560
+ scopes: {
1561
+ "/a/": { vue: "/shared/vue.final.mjs" },
1562
+ "/b/": { vue: "/shared/vue.final.mjs" }
1563
+ }
1564
+ });
1565
+ });
1566
+ test("returns new object and keeps input unchanged", () => {
1567
+ const original = {
1568
+ imports: {},
1569
+ scopes: {
1570
+ "/a/": { vue: "/x/vue.final.mjs" },
1571
+ "/b/": { vue: "/x/vue.final.mjs" },
1572
+ "/c/": { vue: "/y/vue2.final.mjs" }
1573
+ }
1574
+ };
1575
+ const importMap = JSON.parse(JSON.stringify(original));
1576
+ const result = compressImportMap(importMap);
1577
+ assert.notStrictEqual(result, importMap);
1578
+ assert.notStrictEqual(result.imports, importMap.imports);
1579
+ assert.notStrictEqual(result.scopes, importMap.scopes);
1580
+ assert.deepEqual(importMap, original);
1581
+ assert.deepEqual(result, {
1582
+ imports: { vue: "/x/vue.final.mjs" },
1583
+ scopes: { "/c/": { vue: "/y/vue2.final.mjs" } }
1584
+ });
1585
+ });
1586
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esmx/core",
3
- "description": "A high-performance microfrontend framework supporting Vue, React, Preact, Solid, and Svelte with SSR and Module Federation capabilities.",
3
+ "description": "A high-performance microfrontend framework supporting Vue, React, Preact, Solid, and Svelte with SSR and Module Linking capabilities.",
4
4
  "contributors": [
5
5
  {
6
6
  "name": "lzxb",
@@ -39,7 +39,7 @@
39
39
  "Microfrontend",
40
40
  "SSR",
41
41
  "Rspack",
42
- "Module Federation",
42
+ "Module Linking",
43
43
  "High Performance",
44
44
  "TypeScript"
45
45
  ],
@@ -59,7 +59,7 @@
59
59
  "build": "unbuild"
60
60
  },
61
61
  "dependencies": {
62
- "@esmx/import": "3.0.0-rc.70",
62
+ "@esmx/import": "3.0.0-rc.71",
63
63
  "@types/serialize-javascript": "^5.0.4",
64
64
  "es-module-lexer": "^1.7.0",
65
65
  "find": "^0.3.0",
@@ -69,15 +69,15 @@
69
69
  "devDependencies": {
70
70
  "@biomejs/biome": "1.9.4",
71
71
  "@types/find": "^0.2.4",
72
- "@types/node": "^24.0.0",
73
- "@types/send": "^0.17.4",
72
+ "@types/node": "^24.10.0",
73
+ "@types/send": "^1.2.1",
74
74
  "@types/write": "^2.0.4",
75
75
  "@vitest/coverage-v8": "3.2.4",
76
- "typescript": "5.9.2",
77
- "unbuild": "3.6.0",
76
+ "typescript": "5.9.3",
77
+ "unbuild": "3.6.1",
78
78
  "vitest": "3.2.4"
79
79
  },
80
- "version": "3.0.0-rc.70",
80
+ "version": "3.0.0-rc.71",
81
81
  "type": "module",
82
82
  "private": false,
83
83
  "exports": {
@@ -100,5 +100,5 @@
100
100
  "template",
101
101
  "public"
102
102
  ],
103
- "gitHead": "9aa452ae73e450d285e4ddbd35a4ac8b53427d95"
103
+ "gitHead": "1616c7a1f820387d4d14bac0babd42356f6f7f33"
104
104
  }
package/src/cli/cli.ts CHANGED
@@ -13,13 +13,19 @@ async function getSrcOptions(): Promise<EsmxOptions> {
13
13
 
14
14
  async function getDistOptions(): Promise<EsmxOptions> {
15
15
  try {
16
- return import(
17
- resolveImportPath(process.cwd(), './dist/index.mjs')
18
- ).then((m) => m.default);
16
+ const m = await import(
17
+ resolveImportPath(process.cwd(), './dist/node/src/entry.node.mjs')
18
+ );
19
+ return m.default;
19
20
  } catch (e) {
20
- throw new Error(
21
- `Failed to load configuration from dist/index.mjs. Please make sure you have run 'esmx build' first.\n${e}`
21
+ console.error(
22
+ styleText(
23
+ 'red',
24
+ 'Failed to load dist entry: dist/node/src/entry.node.mjs'
25
+ )
22
26
  );
27
+ console.error(styleText('yellow', 'Run `esmx build` and try again.'));
28
+ process.exit(17);
23
29
  }
24
30
  }
25
31
 
package/src/core.ts CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  import type { ImportmapMode } from './render-context';
23
23
  import type { RenderContext, RenderContextOptions } from './render-context';
24
24
  import { type CacheHandle, createCache } from './utils/cache';
25
- import { createImportMap } from './utils/import-map';
25
+ import { createClientImportMap, createImportMap } from './utils/import-map';
26
26
  import type { Middleware } from './utils/middleware';
27
27
  import { type ProjectPath, resolvePath } from './utils/resolve-path';
28
28
  import { getImportPreloadInfo as getStaticImportPaths } from './utils/static-import-lexer';
@@ -854,7 +854,7 @@ export class Esmx {
854
854
  let json: ImportMap = {};
855
855
  switch (env) {
856
856
  case 'client': {
857
- json = createImportMap({
857
+ json = createClientImportMap({
858
858
  manifests,
859
859
  getScope(name, scope) {
860
860
  return `/${name}${scope}`;
@@ -1,5 +1,6 @@
1
1
  import { assert, describe, test } from 'vitest';
2
2
  import {
3
+ compressImportMap,
3
4
  createImportMap,
4
5
  createImportsMap,
5
6
  createScopesMap,
@@ -461,20 +462,17 @@ describe('fixImportMapNestedScopes', () => {
461
462
  };
462
463
 
463
464
  const result = fixImportMapNestedScopes(importMap);
464
-
465
- assert.deepEqual(result.imports, importMap.imports);
466
- assert.deepEqual(
467
- result.scopes['/shared-modules/vue2/'],
468
- importMap.scopes['/shared-modules/vue2/']
469
- );
470
-
471
- assert.deepEqual(
472
- result.scopes['/shared-modules/vue2/component.u5v4w3x2.final.mjs'],
473
- {
474
- vue: '/shared-modules/vue2.q9r8s7t6.final.mjs',
475
- 'vue-router': '/shared-modules/vue2/router.y1z0a9b8.final.mjs'
465
+ const expected = {
466
+ imports: importMap.imports,
467
+ scopes: {
468
+ '/shared-modules/vue2/component.u5v4w3x2.final.mjs': {
469
+ vue: '/shared-modules/vue2.q9r8s7t6.final.mjs',
470
+ 'vue-router':
471
+ '/shared-modules/vue2/router.y1z0a9b8.final.mjs'
472
+ }
476
473
  }
477
- );
474
+ };
475
+ assert.deepEqual(result, expected);
478
476
  });
479
477
 
480
478
  test('should handle complex priority scenarios with multiple nested levels', () => {
@@ -747,8 +745,11 @@ describe('fixImportMapNestedScopes', () => {
747
745
  };
748
746
 
749
747
  const result = fixImportMapNestedScopes(importMap);
750
- assert.deepEqual(result.imports, importMap.imports);
751
- assert.isUndefined(result.scopes['/shared/modules/vue2/']);
748
+ const expected = {
749
+ imports: importMap.imports,
750
+ scopes: {}
751
+ };
752
+ assert.deepEqual(result, expected);
752
753
  assert.doesNotThrow(() => {
753
754
  fixImportMapNestedScopes(importMap);
754
755
  });
@@ -1620,3 +1621,160 @@ describe('createImportMap', () => {
1620
1621
  });
1621
1622
  });
1622
1623
  });
1624
+
1625
+ describe('compressImportMap', () => {
1626
+ test('does not promote when no global exists; keeps scopes intact', () => {
1627
+ const importMap = {
1628
+ imports: {},
1629
+ scopes: {
1630
+ '/a/': {
1631
+ vue: '/a/vue.final.mjs'
1632
+ },
1633
+ '/b/': {
1634
+ vue: '/a/vue.final.mjs'
1635
+ }
1636
+ }
1637
+ };
1638
+
1639
+ const result = compressImportMap(importMap);
1640
+ assert.deepEqual(result, {
1641
+ imports: { vue: '/a/vue.final.mjs' }
1642
+ });
1643
+ });
1644
+
1645
+ test('does not promote when scoped mappings conflict across scopes', () => {
1646
+ const importMap = {
1647
+ imports: {},
1648
+ scopes: {
1649
+ '/a/': { vue: '/a/vue.final.mjs' },
1650
+ '/b/': { vue: '/b/vue.final.mjs' }
1651
+ }
1652
+ };
1653
+
1654
+ const result = compressImportMap(importMap);
1655
+ assert.deepEqual(result, importMap);
1656
+ });
1657
+
1658
+ test('removes scoped entries that equal global mapping', () => {
1659
+ const importMap = {
1660
+ imports: { vue: '/shared/vue.final.mjs' },
1661
+ scopes: {
1662
+ '/a/': {
1663
+ vue: '/shared/vue.final.mjs',
1664
+ lodash: '/a/lodash.final.mjs'
1665
+ }
1666
+ }
1667
+ };
1668
+
1669
+ const result = compressImportMap(importMap);
1670
+ const expected = {
1671
+ imports: {
1672
+ vue: '/shared/vue.final.mjs',
1673
+ lodash: '/a/lodash.final.mjs'
1674
+ }
1675
+ };
1676
+ assert.deepEqual(result, expected);
1677
+ });
1678
+
1679
+ test('promotes to global when global matches single unique target across scopes', () => {
1680
+ const importMap = {
1681
+ imports: { vue: '/shared/vue.final.mjs' },
1682
+ scopes: {
1683
+ '/a/': { vue: '/shared/vue.final.mjs' },
1684
+ '/b/': { vue: '/shared/vue.final.mjs' }
1685
+ }
1686
+ };
1687
+
1688
+ const result = compressImportMap(importMap);
1689
+ assert.deepEqual(result, {
1690
+ imports: { vue: '/shared/vue.final.mjs' }
1691
+ });
1692
+ });
1693
+
1694
+ test('promotes to global when no global exists and targets are consistent across scopes (promote mode)', () => {
1695
+ const importMap = {
1696
+ imports: {},
1697
+ scopes: {
1698
+ '/a/': { vue: '/x/vue.final.mjs' },
1699
+ '/b/': { vue: '/x/vue.final.mjs' }
1700
+ }
1701
+ };
1702
+ const result = compressImportMap(importMap);
1703
+ assert.deepEqual(result, {
1704
+ imports: { vue: '/x/vue.final.mjs' }
1705
+ });
1706
+ });
1707
+
1708
+ test('promotes dominant target and keeps exceptions in scopes (promote mode)', () => {
1709
+ const importMap = {
1710
+ imports: {},
1711
+ scopes: {
1712
+ '/a/': { vue: '/x/vue.final.mjs' },
1713
+ '/b/': { vue: '/x/vue.final.mjs' },
1714
+ '/c/': { vue: '/y/vue2.final.mjs' }
1715
+ }
1716
+ };
1717
+ const result = compressImportMap(importMap);
1718
+ assert.deepEqual(result, {
1719
+ imports: { vue: '/x/vue.final.mjs' },
1720
+ scopes: {
1721
+ '/c/': { vue: '/y/vue2.final.mjs' }
1722
+ }
1723
+ });
1724
+ });
1725
+
1726
+ test('does not promote when no global exists; keeps scopes intact', () => {
1727
+ const importMap = {
1728
+ imports: {},
1729
+ scopes: {
1730
+ '/a/': { vue: '/shared/vue.final.mjs' },
1731
+ '/b/': { vue: '/shared/vue.final.mjs' },
1732
+ '/c/': { vue: '/c/vue2.final.mjs' }
1733
+ }
1734
+ };
1735
+ const result = compressImportMap(importMap);
1736
+ assert.deepEqual(result, {
1737
+ imports: { vue: '/shared/vue.final.mjs' },
1738
+ scopes: { '/c/': { vue: '/c/vue2.final.mjs' } }
1739
+ });
1740
+ });
1741
+
1742
+ test('aggressive default does not override different existing global', () => {
1743
+ const importMap = {
1744
+ imports: { vue: '/global/vue.final.mjs' },
1745
+ scopes: {
1746
+ '/a/': { vue: '/shared/vue.final.mjs' },
1747
+ '/b/': { vue: '/shared/vue.final.mjs' }
1748
+ }
1749
+ };
1750
+ const result = compressImportMap(importMap);
1751
+ assert.deepEqual(result, {
1752
+ imports: { vue: '/global/vue.final.mjs' },
1753
+ scopes: {
1754
+ '/a/': { vue: '/shared/vue.final.mjs' },
1755
+ '/b/': { vue: '/shared/vue.final.mjs' }
1756
+ }
1757
+ });
1758
+ });
1759
+
1760
+ test('returns new object and keeps input unchanged', () => {
1761
+ const original = {
1762
+ imports: {},
1763
+ scopes: {
1764
+ '/a/': { vue: '/x/vue.final.mjs' },
1765
+ '/b/': { vue: '/x/vue.final.mjs' },
1766
+ '/c/': { vue: '/y/vue2.final.mjs' }
1767
+ }
1768
+ };
1769
+ const importMap = JSON.parse(JSON.stringify(original));
1770
+ const result = compressImportMap(importMap);
1771
+ assert.notStrictEqual(result, importMap);
1772
+ assert.notStrictEqual(result.imports, importMap.imports);
1773
+ assert.notStrictEqual(result.scopes, importMap.scopes);
1774
+ assert.deepEqual(importMap, original);
1775
+ assert.deepEqual(result, {
1776
+ imports: { vue: '/x/vue.final.mjs' },
1777
+ scopes: { '/c/': { vue: '/y/vue2.final.mjs' } }
1778
+ });
1779
+ });
1780
+ });
@@ -126,6 +126,61 @@ export function fixImportMapNestedScopes(
126
126
  return importMap;
127
127
  }
128
128
 
129
+ export function compressImportMap(importMap: Required<ImportMap>): ImportMap {
130
+ const compressed: Required<ImportMap> = {
131
+ imports: { ...importMap.imports },
132
+ scopes: {}
133
+ };
134
+
135
+ const counts: Record<string, Record<string, number>> = {};
136
+ Object.values(importMap.scopes).forEach((scopeMappings) => {
137
+ Object.entries(scopeMappings).forEach(([specifier, target]) => {
138
+ if (Object.hasOwn(importMap.imports, specifier)) return;
139
+ counts[specifier] ??= {};
140
+ counts[specifier][target] = (counts[specifier][target] ?? 0) + 1;
141
+ });
142
+ });
143
+
144
+ Object.entries(counts).forEach(([specifier, targetCounts]) => {
145
+ const entries = Object.entries(targetCounts);
146
+
147
+ let best: [string, number] | null = null;
148
+ let secondBestCount = 0;
149
+ for (const [t, c] of entries) {
150
+ if (!best || c > best[1]) {
151
+ secondBestCount = best
152
+ ? Math.max(secondBestCount, best[1])
153
+ : secondBestCount;
154
+ best = [t, c];
155
+ } else {
156
+ secondBestCount = Math.max(secondBestCount, c);
157
+ }
158
+ }
159
+ if (best && best[1] > secondBestCount) {
160
+ compressed.imports[specifier] = best[0];
161
+ }
162
+ });
163
+
164
+ Object.entries(importMap.scopes).forEach(([scopePath, scopeMappings]) => {
165
+ const filtered: SpecifierMap = {};
166
+
167
+ Object.entries(scopeMappings).forEach(([specifier, target]) => {
168
+ const globalTarget = compressed.imports[specifier];
169
+ if (globalTarget === target) {
170
+ return;
171
+ }
172
+ filtered[specifier] = target;
173
+ });
174
+
175
+ if (Object.keys(filtered).length > 0) {
176
+ compressed.scopes[scopePath] = filtered;
177
+ }
178
+ });
179
+
180
+ const hasScopes = Object.keys(compressed.scopes).length > 0;
181
+ return hasScopes ? compressed : { imports: compressed.imports };
182
+ }
183
+
129
184
  export function createImportMap({
130
185
  manifests,
131
186
  getFile,
@@ -139,3 +194,9 @@ export function createImportMap({
139
194
  scopes
140
195
  };
141
196
  }
197
+
198
+ export function createClientImportMap(options: GetImportMapOptions): ImportMap {
199
+ const base = createImportMap(options);
200
+ const fixed = fixImportMapNestedScopes(base);
201
+ return compressImportMap(fixed);
202
+ }