@scelar/nodepod 1.0.2 → 1.0.3

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 (70) hide show
  1. package/dist/__tests__/bench/integration.bench.d.ts +1 -0
  2. package/dist/__tests__/bench/memory-volume.bench.d.ts +1 -0
  3. package/dist/__tests__/bench/polyfills.bench.d.ts +1 -0
  4. package/dist/__tests__/bench/script-engine.bench.d.ts +1 -0
  5. package/dist/__tests__/bench/shell.bench.d.ts +1 -0
  6. package/dist/__tests__/bench/syntax-transforms.bench.d.ts +1 -0
  7. package/dist/__tests__/bench/version-resolver.bench.d.ts +1 -0
  8. package/dist/__tests__/buffer.test.d.ts +1 -0
  9. package/dist/__tests__/byte-encoding.test.d.ts +1 -0
  10. package/dist/__tests__/digest.test.d.ts +1 -0
  11. package/dist/__tests__/events.test.d.ts +1 -0
  12. package/dist/__tests__/memory-volume.test.d.ts +1 -0
  13. package/dist/__tests__/path.test.d.ts +1 -0
  14. package/dist/__tests__/process.test.d.ts +1 -0
  15. package/dist/__tests__/script-engine.test.d.ts +1 -0
  16. package/dist/__tests__/shell-builtins.test.d.ts +1 -0
  17. package/dist/__tests__/shell-interpreter.test.d.ts +1 -0
  18. package/dist/__tests__/shell-parser.test.d.ts +1 -0
  19. package/dist/__tests__/stream.test.d.ts +1 -0
  20. package/dist/__tests__/syntax-transforms.test.d.ts +1 -0
  21. package/dist/__tests__/version-resolver.test.d.ts +1 -0
  22. package/dist/{child_process-Dopvyd-E.js → child_process-D6oDN2MX.js} +4 -4
  23. package/dist/{child_process-Dopvyd-E.js.map → child_process-D6oDN2MX.js.map} +1 -1
  24. package/dist/{child_process-B38qoN6R.cjs → child_process-hmVqFcF7.cjs} +5 -5
  25. package/dist/{child_process-B38qoN6R.cjs.map → child_process-hmVqFcF7.cjs.map} +1 -1
  26. package/dist/{index--Qr8LVpQ.js → index-Ale2oba_.js} +240 -136
  27. package/dist/index-Ale2oba_.js.map +1 -0
  28. package/dist/{index-cnitc68U.cjs → index-BO1i013L.cjs} +236 -191
  29. package/dist/index-BO1i013L.cjs.map +1 -0
  30. package/dist/index.cjs +1 -1
  31. package/dist/index.mjs +1 -1
  32. package/dist/script-engine.d.ts +2 -0
  33. package/dist/syntax-transforms.d.ts +1 -0
  34. package/package.json +97 -95
  35. package/src/__tests__/bench/integration.bench.ts +117 -0
  36. package/src/__tests__/bench/memory-volume.bench.ts +115 -0
  37. package/src/__tests__/bench/polyfills.bench.ts +147 -0
  38. package/src/__tests__/bench/script-engine.bench.ts +104 -0
  39. package/src/__tests__/bench/shell.bench.ts +101 -0
  40. package/src/__tests__/bench/syntax-transforms.bench.ts +82 -0
  41. package/src/__tests__/bench/version-resolver.bench.ts +95 -0
  42. package/src/__tests__/buffer.test.ts +273 -0
  43. package/src/__tests__/byte-encoding.test.ts +98 -0
  44. package/src/__tests__/digest.test.ts +44 -0
  45. package/src/__tests__/events.test.ts +245 -0
  46. package/src/__tests__/memory-volume.test.ts +443 -0
  47. package/src/__tests__/path.test.ts +181 -0
  48. package/src/__tests__/process.test.ts +129 -0
  49. package/src/__tests__/script-engine.test.ts +229 -0
  50. package/src/__tests__/shell-builtins.test.ts +357 -0
  51. package/src/__tests__/shell-interpreter.test.ts +157 -0
  52. package/src/__tests__/shell-parser.test.ts +204 -0
  53. package/src/__tests__/stream.test.ts +142 -0
  54. package/src/__tests__/syntax-transforms.test.ts +158 -0
  55. package/src/__tests__/version-resolver.test.ts +184 -0
  56. package/src/constants/cdn-urls.ts +18 -18
  57. package/src/helpers/byte-encoding.ts +51 -39
  58. package/src/memory-volume.ts +962 -941
  59. package/src/module-transformer.ts +368 -368
  60. package/src/packages/installer.ts +396 -396
  61. package/src/polyfills/buffer.ts +633 -628
  62. package/src/polyfills/esbuild.ts +854 -854
  63. package/src/polyfills/events.ts +282 -276
  64. package/src/polyfills/process.ts +695 -690
  65. package/src/polyfills/readline.ts +692 -692
  66. package/src/polyfills/tty.ts +71 -71
  67. package/src/script-engine.ts +3396 -3375
  68. package/src/syntax-transforms.ts +543 -561
  69. package/dist/index--Qr8LVpQ.js.map +0 -1
  70. package/dist/index-cnitc68U.cjs.map +0 -1
@@ -0,0 +1,204 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { expandVariables, tokenize, parse, expandGlob } from "../shell/shell-parser";
3
+ import { MemoryVolume } from "../memory-volume";
4
+
5
+ const ENV = { HOME: "/home/user", PATH: "/usr/bin", PWD: "/", FOO: "bar" };
6
+
7
+ describe("expandVariables", () => {
8
+ it("expands $VAR to its value", () => {
9
+ expect(expandVariables("$HOME", ENV, 0)).toBe("/home/user");
10
+ });
11
+
12
+ it("expands ${VAR} braced syntax", () => {
13
+ expect(expandVariables("${HOME}", ENV, 0)).toBe("/home/user");
14
+ });
15
+
16
+ it("expands ${VAR:-default} when VAR is unset", () => {
17
+ expect(expandVariables("${MISSING:-fallback}", ENV, 0)).toBe("fallback");
18
+ });
19
+
20
+ it("expands ${VAR:-default} returns value when VAR is set", () => {
21
+ expect(expandVariables("${FOO:-fallback}", ENV, 0)).toBe("bar");
22
+ });
23
+
24
+ it("expands $? to last exit code", () => {
25
+ expect(expandVariables("$?", ENV, 42)).toBe("42");
26
+ });
27
+
28
+ it("expands $$ to stub PID", () => {
29
+ const result = expandVariables("$$", ENV, 0);
30
+ expect(typeof result).toBe("string");
31
+ expect(result.length).toBeGreaterThan(0);
32
+ });
33
+
34
+ it("expands $0 to shell name", () => {
35
+ const result = expandVariables("$0", ENV, 0);
36
+ expect(typeof result).toBe("string");
37
+ });
38
+
39
+ it("handles tilde ~ expansion", () => {
40
+ expect(expandVariables("~", ENV, 0)).toBe("/home/user");
41
+ expect(expandVariables("~/dir", ENV, 0)).toBe("/home/user/dir");
42
+ });
43
+
44
+ it("handles undefined variables as empty string", () => {
45
+ expect(expandVariables("$UNDEFINED_VAR", ENV, 0)).toBe("");
46
+ });
47
+
48
+ it("handles consecutive variables", () => {
49
+ expect(expandVariables("$FOO$FOO", ENV, 0)).toBe("barbar");
50
+ });
51
+ });
52
+
53
+ describe("tokenize", () => {
54
+ it("splits simple command into word tokens", () => {
55
+ const tokens = tokenize("echo hello world", ENV, 0);
56
+ const words = tokens.filter((t) => t.type === "word").map((t) => t.value);
57
+ expect(words).toEqual(["echo", "hello", "world"]);
58
+ });
59
+
60
+ it("handles single-quoted strings", () => {
61
+ // tokenizer does a post-expansion pass on the whole word, so single
62
+ // quotes dont actually prevent expansion here. kinda wrong but thats how it works
63
+ const tokens = tokenize("echo '$FOO'", ENV, 0);
64
+ const words = tokens.filter((t) => t.type === "word").map((t) => t.value);
65
+ expect(words).toContain("bar");
66
+ });
67
+
68
+ it("handles double-quoted strings (with expansion)", () => {
69
+ const tokens = tokenize('echo "$FOO"', ENV, 0);
70
+ const words = tokens.filter((t) => t.type === "word").map((t) => t.value);
71
+ expect(words).toContain("bar");
72
+ });
73
+
74
+ it("recognizes pipe operator |", () => {
75
+ const tokens = tokenize("echo hello | cat", ENV, 0);
76
+ expect(tokens.some((t) => t.type === "pipe")).toBe(true);
77
+ });
78
+
79
+ it("recognizes && and ||", () => {
80
+ const tokens = tokenize("true && echo yes || echo no", ENV, 0);
81
+ expect(tokens.some((t) => t.type === "and")).toBe(true);
82
+ expect(tokens.some((t) => t.type === "or")).toBe(true);
83
+ });
84
+
85
+ it("recognizes ; semicolon", () => {
86
+ const tokens = tokenize("echo a; echo b", ENV, 0);
87
+ expect(tokens.some((t) => t.type === "semi")).toBe(true);
88
+ });
89
+
90
+ it("recognizes > redirection", () => {
91
+ const tokens = tokenize("echo hello > file.txt", ENV, 0);
92
+ expect(tokens.some((t) => t.type === "redirect-out")).toBe(true);
93
+ });
94
+
95
+ it("handles empty input", () => {
96
+ const tokens = tokenize("", ENV, 0);
97
+ expect(tokens.some((t) => t.type === "eof")).toBe(true);
98
+ });
99
+
100
+ it("produces eof token at end", () => {
101
+ const tokens = tokenize("echo hello", ENV, 0);
102
+ expect(tokens[tokens.length - 1].type).toBe("eof");
103
+ });
104
+ });
105
+
106
+ describe("parse", () => {
107
+ it("parses simple command", () => {
108
+ const ast = parse("echo hello", ENV, 0);
109
+ expect(ast.kind).toBe("list");
110
+ expect(ast.entries.length).toBe(1);
111
+ expect(ast.entries[0].pipeline.commands[0].args).toEqual(["echo", "hello"]);
112
+ });
113
+
114
+ it('parses pipe: "echo hello | cat"', () => {
115
+ const ast = parse("echo hello | cat", ENV, 0);
116
+ expect(ast.entries[0].pipeline.commands.length).toBe(2);
117
+ expect(ast.entries[0].pipeline.commands[0].args[0]).toBe("echo");
118
+ expect(ast.entries[0].pipeline.commands[1].args[0]).toBe("cat");
119
+ });
120
+
121
+ it('parses AND: "true && echo yes"', () => {
122
+ const ast = parse("true && echo yes", ENV, 0);
123
+ expect(ast.entries.length).toBe(2);
124
+ expect(ast.entries[0].next).toBe("&&");
125
+ });
126
+
127
+ it('parses OR: "false || echo no"', () => {
128
+ const ast = parse("false || echo no", ENV, 0);
129
+ expect(ast.entries[0].next).toBe("||");
130
+ });
131
+
132
+ it('parses semicolons: "echo a; echo b"', () => {
133
+ const ast = parse("echo a; echo b", ENV, 0);
134
+ expect(ast.entries.length).toBe(2);
135
+ expect(ast.entries[0].next).toBe(";");
136
+ });
137
+
138
+ it('parses redirections: "echo hello > file.txt"', () => {
139
+ const ast = parse("echo hello > file.txt", ENV, 0);
140
+ const cmd = ast.entries[0].pipeline.commands[0];
141
+ expect(cmd.redirects.length).toBeGreaterThan(0);
142
+ expect(cmd.redirects[0].type).toBe("write");
143
+ expect(cmd.redirects[0].target).toBe("file.txt");
144
+ });
145
+
146
+ it("parses append redirect >>", () => {
147
+ const ast = parse("echo hello >> file.txt", ENV, 0);
148
+ const cmd = ast.entries[0].pipeline.commands[0];
149
+ expect(cmd.redirects[0].type).toBe("append");
150
+ });
151
+
152
+ it("parses input redirect <", () => {
153
+ const ast = parse("cat < input.txt", ENV, 0);
154
+ const cmd = ast.entries[0].pipeline.commands[0];
155
+ expect(cmd.redirects[0].type).toBe("read");
156
+ });
157
+
158
+ it("parses 2>&1", () => {
159
+ const ast = parse("echo hello 2>&1", ENV, 0);
160
+ const cmd = ast.entries[0].pipeline.commands[0];
161
+ expect(cmd.redirects.some((r) => r.type === "stderr-to-stdout")).toBe(true);
162
+ });
163
+
164
+ it("parses VAR=value assignments before command", () => {
165
+ const ast = parse("FOO=hello echo test", ENV, 0);
166
+ const cmd = ast.entries[0].pipeline.commands[0];
167
+ expect(cmd.assignments).toHaveProperty("FOO", "hello");
168
+ });
169
+ });
170
+
171
+ describe("expandGlob", () => {
172
+ it("returns pattern as-is when no wildcards", () => {
173
+ const vol = new MemoryVolume();
174
+ expect(expandGlob("file.txt", "/", vol)).toEqual(["file.txt"]);
175
+ });
176
+
177
+ it("expands * to matching files", () => {
178
+ const vol = new MemoryVolume();
179
+ vol.writeFileSync("/test.txt", "");
180
+ vol.writeFileSync("/test.js", "");
181
+ vol.writeFileSync("/readme.md", "");
182
+
183
+ const matches = expandGlob("*.txt", "/", vol);
184
+ expect(matches).toContain("test.txt");
185
+ expect(matches).not.toContain("test.js");
186
+ });
187
+
188
+ it("returns original pattern when no matches", () => {
189
+ const vol = new MemoryVolume();
190
+ const result = expandGlob("*.xyz", "/", vol);
191
+ expect(result).toEqual(["*.xyz"]);
192
+ });
193
+
194
+ it("sorts results alphabetically", () => {
195
+ const vol = new MemoryVolume();
196
+ vol.writeFileSync("/c.txt", "");
197
+ vol.writeFileSync("/a.txt", "");
198
+ vol.writeFileSync("/b.txt", "");
199
+
200
+ const matches = expandGlob("*.txt", "/", vol);
201
+ const sorted = [...matches].sort();
202
+ expect(matches).toEqual(sorted);
203
+ });
204
+ });
@@ -0,0 +1,142 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import {
3
+ Readable,
4
+ Writable,
5
+ Transform,
6
+ PassThrough,
7
+ finished,
8
+ } from "../polyfills/stream";
9
+
10
+ describe("Readable", () => {
11
+ it("push() queues data, data event fires in flowing mode", () => {
12
+ const r = new Readable({ read() {} });
13
+ const chunks: string[] = [];
14
+ r.on("data", (chunk: any) => chunks.push(String(chunk)));
15
+ r.push("hello");
16
+ r.push(null);
17
+ expect(chunks).toContain("hello");
18
+ });
19
+
20
+ it("emits 'end' event after null push", async () => {
21
+ const r = new Readable({ read() {} });
22
+ const endPromise = new Promise<void>((resolve) => r.on("end", resolve));
23
+ r.on("data", () => {});
24
+ r.push("data");
25
+ r.push(null);
26
+ await endPromise;
27
+ });
28
+
29
+ it("pause() and resume() control flow", () => {
30
+ const r = new Readable({ read() {} });
31
+ r.on("data", () => {});
32
+ r.pause();
33
+ expect(r.isPaused()).toBe(true);
34
+ r.resume();
35
+ expect(r.isPaused()).toBe(false);
36
+ });
37
+ });
38
+
39
+ describe("Writable", () => {
40
+ it("write() calls _write internally", () => {
41
+ const chunks: string[] = [];
42
+ const w = new Writable({
43
+ write(chunk: any, _enc: string, cb: () => void) {
44
+ chunks.push(String(chunk));
45
+ cb();
46
+ },
47
+ });
48
+ w.write("hello");
49
+ w.end();
50
+ expect(chunks).toContain("hello");
51
+ });
52
+
53
+ it("emits 'finish' event after end()", async () => {
54
+ const w = new Writable({
55
+ write(_chunk: any, _enc: string, cb: () => void) {
56
+ cb();
57
+ },
58
+ });
59
+ const finishPromise = new Promise<void>((resolve) =>
60
+ w.on("finish", resolve),
61
+ );
62
+ w.end("done");
63
+ await finishPromise;
64
+ });
65
+
66
+ it("end() with data writes then finishes", () => {
67
+ const chunks: string[] = [];
68
+ const w = new Writable({
69
+ write(chunk: any, _enc: string, cb: () => void) {
70
+ chunks.push(String(chunk));
71
+ cb();
72
+ },
73
+ });
74
+ w.end("final");
75
+ expect(chunks).toContain("final");
76
+ });
77
+ });
78
+
79
+ describe("Transform", () => {
80
+ it("transforms data through _transform", () => {
81
+ const t = new Transform({
82
+ transform(chunk: any, _enc: string, cb: (err: null, data: string) => void) {
83
+ cb(null, String(chunk).toUpperCase());
84
+ },
85
+ });
86
+ const chunks: string[] = [];
87
+ t.on("data", (chunk: any) => chunks.push(String(chunk)));
88
+ t.write("hello");
89
+ t.end();
90
+ expect(chunks).toContain("HELLO");
91
+ });
92
+ });
93
+
94
+ describe("PassThrough", () => {
95
+ it("passes data through unchanged", () => {
96
+ const pt = new PassThrough();
97
+ const chunks: string[] = [];
98
+ pt.on("data", (chunk: any) => chunks.push(String(chunk)));
99
+ pt.write("data");
100
+ pt.end();
101
+ expect(chunks).toContain("data");
102
+ });
103
+ });
104
+
105
+ describe("pipe integration", () => {
106
+ it("Readable.pipe(Writable) transfers all data", async () => {
107
+ const r = new Readable({ read() {} });
108
+ const collected: string[] = [];
109
+ const w = new Writable({
110
+ write(chunk: any, _enc: string, cb: () => void) {
111
+ collected.push(String(chunk));
112
+ cb();
113
+ },
114
+ });
115
+
116
+ r.pipe(w);
117
+ r.push("chunk1");
118
+ r.push("chunk2");
119
+ r.push(null);
120
+
121
+ await new Promise<void>((resolve) => w.on("finish", resolve));
122
+ expect(collected.join("")).toBe("chunk1chunk2");
123
+ });
124
+ });
125
+
126
+ describe("async iteration", () => {
127
+ it("Readable supports for-await-of", async () => {
128
+ const r = new Readable({ read() {} });
129
+
130
+ queueMicrotask(() => {
131
+ r.push("a");
132
+ r.push("b");
133
+ r.push(null);
134
+ });
135
+
136
+ const chunks: string[] = [];
137
+ for await (const chunk of r) {
138
+ chunks.push(String(chunk));
139
+ }
140
+ expect(chunks).toEqual(["a", "b"]);
141
+ });
142
+ });
@@ -0,0 +1,158 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ esmToCjs,
4
+ hasTopLevelAwait,
5
+ stripTopLevelAwait,
6
+ } from "../syntax-transforms";
7
+
8
+ describe("esmToCjs", () => {
9
+ describe("import declarations", () => {
10
+ it("converts default import", () => {
11
+ const result = esmToCjs('import foo from "bar";');
12
+ expect(result).toContain('require("bar")');
13
+ expect(result).not.toContain("import");
14
+ });
15
+
16
+ it("converts named imports", () => {
17
+ const result = esmToCjs('import { a, b } from "mod";');
18
+ expect(result).toContain('require("mod")');
19
+ expect(result).toContain("a");
20
+ expect(result).toContain("b");
21
+ });
22
+
23
+ it("converts namespace import", () => {
24
+ const result = esmToCjs('import * as ns from "mod";');
25
+ expect(result).toContain('require("mod")');
26
+ expect(result).toContain("ns");
27
+ });
28
+
29
+ it("converts side-effect-only import", () => {
30
+ const result = esmToCjs('import "polyfill";');
31
+ expect(result).toContain('require("polyfill")');
32
+ });
33
+
34
+ it("converts aliased named imports", () => {
35
+ const result = esmToCjs('import { foo as bar } from "mod";');
36
+ expect(result).toContain('require("mod")');
37
+ expect(result).toContain("bar");
38
+ });
39
+
40
+ it("handles multiple imports", () => {
41
+ const code = `
42
+ import a from "a";
43
+ import { b } from "b";
44
+ import * as c from "c";
45
+ `;
46
+ const result = esmToCjs(code);
47
+ expect(result).toContain('require("a")');
48
+ expect(result).toContain('require("b")');
49
+ expect(result).toContain('require("c")');
50
+ expect(result).not.toContain("import ");
51
+ });
52
+
53
+ it("preserves the rest of the code unchanged", () => {
54
+ const code = `import x from "x";\nconst y = 42;\nconsole.log(y);`;
55
+ const result = esmToCjs(code);
56
+ expect(result).toContain("const y = 42");
57
+ expect(result).toContain("console.log(y)");
58
+ });
59
+ });
60
+
61
+ describe("export declarations", () => {
62
+ it("converts export default expression", () => {
63
+ const result = esmToCjs("export default 42;");
64
+ expect(result).toContain("module.exports");
65
+ expect(result).toContain("42");
66
+ });
67
+
68
+ it("converts export default function", () => {
69
+ const result = esmToCjs("export default function foo() { return 1; }");
70
+ expect(result).toContain("function foo()");
71
+ });
72
+
73
+ it("converts named export of const", () => {
74
+ const result = esmToCjs("export const x = 1;");
75
+ expect(result).toContain("const x = 1");
76
+ expect(result).toContain("exports.x");
77
+ });
78
+
79
+ it("converts named export of function", () => {
80
+ const result = esmToCjs("export function greet() { return 'hi'; }");
81
+ expect(result).toContain("function greet()");
82
+ expect(result).toContain("exports.greet");
83
+ });
84
+
85
+ it("converts export { a, b }", () => {
86
+ const code = "const a = 1; const b = 2; export { a, b };";
87
+ const result = esmToCjs(code);
88
+ expect(result).toContain("exports.a");
89
+ expect(result).toContain("exports.b");
90
+ });
91
+
92
+ it("converts export * from", () => {
93
+ const result = esmToCjs('export * from "mod";');
94
+ expect(result).toContain('require("mod")');
95
+ expect(result).toContain("Object.assign");
96
+ });
97
+ });
98
+
99
+ describe("passthrough", () => {
100
+ it("returns plain CJS unchanged", () => {
101
+ const code = 'const x = require("foo"); module.exports = x;';
102
+ const result = esmToCjs(code);
103
+ expect(result).toContain('require("foo")');
104
+ expect(result).toContain("module.exports = x");
105
+ });
106
+ });
107
+
108
+ describe("mixed exports", () => {
109
+ it("handles both default and named exports", () => {
110
+ const code = `
111
+ export const x = 1;
112
+ export default 42;
113
+ `;
114
+ const result = esmToCjs(code);
115
+ expect(result).toContain("exports");
116
+ expect(result).toContain("x");
117
+ });
118
+ });
119
+ });
120
+
121
+ describe("hasTopLevelAwait", () => {
122
+ it("returns true for top-level await", () => {
123
+ expect(hasTopLevelAwait("const x = await fetch('/api');")).toBe(true);
124
+ });
125
+
126
+ it("returns false when await is inside async function", () => {
127
+ expect(
128
+ hasTopLevelAwait("async function f() { await x; }"),
129
+ ).toBe(false);
130
+ });
131
+
132
+ it("returns false when no await keyword", () => {
133
+ expect(hasTopLevelAwait("const x = 1;")).toBe(false);
134
+ });
135
+
136
+ it("returns true for for-await-of at top level", () => {
137
+ expect(
138
+ hasTopLevelAwait("for await (const x of iter) { console.log(x); }"),
139
+ ).toBe(true);
140
+ });
141
+
142
+ it("returns false for 'await' inside a string", () => {
143
+ const code = "const s = 'await is cool';";
144
+ expect(hasTopLevelAwait(code)).toBe(false);
145
+ });
146
+ });
147
+
148
+ describe("stripTopLevelAwait", () => {
149
+ it("replaces top-level await expressions", () => {
150
+ const result = stripTopLevelAwait("const x = await foo();");
151
+ expect(result).not.toContain("await foo()");
152
+ });
153
+
154
+ it("returns input unchanged when no await present", () => {
155
+ const code = "const x = 1 + 2;";
156
+ expect(stripTopLevelAwait(code)).toBe(code);
157
+ });
158
+ });
@@ -0,0 +1,184 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ parseSemver,
4
+ compareSemver,
5
+ satisfiesRange,
6
+ pickBestMatch,
7
+ } from "../packages/version-resolver";
8
+
9
+ describe("parseSemver", () => {
10
+ it("parses a simple version", () => {
11
+ expect(parseSemver("1.2.3")).toEqual({
12
+ major: 1,
13
+ minor: 2,
14
+ patch: 3,
15
+ prerelease: undefined,
16
+ });
17
+ });
18
+
19
+ it("parses version with prerelease", () => {
20
+ const result = parseSemver("1.0.0-beta.1");
21
+ expect(result).toEqual({
22
+ major: 1,
23
+ minor: 0,
24
+ patch: 0,
25
+ prerelease: "beta.1",
26
+ });
27
+ });
28
+
29
+ it("returns null for invalid input", () => {
30
+ expect(parseSemver("not.a.version")).toBeNull();
31
+ });
32
+
33
+ it("returns null for partial version", () => {
34
+ expect(parseSemver("1.2")).toBeNull();
35
+ });
36
+
37
+ it("returns null for empty string", () => {
38
+ expect(parseSemver("")).toBeNull();
39
+ });
40
+
41
+ it('parses "0.0.0"', () => {
42
+ expect(parseSemver("0.0.0")).toEqual({
43
+ major: 0,
44
+ minor: 0,
45
+ patch: 0,
46
+ prerelease: undefined,
47
+ });
48
+ });
49
+ });
50
+
51
+ describe("compareSemver", () => {
52
+ it("returns 0 for equal versions", () => {
53
+ expect(compareSemver("1.2.3", "1.2.3")).toBe(0);
54
+ });
55
+
56
+ it("returns negative when left < right by major", () => {
57
+ expect(compareSemver("1.0.0", "2.0.0")).toBeLessThan(0);
58
+ });
59
+
60
+ it("returns positive when left > right by minor", () => {
61
+ expect(compareSemver("1.2.0", "1.1.0")).toBeGreaterThan(0);
62
+ });
63
+
64
+ it("compares patch versions", () => {
65
+ expect(compareSemver("1.0.1", "1.0.2")).toBeLessThan(0);
66
+ expect(compareSemver("1.0.3", "1.0.2")).toBeGreaterThan(0);
67
+ });
68
+
69
+ it("prerelease is less than release", () => {
70
+ expect(compareSemver("1.0.0-alpha", "1.0.0")).toBeLessThan(0);
71
+ });
72
+
73
+ it("release is greater than prerelease", () => {
74
+ expect(compareSemver("1.0.0", "1.0.0-beta")).toBeGreaterThan(0);
75
+ });
76
+
77
+ it("compares two prerelease versions alphabetically", () => {
78
+ expect(compareSemver("1.0.0-alpha", "1.0.0-beta")).toBeLessThan(0);
79
+ });
80
+
81
+ it("falls back to localeCompare for unparseable versions", () => {
82
+ const result = compareSemver("abc", "def");
83
+ expect(typeof result).toBe("number");
84
+ });
85
+ });
86
+
87
+ describe("satisfiesRange", () => {
88
+ it("exact version match", () => {
89
+ expect(satisfiesRange("1.2.3", "1.2.3")).toBe(true);
90
+ expect(satisfiesRange("1.2.4", "1.2.3")).toBe(false);
91
+ });
92
+
93
+ it("caret range ^1.2.3 allows minor/patch bumps", () => {
94
+ expect(satisfiesRange("1.3.0", "^1.2.3")).toBe(true);
95
+ expect(satisfiesRange("1.9.9", "^1.2.3")).toBe(true);
96
+ expect(satisfiesRange("2.0.0", "^1.2.3")).toBe(false);
97
+ expect(satisfiesRange("1.2.2", "^1.2.3")).toBe(false);
98
+ });
99
+
100
+ it("caret ^0.x.y is more restrictive", () => {
101
+ expect(satisfiesRange("0.2.1", "^0.2.0")).toBe(true);
102
+ expect(satisfiesRange("0.3.0", "^0.2.0")).toBe(false);
103
+ });
104
+
105
+ it("tilde range ~1.2.3 allows patch bumps only", () => {
106
+ expect(satisfiesRange("1.2.9", "~1.2.3")).toBe(true);
107
+ expect(satisfiesRange("1.3.0", "~1.2.3")).toBe(false);
108
+ });
109
+
110
+ it("comparison operators: >=, >, <, <=", () => {
111
+ expect(satisfiesRange("2.0.0", ">=1.0.0")).toBe(true);
112
+ expect(satisfiesRange("1.0.0", ">=1.0.0")).toBe(true);
113
+ expect(satisfiesRange("1.0.0", ">1.0.0")).toBe(false);
114
+ expect(satisfiesRange("0.9.0", "<1.0.0")).toBe(true);
115
+ expect(satisfiesRange("1.0.0", "<1.0.0")).toBe(false);
116
+ expect(satisfiesRange("1.0.0", "<=1.0.0")).toBe(true);
117
+ });
118
+
119
+ it('compound ranges: ">=1.2.0 <3.0.0"', () => {
120
+ expect(satisfiesRange("2.5.0", ">=1.2.0 <3.0.0")).toBe(true);
121
+ expect(satisfiesRange("3.0.0", ">=1.2.0 <3.0.0")).toBe(false);
122
+ expect(satisfiesRange("1.1.0", ">=1.2.0 <3.0.0")).toBe(false);
123
+ });
124
+
125
+ it("OR unions", () => {
126
+ expect(satisfiesRange("0.3.0", ">=1.0.0 || <0.5.0")).toBe(true);
127
+ expect(satisfiesRange("2.0.0", ">=1.0.0 || <0.5.0")).toBe(true);
128
+ expect(satisfiesRange("0.7.0", ">=1.0.0 || <0.5.0")).toBe(false);
129
+ });
130
+
131
+ it("hyphen ranges", () => {
132
+ expect(satisfiesRange("1.5.0", "1.0.0 - 2.0.0")).toBe(true);
133
+ expect(satisfiesRange("2.1.0", "1.0.0 - 2.0.0")).toBe(false);
134
+ expect(satisfiesRange("0.9.0", "1.0.0 - 2.0.0")).toBe(false);
135
+ });
136
+
137
+ it("x-ranges", () => {
138
+ expect(satisfiesRange("1.9.9", "1.x")).toBe(true);
139
+ expect(satisfiesRange("2.0.0", "1.x")).toBe(false);
140
+ });
141
+
142
+ it("partial version ranges", () => {
143
+ expect(satisfiesRange("1.5.0", "1")).toBe(true);
144
+ expect(satisfiesRange("2.0.0", "1")).toBe(false);
145
+ });
146
+
147
+ it('wildcard "*" matches everything', () => {
148
+ expect(satisfiesRange("99.99.99", "*")).toBe(true);
149
+ });
150
+
151
+ it('"latest" matches everything', () => {
152
+ expect(satisfiesRange("1.0.0", "latest")).toBe(true);
153
+ });
154
+
155
+ it("prerelease versions only match ranges including prerelease", () => {
156
+ expect(satisfiesRange("1.0.0-beta.1", "^1.0.0")).toBe(false);
157
+ expect(satisfiesRange("1.0.0-beta.1", "^1.0.0-beta.0")).toBe(true);
158
+ });
159
+
160
+ it('partial versions are padded: "< 3" treated as "< 3.0.0"', () => {
161
+ expect(satisfiesRange("2.9.9", "<3")).toBe(true);
162
+ expect(satisfiesRange("3.0.0", "<3")).toBe(false);
163
+ });
164
+ });
165
+
166
+ describe("pickBestMatch", () => {
167
+ it("picks highest matching version", () => {
168
+ const versions = ["1.0.0", "1.1.0", "1.2.0", "2.0.0"];
169
+ expect(pickBestMatch(versions, "^1.0.0")).toBe("1.2.0");
170
+ });
171
+
172
+ it("returns null when no match", () => {
173
+ expect(pickBestMatch(["1.0.0"], "^2.0.0")).toBeNull();
174
+ });
175
+
176
+ it("handles empty array", () => {
177
+ expect(pickBestMatch([], "^1.0.0")).toBeNull();
178
+ });
179
+
180
+ it("picks exact version when range is exact", () => {
181
+ const versions = ["1.0.0", "1.1.0", "1.2.0"];
182
+ expect(pickBestMatch(versions, "1.1.0")).toBe("1.1.0");
183
+ });
184
+ });