@esmx/core 3.0.0-rc.116 → 3.0.0-rc.118
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +17 -3
- package/README.zh-CN.md +20 -6
- package/dist/app.mjs +4 -2
- package/dist/cli/cli.d.ts +1 -0
- package/dist/cli/cli.mjs +28 -1
- package/dist/cli/validate.d.ts +15 -0
- package/dist/cli/validate.mjs +139 -0
- package/dist/cli/validate.test.d.ts +1 -0
- package/dist/cli/validate.test.mjs +216 -0
- package/dist/core.d.ts +31 -0
- package/dist/core.mjs +71 -5
- package/dist/declaration/index.d.ts +47 -0
- package/dist/declaration/index.mjs +63 -0
- package/dist/declaration/index.test.d.ts +1 -0
- package/dist/declaration/index.test.mjs +202 -0
- package/dist/declaration/lower.d.ts +11 -0
- package/dist/declaration/lower.mjs +78 -0
- package/dist/declaration/lower.test.d.ts +1 -0
- package/dist/declaration/lower.test.mjs +333 -0
- package/dist/declaration/reader.d.ts +28 -0
- package/dist/declaration/reader.mjs +55 -0
- package/dist/declaration/reinit.e2e.test.d.ts +1 -0
- package/dist/declaration/reinit.e2e.test.mjs +117 -0
- package/dist/declaration/resolver.d.ts +59 -0
- package/dist/declaration/resolver.mjs +430 -0
- package/dist/declaration/resolver.test.d.ts +1 -0
- package/dist/declaration/resolver.test.mjs +1005 -0
- package/dist/declaration/schema.d.ts +89 -0
- package/dist/declaration/schema.mjs +282 -0
- package/dist/declaration/schema.test.d.ts +1 -0
- package/dist/declaration/schema.test.mjs +101 -0
- package/dist/declaration/semver.d.ts +22 -0
- package/dist/declaration/semver.mjs +229 -0
- package/dist/declaration/semver.test.d.ts +1 -0
- package/dist/declaration/semver.test.mjs +87 -0
- package/dist/declaration/test-fixtures.d.ts +18 -0
- package/dist/declaration/test-fixtures.mjs +69 -0
- package/dist/declaration/types.d.ts +58 -0
- package/dist/declaration/types.mjs +21 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.mjs +10 -0
- package/dist/manifest-json.d.ts +61 -0
- package/dist/manifest-json.mjs +68 -5
- package/dist/manifest-json.test.d.ts +1 -0
- package/dist/manifest-json.test.mjs +196 -0
- package/dist/module-config.d.ts +45 -5
- package/dist/module-config.mjs +60 -49
- package/dist/module-config.test.mjs +227 -91
- package/dist/pack-config.d.ts +2 -9
- package/dist/render-context.mjs +22 -5
- package/dist/utils/import-map.d.ts +23 -3
- package/dist/utils/import-map.mjs +38 -1
- package/dist/utils/import-map.test.mjs +228 -0
- package/package.json +9 -6
- package/src/app.ts +5 -2
- package/src/cli/cli.ts +44 -1
- package/src/cli/validate.test.ts +251 -0
- package/src/cli/validate.ts +196 -0
- package/src/core.ts +84 -5
- package/src/declaration/index.test.ts +223 -0
- package/src/declaration/index.ts +135 -0
- package/src/declaration/lower.test.ts +372 -0
- package/src/declaration/lower.ts +135 -0
- package/src/declaration/reader.ts +93 -0
- package/src/declaration/reinit.e2e.test.ts +148 -0
- package/src/declaration/resolver.test.ts +1111 -0
- package/src/declaration/resolver.ts +638 -0
- package/src/declaration/schema.test.ts +118 -0
- package/src/declaration/schema.ts +339 -0
- package/src/declaration/semver.test.ts +101 -0
- package/src/declaration/semver.ts +278 -0
- package/src/declaration/test-fixtures.ts +96 -0
- package/src/declaration/types.ts +71 -0
- package/src/index.ts +28 -1
- package/src/manifest-json.test.ts +236 -0
- package/src/manifest-json.ts +166 -5
- package/src/module-config.test.ts +266 -105
- package/src/module-config.ts +130 -58
- package/src/pack-config.ts +2 -9
- package/src/render-context.ts +34 -6
- package/src/utils/import-map.test.ts +261 -0
- package/src/utils/import-map.ts +92 -6
package/src/pack-config.ts
CHANGED
|
@@ -20,15 +20,8 @@ import type { Esmx } from './core';
|
|
|
20
20
|
* import type { EsmxOptions } from '@esmx/core';
|
|
21
21
|
*
|
|
22
22
|
* export default {
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
package/src/render-context.ts
CHANGED
|
@@ -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: {
|
|
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
|
|
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}
|
|
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
|
-
|
|
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) =>
|
|
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
|
+
});
|
package/src/utils/import-map.ts
CHANGED
|
@@ -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:
|
|
106
|
-
):
|
|
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
|
-
|
|
129
|
-
|
|
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):
|
|
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
|
-
|
|
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
|
}
|