@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
@@ -20,15 +20,8 @@ import type { Esmx } from './core';
20
20
  * import type { EsmxOptions } from '@esmx/core';
21
21
  *
22
22
  * export default {
23
- * modules: {
24
- * // Configure modules to export
25
- * exports: [
26
- * 'root:src/components/button.vue',
27
- * 'root:src/utils/format.ts',
28
- * 'pkg:vue',
29
- * 'pkg:vue-router'
30
- * ]
31
- * },
23
+ * // Module exports/provides are declared in package.json "esmx"
24
+ * // (RFC 0001), not here. entry.node.ts keeps behavior + packaging.
32
25
  * // Packaging configuration
33
26
  * pack: {
34
27
  * // Enable packaging functionality
@@ -1,6 +1,20 @@
1
1
  import path from 'node:path';
2
2
  import serialize from 'serialize-javascript';
3
3
  import type { Esmx } from './core';
4
+ import {
5
+ DEFAULT_MODULE_ENTRY,
6
+ getEntryChunkId,
7
+ type ParsedModuleConfigEntryTarget
8
+ } from './module-config';
9
+
10
+ /**
11
+ * Resolves the client entry target threaded through the module config
12
+ * (RFC 0001 Phase 2), falling back to the legacy default for modules
13
+ * without a resolved client entry (e.g. `lib: true`).
14
+ */
15
+ function getClientEntry(esmx: Esmx): ParsedModuleConfigEntryTarget {
16
+ return esmx.moduleConfig.entry.client ?? DEFAULT_MODULE_ENTRY.client;
17
+ }
4
18
 
5
19
  /**
6
20
  * Configuration options interface for RenderContext
@@ -676,9 +690,14 @@ export class RenderContext {
676
690
  modulepreload: [],
677
691
  resources: []
678
692
  };
679
- private _importMap: { src: string | null; code: string } = {
693
+ private _importMap: {
694
+ src: string | null;
695
+ code: string;
696
+ integrity: Record<string, string> | null;
697
+ } = {
680
698
  src: '',
681
- code: ''
699
+ code: '',
700
+ integrity: null
682
701
  };
683
702
  /**
684
703
  * Define the generation mode for importmap
@@ -1001,7 +1020,8 @@ export class RenderContext {
1001
1020
  */
1002
1021
  public async commit() {
1003
1022
  const { esmx } = this;
1004
- const chunkSet = new Set([`${esmx.name}@src/entry.client.ts`]);
1023
+ const clientEntry = getClientEntry(esmx);
1024
+ const chunkSet = new Set([getEntryChunkId(esmx.name, clientEntry)]);
1005
1025
  for (const item of this.importMetaSet) {
1006
1026
  if ('chunkName' in item && typeof item.chunkName === 'string') {
1007
1027
  chunkSet.add(item.chunkName);
@@ -1039,7 +1059,7 @@ export class RenderContext {
1039
1059
 
1040
1060
  const paths = await esmx.getStaticImportPaths(
1041
1061
  'client',
1042
- `${esmx.name}/src/entry.client`
1062
+ `${esmx.name}/${clientEntry.name}`
1043
1063
  );
1044
1064
  paths?.forEach((filepath) =>
1045
1065
  files.modulepreload.add(getUrlPath(filepath))
@@ -1269,7 +1289,8 @@ export class RenderContext {
1269
1289
  * ```
1270
1290
  */
1271
1291
  public moduleEntry() {
1272
- return `<script type="module">import "${this.esmx.name}/src/entry.client";</script>`;
1292
+ const clientEntry = getClientEntry(this.esmx);
1293
+ return `<script type="module">import "${this.esmx.name}/${clientEntry.name}";</script>`;
1273
1294
  }
1274
1295
 
1275
1296
  /**
@@ -1319,8 +1340,15 @@ export class RenderContext {
1319
1340
  * ```
1320
1341
  */
1321
1342
  public modulePreload() {
1343
+ const { integrity } = this._importMap;
1322
1344
  return this.files.modulepreload
1323
- .map((url) => `<link rel="modulepreload" href="${url}">`)
1345
+ .map((url) => {
1346
+ const hash = integrity?.[url];
1347
+ if (hash) {
1348
+ return `<link rel="modulepreload" href="${url}" integrity="${hash}">`;
1349
+ }
1350
+ return `<link rel="modulepreload" href="${url}">`;
1351
+ })
1324
1352
  .join('');
1325
1353
  }
1326
1354
  }
@@ -2,6 +2,7 @@ import { assert, describe, test } from 'vitest';
2
2
  import type { GetImportMapOptions, ImportMapManifest } from './import-map';
3
3
  import {
4
4
  compressImportMap,
5
+ createClientImportMap,
5
6
  createImportMap,
6
7
  createImportsMap,
7
8
  createScopesMap,
@@ -1778,3 +1779,263 @@ describe('compressImportMap', () => {
1778
1779
  });
1779
1780
  });
1780
1781
  });
1782
+
1783
+ describe('createClientImportMap integrity', () => {
1784
+ const getFile = (name: string, file: string) => `/${name}/${file}`;
1785
+ const getScope = (name: string, scope: string) => scope;
1786
+
1787
+ test('omits integrity when no manifest provides it', () => {
1788
+ const manifests: ImportMapManifest[] = [
1789
+ {
1790
+ name: 'module-a',
1791
+ exports: {
1792
+ index: {
1793
+ name: 'index',
1794
+ pkg: false,
1795
+ file: 'src/index.mjs',
1796
+ identifier: 'module-a'
1797
+ }
1798
+ },
1799
+ scopes: {}
1800
+ }
1801
+ ];
1802
+
1803
+ const result = createClientImportMap({ manifests, getFile, getScope });
1804
+
1805
+ assert.isUndefined(result.integrity);
1806
+ });
1807
+
1808
+ test('converts relative integrity paths to absolute URL paths', () => {
1809
+ const manifests: ImportMapManifest[] = [
1810
+ {
1811
+ name: 'ssr-micro-vue2',
1812
+ exports: {
1813
+ routes: {
1814
+ name: 'routes',
1815
+ pkg: false,
1816
+ file: 'src/routes.xxx.mjs',
1817
+ identifier: 'ssr-micro-vue2/routes'
1818
+ }
1819
+ },
1820
+ scopes: {},
1821
+ integrity: {
1822
+ 'src/routes.xxx.mjs': 'sha384-abc'
1823
+ }
1824
+ }
1825
+ ];
1826
+
1827
+ const result = createClientImportMap({ manifests, getFile, getScope });
1828
+
1829
+ assert.deepEqual(result.integrity, {
1830
+ '/ssr-micro-vue2/src/routes.xxx.mjs': 'sha384-abc'
1831
+ });
1832
+ });
1833
+
1834
+ test('merges integrity across multiple manifests', () => {
1835
+ const manifests: ImportMapManifest[] = [
1836
+ {
1837
+ name: 'module-a',
1838
+ exports: {},
1839
+ scopes: {},
1840
+ integrity: { 'src/a.mjs': 'sha384-a' }
1841
+ },
1842
+ {
1843
+ name: 'module-b',
1844
+ exports: {},
1845
+ scopes: {},
1846
+ integrity: { 'src/b.mjs': 'sha384-b' }
1847
+ }
1848
+ ];
1849
+
1850
+ const result = createClientImportMap({ manifests, getFile, getScope });
1851
+
1852
+ assert.deepEqual(result.integrity, {
1853
+ '/module-a/src/a.mjs': 'sha384-a',
1854
+ '/module-b/src/b.mjs': 'sha384-b'
1855
+ });
1856
+ });
1857
+ });
1858
+
1859
+ describe('createClientImportMap code-split chunk scopes', () => {
1860
+ const getFile = (name: string, file: string) => `/${name}/${file}`;
1861
+ const getScope = (name: string, scope: string) => `/${name}${scope}`;
1862
+
1863
+ const makeVueModule = (name: string, hash: string): ImportMapManifest => ({
1864
+ name,
1865
+ exports: {
1866
+ entry: {
1867
+ name: 'entry',
1868
+ pkg: false,
1869
+ file: `src/entry.client.${hash}.mjs`,
1870
+ identifier: `${name}/src/entry.client`
1871
+ },
1872
+ vue: {
1873
+ name: 'vue',
1874
+ pkg: true,
1875
+ file: `vue.${hash}.mjs`,
1876
+ identifier: `${name}/vue`
1877
+ }
1878
+ },
1879
+ scopes: { '': { vue: `${name}/vue` } },
1880
+ // A Vite-style code-split chunk that is NOT an export.
1881
+ chunks: { routesChunk: { js: `chunks/routes.${hash}.mjs` } }
1882
+ });
1883
+
1884
+ test('scopes a module code-split chunk to its own externals', () => {
1885
+ // Two modules each bundling a different "vue" file → vue must stay
1886
+ // scoped (never promoted to a single global import).
1887
+ const manifests = [
1888
+ makeVueModule('app-a', 'aaa'),
1889
+ makeVueModule('app-b', 'bbb')
1890
+ ];
1891
+
1892
+ const result = createClientImportMap({ manifests, getFile, getScope });
1893
+
1894
+ assert.isUndefined(result.imports?.vue);
1895
+ assert.equal(
1896
+ result.scopes?.['/app-a/chunks/routes.aaa.mjs']?.vue,
1897
+ '/app-a/vue.aaa.mjs'
1898
+ );
1899
+ assert.equal(
1900
+ result.scopes?.['/app-b/chunks/routes.bbb.mjs']?.vue,
1901
+ '/app-b/vue.bbb.mjs'
1902
+ );
1903
+ });
1904
+
1905
+ test('skips a chunk scope when a global import already resolves it', () => {
1906
+ // A single module → its sole "vue" target gets promoted to a global
1907
+ // import, so the chunk needs no redundant scope of its own.
1908
+ const result = createClientImportMap({
1909
+ manifests: [makeVueModule('solo', 'aaa')],
1910
+ getFile,
1911
+ getScope
1912
+ });
1913
+
1914
+ assert.equal(result.imports?.vue, '/solo/vue.aaa.mjs');
1915
+ assert.isUndefined(result.scopes?.['/solo/chunks/routes.aaa.mjs']);
1916
+ });
1917
+
1918
+ test('is a no-op for all-in-one manifests without code-split chunks', () => {
1919
+ const allInOne: ImportMapManifest = {
1920
+ name: 'rspack-app',
1921
+ exports: {
1922
+ entry: {
1923
+ name: 'entry',
1924
+ pkg: false,
1925
+ file: 'src/entry.client.zzz.mjs',
1926
+ identifier: 'rspack-app/src/entry.client'
1927
+ },
1928
+ vue: {
1929
+ name: 'vue',
1930
+ pkg: true,
1931
+ file: 'vue.zzz.mjs',
1932
+ identifier: 'rspack-app/vue'
1933
+ }
1934
+ },
1935
+ scopes: { '': { vue: 'rspack-app/vue' } }
1936
+ // no `chunks` — rspack all-in-one output
1937
+ };
1938
+ // Pair with a different-vue module so vue stays scoped, not promoted.
1939
+ const result = createClientImportMap({
1940
+ manifests: [allInOne, makeVueModule('app-b', 'bbb')],
1941
+ getFile,
1942
+ getScope
1943
+ });
1944
+
1945
+ const invented = Object.keys(result.scopes ?? {}).filter((k) =>
1946
+ k.startsWith('/rspack-app/chunks/')
1947
+ );
1948
+ assert.deepEqual(invented, []);
1949
+ });
1950
+
1951
+ test('applies all of a module externals to its chunk', () => {
1952
+ const make = (name: string, hash: string): ImportMapManifest => ({
1953
+ name,
1954
+ exports: {
1955
+ vue: {
1956
+ name: 'vue',
1957
+ pkg: true,
1958
+ file: `vue.${hash}.mjs`,
1959
+ identifier: `${name}/vue`
1960
+ },
1961
+ pinia: {
1962
+ name: 'pinia',
1963
+ pkg: true,
1964
+ file: `pinia.${hash}.mjs`,
1965
+ identifier: `${name}/pinia`
1966
+ }
1967
+ },
1968
+ scopes: { '': { vue: `${name}/vue`, pinia: `${name}/pinia` } },
1969
+ chunks: { storeChunk: { js: `chunks/store.${hash}.mjs` } }
1970
+ });
1971
+ // Two modules with different targets → both externals stay scoped.
1972
+ const result = createClientImportMap({
1973
+ manifests: [make('app-a', 'aaa'), make('app-b', 'bbb')],
1974
+ getFile,
1975
+ getScope
1976
+ });
1977
+
1978
+ const scope = result.scopes?.['/app-a/chunks/store.aaa.mjs'];
1979
+ assert.equal(scope?.vue, '/app-a/vue.aaa.mjs');
1980
+ assert.equal(scope?.pinia, '/app-a/pinia.aaa.mjs');
1981
+ });
1982
+ });
1983
+
1984
+ describe('multi-version coexistence (scope isolation)', () => {
1985
+ // Two providers each declare their own copy of `vue` (single-owner per
1986
+ // (package,major) still allows legitimate cross-major coexistence). Each
1987
+ // copy must stay isolated in its own module scope, never collapsed.
1988
+ const layeredManifests: ImportMapManifest[] = [
1989
+ {
1990
+ name: 'base',
1991
+ exports: {
1992
+ vue: {
1993
+ name: 'vue',
1994
+ pkg: true,
1995
+ file: 'vue.base.mjs',
1996
+ identifier: 'base/vue'
1997
+ }
1998
+ },
1999
+ scopes: { '': { vue: 'base/vue' } },
2000
+ chunks: { 'base@src/app.ts': { js: 'app.base.mjs' } },
2001
+ integrity: {
2002
+ 'vue.base.mjs': 'sha384-base',
2003
+ 'app.base.mjs': 'sha384-base-app'
2004
+ }
2005
+ },
2006
+ {
2007
+ name: 'vue-base',
2008
+ exports: {
2009
+ vue: {
2010
+ name: 'vue',
2011
+ pkg: true,
2012
+ file: 'vue.winner.mjs',
2013
+ identifier: 'vue-base/vue'
2014
+ }
2015
+ },
2016
+ scopes: { '': { vue: 'vue-base/vue' } },
2017
+ integrity: { 'vue.winner.mjs': 'sha384-winner' }
2018
+ }
2019
+ ];
2020
+ const getFile = (name: string, file: string) => `/${name}/${file}`;
2021
+ const getScope = (name: string, scope: string) => `/${name}${scope}`;
2022
+
2023
+ test('each copy stays isolated in its own scope', () => {
2024
+ const result = createImportMap({
2025
+ manifests: layeredManifests,
2026
+ getFile,
2027
+ getScope
2028
+ });
2029
+
2030
+ assert.equal(result.imports['base/vue'], '/base/vue.base.mjs');
2031
+ assert.equal(
2032
+ result.imports['vue-base/vue'],
2033
+ '/vue-base/vue.winner.mjs'
2034
+ );
2035
+ assert.equal(result.scopes['/base/']?.vue, '/base/vue.base.mjs');
2036
+ assert.equal(
2037
+ result.scopes['/vue-base/']?.vue,
2038
+ '/vue-base/vue.winner.mjs'
2039
+ );
2040
+ });
2041
+ });
@@ -1,6 +1,12 @@
1
1
  import type { ImportMap, ScopesMap, SpecifierMap } from '@esmx/import';
2
2
  import { pathWithoutIndex } from './path-without-index';
3
3
 
4
+ /**
5
+ * Import map with `imports` and `scopes` fully resolved. The optional
6
+ * `integrity` field is layered on later by {@link createClientImportMap}.
7
+ */
8
+ type ResolvedImportMap = Required<Pick<ImportMap, 'imports' | 'scopes'>>;
9
+
4
10
  export interface ImportMapManifest {
5
11
  name: string;
6
12
  exports: Record<
@@ -13,6 +19,18 @@ export interface ImportMapManifest {
13
19
  }
14
20
  >;
15
21
  scopes: Record<string, Record<string, string>>;
22
+ /**
23
+ * Emitted chunk files (keyed arbitrarily). Each chunk's `js` is a relative
24
+ * path (e.g. "chunks/routes.xxx.mjs"). Used so a module's scope also covers
25
+ * its code-split chunks, not just its export files.
26
+ */
27
+ chunks?: Record<string, { js: string }>;
28
+ /**
29
+ * Subresource Integrity hashes for this module's build output files.
30
+ * Keys are relative file paths (e.g. "src/routes.xxx.mjs"), values are
31
+ * integrity strings (e.g. "sha384-..."). Only present in production builds.
32
+ */
33
+ integrity?: Record<string, string>;
16
34
  }
17
35
 
18
36
  export interface GetImportMapOptions {
@@ -102,8 +120,8 @@ export function createScopesMap(
102
120
  * @see https://issues.chromium.org/issues/453147451
103
121
  */
104
122
  export function fixImportMapNestedScopes(
105
- importMap: Required<ImportMap>
106
- ): Required<ImportMap> {
123
+ importMap: ResolvedImportMap
124
+ ): ResolvedImportMap {
107
125
  Object.entries(importMap.scopes)
108
126
  .sort(([pathA], [pathB]) => {
109
127
  const depthA = pathA.split('/').length;
@@ -125,8 +143,48 @@ export function fixImportMapNestedScopes(
125
143
  return importMap;
126
144
  }
127
145
 
128
- export function compressImportMap(importMap: Required<ImportMap>): ImportMap {
129
- const compressed: Required<ImportMap> = {
146
+ /**
147
+ * A module's code-split chunk files (e.g. Vite/Rollup facade+impl splits) are
148
+ * not exports, so they are absent from `imports` and miss the per-file scopes
149
+ * produced by {@link fixImportMapNestedScopes}. Those chunks still import the
150
+ * module's bare externals (e.g. "vue"), so without a scope the browser throws
151
+ * "Failed to resolve module specifier". This adds, for each chunk file, the
152
+ * module's bare-specifier mappings — but only the ones NOT already satisfied by
153
+ * a matching global `imports` entry. Runs AFTER compression so it never skews
154
+ * the global-promotion heuristic (which would wrongly hoist a multi-version
155
+ * dep like "vue"). rspack's all-in-one output has no such chunks, so this is a
156
+ * no-op there.
157
+ */
158
+ function addCodeSplitChunkScopes(
159
+ importMap: ImportMap,
160
+ base: ResolvedImportMap,
161
+ options: GetImportMapOptions
162
+ ): void {
163
+ for (const manifest of options.manifests) {
164
+ const moduleScope = manifest.scopes?.[''];
165
+ const chunks = manifest.chunks;
166
+ if (!moduleScope || !chunks) continue;
167
+
168
+ const resolved: SpecifierMap = {};
169
+ for (const [specifier, identifier] of Object.entries(moduleScope)) {
170
+ const target = base.imports[identifier] ?? identifier;
171
+ // Skip specifiers a global import already resolves to the same file.
172
+ if (importMap.imports?.[specifier] === target) continue;
173
+ resolved[specifier] = target;
174
+ }
175
+ if (Object.keys(resolved).length === 0) continue;
176
+
177
+ for (const chunk of Object.values(chunks)) {
178
+ if (!chunk.js) continue;
179
+ const url = options.getFile(manifest.name, chunk.js);
180
+ importMap.scopes ??= {};
181
+ importMap.scopes[url] = { ...importMap.scopes[url], ...resolved };
182
+ }
183
+ }
184
+ }
185
+
186
+ export function compressImportMap(importMap: ResolvedImportMap): ImportMap {
187
+ const compressed: ResolvedImportMap = {
130
188
  imports: { ...importMap.imports },
131
189
  scopes: {}
132
190
  };
@@ -184,7 +242,7 @@ export function createImportMap({
184
242
  manifests,
185
243
  getFile,
186
244
  getScope
187
- }: GetImportMapOptions): Required<ImportMap> {
245
+ }: GetImportMapOptions): ResolvedImportMap {
188
246
  const imports = createImportsMap(manifests, getFile);
189
247
 
190
248
  const scopes = createScopesMap(imports, manifests, getScope);
@@ -197,5 +255,33 @@ export function createImportMap({
197
255
  export function createClientImportMap(options: GetImportMapOptions): ImportMap {
198
256
  const base = createImportMap(options);
199
257
  const fixed = fixImportMapNestedScopes(base);
200
- return compressImportMap(fixed);
258
+ const compressed = compressImportMap(fixed);
259
+ // Code-split chunk files are not exports, so they miss the per-file scopes
260
+ // above. Add them post-compression so it never skews global promotion.
261
+ addCodeSplitChunkScopes(compressed, base, options);
262
+
263
+ // Collect integrity from all manifests
264
+ // Manifest integrity keys are relative filenames (e.g., "src/routes.xxx.mjs")
265
+ // Import map values are absolute URLs (e.g., "/ssr-micro-vue2/src/routes.xxx.mjs")
266
+ // Must convert relative paths to absolute URLs to match browser expectations
267
+ const integrity: Record<string, string> = {};
268
+ for (const manifest of options.manifests) {
269
+ if (manifest.integrity) {
270
+ for (const [filePath, hash] of Object.entries(manifest.integrity)) {
271
+ // Convert relative file path to absolute URL path
272
+ // e.g., "src/routes.xxx.mjs" -> "/ssr-micro-vue2/src/routes.xxx.mjs"
273
+ const urlPath = `/${manifest.name}/${filePath}`;
274
+ integrity[urlPath] = hash;
275
+ }
276
+ }
277
+ }
278
+
279
+ if (Object.keys(integrity).length > 0) {
280
+ return {
281
+ ...compressed,
282
+ integrity
283
+ };
284
+ }
285
+
286
+ return compressed;
201
287
  }