@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
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { esmxDeclarationSchema, validateDeclaration } from './schema';
|
|
4
|
+
import { DiagnosticCode } from './types';
|
|
5
|
+
|
|
6
|
+
describe('esmxDeclarationSchema', () => {
|
|
7
|
+
it('should be a draft 2020-12 schema for the esmx field', () => {
|
|
8
|
+
expect(esmxDeclarationSchema.$schema).toBe(
|
|
9
|
+
'https://json-schema.org/draft/2020-12/schema'
|
|
10
|
+
);
|
|
11
|
+
expect(Object.keys(esmxDeclarationSchema.properties)).toEqual([
|
|
12
|
+
'entry',
|
|
13
|
+
'exports',
|
|
14
|
+
'provides',
|
|
15
|
+
'uses'
|
|
16
|
+
]);
|
|
17
|
+
expect(esmxDeclarationSchema.additionalProperties).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('validateDeclaration', () => {
|
|
22
|
+
it('should accept a full valid declaration without diagnostics', () => {
|
|
23
|
+
const input = {
|
|
24
|
+
entry: {
|
|
25
|
+
client: './src/entry.client.ts',
|
|
26
|
+
server: './src/entry.server.ts'
|
|
27
|
+
},
|
|
28
|
+
exports: {
|
|
29
|
+
'./ui': './src/ui/index.ts',
|
|
30
|
+
'./store': {
|
|
31
|
+
client: './src/store.client.ts',
|
|
32
|
+
server: false
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
provides: ['vue', '@esmx/router'],
|
|
36
|
+
uses: ['shared']
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const result = validateDeclaration(input, 'cart');
|
|
40
|
+
|
|
41
|
+
expect(result.diagnostics).toEqual([]);
|
|
42
|
+
expect(result.declaration).toEqual(input);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should reject a non-object esmx field', () => {
|
|
46
|
+
const result = validateDeclaration('nope', 'cart');
|
|
47
|
+
|
|
48
|
+
expect(result.declaration).toBeNull();
|
|
49
|
+
expect(result.diagnostics).toHaveLength(1);
|
|
50
|
+
expect(result.diagnostics[0].code).toBe(DiagnosticCode.E_SCHEMA);
|
|
51
|
+
expect(result.diagnostics[0].severity).toBe('error');
|
|
52
|
+
expect(result.diagnostics[0].module).toBe('cart');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should reject export keys without a ./ subpath prefix', () => {
|
|
56
|
+
const result = validateDeclaration(
|
|
57
|
+
{ exports: { widget: './src/widget.ts' } },
|
|
58
|
+
'cart'
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(result.diagnostics).toHaveLength(1);
|
|
62
|
+
expect(result.diagnostics[0].code).toBe(DiagnosticCode.E_SCHEMA);
|
|
63
|
+
expect(result.diagnostics[0].message).toContain('"widget"');
|
|
64
|
+
expect(result.declaration?.exports).toEqual({});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should reject non-relative export file paths', () => {
|
|
68
|
+
const result = validateDeclaration(
|
|
69
|
+
{ exports: { './widget': 'src/widget.ts' } },
|
|
70
|
+
'cart'
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(result.diagnostics).toHaveLength(1);
|
|
74
|
+
expect(result.diagnostics[0].code).toBe(DiagnosticCode.E_SCHEMA);
|
|
75
|
+
expect(result.diagnostics[0].message).toContain('./');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should reject wrong types in provides and uses', () => {
|
|
79
|
+
const result = validateDeclaration(
|
|
80
|
+
{ provides: 'vue', uses: ['shared', '', 42] },
|
|
81
|
+
'cart'
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const codes = result.diagnostics.map((d) => d.code);
|
|
85
|
+
expect(codes).toEqual([
|
|
86
|
+
DiagnosticCode.E_SCHEMA,
|
|
87
|
+
DiagnosticCode.E_SCHEMA,
|
|
88
|
+
DiagnosticCode.E_SCHEMA
|
|
89
|
+
]);
|
|
90
|
+
expect(result.declaration?.provides).toBeUndefined();
|
|
91
|
+
expect(result.declaration?.uses).toEqual(['shared']);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should reject non-relative entry paths and unknown keys', () => {
|
|
95
|
+
const result = validateDeclaration(
|
|
96
|
+
{
|
|
97
|
+
entry: { client: 'src/entry.client.ts', node: './x.ts' },
|
|
98
|
+
bogus: true
|
|
99
|
+
},
|
|
100
|
+
'cart'
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const codes = result.diagnostics.map((d) => d.code);
|
|
104
|
+
expect(codes).toHaveLength(3);
|
|
105
|
+
expect(new Set(codes)).toEqual(new Set([DiagnosticCode.E_SCHEMA]));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should reject invalid env-fork values', () => {
|
|
109
|
+
const result = validateDeclaration(
|
|
110
|
+
{ exports: { './store': { client: true, server: './s.ts' } } },
|
|
111
|
+
'cart'
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(result.diagnostics).toHaveLength(1);
|
|
115
|
+
expect(result.diagnostics[0].code).toBe(DiagnosticCode.E_SCHEMA);
|
|
116
|
+
expect(result.declaration?.exports).toEqual({});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EsmxDeclaration,
|
|
3
|
+
EsmxDeclarationEntry,
|
|
4
|
+
EsmxDeclarationExportFork,
|
|
5
|
+
EsmxDeclarationExportValue
|
|
6
|
+
} from './types';
|
|
7
|
+
import { type Diagnostic, DiagnosticCode } from './types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* JSON Schema (draft 2020-12) for the package.json `esmx` field.
|
|
11
|
+
* Exported for external tooling and publishing; `validateDeclaration`
|
|
12
|
+
* enforces the same constraints structurally without a schema-validator
|
|
13
|
+
* dependency.
|
|
14
|
+
*/
|
|
15
|
+
export const esmxDeclarationSchema = {
|
|
16
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
17
|
+
$id: 'https://esmx.dev/schema/esmx-declaration.json',
|
|
18
|
+
title: 'Esmx module declaration (package.json "esmx" field)',
|
|
19
|
+
type: 'object',
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
properties: {
|
|
22
|
+
entry: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
properties: {
|
|
26
|
+
client: { $ref: '#/$defs/relativePath' },
|
|
27
|
+
server: { $ref: '#/$defs/relativePath' }
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
exports: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
propertyNames: { pattern: '^\\./.+' },
|
|
33
|
+
additionalProperties: {
|
|
34
|
+
anyOf: [
|
|
35
|
+
{ $ref: '#/$defs/relativePath' },
|
|
36
|
+
{
|
|
37
|
+
type: 'object',
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
properties: {
|
|
40
|
+
client: { $ref: '#/$defs/forkValue' },
|
|
41
|
+
server: { $ref: '#/$defs/forkValue' }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
provides: {
|
|
48
|
+
type: 'array',
|
|
49
|
+
items: { type: 'string', minLength: 1 }
|
|
50
|
+
},
|
|
51
|
+
uses: {
|
|
52
|
+
type: 'array',
|
|
53
|
+
items: { type: 'string', minLength: 1 }
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
$defs: {
|
|
57
|
+
relativePath: { type: 'string', pattern: '^\\./.+' },
|
|
58
|
+
forkValue: {
|
|
59
|
+
anyOf: [{ $ref: '#/$defs/relativePath' }, { const: false }]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} as const;
|
|
63
|
+
|
|
64
|
+
export interface ValidateDeclarationResult {
|
|
65
|
+
declaration: EsmxDeclaration | null;
|
|
66
|
+
diagnostics: Diagnostic[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function schemaError(
|
|
70
|
+
moduleName: string,
|
|
71
|
+
message: string,
|
|
72
|
+
fix: string
|
|
73
|
+
): Diagnostic {
|
|
74
|
+
return {
|
|
75
|
+
code: DiagnosticCode.E_SCHEMA,
|
|
76
|
+
severity: 'error',
|
|
77
|
+
module: moduleName,
|
|
78
|
+
message,
|
|
79
|
+
fix
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isRelativePath(value: unknown): value is string {
|
|
84
|
+
return typeof value === 'string' && /^\.\/.+/.test(value);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
88
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function validateEntry(
|
|
92
|
+
value: unknown,
|
|
93
|
+
moduleName: string,
|
|
94
|
+
diagnostics: Diagnostic[]
|
|
95
|
+
): EsmxDeclarationEntry | undefined {
|
|
96
|
+
if (!isRecord(value)) {
|
|
97
|
+
diagnostics.push(
|
|
98
|
+
schemaError(
|
|
99
|
+
moduleName,
|
|
100
|
+
`"esmx.entry" must be an object with optional "client"/"server" keys.`,
|
|
101
|
+
`Declare entry as { "client": "./src/entry.client.ts", "server": "./src/entry.server.ts" }.`
|
|
102
|
+
)
|
|
103
|
+
);
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const entry: EsmxDeclarationEntry = {};
|
|
107
|
+
for (const key of Object.keys(value)) {
|
|
108
|
+
if (key !== 'client' && key !== 'server') {
|
|
109
|
+
diagnostics.push(
|
|
110
|
+
schemaError(
|
|
111
|
+
moduleName,
|
|
112
|
+
`"esmx.entry" has unknown key "${key}".`,
|
|
113
|
+
`Only "client" and "server" are allowed in "esmx.entry".`
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const side = value[key];
|
|
119
|
+
if (!isRelativePath(side)) {
|
|
120
|
+
diagnostics.push(
|
|
121
|
+
schemaError(
|
|
122
|
+
moduleName,
|
|
123
|
+
`"esmx.entry.${key}" must be a relative "./" path, got ${JSON.stringify(side)}.`,
|
|
124
|
+
`Use a relative source path like "./src/entry.${key}.ts".`
|
|
125
|
+
)
|
|
126
|
+
);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
entry[key] = side;
|
|
130
|
+
}
|
|
131
|
+
return entry;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function validateExportFork(
|
|
135
|
+
key: string,
|
|
136
|
+
value: Record<string, unknown>,
|
|
137
|
+
moduleName: string,
|
|
138
|
+
diagnostics: Diagnostic[]
|
|
139
|
+
): EsmxDeclarationExportFork | null {
|
|
140
|
+
const fork: EsmxDeclarationExportFork = {};
|
|
141
|
+
let valid = true;
|
|
142
|
+
for (const sideKey of Object.keys(value)) {
|
|
143
|
+
if (sideKey !== 'client' && sideKey !== 'server') {
|
|
144
|
+
diagnostics.push(
|
|
145
|
+
schemaError(
|
|
146
|
+
moduleName,
|
|
147
|
+
`"esmx.exports['${key}']" has unknown key "${sideKey}".`,
|
|
148
|
+
`Env-fork export values allow only "client" and "server".`
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
valid = false;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const side = value[sideKey];
|
|
155
|
+
if (side === false || isRelativePath(side)) {
|
|
156
|
+
fork[sideKey] = side;
|
|
157
|
+
} else {
|
|
158
|
+
diagnostics.push(
|
|
159
|
+
schemaError(
|
|
160
|
+
moduleName,
|
|
161
|
+
`"esmx.exports['${key}'].${sideKey}" must be a relative "./" path or false, got ${JSON.stringify(side)}.`,
|
|
162
|
+
`Point ${sideKey} at a relative source file, or use false to disable that side.`
|
|
163
|
+
)
|
|
164
|
+
);
|
|
165
|
+
valid = false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return valid ? fork : null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function validateExports(
|
|
172
|
+
value: unknown,
|
|
173
|
+
moduleName: string,
|
|
174
|
+
diagnostics: Diagnostic[]
|
|
175
|
+
): Record<string, EsmxDeclarationExportValue> | undefined {
|
|
176
|
+
if (!isRecord(value)) {
|
|
177
|
+
diagnostics.push(
|
|
178
|
+
schemaError(
|
|
179
|
+
moduleName,
|
|
180
|
+
`"esmx.exports" must be an object mapping "./<name>" subpaths to source files.`,
|
|
181
|
+
`Declare exports as { "./widget": "./src/widget.ts" }.`
|
|
182
|
+
)
|
|
183
|
+
);
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
const exports: Record<string, EsmxDeclarationExportValue> = {};
|
|
187
|
+
for (const [key, exportValue] of Object.entries(value)) {
|
|
188
|
+
if (!/^\.\/.+/.test(key)) {
|
|
189
|
+
diagnostics.push(
|
|
190
|
+
schemaError(
|
|
191
|
+
moduleName,
|
|
192
|
+
`"esmx.exports" key "${key}" must be a "./<name>" subpath.`,
|
|
193
|
+
`Rename the key to "./${key.replace(/^\.?\/?/, '')}".`
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (isRelativePath(exportValue)) {
|
|
199
|
+
exports[key] = exportValue;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (isRecord(exportValue)) {
|
|
203
|
+
const fork = validateExportFork(
|
|
204
|
+
key,
|
|
205
|
+
exportValue,
|
|
206
|
+
moduleName,
|
|
207
|
+
diagnostics
|
|
208
|
+
);
|
|
209
|
+
if (fork) {
|
|
210
|
+
exports[key] = fork;
|
|
211
|
+
}
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
diagnostics.push(
|
|
215
|
+
schemaError(
|
|
216
|
+
moduleName,
|
|
217
|
+
`"esmx.exports['${key}']" must be a relative "./" path or a { client, server } fork, got ${JSON.stringify(exportValue)}.`,
|
|
218
|
+
`Point the export at a relative source file like "./src/widget.ts".`
|
|
219
|
+
)
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return exports;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function validateNameArray(
|
|
226
|
+
field: 'provides' | 'uses',
|
|
227
|
+
value: unknown,
|
|
228
|
+
moduleName: string,
|
|
229
|
+
diagnostics: Diagnostic[]
|
|
230
|
+
): string[] | undefined {
|
|
231
|
+
if (!Array.isArray(value)) {
|
|
232
|
+
diagnostics.push(
|
|
233
|
+
schemaError(
|
|
234
|
+
moduleName,
|
|
235
|
+
`"esmx.${field}" must be an array of non-empty strings.`,
|
|
236
|
+
`Declare ${field} as a string array, e.g. ["vue"].`
|
|
237
|
+
)
|
|
238
|
+
);
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
const names: string[] = [];
|
|
242
|
+
for (const item of value) {
|
|
243
|
+
if (typeof item !== 'string' || item.length === 0) {
|
|
244
|
+
diagnostics.push(
|
|
245
|
+
schemaError(
|
|
246
|
+
moduleName,
|
|
247
|
+
`"esmx.${field}" entries must be non-empty strings, got ${JSON.stringify(item)}.`,
|
|
248
|
+
`Remove or replace the invalid ${field} entry.`
|
|
249
|
+
)
|
|
250
|
+
);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
names.push(item);
|
|
254
|
+
}
|
|
255
|
+
return names;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Structural validator enforcing `esmxDeclarationSchema`. Invalid pieces
|
|
260
|
+
* are dropped and reported; the returned declaration keeps the valid rest
|
|
261
|
+
* so downstream diagnostics stay meaningful. Returns a null declaration
|
|
262
|
+
* only when the field itself is not an object.
|
|
263
|
+
*/
|
|
264
|
+
export function validateDeclaration(
|
|
265
|
+
value: unknown,
|
|
266
|
+
moduleName: string
|
|
267
|
+
): ValidateDeclarationResult {
|
|
268
|
+
const diagnostics: Diagnostic[] = [];
|
|
269
|
+
if (!isRecord(value)) {
|
|
270
|
+
diagnostics.push(
|
|
271
|
+
schemaError(
|
|
272
|
+
moduleName,
|
|
273
|
+
`"esmx" field must be an object, got ${JSON.stringify(value)}.`,
|
|
274
|
+
`Declare protocol facts as an object: { "entry": ..., "exports": ..., "provides": ..., "uses": ... }.`
|
|
275
|
+
)
|
|
276
|
+
);
|
|
277
|
+
return { declaration: null, diagnostics };
|
|
278
|
+
}
|
|
279
|
+
const declaration: EsmxDeclaration = {};
|
|
280
|
+
for (const key of Object.keys(value)) {
|
|
281
|
+
switch (key) {
|
|
282
|
+
case 'entry': {
|
|
283
|
+
const entry = validateEntry(
|
|
284
|
+
value.entry,
|
|
285
|
+
moduleName,
|
|
286
|
+
diagnostics
|
|
287
|
+
);
|
|
288
|
+
if (entry) {
|
|
289
|
+
declaration.entry = entry;
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
case 'exports': {
|
|
294
|
+
const exports = validateExports(
|
|
295
|
+
value.exports,
|
|
296
|
+
moduleName,
|
|
297
|
+
diagnostics
|
|
298
|
+
);
|
|
299
|
+
if (exports) {
|
|
300
|
+
declaration.exports = exports;
|
|
301
|
+
}
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
case 'provides': {
|
|
305
|
+
const provides = validateNameArray(
|
|
306
|
+
'provides',
|
|
307
|
+
value.provides,
|
|
308
|
+
moduleName,
|
|
309
|
+
diagnostics
|
|
310
|
+
);
|
|
311
|
+
if (provides) {
|
|
312
|
+
declaration.provides = provides;
|
|
313
|
+
}
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
case 'uses': {
|
|
317
|
+
const uses = validateNameArray(
|
|
318
|
+
'uses',
|
|
319
|
+
value.uses,
|
|
320
|
+
moduleName,
|
|
321
|
+
diagnostics
|
|
322
|
+
);
|
|
323
|
+
if (uses) {
|
|
324
|
+
declaration.uses = uses;
|
|
325
|
+
}
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
default:
|
|
329
|
+
diagnostics.push(
|
|
330
|
+
schemaError(
|
|
331
|
+
moduleName,
|
|
332
|
+
`"esmx" has unknown key "${key}".`,
|
|
333
|
+
`Allowed keys are "entry", "exports", "provides", "uses".`
|
|
334
|
+
)
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return { declaration, diagnostics };
|
|
339
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { compareSemver, parseSemver, satisfiesRange } from './semver';
|
|
4
|
+
|
|
5
|
+
describe('parseSemver', () => {
|
|
6
|
+
it('should parse a plain version', () => {
|
|
7
|
+
const result = parseSemver('3.4.21');
|
|
8
|
+
|
|
9
|
+
expect(result).toEqual({
|
|
10
|
+
major: 3,
|
|
11
|
+
minor: 4,
|
|
12
|
+
patch: 21,
|
|
13
|
+
prerelease: []
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should parse a prerelease version with build metadata', () => {
|
|
18
|
+
const result = parseSemver('1.2.3-beta.1+build.5');
|
|
19
|
+
|
|
20
|
+
expect(result).toEqual({
|
|
21
|
+
major: 1,
|
|
22
|
+
minor: 2,
|
|
23
|
+
patch: 3,
|
|
24
|
+
prerelease: ['beta', '1']
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return null for non-version input', () => {
|
|
29
|
+
expect(parseSemver('workspace:*')).toBeNull();
|
|
30
|
+
expect(parseSemver('not-a-version')).toBeNull();
|
|
31
|
+
expect(parseSemver('1.2')).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('compareSemver', () => {
|
|
36
|
+
it('should order versions by triple then prerelease', () => {
|
|
37
|
+
const v340 = parseSemver('3.4.0');
|
|
38
|
+
const v352 = parseSemver('3.5.2');
|
|
39
|
+
const v352beta = parseSemver('3.5.2-beta.1');
|
|
40
|
+
if (!v340 || !v352 || !v352beta) {
|
|
41
|
+
throw new Error('fixture versions must parse');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
expect(compareSemver(v340, v352)).toBe(-1);
|
|
45
|
+
expect(compareSemver(v352, v340)).toBe(1);
|
|
46
|
+
expect(compareSemver(v352, v352)).toBe(0);
|
|
47
|
+
expect(compareSemver(v352beta, v352)).toBe(-1);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('satisfiesRange', () => {
|
|
52
|
+
it('should handle caret ranges', () => {
|
|
53
|
+
expect(satisfiesRange('3.5.2', '^3.4.0')).toBe(true);
|
|
54
|
+
expect(satisfiesRange('4.0.0', '^3.4.0')).toBe(false);
|
|
55
|
+
expect(satisfiesRange('3.3.9', '^3.4.0')).toBe(false);
|
|
56
|
+
expect(satisfiesRange('0.2.5', '^0.2.3')).toBe(true);
|
|
57
|
+
expect(satisfiesRange('0.3.0', '^0.2.3')).toBe(false);
|
|
58
|
+
expect(satisfiesRange('0.0.3', '^0.0.3')).toBe(true);
|
|
59
|
+
expect(satisfiesRange('0.0.4', '^0.0.3')).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should handle tilde ranges', () => {
|
|
63
|
+
expect(satisfiesRange('1.2.9', '~1.2.3')).toBe(true);
|
|
64
|
+
expect(satisfiesRange('1.3.0', '~1.2.3')).toBe(false);
|
|
65
|
+
expect(satisfiesRange('1.5.0', '~1')).toBe(true);
|
|
66
|
+
expect(satisfiesRange('2.0.0', '~1')).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should handle comparator ranges', () => {
|
|
70
|
+
expect(satisfiesRange('3.4.0', '>=3.4.0')).toBe(true);
|
|
71
|
+
expect(satisfiesRange('3.3.9', '>=3.4.0')).toBe(false);
|
|
72
|
+
expect(satisfiesRange('3.4.0', '>3.4.0')).toBe(false);
|
|
73
|
+
expect(satisfiesRange('3.4.0', '<=3.4.0')).toBe(true);
|
|
74
|
+
expect(satisfiesRange('3.4.0', '<3.4.0')).toBe(false);
|
|
75
|
+
expect(satisfiesRange('3.4.0', '=3.4.0')).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should handle exact versions, star and x-ranges', () => {
|
|
79
|
+
expect(satisfiesRange('3.4.21', '3.4.21')).toBe(true);
|
|
80
|
+
expect(satisfiesRange('3.4.22', '3.4.21')).toBe(false);
|
|
81
|
+
expect(satisfiesRange('9.9.9', '*')).toBe(true);
|
|
82
|
+
expect(satisfiesRange('1.7.0', '1.x')).toBe(true);
|
|
83
|
+
expect(satisfiesRange('2.0.0', '1.x')).toBe(false);
|
|
84
|
+
expect(satisfiesRange('1.2.9', '1.2.x')).toBe(true);
|
|
85
|
+
expect(satisfiesRange('1.3.0', '1.2.x')).toBe(false);
|
|
86
|
+
expect(satisfiesRange('1.5.0', '1')).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should handle compound AND and OR clauses', () => {
|
|
90
|
+
expect(satisfiesRange('1.5.0', '>=1.2.0 <2.0.0')).toBe(true);
|
|
91
|
+
expect(satisfiesRange('2.1.0', '>=1.2.0 <2.0.0')).toBe(false);
|
|
92
|
+
expect(satisfiesRange('2.1.0', '^1.0.0 || ^2.0.0')).toBe(true);
|
|
93
|
+
expect(satisfiesRange('3.0.0', '^1.0.0 || ^2.0.0')).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should return null for unparsable ranges or versions', () => {
|
|
97
|
+
expect(satisfiesRange('1.0.0', 'workspace:*')).toBeNull();
|
|
98
|
+
expect(satisfiesRange('1.0.0', 'npm:vue@^3.0.0')).toBeNull();
|
|
99
|
+
expect(satisfiesRange('not-a-version', '^1.0.0')).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
});
|