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