@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
@@ -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
+ });