@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.
- 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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { assert, describe, test } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
compressImportMap,
|
|
4
|
+
createClientImportMap,
|
|
4
5
|
createImportMap,
|
|
5
6
|
createImportsMap,
|
|
6
7
|
createScopesMap,
|
|
@@ -1584,3 +1585,230 @@ describe("compressImportMap", () => {
|
|
|
1584
1585
|
});
|
|
1585
1586
|
});
|
|
1586
1587
|
});
|
|
1588
|
+
describe("createClientImportMap integrity", () => {
|
|
1589
|
+
const getFile = (name, file) => `/${name}/${file}`;
|
|
1590
|
+
const getScope = (name, scope) => scope;
|
|
1591
|
+
test("omits integrity when no manifest provides it", () => {
|
|
1592
|
+
const manifests = [
|
|
1593
|
+
{
|
|
1594
|
+
name: "module-a",
|
|
1595
|
+
exports: {
|
|
1596
|
+
index: {
|
|
1597
|
+
name: "index",
|
|
1598
|
+
pkg: false,
|
|
1599
|
+
file: "src/index.mjs",
|
|
1600
|
+
identifier: "module-a"
|
|
1601
|
+
}
|
|
1602
|
+
},
|
|
1603
|
+
scopes: {}
|
|
1604
|
+
}
|
|
1605
|
+
];
|
|
1606
|
+
const result = createClientImportMap({ manifests, getFile, getScope });
|
|
1607
|
+
assert.isUndefined(result.integrity);
|
|
1608
|
+
});
|
|
1609
|
+
test("converts relative integrity paths to absolute URL paths", () => {
|
|
1610
|
+
const manifests = [
|
|
1611
|
+
{
|
|
1612
|
+
name: "ssr-micro-vue2",
|
|
1613
|
+
exports: {
|
|
1614
|
+
routes: {
|
|
1615
|
+
name: "routes",
|
|
1616
|
+
pkg: false,
|
|
1617
|
+
file: "src/routes.xxx.mjs",
|
|
1618
|
+
identifier: "ssr-micro-vue2/routes"
|
|
1619
|
+
}
|
|
1620
|
+
},
|
|
1621
|
+
scopes: {},
|
|
1622
|
+
integrity: {
|
|
1623
|
+
"src/routes.xxx.mjs": "sha384-abc"
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
];
|
|
1627
|
+
const result = createClientImportMap({ manifests, getFile, getScope });
|
|
1628
|
+
assert.deepEqual(result.integrity, {
|
|
1629
|
+
"/ssr-micro-vue2/src/routes.xxx.mjs": "sha384-abc"
|
|
1630
|
+
});
|
|
1631
|
+
});
|
|
1632
|
+
test("merges integrity across multiple manifests", () => {
|
|
1633
|
+
const manifests = [
|
|
1634
|
+
{
|
|
1635
|
+
name: "module-a",
|
|
1636
|
+
exports: {},
|
|
1637
|
+
scopes: {},
|
|
1638
|
+
integrity: { "src/a.mjs": "sha384-a" }
|
|
1639
|
+
},
|
|
1640
|
+
{
|
|
1641
|
+
name: "module-b",
|
|
1642
|
+
exports: {},
|
|
1643
|
+
scopes: {},
|
|
1644
|
+
integrity: { "src/b.mjs": "sha384-b" }
|
|
1645
|
+
}
|
|
1646
|
+
];
|
|
1647
|
+
const result = createClientImportMap({ manifests, getFile, getScope });
|
|
1648
|
+
assert.deepEqual(result.integrity, {
|
|
1649
|
+
"/module-a/src/a.mjs": "sha384-a",
|
|
1650
|
+
"/module-b/src/b.mjs": "sha384-b"
|
|
1651
|
+
});
|
|
1652
|
+
});
|
|
1653
|
+
});
|
|
1654
|
+
describe("createClientImportMap code-split chunk scopes", () => {
|
|
1655
|
+
const getFile = (name, file) => `/${name}/${file}`;
|
|
1656
|
+
const getScope = (name, scope) => `/${name}${scope}`;
|
|
1657
|
+
const makeVueModule = (name, hash) => ({
|
|
1658
|
+
name,
|
|
1659
|
+
exports: {
|
|
1660
|
+
entry: {
|
|
1661
|
+
name: "entry",
|
|
1662
|
+
pkg: false,
|
|
1663
|
+
file: `src/entry.client.${hash}.mjs`,
|
|
1664
|
+
identifier: `${name}/src/entry.client`
|
|
1665
|
+
},
|
|
1666
|
+
vue: {
|
|
1667
|
+
name: "vue",
|
|
1668
|
+
pkg: true,
|
|
1669
|
+
file: `vue.${hash}.mjs`,
|
|
1670
|
+
identifier: `${name}/vue`
|
|
1671
|
+
}
|
|
1672
|
+
},
|
|
1673
|
+
scopes: { "": { vue: `${name}/vue` } },
|
|
1674
|
+
// A Vite-style code-split chunk that is NOT an export.
|
|
1675
|
+
chunks: { routesChunk: { js: `chunks/routes.${hash}.mjs` } }
|
|
1676
|
+
});
|
|
1677
|
+
test("scopes a module code-split chunk to its own externals", () => {
|
|
1678
|
+
const manifests = [
|
|
1679
|
+
makeVueModule("app-a", "aaa"),
|
|
1680
|
+
makeVueModule("app-b", "bbb")
|
|
1681
|
+
];
|
|
1682
|
+
const result = createClientImportMap({ manifests, getFile, getScope });
|
|
1683
|
+
assert.isUndefined(result.imports?.vue);
|
|
1684
|
+
assert.equal(
|
|
1685
|
+
result.scopes?.["/app-a/chunks/routes.aaa.mjs"]?.vue,
|
|
1686
|
+
"/app-a/vue.aaa.mjs"
|
|
1687
|
+
);
|
|
1688
|
+
assert.equal(
|
|
1689
|
+
result.scopes?.["/app-b/chunks/routes.bbb.mjs"]?.vue,
|
|
1690
|
+
"/app-b/vue.bbb.mjs"
|
|
1691
|
+
);
|
|
1692
|
+
});
|
|
1693
|
+
test("skips a chunk scope when a global import already resolves it", () => {
|
|
1694
|
+
const result = createClientImportMap({
|
|
1695
|
+
manifests: [makeVueModule("solo", "aaa")],
|
|
1696
|
+
getFile,
|
|
1697
|
+
getScope
|
|
1698
|
+
});
|
|
1699
|
+
assert.equal(result.imports?.vue, "/solo/vue.aaa.mjs");
|
|
1700
|
+
assert.isUndefined(result.scopes?.["/solo/chunks/routes.aaa.mjs"]);
|
|
1701
|
+
});
|
|
1702
|
+
test("is a no-op for all-in-one manifests without code-split chunks", () => {
|
|
1703
|
+
const allInOne = {
|
|
1704
|
+
name: "rspack-app",
|
|
1705
|
+
exports: {
|
|
1706
|
+
entry: {
|
|
1707
|
+
name: "entry",
|
|
1708
|
+
pkg: false,
|
|
1709
|
+
file: "src/entry.client.zzz.mjs",
|
|
1710
|
+
identifier: "rspack-app/src/entry.client"
|
|
1711
|
+
},
|
|
1712
|
+
vue: {
|
|
1713
|
+
name: "vue",
|
|
1714
|
+
pkg: true,
|
|
1715
|
+
file: "vue.zzz.mjs",
|
|
1716
|
+
identifier: "rspack-app/vue"
|
|
1717
|
+
}
|
|
1718
|
+
},
|
|
1719
|
+
scopes: { "": { vue: "rspack-app/vue" } }
|
|
1720
|
+
// no `chunks` — rspack all-in-one output
|
|
1721
|
+
};
|
|
1722
|
+
const result = createClientImportMap({
|
|
1723
|
+
manifests: [allInOne, makeVueModule("app-b", "bbb")],
|
|
1724
|
+
getFile,
|
|
1725
|
+
getScope
|
|
1726
|
+
});
|
|
1727
|
+
const invented = Object.keys(result.scopes ?? {}).filter(
|
|
1728
|
+
(k) => k.startsWith("/rspack-app/chunks/")
|
|
1729
|
+
);
|
|
1730
|
+
assert.deepEqual(invented, []);
|
|
1731
|
+
});
|
|
1732
|
+
test("applies all of a module externals to its chunk", () => {
|
|
1733
|
+
const make = (name, hash) => ({
|
|
1734
|
+
name,
|
|
1735
|
+
exports: {
|
|
1736
|
+
vue: {
|
|
1737
|
+
name: "vue",
|
|
1738
|
+
pkg: true,
|
|
1739
|
+
file: `vue.${hash}.mjs`,
|
|
1740
|
+
identifier: `${name}/vue`
|
|
1741
|
+
},
|
|
1742
|
+
pinia: {
|
|
1743
|
+
name: "pinia",
|
|
1744
|
+
pkg: true,
|
|
1745
|
+
file: `pinia.${hash}.mjs`,
|
|
1746
|
+
identifier: `${name}/pinia`
|
|
1747
|
+
}
|
|
1748
|
+
},
|
|
1749
|
+
scopes: { "": { vue: `${name}/vue`, pinia: `${name}/pinia` } },
|
|
1750
|
+
chunks: { storeChunk: { js: `chunks/store.${hash}.mjs` } }
|
|
1751
|
+
});
|
|
1752
|
+
const result = createClientImportMap({
|
|
1753
|
+
manifests: [make("app-a", "aaa"), make("app-b", "bbb")],
|
|
1754
|
+
getFile,
|
|
1755
|
+
getScope
|
|
1756
|
+
});
|
|
1757
|
+
const scope = result.scopes?.["/app-a/chunks/store.aaa.mjs"];
|
|
1758
|
+
assert.equal(scope?.vue, "/app-a/vue.aaa.mjs");
|
|
1759
|
+
assert.equal(scope?.pinia, "/app-a/pinia.aaa.mjs");
|
|
1760
|
+
});
|
|
1761
|
+
});
|
|
1762
|
+
describe("multi-version coexistence (scope isolation)", () => {
|
|
1763
|
+
const layeredManifests = [
|
|
1764
|
+
{
|
|
1765
|
+
name: "base",
|
|
1766
|
+
exports: {
|
|
1767
|
+
vue: {
|
|
1768
|
+
name: "vue",
|
|
1769
|
+
pkg: true,
|
|
1770
|
+
file: "vue.base.mjs",
|
|
1771
|
+
identifier: "base/vue"
|
|
1772
|
+
}
|
|
1773
|
+
},
|
|
1774
|
+
scopes: { "": { vue: "base/vue" } },
|
|
1775
|
+
chunks: { "base@src/app.ts": { js: "app.base.mjs" } },
|
|
1776
|
+
integrity: {
|
|
1777
|
+
"vue.base.mjs": "sha384-base",
|
|
1778
|
+
"app.base.mjs": "sha384-base-app"
|
|
1779
|
+
}
|
|
1780
|
+
},
|
|
1781
|
+
{
|
|
1782
|
+
name: "vue-base",
|
|
1783
|
+
exports: {
|
|
1784
|
+
vue: {
|
|
1785
|
+
name: "vue",
|
|
1786
|
+
pkg: true,
|
|
1787
|
+
file: "vue.winner.mjs",
|
|
1788
|
+
identifier: "vue-base/vue"
|
|
1789
|
+
}
|
|
1790
|
+
},
|
|
1791
|
+
scopes: { "": { vue: "vue-base/vue" } },
|
|
1792
|
+
integrity: { "vue.winner.mjs": "sha384-winner" }
|
|
1793
|
+
}
|
|
1794
|
+
];
|
|
1795
|
+
const getFile = (name, file) => `/${name}/${file}`;
|
|
1796
|
+
const getScope = (name, scope) => `/${name}${scope}`;
|
|
1797
|
+
test("each copy stays isolated in its own scope", () => {
|
|
1798
|
+
const result = createImportMap({
|
|
1799
|
+
manifests: layeredManifests,
|
|
1800
|
+
getFile,
|
|
1801
|
+
getScope
|
|
1802
|
+
});
|
|
1803
|
+
assert.equal(result.imports["base/vue"], "/base/vue.base.mjs");
|
|
1804
|
+
assert.equal(
|
|
1805
|
+
result.imports["vue-base/vue"],
|
|
1806
|
+
"/vue-base/vue.winner.mjs"
|
|
1807
|
+
);
|
|
1808
|
+
assert.equal(result.scopes["/base/"]?.vue, "/base/vue.base.mjs");
|
|
1809
|
+
assert.equal(
|
|
1810
|
+
result.scopes["/vue-base/"]?.vue,
|
|
1811
|
+
"/vue-base/vue.winner.mjs"
|
|
1812
|
+
);
|
|
1813
|
+
});
|
|
1814
|
+
});
|
package/package.json
CHANGED
|
@@ -59,12 +59,12 @@
|
|
|
59
59
|
"build": "unbuild"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@esmx/import": "3.0.0-rc.
|
|
62
|
+
"@esmx/import": "3.0.0-rc.118",
|
|
63
63
|
"@types/serialize-javascript": "^5.0.4",
|
|
64
64
|
"es-module-lexer": "^1.7.0",
|
|
65
65
|
"find": "^0.3.0",
|
|
66
66
|
"send": "^1.2.0",
|
|
67
|
-
"serialize-javascript": "^
|
|
67
|
+
"serialize-javascript": "^7.0.5"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@biomejs/biome": "2.3.7",
|
|
@@ -72,12 +72,12 @@
|
|
|
72
72
|
"@types/node": "^24.0.0",
|
|
73
73
|
"@types/send": "^1.2.1",
|
|
74
74
|
"@types/write": "^2.0.4",
|
|
75
|
-
"@vitest/coverage-v8": "3.2.
|
|
75
|
+
"@vitest/coverage-v8": "3.2.6",
|
|
76
76
|
"typescript": "5.9.3",
|
|
77
77
|
"unbuild": "3.6.1",
|
|
78
|
-
"vitest": "3.2.
|
|
78
|
+
"vitest": "3.2.6"
|
|
79
79
|
},
|
|
80
|
-
"version": "3.0.0-rc.
|
|
80
|
+
"version": "3.0.0-rc.118",
|
|
81
81
|
"type": "module",
|
|
82
82
|
"private": false,
|
|
83
83
|
"exports": {
|
|
@@ -100,5 +100,8 @@
|
|
|
100
100
|
"template",
|
|
101
101
|
"public"
|
|
102
102
|
],
|
|
103
|
-
"gitHead": "
|
|
103
|
+
"gitHead": "2fdbd62470f64e6e45568550941c78cb259e4917",
|
|
104
|
+
"publishConfig": {
|
|
105
|
+
"access": "public"
|
|
106
|
+
}
|
|
104
107
|
}
|
package/src/app.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { pathToFileURL } from 'node:url';
|
|
2
2
|
import { createLoaderImport } from '@esmx/import';
|
|
3
3
|
import type { COMMAND, Esmx } from './core';
|
|
4
|
+
import { DEFAULT_MODULE_ENTRY } from './module-config';
|
|
4
5
|
import {
|
|
5
6
|
RenderContext,
|
|
6
7
|
type RenderContextOptions,
|
|
@@ -123,10 +124,12 @@ async function createStartRender(esmx: Esmx) {
|
|
|
123
124
|
const baseURL = pathToFileURL(esmx.resolvePath('dist/server')) as URL;
|
|
124
125
|
const importMap = await esmx.getImportMap('server');
|
|
125
126
|
const loaderImport = createLoaderImport(baseURL, importMap);
|
|
127
|
+
const serverEntry =
|
|
128
|
+
esmx.moduleConfig.entry.server ?? DEFAULT_MODULE_ENTRY.server;
|
|
126
129
|
|
|
127
130
|
return async (options?: RenderContextOptions): Promise<RenderContext> => {
|
|
128
131
|
const rc = new RenderContext(esmx, options);
|
|
129
|
-
const result = await loaderImport(`${esmx.name}
|
|
132
|
+
const result = await loaderImport(`${esmx.name}/${serverEntry.name}`);
|
|
130
133
|
const serverRender: ServerRenderHandle = result[rc.entryName];
|
|
131
134
|
if (typeof serverRender === 'function') {
|
|
132
135
|
await serverRender(rc);
|
|
@@ -138,7 +141,7 @@ async function createStartRender(esmx: Esmx) {
|
|
|
138
141
|
function createErrorRender(esmx: Esmx) {
|
|
139
142
|
return (options?: RenderContextOptions) => {
|
|
140
143
|
throw new Error(
|
|
141
|
-
`
|
|
144
|
+
`[@esmx/core] esmx.render() is only available in production. The current process is in dev mode and has no built artifacts. Run \`esmx build\` first, then start the server with NODE_ENV=production node dist/index.mjs.`
|
|
142
145
|
);
|
|
143
146
|
};
|
|
144
147
|
}
|
package/src/cli/cli.ts
CHANGED
|
@@ -4,6 +4,7 @@ import pkg from '../../package.json' with { type: 'json' };
|
|
|
4
4
|
|
|
5
5
|
import { COMMAND, Esmx, type EsmxOptions } from '../core';
|
|
6
6
|
import { resolveImportPath } from '../utils/resolve-path';
|
|
7
|
+
import { runValidate, VALIDATE_HELP } from './validate';
|
|
7
8
|
|
|
8
9
|
async function getSrcOptions(): Promise<EsmxOptions> {
|
|
9
10
|
return import(resolveImportPath(process.cwd(), './src/entry.node.ts')).then(
|
|
@@ -30,6 +31,22 @@ async function getDistOptions(): Promise<EsmxOptions> {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export async function cli(command: string) {
|
|
34
|
+
if (command === 'validate') {
|
|
35
|
+
// Handled before the banner: `--json` must keep stdout pure JSON.
|
|
36
|
+
const flags = process.argv.slice(3);
|
|
37
|
+
if (flags.includes('--help')) {
|
|
38
|
+
console.log(VALIDATE_HELP);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const result = await runValidate(process.cwd(), {
|
|
42
|
+
json: flags.includes('--json')
|
|
43
|
+
});
|
|
44
|
+
console.log(result.output);
|
|
45
|
+
if (result.exitCode !== 0) {
|
|
46
|
+
process.exit(result.exitCode);
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
33
50
|
console.log(`🔥 ${styleText('yellow', 'Esmx')} v${pkg.version}
|
|
34
51
|
`);
|
|
35
52
|
if (
|
|
@@ -111,7 +128,7 @@ export function resolve(
|
|
|
111
128
|
nextResolve: Function
|
|
112
129
|
) {
|
|
113
130
|
if (
|
|
114
|
-
context?.parentURL
|
|
131
|
+
context?.parentURL?.endsWith('.ts') &&
|
|
115
132
|
specifier.startsWith('.') &&
|
|
116
133
|
!specifier.endsWith('.ts')
|
|
117
134
|
) {
|
|
@@ -119,3 +136,29 @@ export function resolve(
|
|
|
119
136
|
}
|
|
120
137
|
return nextResolve(specifier, context);
|
|
121
138
|
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Style assets imported via standard `import './x.css'` are part of esmx's
|
|
142
|
+
* federation contract (manifest's `chunks[*].css[]` — see G section). On the
|
|
143
|
+
* cli/build path Node's native loader hits these when reading the user's
|
|
144
|
+
* `entry.node.ts` config (which transitively imports a remote's source). They
|
|
145
|
+
* have no server-side behaviour — emit a no-op ESM module so the loader chain
|
|
146
|
+
* continues cleanly. Mirrors `@esmx/import`'s VM-linker hook.
|
|
147
|
+
*/
|
|
148
|
+
const STYLE_ASSET_RE =
|
|
149
|
+
/\.(?:css|scss|sass|less|stylus|styl|pcss|postcss)(?:\?.*)?$/i;
|
|
150
|
+
|
|
151
|
+
export async function load(
|
|
152
|
+
url: string,
|
|
153
|
+
context: Record<string, any>,
|
|
154
|
+
nextLoad: Function
|
|
155
|
+
) {
|
|
156
|
+
if (STYLE_ASSET_RE.test(url)) {
|
|
157
|
+
return {
|
|
158
|
+
format: 'module',
|
|
159
|
+
shortCircuit: true,
|
|
160
|
+
source: `export default ${JSON.stringify(url)}; export const href = ${JSON.stringify(url)};`
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return nextLoad(url, context);
|
|
164
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import url from 'node:url';
|
|
4
|
+
import util from 'node:util';
|
|
5
|
+
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
createFixtureRoot,
|
|
10
|
+
removeFixtureRoot,
|
|
11
|
+
writeFixturePackage
|
|
12
|
+
} from '../declaration/test-fixtures';
|
|
13
|
+
import { runValidate } from './validate';
|
|
14
|
+
|
|
15
|
+
const fixtureRoots: string[] = [];
|
|
16
|
+
|
|
17
|
+
async function fixtureRoot(): Promise<string> {
|
|
18
|
+
const root = await createFixtureRoot();
|
|
19
|
+
fixtureRoots.push(root);
|
|
20
|
+
return root;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
vi.restoreAllMocks();
|
|
25
|
+
await Promise.all(fixtureRoots.splice(0).map(removeFixtureRoot));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
async function legacyFixture(): Promise<string> {
|
|
29
|
+
const root = await fixtureRoot();
|
|
30
|
+
return writeFixturePackage(root, {
|
|
31
|
+
dir: 'legacy',
|
|
32
|
+
packageJson: { name: 'legacy', version: '1.0.0' }
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** App using `base`, which provides vue but app declares no range → W_NO_RANGE. */
|
|
37
|
+
async function warningOnlyFixture(): Promise<string> {
|
|
38
|
+
const root = await fixtureRoot();
|
|
39
|
+
writeFixturePackage(root, {
|
|
40
|
+
dir: 'app/node_modules/base',
|
|
41
|
+
packageJson: {
|
|
42
|
+
name: 'base',
|
|
43
|
+
version: '1.0.0',
|
|
44
|
+
dependencies: { vue: '^3.4.0' },
|
|
45
|
+
esmx: { provides: ['vue'] }
|
|
46
|
+
},
|
|
47
|
+
built: true
|
|
48
|
+
});
|
|
49
|
+
writeFixturePackage(root, {
|
|
50
|
+
dir: 'app/node_modules/base/node_modules/vue',
|
|
51
|
+
packageJson: { name: 'vue', version: '3.4.21' }
|
|
52
|
+
});
|
|
53
|
+
return writeFixturePackage(root, {
|
|
54
|
+
dir: 'app',
|
|
55
|
+
packageJson: {
|
|
56
|
+
name: 'app',
|
|
57
|
+
version: '1.0.0',
|
|
58
|
+
esmx: {
|
|
59
|
+
entry: { client: './src/entry.client.ts' },
|
|
60
|
+
uses: ['base']
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Layered merge (Gate 2): vue-base wins over base → one election. */
|
|
67
|
+
async function dupProviderFixture(): Promise<string> {
|
|
68
|
+
const root = await fixtureRoot();
|
|
69
|
+
writeFixturePackage(root, {
|
|
70
|
+
dir: 'node_modules/base',
|
|
71
|
+
packageJson: {
|
|
72
|
+
name: 'base',
|
|
73
|
+
version: '1.0.0',
|
|
74
|
+
dependencies: { vue: '^3.4.0' },
|
|
75
|
+
esmx: { provides: ['vue'] }
|
|
76
|
+
},
|
|
77
|
+
built: true
|
|
78
|
+
});
|
|
79
|
+
writeFixturePackage(root, {
|
|
80
|
+
dir: 'node_modules/base/node_modules/vue',
|
|
81
|
+
packageJson: { name: 'vue', version: '3.4.21' }
|
|
82
|
+
});
|
|
83
|
+
writeFixturePackage(root, {
|
|
84
|
+
dir: 'node_modules/vue-base',
|
|
85
|
+
packageJson: {
|
|
86
|
+
name: 'vue-base',
|
|
87
|
+
version: '1.0.0',
|
|
88
|
+
dependencies: { vue: '^3.4.0' },
|
|
89
|
+
esmx: { uses: ['base'], provides: ['vue'] }
|
|
90
|
+
},
|
|
91
|
+
built: true
|
|
92
|
+
});
|
|
93
|
+
writeFixturePackage(root, {
|
|
94
|
+
dir: 'node_modules/vue-base/node_modules/vue',
|
|
95
|
+
packageJson: { name: 'vue', version: '3.5.13' }
|
|
96
|
+
});
|
|
97
|
+
return writeFixturePackage(root, {
|
|
98
|
+
dir: 'app',
|
|
99
|
+
packageJson: {
|
|
100
|
+
name: 'app',
|
|
101
|
+
version: '1.0.0',
|
|
102
|
+
dependencies: { vue: '^3.4.0' },
|
|
103
|
+
esmx: { uses: ['vue-base'] }
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function notLinkedFixture(): Promise<string> {
|
|
109
|
+
const root = await fixtureRoot();
|
|
110
|
+
return writeFixturePackage(root, {
|
|
111
|
+
dir: 'app',
|
|
112
|
+
packageJson: {
|
|
113
|
+
name: 'app',
|
|
114
|
+
version: '1.0.0',
|
|
115
|
+
esmx: {
|
|
116
|
+
entry: { client: './src/entry.client.ts' },
|
|
117
|
+
uses: ['ghost']
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
describe('runValidate', () => {
|
|
124
|
+
it('should exit 0 with protocol legacy for a package without esmx field', async () => {
|
|
125
|
+
const appDir = await legacyFixture();
|
|
126
|
+
|
|
127
|
+
const jsonResult = await runValidate(appDir, { json: true });
|
|
128
|
+
const humanResult = await runValidate(appDir);
|
|
129
|
+
|
|
130
|
+
expect(jsonResult.exitCode).toBe(0);
|
|
131
|
+
expect(JSON.parse(jsonResult.output)).toEqual({
|
|
132
|
+
protocol: 'legacy',
|
|
133
|
+
diagnostics: []
|
|
134
|
+
});
|
|
135
|
+
expect(humanResult.exitCode).toBe(0);
|
|
136
|
+
expect(humanResult.output).toContain('entry.node.ts');
|
|
137
|
+
expect(humanResult.output).toContain('opt-in');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should exit 0 when the declaration only has warnings', async () => {
|
|
141
|
+
const appDir = await warningOnlyFixture();
|
|
142
|
+
|
|
143
|
+
const result = await runValidate(appDir, { json: true });
|
|
144
|
+
|
|
145
|
+
const envelope = JSON.parse(result.output);
|
|
146
|
+
expect(result.exitCode).toBe(0);
|
|
147
|
+
expect(envelope.diagnostics).toHaveLength(1);
|
|
148
|
+
expect(envelope.diagnostics[0].code).toBe('W_NO_RANGE');
|
|
149
|
+
expect(envelope.supply.vue).toEqual({
|
|
150
|
+
groups: [{ major: 3, provider: 'base', version: '3.4.21' }]
|
|
151
|
+
});
|
|
152
|
+
expect(Object.keys(envelope.mounts)).toEqual(['base']);
|
|
153
|
+
expect(envelope.mounts.base.built).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should exit non-zero for E_NOT_LINKED with the exact envelope entry shape', async () => {
|
|
157
|
+
const appDir = await notLinkedFixture();
|
|
158
|
+
|
|
159
|
+
const result = await runValidate(appDir, { json: true });
|
|
160
|
+
|
|
161
|
+
const envelope = JSON.parse(result.output);
|
|
162
|
+
expect(result.exitCode).toBe(1);
|
|
163
|
+
const entry = envelope.diagnostics.find(
|
|
164
|
+
(d: { code: string }) => d.code === 'E_NOT_LINKED'
|
|
165
|
+
);
|
|
166
|
+
expect(entry).toBeDefined();
|
|
167
|
+
expect(entry.module).toBe('app');
|
|
168
|
+
expect(typeof entry.message).toBe('string');
|
|
169
|
+
expect(typeof entry.fix).toBe('string');
|
|
170
|
+
expect(entry).not.toHaveProperty('severity');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should exit non-zero and surface E_DUP_PROVIDER in the JSON envelope', async () => {
|
|
174
|
+
const appDir = await dupProviderFixture();
|
|
175
|
+
|
|
176
|
+
const result = await runValidate(appDir, { json: true });
|
|
177
|
+
|
|
178
|
+
const envelope = JSON.parse(result.output);
|
|
179
|
+
expect(result.exitCode).toBe(1);
|
|
180
|
+
const entry = envelope.diagnostics.find(
|
|
181
|
+
(d: { code: string }) => d.code === 'E_DUP_PROVIDER'
|
|
182
|
+
);
|
|
183
|
+
expect(entry).toBeDefined();
|
|
184
|
+
expect(entry.module).toBe('vue-base');
|
|
185
|
+
expect(entry.package).toBe('vue');
|
|
186
|
+
expect(entry.found).toBe('base, vue-base');
|
|
187
|
+
expect(envelope).not.toHaveProperty('elections');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should print E_DUP_PROVIDER in the human report', async () => {
|
|
191
|
+
const appDir = await dupProviderFixture();
|
|
192
|
+
|
|
193
|
+
const result = await runValidate(appDir);
|
|
194
|
+
|
|
195
|
+
const plain = util.stripVTControlCharacters(result.output);
|
|
196
|
+
expect(plain).toContain('[E_DUP_PROVIDER]');
|
|
197
|
+
expect(plain).toContain('vue-base → vue');
|
|
198
|
+
expect(plain).toContain('base, vue-base');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should report diagnostics, supply and mounts in human mode', async () => {
|
|
202
|
+
const appDir = await warningOnlyFixture();
|
|
203
|
+
|
|
204
|
+
const result = await runValidate(appDir);
|
|
205
|
+
|
|
206
|
+
const plain = util.stripVTControlCharacters(result.output);
|
|
207
|
+
expect(result.exitCode).toBe(0);
|
|
208
|
+
expect(plain).toContain('[W_NO_RANGE]');
|
|
209
|
+
expect(plain).toContain('fix:');
|
|
210
|
+
expect(plain).toContain('vue → base@3.4.21');
|
|
211
|
+
expect(plain).toMatch(/base → .*dist \(built\)/);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should produce pure JSON output without writing to the console', async () => {
|
|
215
|
+
const appDir = await notLinkedFixture();
|
|
216
|
+
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
217
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
218
|
+
const error = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
219
|
+
|
|
220
|
+
const result = await runValidate(appDir, { json: true });
|
|
221
|
+
|
|
222
|
+
expect(() => JSON.parse(result.output)).not.toThrow();
|
|
223
|
+
expect(log).not.toHaveBeenCalled();
|
|
224
|
+
expect(warn).not.toHaveBeenCalled();
|
|
225
|
+
expect(error).not.toHaveBeenCalled();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const distCli = path.resolve(
|
|
230
|
+
path.dirname(url.fileURLToPath(import.meta.url)),
|
|
231
|
+
'../../dist/cli/index.mjs'
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Spawn-based smoke test: only meaningful against a built CLI; building
|
|
235
|
+
// inside tests is out of scope, so skip when dist is absent.
|
|
236
|
+
describe.skipIf(!fs.existsSync(distCli))('esmx validate (spawned CLI)', () => {
|
|
237
|
+
it('should emit only the JSON envelope on stdout and exit non-zero on errors', async () => {
|
|
238
|
+
const childProcess = await import('node:child_process');
|
|
239
|
+
const appDir = await notLinkedFixture();
|
|
240
|
+
|
|
241
|
+
const spawned = childProcess.spawnSync(
|
|
242
|
+
process.execPath,
|
|
243
|
+
[distCli, 'validate', '--json'],
|
|
244
|
+
{ cwd: appDir, encoding: 'utf-8' }
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
expect(spawned.status).toBe(1);
|
|
248
|
+
const envelope = JSON.parse(spawned.stdout);
|
|
249
|
+
expect(envelope.diagnostics[0].code).toBe('E_NOT_LINKED');
|
|
250
|
+
});
|
|
251
|
+
});
|