@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.
- package/LICENSE +21 -0
- package/README.md +27 -0
- package/dist/commands/build.d.ts +2 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +1251 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/deploy-check.d.ts +2 -0
- package/dist/commands/deploy-check.d.ts.map +1 -0
- package/dist/commands/deploy-check.js +157 -0
- package/dist/commands/deploy-check.js.map +1 -0
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +111 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/preview.d.ts +2 -0
- package/dist/commands/preview.d.ts.map +1 -0
- package/dist/commands/preview.js +223 -0
- package/dist/commands/preview.js.map +1 -0
- package/dist/commands/release-check.d.ts +2 -0
- package/dist/commands/release-check.d.ts.map +1 -0
- package/dist/commands/release-check.js +9 -0
- package/dist/commands/release-check.js.map +1 -0
- package/dist/commands/start.d.ts +4 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +72 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/worker.d.ts +2 -0
- package/dist/commands/worker.d.ts.map +1 -0
- package/dist/commands/worker.js +178 -0
- package/dist/commands/worker.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +685 -0
- package/dist/index.test.js.map +1 -0
- 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, "&")
|
|
54
|
+
.replace(/</g, "<")
|
|
55
|
+
.replace(/>/g, ">")
|
|
56
|
+
.replace(/"/g, """)
|
|
57
|
+
.replace(/'/g, "'");
|
|
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&b");
|
|
308
|
+
});
|
|
309
|
+
it("escapes angle brackets", () => {
|
|
310
|
+
assert.equal(escapeHtml("<script>"), "<script>");
|
|
311
|
+
});
|
|
312
|
+
it("escapes double quotes", () => {
|
|
313
|
+
assert.equal(escapeHtml('"hello"'), ""hello"");
|
|
314
|
+
});
|
|
315
|
+
it("escapes single quotes", () => {
|
|
316
|
+
assert.equal(escapeHtml("it's"), "it'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
|