@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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { validateDeclaration } from './schema';
|
|
5
|
+
|
|
6
|
+
import type { Diagnostic, EsmxDeclaration } from './types';
|
|
7
|
+
|
|
8
|
+
/** Package facts the resolver needs, read from a package root directory. */
|
|
9
|
+
export interface PackageRecord {
|
|
10
|
+
name: string;
|
|
11
|
+
version: string;
|
|
12
|
+
root: string;
|
|
13
|
+
private: boolean;
|
|
14
|
+
dependencies: Record<string, string>;
|
|
15
|
+
peerDependencies: Record<string, string>;
|
|
16
|
+
devDependencies: Record<string, string>;
|
|
17
|
+
declaration: EsmxDeclaration | null;
|
|
18
|
+
diagnostics: Diagnostic[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ReadDeclarationResult extends PackageRecord {
|
|
22
|
+
declaration: EsmxDeclaration;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readJsonFile(filePath: string): unknown | null {
|
|
26
|
+
// Boundary adapter: fs/JSON throw; absence or malformed JSON means
|
|
27
|
+
// "no readable package" for the resolver, which reports E_NOT_LINKED.
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toStringRecord(value: unknown): Record<string, string> {
|
|
36
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
const result: Record<string, string> = {};
|
|
40
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
41
|
+
if (typeof entry === 'string') {
|
|
42
|
+
result[key] = entry;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Reads a package root's package.json into a PackageRecord, whether or not
|
|
50
|
+
* it carries an `esmx` declaration. Returns null when no package.json can
|
|
51
|
+
* be read.
|
|
52
|
+
*/
|
|
53
|
+
export function readPackageRecord(packageDir: string): PackageRecord | null {
|
|
54
|
+
const json = readJsonFile(path.resolve(packageDir, 'package.json'));
|
|
55
|
+
if (typeof json !== 'object' || json === null || Array.isArray(json)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const pkg = json as Record<string, unknown>;
|
|
59
|
+
const name = typeof pkg.name === 'string' ? pkg.name : '';
|
|
60
|
+
const diagnostics: Diagnostic[] = [];
|
|
61
|
+
let declaration: EsmxDeclaration | null = null;
|
|
62
|
+
if (pkg.esmx !== undefined) {
|
|
63
|
+
const validated = validateDeclaration(pkg.esmx, name || packageDir);
|
|
64
|
+
diagnostics.push(...validated.diagnostics);
|
|
65
|
+
declaration = validated.declaration ?? {};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
name,
|
|
69
|
+
version: typeof pkg.version === 'string' ? pkg.version : '',
|
|
70
|
+
root: packageDir,
|
|
71
|
+
private: pkg.private === true,
|
|
72
|
+
dependencies: toStringRecord(pkg.dependencies),
|
|
73
|
+
peerDependencies: toStringRecord(pkg.peerDependencies),
|
|
74
|
+
devDependencies: toStringRecord(pkg.devDependencies),
|
|
75
|
+
declaration,
|
|
76
|
+
diagnostics
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Reads a package's `esmx` declaration. Returns null when the package has
|
|
82
|
+
* no `esmx` field (legacy module) or no readable package.json. Schema
|
|
83
|
+
* violations are reported in `diagnostics` with the valid remainder kept.
|
|
84
|
+
*/
|
|
85
|
+
export function readDeclaration(
|
|
86
|
+
packageDir: string
|
|
87
|
+
): ReadDeclarationResult | null {
|
|
88
|
+
const record = readPackageRecord(packageDir);
|
|
89
|
+
if (!record || record.declaration === null) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return { ...record, declaration: record.declaration };
|
|
93
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
4
|
+
import { COMMAND, Esmx } from '../core';
|
|
5
|
+
import type { ManifestJson } from '../manifest-json';
|
|
6
|
+
import { MANIFEST_PROTOCOL_VERSION } from '../manifest-json';
|
|
7
|
+
import { createFixtureRoot, removeFixtureRoot } from './test-fixtures';
|
|
8
|
+
|
|
9
|
+
const fixtureRoots: string[] = [];
|
|
10
|
+
|
|
11
|
+
async function fixtureRoot(): Promise<string> {
|
|
12
|
+
const root = await createFixtureRoot();
|
|
13
|
+
fixtureRoots.push(root);
|
|
14
|
+
return root;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
await Promise.all(fixtureRoots.splice(0).map(removeFixtureRoot));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function writeModule(
|
|
22
|
+
rootDir: string,
|
|
23
|
+
dir: string,
|
|
24
|
+
packageJson: Record<string, unknown>,
|
|
25
|
+
manifest: Partial<ManifestJson>
|
|
26
|
+
): string {
|
|
27
|
+
const moduleDir = path.join(rootDir, dir);
|
|
28
|
+
fs.mkdirSync(moduleDir, { recursive: true });
|
|
29
|
+
fs.writeFileSync(
|
|
30
|
+
path.join(moduleDir, 'package.json'),
|
|
31
|
+
JSON.stringify(packageJson, null, 4)
|
|
32
|
+
);
|
|
33
|
+
const full: ManifestJson = {
|
|
34
|
+
protocol: MANIFEST_PROTOCOL_VERSION,
|
|
35
|
+
name: String(packageJson.name),
|
|
36
|
+
version: String(packageJson.version ?? '0.0.0'),
|
|
37
|
+
provides: {},
|
|
38
|
+
uses: [],
|
|
39
|
+
scopes: {},
|
|
40
|
+
exports: {},
|
|
41
|
+
files: [],
|
|
42
|
+
chunks: {},
|
|
43
|
+
...manifest
|
|
44
|
+
};
|
|
45
|
+
for (const env of ['client', 'server'] as const) {
|
|
46
|
+
const envDir = path.join(moduleDir, 'dist', env);
|
|
47
|
+
fs.mkdirSync(envDir, { recursive: true });
|
|
48
|
+
fs.writeFileSync(
|
|
49
|
+
path.join(envDir, 'manifest.json'),
|
|
50
|
+
JSON.stringify(full, null, 4)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return moduleDir;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** App mounting `shared`, which exports a module-export `shared/ui`. */
|
|
57
|
+
function buildFixture(root: string, sharedExportFile: string): string {
|
|
58
|
+
writeModule(
|
|
59
|
+
root,
|
|
60
|
+
'app/node_modules/shared',
|
|
61
|
+
{
|
|
62
|
+
name: 'shared',
|
|
63
|
+
version: '1.0.0',
|
|
64
|
+
esmx: { exports: { './ui': './src/ui.ts' } }
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
exports: {
|
|
68
|
+
ui: {
|
|
69
|
+
name: 'ui',
|
|
70
|
+
pkg: false,
|
|
71
|
+
file: sharedExportFile,
|
|
72
|
+
identifier: 'shared/ui'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
return writeModule(
|
|
78
|
+
root,
|
|
79
|
+
'app',
|
|
80
|
+
{
|
|
81
|
+
name: 'app',
|
|
82
|
+
version: '1.0.0',
|
|
83
|
+
esmx: { uses: ['shared'] }
|
|
84
|
+
},
|
|
85
|
+
{}
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Overwrites the shared module's client manifest export file (a new build). */
|
|
90
|
+
function rewriteSharedExport(appDir: string, file: string): void {
|
|
91
|
+
const manifestPath = path.join(
|
|
92
|
+
appDir,
|
|
93
|
+
'node_modules/shared/dist/client/manifest.json'
|
|
94
|
+
);
|
|
95
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
96
|
+
manifest.exports.ui.file = file;
|
|
97
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 4));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
describe('Esmx.reinit generational relink (RFC 0001 §9)', () => {
|
|
101
|
+
it('adopts a republished remote on a new generation', async () => {
|
|
102
|
+
const root = await fixtureRoot();
|
|
103
|
+
const appDir = buildFixture(root, 'ui.gen1.mjs');
|
|
104
|
+
|
|
105
|
+
const esmx = new Esmx({ root: appDir, isProd: true });
|
|
106
|
+
await esmx.init(COMMAND.preview);
|
|
107
|
+
|
|
108
|
+
const before = await esmx.getImportMap('client');
|
|
109
|
+
expect(before.imports?.['shared/ui']).toBe('/shared/ui.gen1.mjs');
|
|
110
|
+
|
|
111
|
+
// The remote republishes with a new build output; the cached first
|
|
112
|
+
// generation must NOT see it until a relink.
|
|
113
|
+
rewriteSharedExport(appDir, 'ui.gen2.mjs');
|
|
114
|
+
const stillCached = await esmx.getImportMap('client');
|
|
115
|
+
expect(stillCached.imports?.['shared/ui']).toBe('/shared/ui.gen1.mjs');
|
|
116
|
+
|
|
117
|
+
const ok = await esmx.reinit();
|
|
118
|
+
|
|
119
|
+
expect(ok).toBe(true);
|
|
120
|
+
const after = await esmx.getImportMap('client');
|
|
121
|
+
expect(after.imports?.['shared/ui']).toBe('/shared/ui.gen2.mjs');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('rolls back to the previous generation when the relink fails', async () => {
|
|
125
|
+
const root = await fixtureRoot();
|
|
126
|
+
const appDir = buildFixture(root, 'ui.gen1.mjs');
|
|
127
|
+
|
|
128
|
+
const esmx = new Esmx({ root: appDir, isProd: true });
|
|
129
|
+
await esmx.init(COMMAND.preview);
|
|
130
|
+
const moduleConfigBefore = esmx.moduleConfig;
|
|
131
|
+
|
|
132
|
+
// Corrupt the app's own package.json so the relink's re-read throws.
|
|
133
|
+
fs.writeFileSync(path.join(appDir, 'package.json'), '{ not json');
|
|
134
|
+
|
|
135
|
+
await expect(esmx.reinit()).rejects.toThrow();
|
|
136
|
+
|
|
137
|
+
// The previous generation is intact and still serves.
|
|
138
|
+
expect(esmx.moduleConfig).toBe(moduleConfigBefore);
|
|
139
|
+
const after = await esmx.getImportMap('client');
|
|
140
|
+
expect(after.imports?.['shared/ui']).toBe('/shared/ui.gen1.mjs');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('throws when called before init', async () => {
|
|
144
|
+
const esmx = new Esmx({ root: '/nonexistent' });
|
|
145
|
+
|
|
146
|
+
await expect(esmx.reinit()).rejects.toThrow();
|
|
147
|
+
});
|
|
148
|
+
});
|