@neutron-build/cli 0.0.1

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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +27 -0
  3. package/dist/commands/build.d.ts +2 -0
  4. package/dist/commands/build.d.ts.map +1 -0
  5. package/dist/commands/build.js +1251 -0
  6. package/dist/commands/build.js.map +1 -0
  7. package/dist/commands/deploy-check.d.ts +2 -0
  8. package/dist/commands/deploy-check.d.ts.map +1 -0
  9. package/dist/commands/deploy-check.js +157 -0
  10. package/dist/commands/deploy-check.js.map +1 -0
  11. package/dist/commands/dev.d.ts +2 -0
  12. package/dist/commands/dev.d.ts.map +1 -0
  13. package/dist/commands/dev.js +111 -0
  14. package/dist/commands/dev.js.map +1 -0
  15. package/dist/commands/preview.d.ts +2 -0
  16. package/dist/commands/preview.d.ts.map +1 -0
  17. package/dist/commands/preview.js +223 -0
  18. package/dist/commands/preview.js.map +1 -0
  19. package/dist/commands/release-check.d.ts +2 -0
  20. package/dist/commands/release-check.d.ts.map +1 -0
  21. package/dist/commands/release-check.js +9 -0
  22. package/dist/commands/release-check.js.map +1 -0
  23. package/dist/commands/start.d.ts +4 -0
  24. package/dist/commands/start.d.ts.map +1 -0
  25. package/dist/commands/start.js +72 -0
  26. package/dist/commands/start.js.map +1 -0
  27. package/dist/commands/worker.d.ts +2 -0
  28. package/dist/commands/worker.d.ts.map +1 -0
  29. package/dist/commands/worker.js +178 -0
  30. package/dist/commands/worker.js.map +1 -0
  31. package/dist/index.d.ts +3 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +65 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/index.test.d.ts +2 -0
  36. package/dist/index.test.d.ts.map +1 -0
  37. package/dist/index.test.js +685 -0
  38. package/dist/index.test.js.map +1 -0
  39. package/package.json +57 -0
@@ -0,0 +1,685 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import * as path from "node:path";
4
+ import * as fs from "node:fs";
5
+ import * as os from "node:os";
6
+ function parseBuildArgs(argv) {
7
+ let preset = null;
8
+ let cloudflareMode = "pages";
9
+ for (let i = 0; i < argv.length; i++) {
10
+ const arg = argv[i];
11
+ if (arg === "--preset" && argv[i + 1]) {
12
+ const value = argv[++i];
13
+ if (value === "vercel" || value === "cloudflare" || value === "docker" || value === "static") {
14
+ preset = value;
15
+ }
16
+ continue;
17
+ }
18
+ if (arg.startsWith("--preset=")) {
19
+ const value = arg.split("=")[1];
20
+ if (value === "vercel" || value === "cloudflare" || value === "docker" || value === "static") {
21
+ preset = value;
22
+ }
23
+ continue;
24
+ }
25
+ if (arg === "--cloudflare-mode" && argv[i + 1]) {
26
+ const value = argv[++i];
27
+ if (value === "pages" || value === "workers") {
28
+ cloudflareMode = value;
29
+ }
30
+ continue;
31
+ }
32
+ if (arg.startsWith("--cloudflare-mode=")) {
33
+ const value = arg.split("=")[1];
34
+ if (value === "pages" || value === "workers") {
35
+ cloudflareMode = value;
36
+ }
37
+ }
38
+ }
39
+ return { preset, cloudflareMode };
40
+ }
41
+ // -- build.ts: resolvePath ---------------------------------------------------
42
+ function resolvePath(pattern, params) {
43
+ let resolved = pattern;
44
+ for (const [key, value] of Object.entries(params)) {
45
+ resolved = resolved.replace(`[${key}]`, value);
46
+ resolved = resolved.replace(`:${key}`, value);
47
+ }
48
+ return resolved;
49
+ }
50
+ // -- build.ts: escapeHtml ----------------------------------------------------
51
+ function escapeHtml(str) {
52
+ return str
53
+ .replace(/&/g, "&amp;")
54
+ .replace(/</g, "&lt;")
55
+ .replace(/>/g, "&gt;")
56
+ .replace(/"/g, "&quot;")
57
+ .replace(/'/g, "&#039;");
58
+ }
59
+ // -- build.ts: getOutputPath -------------------------------------------------
60
+ function getOutputPath(outputDir, routePath) {
61
+ if (routePath === "/") {
62
+ return path.join(outputDir, "index.html");
63
+ }
64
+ const cleanPath = routePath.replace(/\/$/, "");
65
+ return path.join(outputDir, cleanPath, "index.html");
66
+ }
67
+ // -- build.ts: normalizeHeaders ----------------------------------------------
68
+ function normalizeHeaders(value) {
69
+ if (!value) {
70
+ return {};
71
+ }
72
+ if (value instanceof Headers) {
73
+ return headersToRecord(value);
74
+ }
75
+ const output = {};
76
+ for (const [name, headerValue] of Object.entries(value)) {
77
+ const lower = name.toLowerCase();
78
+ if (lower === "content-length" || lower === "set-cookie") {
79
+ continue;
80
+ }
81
+ output[name] = String(headerValue);
82
+ }
83
+ return output;
84
+ }
85
+ function headersToRecord(headers) {
86
+ const output = {};
87
+ headers.forEach((value, name) => {
88
+ const lower = name.toLowerCase();
89
+ if (lower === "content-length" || lower === "set-cookie") {
90
+ return;
91
+ }
92
+ output[name] = value;
93
+ });
94
+ return output;
95
+ }
96
+ // -- build.ts: relativeImportPath --------------------------------------------
97
+ function relativeImportPath(fromDir, filePath) {
98
+ const rel = path.relative(fromDir, filePath).split(path.sep).join("/");
99
+ return rel.startsWith(".") ? rel : `./${rel}`;
100
+ }
101
+ // -- build.ts: escapeJsString ------------------------------------------------
102
+ function escapeJsString(value) {
103
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
104
+ }
105
+ function parseDeployCheckArgs(argv) {
106
+ let preset = null;
107
+ let distDir = "dist";
108
+ for (let i = 0; i < argv.length; i++) {
109
+ const arg = argv[i];
110
+ if (arg === "--preset" && argv[i + 1]) {
111
+ const value = argv[++i];
112
+ if (value === "vercel" || value === "cloudflare" || value === "docker" || value === "static") {
113
+ preset = value;
114
+ }
115
+ continue;
116
+ }
117
+ if (arg.startsWith("--preset=")) {
118
+ const value = arg.split("=")[1];
119
+ if (value === "vercel" || value === "cloudflare" || value === "docker" || value === "static") {
120
+ preset = value;
121
+ }
122
+ continue;
123
+ }
124
+ if (arg === "--dist" && argv[i + 1]) {
125
+ distDir = argv[++i];
126
+ continue;
127
+ }
128
+ if (arg.startsWith("--dist=")) {
129
+ distDir = arg.split("=")[1];
130
+ }
131
+ }
132
+ return { preset, distDir };
133
+ }
134
+ // -- deploy-check.ts: detectPresetsFromDist ----------------------------------
135
+ function detectPresetsFromDist(distDir) {
136
+ const output = [];
137
+ if (fs.existsSync(path.join(distDir, ".neutron-adapter-vercel.json"))) {
138
+ output.push("vercel");
139
+ }
140
+ if (fs.existsSync(path.join(distDir, ".neutron-adapter-cloudflare.json"))) {
141
+ output.push("cloudflare");
142
+ }
143
+ if (fs.existsSync(path.join(distDir, ".neutron-adapter-docker.json"))) {
144
+ output.push("docker");
145
+ }
146
+ if (fs.existsSync(path.join(distDir, ".neutron-adapter-static.json"))) {
147
+ output.push("static");
148
+ }
149
+ return output;
150
+ }
151
+ function parseWorkerArgs(argv) {
152
+ let entry;
153
+ let mode = "development";
154
+ let once = false;
155
+ const passthroughIndex = argv.indexOf("--");
156
+ const workerArgs = passthroughIndex >= 0 ? argv.slice(passthroughIndex + 1) : [];
157
+ const parsedArgs = passthroughIndex >= 0 ? argv.slice(0, passthroughIndex) : argv;
158
+ for (let i = 0; i < parsedArgs.length; i++) {
159
+ const arg = parsedArgs[i];
160
+ if (arg === "--entry" && parsedArgs[i + 1]) {
161
+ entry = parsedArgs[++i];
162
+ continue;
163
+ }
164
+ if (arg.startsWith("--entry=")) {
165
+ entry = arg.split("=")[1];
166
+ continue;
167
+ }
168
+ if (arg === "--mode" && parsedArgs[i + 1]) {
169
+ mode = parsedArgs[++i];
170
+ continue;
171
+ }
172
+ if (arg.startsWith("--mode=")) {
173
+ mode = arg.split("=")[1];
174
+ continue;
175
+ }
176
+ if (arg === "--once") {
177
+ once = true;
178
+ }
179
+ }
180
+ return { entry, mode, once, workerArgs };
181
+ }
182
+ // -- worker.ts: resolveWorkerEntry -------------------------------------------
183
+ const WORKER_ENTRY_CANDIDATES = [
184
+ "src/worker.ts",
185
+ "src/worker.tsx",
186
+ "src/worker/index.ts",
187
+ "src/worker/index.tsx",
188
+ "worker.ts",
189
+ "worker.js",
190
+ ];
191
+ function resolveWorkerEntry(cwd, cliEntry, configEntry) {
192
+ const candidates = [cliEntry, configEntry, ...WORKER_ENTRY_CANDIDATES].filter((candidate) => Boolean(candidate));
193
+ for (const candidate of candidates) {
194
+ const absolutePath = path.resolve(cwd, candidate);
195
+ if (fs.existsSync(absolutePath)) {
196
+ return absolutePath;
197
+ }
198
+ }
199
+ return null;
200
+ }
201
+ // -- preview.ts: normalizePathname -------------------------------------------
202
+ function normalizePathname(pathname) {
203
+ if (!pathname) {
204
+ return "/";
205
+ }
206
+ let decoded;
207
+ try {
208
+ decoded = decodeURIComponent(pathname);
209
+ }
210
+ catch {
211
+ return "";
212
+ }
213
+ if (!decoded.startsWith("/") || decoded.includes("..")) {
214
+ return "";
215
+ }
216
+ if (decoded.length > 1 && decoded.endsWith("/")) {
217
+ return decoded.slice(0, -1);
218
+ }
219
+ return decoded;
220
+ }
221
+ // -- preview.ts: isWithinDirectory -------------------------------------------
222
+ function isWithinDirectory(baseDir, candidatePath) {
223
+ const relative = path.relative(baseDir, candidatePath);
224
+ return (relative === "" ||
225
+ (!relative.startsWith("..") && !path.isAbsolute(relative)));
226
+ }
227
+ // -- preview.ts: resolveDistFilePath -----------------------------------------
228
+ function resolveDistFilePath(distDir, pathname) {
229
+ const resolved = path.resolve(distDir, `.${pathname}`);
230
+ return isWithinDirectory(distDir, resolved) ? resolved : null;
231
+ }
232
+ // -- index.ts: CLI dispatch --------------------------------------------------
233
+ const VALID_COMMANDS = ["dev", "build", "preview", "start", "deploy-check", "release-check", "worker"];
234
+ // =========================================================================
235
+ // Tests
236
+ // =========================================================================
237
+ // ---------------------------------------------------------------------------
238
+ // parseBuildArgs
239
+ // ---------------------------------------------------------------------------
240
+ describe("parseBuildArgs", () => {
241
+ it("returns default values when no args are provided", () => {
242
+ const result = parseBuildArgs([]);
243
+ assert.equal(result.preset, null);
244
+ assert.equal(result.cloudflareMode, "pages");
245
+ });
246
+ it("parses --preset vercel", () => {
247
+ const result = parseBuildArgs(["--preset", "vercel"]);
248
+ assert.equal(result.preset, "vercel");
249
+ });
250
+ it("parses --preset=cloudflare", () => {
251
+ const result = parseBuildArgs(["--preset=cloudflare"]);
252
+ assert.equal(result.preset, "cloudflare");
253
+ });
254
+ it("parses --preset docker", () => {
255
+ const result = parseBuildArgs(["--preset", "docker"]);
256
+ assert.equal(result.preset, "docker");
257
+ });
258
+ it("parses --preset static", () => {
259
+ const result = parseBuildArgs(["--preset", "static"]);
260
+ assert.equal(result.preset, "static");
261
+ });
262
+ it("ignores invalid preset values", () => {
263
+ const result = parseBuildArgs(["--preset", "invalid"]);
264
+ assert.equal(result.preset, null);
265
+ });
266
+ it("parses --cloudflare-mode workers", () => {
267
+ const result = parseBuildArgs(["--preset", "cloudflare", "--cloudflare-mode", "workers"]);
268
+ assert.equal(result.preset, "cloudflare");
269
+ assert.equal(result.cloudflareMode, "workers");
270
+ });
271
+ it("parses --cloudflare-mode=pages", () => {
272
+ const result = parseBuildArgs(["--cloudflare-mode=pages"]);
273
+ assert.equal(result.cloudflareMode, "pages");
274
+ });
275
+ it("ignores invalid cloudflare-mode values", () => {
276
+ const result = parseBuildArgs(["--cloudflare-mode", "invalid"]);
277
+ assert.equal(result.cloudflareMode, "pages");
278
+ });
279
+ it("handles multiple args together", () => {
280
+ const result = parseBuildArgs(["--preset=vercel", "--cloudflare-mode=workers"]);
281
+ assert.equal(result.preset, "vercel");
282
+ assert.equal(result.cloudflareMode, "workers");
283
+ });
284
+ });
285
+ // ---------------------------------------------------------------------------
286
+ // resolvePath
287
+ // ---------------------------------------------------------------------------
288
+ describe("resolvePath", () => {
289
+ it("replaces bracket params", () => {
290
+ assert.equal(resolvePath("/blog/[slug]", { slug: "hello" }), "/blog/hello");
291
+ });
292
+ it("replaces colon params", () => {
293
+ assert.equal(resolvePath("/users/:id", { id: "42" }), "/users/42");
294
+ });
295
+ it("replaces multiple params", () => {
296
+ assert.equal(resolvePath("/[category]/[slug]", { category: "tech", slug: "post" }), "/tech/post");
297
+ });
298
+ it("leaves pattern unchanged when params are empty", () => {
299
+ assert.equal(resolvePath("/about", {}), "/about");
300
+ });
301
+ });
302
+ // ---------------------------------------------------------------------------
303
+ // escapeHtml
304
+ // ---------------------------------------------------------------------------
305
+ describe("escapeHtml", () => {
306
+ it("escapes ampersands", () => {
307
+ assert.equal(escapeHtml("a&b"), "a&amp;b");
308
+ });
309
+ it("escapes angle brackets", () => {
310
+ assert.equal(escapeHtml("<script>"), "&lt;script&gt;");
311
+ });
312
+ it("escapes double quotes", () => {
313
+ assert.equal(escapeHtml('"hello"'), "&quot;hello&quot;");
314
+ });
315
+ it("escapes single quotes", () => {
316
+ assert.equal(escapeHtml("it's"), "it&#039;s");
317
+ });
318
+ it("leaves safe strings unchanged", () => {
319
+ assert.equal(escapeHtml("hello world"), "hello world");
320
+ });
321
+ });
322
+ // ---------------------------------------------------------------------------
323
+ // getOutputPath
324
+ // ---------------------------------------------------------------------------
325
+ describe("getOutputPath", () => {
326
+ const outDir = "/dist";
327
+ it("returns index.html for root route", () => {
328
+ assert.equal(getOutputPath(outDir, "/"), path.join(outDir, "index.html"));
329
+ });
330
+ it("returns nested index.html for sub-route", () => {
331
+ assert.equal(getOutputPath(outDir, "/about"), path.join(outDir, "about", "index.html"));
332
+ });
333
+ it("strips trailing slash before computing output path", () => {
334
+ assert.equal(getOutputPath(outDir, "/blog/"), path.join(outDir, "blog", "index.html"));
335
+ });
336
+ it("handles deep paths", () => {
337
+ assert.equal(getOutputPath(outDir, "/a/b/c"), path.join(outDir, "a", "b", "c", "index.html"));
338
+ });
339
+ });
340
+ // ---------------------------------------------------------------------------
341
+ // normalizeHeaders
342
+ // ---------------------------------------------------------------------------
343
+ describe("normalizeHeaders", () => {
344
+ it("returns empty object for null", () => {
345
+ assert.deepEqual(normalizeHeaders(null), {});
346
+ });
347
+ it("returns empty object for undefined", () => {
348
+ assert.deepEqual(normalizeHeaders(undefined), {});
349
+ });
350
+ it("converts a plain object", () => {
351
+ const headers = { "X-Custom": "value" };
352
+ const result = normalizeHeaders(headers);
353
+ assert.equal(result["X-Custom"], "value");
354
+ });
355
+ it("filters out content-length", () => {
356
+ const result = normalizeHeaders({ "Content-Length": "100", "X-Custom": "v" });
357
+ assert.equal(result["Content-Length"], undefined);
358
+ assert.equal(result["X-Custom"], "v");
359
+ });
360
+ it("filters out set-cookie", () => {
361
+ const result = normalizeHeaders({ "Set-Cookie": "a=b", "X-Test": "1" });
362
+ assert.equal(result["Set-Cookie"], undefined);
363
+ assert.equal(result["X-Test"], "1");
364
+ });
365
+ it("converts Headers instance", () => {
366
+ const headers = new Headers();
367
+ headers.set("x-foo", "bar");
368
+ const result = normalizeHeaders(headers);
369
+ assert.equal(result["x-foo"], "bar");
370
+ });
371
+ it("filters content-length from Headers instance", () => {
372
+ const headers = new Headers();
373
+ headers.set("content-length", "42");
374
+ headers.set("x-id", "abc");
375
+ const result = normalizeHeaders(headers);
376
+ assert.equal(result["content-length"], undefined);
377
+ assert.equal(result["x-id"], "abc");
378
+ });
379
+ });
380
+ // ---------------------------------------------------------------------------
381
+ // relativeImportPath
382
+ // ---------------------------------------------------------------------------
383
+ describe("relativeImportPath", () => {
384
+ it("prepends ./ for sibling files", () => {
385
+ const result = relativeImportPath("/project/src", "/project/src/route.ts");
386
+ assert.equal(result, "./route.ts");
387
+ });
388
+ it("preserves relative prefix for parent directories", () => {
389
+ const result = relativeImportPath("/project/src/dir", "/project/src/route.ts");
390
+ assert.equal(result, "../route.ts");
391
+ });
392
+ it("prepends ./ for subdirectory files", () => {
393
+ const result = relativeImportPath("/project", "/project/src/index.ts");
394
+ assert.equal(result, "./src/index.ts");
395
+ });
396
+ });
397
+ // ---------------------------------------------------------------------------
398
+ // escapeJsString
399
+ // ---------------------------------------------------------------------------
400
+ describe("escapeJsString", () => {
401
+ it("escapes backslashes", () => {
402
+ assert.equal(escapeJsString("a\\b"), "a\\\\b");
403
+ });
404
+ it("escapes double quotes", () => {
405
+ assert.equal(escapeJsString('say "hi"'), 'say \\"hi\\"');
406
+ });
407
+ it("leaves safe strings unchanged", () => {
408
+ assert.equal(escapeJsString("hello"), "hello");
409
+ });
410
+ });
411
+ // ---------------------------------------------------------------------------
412
+ // parseDeployCheckArgs
413
+ // ---------------------------------------------------------------------------
414
+ describe("parseDeployCheckArgs", () => {
415
+ it("returns defaults when no args provided", () => {
416
+ const result = parseDeployCheckArgs([]);
417
+ assert.equal(result.preset, null);
418
+ assert.equal(result.distDir, "dist");
419
+ });
420
+ it("parses --preset vercel", () => {
421
+ const result = parseDeployCheckArgs(["--preset", "vercel"]);
422
+ assert.equal(result.preset, "vercel");
423
+ });
424
+ it("parses --preset=cloudflare", () => {
425
+ const result = parseDeployCheckArgs(["--preset=cloudflare"]);
426
+ assert.equal(result.preset, "cloudflare");
427
+ });
428
+ it("parses --dist with space", () => {
429
+ const result = parseDeployCheckArgs(["--dist", "build"]);
430
+ assert.equal(result.distDir, "build");
431
+ });
432
+ it("parses --dist= format", () => {
433
+ const result = parseDeployCheckArgs(["--dist=output"]);
434
+ assert.equal(result.distDir, "output");
435
+ });
436
+ it("handles combined args", () => {
437
+ const result = parseDeployCheckArgs(["--preset", "docker", "--dist", "out"]);
438
+ assert.equal(result.preset, "docker");
439
+ assert.equal(result.distDir, "out");
440
+ });
441
+ it("ignores invalid preset", () => {
442
+ const result = parseDeployCheckArgs(["--preset", "netlify"]);
443
+ assert.equal(result.preset, null);
444
+ });
445
+ });
446
+ // ---------------------------------------------------------------------------
447
+ // detectPresetsFromDist
448
+ // ---------------------------------------------------------------------------
449
+ describe("detectPresetsFromDist", () => {
450
+ it("returns empty array for directory with no adapter files", () => {
451
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "deploy-check-"));
452
+ try {
453
+ const result = detectPresetsFromDist(tmpDir);
454
+ assert.deepEqual(result, []);
455
+ }
456
+ finally {
457
+ fs.rmSync(tmpDir, { recursive: true, force: true });
458
+ }
459
+ });
460
+ it("detects vercel adapter", () => {
461
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "deploy-check-"));
462
+ try {
463
+ fs.writeFileSync(path.join(tmpDir, ".neutron-adapter-vercel.json"), "{}");
464
+ const result = detectPresetsFromDist(tmpDir);
465
+ assert.deepEqual(result, ["vercel"]);
466
+ }
467
+ finally {
468
+ fs.rmSync(tmpDir, { recursive: true, force: true });
469
+ }
470
+ });
471
+ it("detects multiple adapters", () => {
472
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "deploy-check-"));
473
+ try {
474
+ fs.writeFileSync(path.join(tmpDir, ".neutron-adapter-vercel.json"), "{}");
475
+ fs.writeFileSync(path.join(tmpDir, ".neutron-adapter-cloudflare.json"), "{}");
476
+ fs.writeFileSync(path.join(tmpDir, ".neutron-adapter-docker.json"), "{}");
477
+ fs.writeFileSync(path.join(tmpDir, ".neutron-adapter-static.json"), "{}");
478
+ const result = detectPresetsFromDist(tmpDir);
479
+ assert.deepEqual(result, ["vercel", "cloudflare", "docker", "static"]);
480
+ }
481
+ finally {
482
+ fs.rmSync(tmpDir, { recursive: true, force: true });
483
+ }
484
+ });
485
+ });
486
+ // ---------------------------------------------------------------------------
487
+ // parseWorkerArgs
488
+ // ---------------------------------------------------------------------------
489
+ describe("parseWorkerArgs", () => {
490
+ it("returns defaults when no args provided", () => {
491
+ const result = parseWorkerArgs([]);
492
+ assert.equal(result.entry, undefined);
493
+ assert.equal(result.mode, "development");
494
+ assert.equal(result.once, false);
495
+ assert.deepEqual(result.workerArgs, []);
496
+ });
497
+ it("parses --entry flag", () => {
498
+ const result = parseWorkerArgs(["--entry", "src/worker.ts"]);
499
+ assert.equal(result.entry, "src/worker.ts");
500
+ });
501
+ it("parses --entry= format", () => {
502
+ const result = parseWorkerArgs(["--entry=worker.js"]);
503
+ assert.equal(result.entry, "worker.js");
504
+ });
505
+ it("parses --mode flag", () => {
506
+ const result = parseWorkerArgs(["--mode", "production"]);
507
+ assert.equal(result.mode, "production");
508
+ });
509
+ it("parses --mode= format", () => {
510
+ const result = parseWorkerArgs(["--mode=production"]);
511
+ assert.equal(result.mode, "production");
512
+ });
513
+ it("parses --once flag", () => {
514
+ const result = parseWorkerArgs(["--once"]);
515
+ assert.equal(result.once, true);
516
+ });
517
+ it("passes through args after --", () => {
518
+ const result = parseWorkerArgs(["--entry", "w.ts", "--", "arg1", "arg2"]);
519
+ assert.equal(result.entry, "w.ts");
520
+ assert.deepEqual(result.workerArgs, ["arg1", "arg2"]);
521
+ });
522
+ it("handles all flags combined", () => {
523
+ const result = parseWorkerArgs(["--entry=w.ts", "--mode=production", "--once", "--", "extra"]);
524
+ assert.equal(result.entry, "w.ts");
525
+ assert.equal(result.mode, "production");
526
+ assert.equal(result.once, true);
527
+ assert.deepEqual(result.workerArgs, ["extra"]);
528
+ });
529
+ });
530
+ // ---------------------------------------------------------------------------
531
+ // resolveWorkerEntry
532
+ // ---------------------------------------------------------------------------
533
+ describe("resolveWorkerEntry", () => {
534
+ it("returns null when no candidates exist", () => {
535
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "worker-entry-"));
536
+ try {
537
+ const result = resolveWorkerEntry(tmpDir);
538
+ assert.equal(result, null);
539
+ }
540
+ finally {
541
+ fs.rmSync(tmpDir, { recursive: true, force: true });
542
+ }
543
+ });
544
+ it("prioritises CLI entry over default candidates", () => {
545
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "worker-entry-"));
546
+ try {
547
+ const workerPath = path.join(tmpDir, "custom-worker.ts");
548
+ fs.writeFileSync(workerPath, "export function run() {}");
549
+ fs.mkdirSync(path.join(tmpDir, "src"), { recursive: true });
550
+ fs.writeFileSync(path.join(tmpDir, "src", "worker.ts"), "");
551
+ const result = resolveWorkerEntry(tmpDir, "custom-worker.ts");
552
+ assert.equal(result, workerPath);
553
+ }
554
+ finally {
555
+ fs.rmSync(tmpDir, { recursive: true, force: true });
556
+ }
557
+ });
558
+ it("falls back to default candidate src/worker.ts", () => {
559
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "worker-entry-"));
560
+ try {
561
+ fs.mkdirSync(path.join(tmpDir, "src"), { recursive: true });
562
+ const workerPath = path.join(tmpDir, "src", "worker.ts");
563
+ fs.writeFileSync(workerPath, "");
564
+ const result = resolveWorkerEntry(tmpDir);
565
+ assert.equal(result, workerPath);
566
+ }
567
+ finally {
568
+ fs.rmSync(tmpDir, { recursive: true, force: true });
569
+ }
570
+ });
571
+ it("uses config entry when CLI entry is undefined", () => {
572
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "worker-entry-"));
573
+ try {
574
+ const workerPath = path.join(tmpDir, "jobs.ts");
575
+ fs.writeFileSync(workerPath, "");
576
+ const result = resolveWorkerEntry(tmpDir, undefined, "jobs.ts");
577
+ assert.equal(result, workerPath);
578
+ }
579
+ finally {
580
+ fs.rmSync(tmpDir, { recursive: true, force: true });
581
+ }
582
+ });
583
+ });
584
+ // ---------------------------------------------------------------------------
585
+ // normalizePathname
586
+ // ---------------------------------------------------------------------------
587
+ describe("normalizePathname", () => {
588
+ it("returns / for empty string", () => {
589
+ assert.equal(normalizePathname(""), "/");
590
+ });
591
+ it("returns the path as-is for a normal path", () => {
592
+ assert.equal(normalizePathname("/about"), "/about");
593
+ });
594
+ it("strips trailing slash", () => {
595
+ assert.equal(normalizePathname("/blog/"), "/blog");
596
+ });
597
+ it("keeps root / as-is", () => {
598
+ assert.equal(normalizePathname("/"), "/");
599
+ });
600
+ it("rejects path traversal", () => {
601
+ assert.equal(normalizePathname("/../../etc"), "");
602
+ });
603
+ it("rejects non-absolute paths", () => {
604
+ assert.equal(normalizePathname("relative"), "");
605
+ });
606
+ it("decodes percent-encoded characters", () => {
607
+ assert.equal(normalizePathname("/hello%20world"), "/hello world");
608
+ });
609
+ it("returns empty string for bad encoding", () => {
610
+ assert.equal(normalizePathname("/%E0%A4%A"), "");
611
+ });
612
+ });
613
+ // ---------------------------------------------------------------------------
614
+ // isWithinDirectory
615
+ // ---------------------------------------------------------------------------
616
+ describe("isWithinDirectory", () => {
617
+ it("returns true for a child path", () => {
618
+ assert.equal(isWithinDirectory("/dist", "/dist/index.html"), true);
619
+ });
620
+ it("returns true for the directory itself", () => {
621
+ assert.equal(isWithinDirectory("/dist", "/dist"), true);
622
+ });
623
+ it("returns false for a parent path", () => {
624
+ assert.equal(isWithinDirectory("/dist", "/etc/passwd"), false);
625
+ });
626
+ it("returns false for path traversal", () => {
627
+ assert.equal(isWithinDirectory("/dist", "/dist/../etc/passwd"), false);
628
+ });
629
+ });
630
+ // ---------------------------------------------------------------------------
631
+ // resolveDistFilePath
632
+ // ---------------------------------------------------------------------------
633
+ describe("resolveDistFilePath", () => {
634
+ it("resolves a normal file path", () => {
635
+ const result = resolveDistFilePath("/dist", "/index.html");
636
+ assert.equal(result, path.resolve("/dist", "./index.html"));
637
+ });
638
+ it("returns null for path traversal attempt", () => {
639
+ const result = resolveDistFilePath("/dist", "/../etc/passwd");
640
+ assert.equal(result, null);
641
+ });
642
+ });
643
+ // ---------------------------------------------------------------------------
644
+ // CLI command dispatch
645
+ // ---------------------------------------------------------------------------
646
+ describe("CLI command dispatch", () => {
647
+ it("recognises all valid commands", () => {
648
+ for (const cmd of VALID_COMMANDS) {
649
+ assert.ok(VALID_COMMANDS.includes(cmd), `${cmd} should be a valid command`);
650
+ }
651
+ });
652
+ it("has 7 valid commands", () => {
653
+ assert.equal(VALID_COMMANDS.length, 7);
654
+ });
655
+ it("includes dev, build, preview, start", () => {
656
+ assert.ok(VALID_COMMANDS.includes("dev"));
657
+ assert.ok(VALID_COMMANDS.includes("build"));
658
+ assert.ok(VALID_COMMANDS.includes("preview"));
659
+ assert.ok(VALID_COMMANDS.includes("start"));
660
+ });
661
+ it("includes worker, deploy-check, release-check", () => {
662
+ assert.ok(VALID_COMMANDS.includes("worker"));
663
+ assert.ok(VALID_COMMANDS.includes("deploy-check"));
664
+ assert.ok(VALID_COMMANDS.includes("release-check"));
665
+ });
666
+ });
667
+ // ---------------------------------------------------------------------------
668
+ // Config file candidate ordering
669
+ // ---------------------------------------------------------------------------
670
+ describe("config file candidates", () => {
671
+ const CONFIG_CANDIDATES = [
672
+ "neutron.config.ts",
673
+ "neutron.config.js",
674
+ "neutron.config.mjs",
675
+ "neutron.config.cjs",
676
+ ];
677
+ it("prefers .ts over .js", () => {
678
+ assert.equal(CONFIG_CANDIDATES[0], "neutron.config.ts");
679
+ assert.equal(CONFIG_CANDIDATES[1], "neutron.config.js");
680
+ });
681
+ it("includes all four extensions", () => {
682
+ assert.equal(CONFIG_CANDIDATES.length, 4);
683
+ });
684
+ });
685
+ //# sourceMappingURL=index.test.js.map