htmlhost-cli 1.5.0 → 1.6.0
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/package.json +1 -1
- package/src/cli.mjs +1 -1
- package/src/commands/deploy.mjs +183 -9
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
package/src/commands/deploy.mjs
CHANGED
|
@@ -50,10 +50,11 @@ function promptChoice(question, options) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* htmlhost deploy [file|dir] [--ttl 7d] [--slug existing-slug] [--title "My Site"] [--new] [--no-assets]
|
|
53
|
+
* htmlhost deploy [file|dir] [--ttl 7d] [--slug existing-slug] [--title "My Site"] [--new] [--no-assets] [--pages]
|
|
54
54
|
*
|
|
55
55
|
* - Defaults to index.html in the current directory
|
|
56
56
|
* - If a directory (or ".") is given, deploys all *.html files as separate linked sites
|
|
57
|
+
* - Use --pages with a directory to deploy all *.html files as pages within ONE multi-page site
|
|
57
58
|
* - Remembers the site(s) via .htmlhost file (auto re-deploy)
|
|
58
59
|
* - Use --new to force fresh deploys
|
|
59
60
|
*/
|
|
@@ -64,6 +65,7 @@ export async function deploy(args) {
|
|
|
64
65
|
const title = getFlag(args, "--title");
|
|
65
66
|
const forceNew = args.includes("--new");
|
|
66
67
|
const skipAssets = args.includes("--no-assets");
|
|
68
|
+
const multiPage = args.includes("--pages");
|
|
67
69
|
|
|
68
70
|
// Check if the target is a directory
|
|
69
71
|
if (file) {
|
|
@@ -71,6 +73,9 @@ export async function deploy(args) {
|
|
|
71
73
|
try {
|
|
72
74
|
const stat = statSync(filePath);
|
|
73
75
|
if (stat.isDirectory()) {
|
|
76
|
+
if (multiPage) {
|
|
77
|
+
return deployMultiPage(filePath, { ttl, title, forceNew, skipAssets });
|
|
78
|
+
}
|
|
74
79
|
return deployDirectory(filePath, { ttl, forceNew, skipAssets });
|
|
75
80
|
}
|
|
76
81
|
} catch {
|
|
@@ -78,6 +83,11 @@ export async function deploy(args) {
|
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
85
|
|
|
86
|
+
// --pages without explicit dir defaults to cwd
|
|
87
|
+
if (multiPage && !file) {
|
|
88
|
+
return deployMultiPage(resolve("."), { ttl, title, forceNew, skipAssets });
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
// Default to index.html in the current directory
|
|
82
92
|
if (!file) {
|
|
83
93
|
const defaultFile = resolve("index.html");
|
|
@@ -101,13 +111,31 @@ export async function deploy(args) {
|
|
|
101
111
|
async function deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug }) {
|
|
102
112
|
const filePath = resolve(file);
|
|
103
113
|
const projectDir = dirname(filePath);
|
|
114
|
+
const fileName = basename(filePath);
|
|
104
115
|
|
|
105
116
|
let isLinked = false;
|
|
106
117
|
|
|
107
118
|
if (!slug && !forceNew) {
|
|
108
119
|
const link = readLink(projectDir);
|
|
109
|
-
|
|
110
|
-
if (link?.
|
|
120
|
+
|
|
121
|
+
if (link?.sites) {
|
|
122
|
+
// Multi-file format — warn that only one file is being deployed
|
|
123
|
+
const siteCount = Object.keys(link.sites).length;
|
|
124
|
+
if (siteCount > 1) {
|
|
125
|
+
console.log("");
|
|
126
|
+
console.log(` ${yellow("⚠")} This project has ${bold(String(siteCount))} linked sites but you're deploying only ${cyan(fileName)}.`);
|
|
127
|
+
console.log(` ${dim(`To deploy all files, run: ${bold("htmlhost deploy .")}`)}`);
|
|
128
|
+
console.log("");
|
|
129
|
+
}
|
|
130
|
+
// Look up this file's entry in the sites map
|
|
131
|
+
const entry = link.sites[fileName];
|
|
132
|
+
if (entry?.slug) {
|
|
133
|
+
slug = entry.slug;
|
|
134
|
+
isLinked = true;
|
|
135
|
+
info(`Linked to ${cyan(slug)} ${dim(`(via .htmlhost sites map)`)}`);
|
|
136
|
+
}
|
|
137
|
+
} else if (link?.slug) {
|
|
138
|
+
// Single-file format (has slug at top level, no sites map)
|
|
111
139
|
if (link.onRedeploy === "overwrite") {
|
|
112
140
|
slug = link.slug;
|
|
113
141
|
isLinked = true;
|
|
@@ -186,13 +214,21 @@ async function deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug }
|
|
|
186
214
|
|
|
187
215
|
const data = await post("/api/sites", body);
|
|
188
216
|
|
|
189
|
-
// Save/update the link
|
|
217
|
+
// Save/update the link — merge into existing .htmlhost instead of clobbering
|
|
190
218
|
const existingLink = readLink(projectDir);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
219
|
+
|
|
220
|
+
if (existingLink?.sites) {
|
|
221
|
+
// Multi-file format: merge this file's entry into the existing sites map
|
|
222
|
+
existingLink.sites[fileName] = { slug: data.slug, url: data.url };
|
|
223
|
+
writeLink(projectDir, existingLink);
|
|
224
|
+
} else {
|
|
225
|
+
// Single-file format: write the flat slug/url (preserve onRedeploy preference)
|
|
226
|
+
writeLink(projectDir, {
|
|
227
|
+
slug: data.slug,
|
|
228
|
+
url: data.url,
|
|
229
|
+
...(existingLink?.onRedeploy ? { onRedeploy: existingLink.onRedeploy } : {}),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
196
232
|
|
|
197
233
|
console.log("");
|
|
198
234
|
ok(`${bold("Live")} at ${cyan(`https://${data.url}`)}`);
|
|
@@ -355,6 +391,144 @@ async function deployDirectory(dirPath, { ttl, forceNew, skipAssets }) {
|
|
|
355
391
|
console.log("");
|
|
356
392
|
}
|
|
357
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Deploy all *.html files in a directory as pages within a single multi-page site.
|
|
396
|
+
* Uses the --pages flag. File names map to URL paths:
|
|
397
|
+
* index.html → /
|
|
398
|
+
* about.html → /about
|
|
399
|
+
* blog/index.html → /blog
|
|
400
|
+
* blog/post.html → /blog/post
|
|
401
|
+
*/
|
|
402
|
+
async function deployMultiPage(dirPath, { ttl, title, forceNew, skipAssets }) {
|
|
403
|
+
// Find all .html files (recursive) — skip dotfiles
|
|
404
|
+
const htmlFiles = findHtmlFiles(dirPath, dirPath);
|
|
405
|
+
|
|
406
|
+
if (htmlFiles.length === 0) {
|
|
407
|
+
err("No .html files found in this directory.");
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Read existing link data
|
|
412
|
+
const link = readLink(dirPath);
|
|
413
|
+
const existingSlug = (!forceNew && link?.multipage?.slug) || null;
|
|
414
|
+
|
|
415
|
+
console.log("");
|
|
416
|
+
info(`Multi-page deploy: ${cyan(bold(String(htmlFiles.length)))} page${htmlFiles.length > 1 ? "s" : ""} in ${cyan(basename(dirPath) || ".")}`);
|
|
417
|
+
console.log("");
|
|
418
|
+
|
|
419
|
+
// Show page mapping
|
|
420
|
+
for (const { relativePath, pagePath } of htmlFiles) {
|
|
421
|
+
console.log(` ${dim("📄")} ${relativePath} → ${cyan(pagePath)}`);
|
|
422
|
+
}
|
|
423
|
+
console.log("");
|
|
424
|
+
|
|
425
|
+
// If we have a linked slug, confirm overwrite
|
|
426
|
+
if (existingSlug) {
|
|
427
|
+
info(`Linked to ${cyan(existingSlug + ".htmlhost.co")} ${dim("(multi-page)")}`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Process assets and build page payloads
|
|
431
|
+
const assetCache = new Map();
|
|
432
|
+
const pagePayloads = [];
|
|
433
|
+
|
|
434
|
+
for (const { filePath, pagePath, relativePath } of htmlFiles) {
|
|
435
|
+
const stat = statSync(filePath);
|
|
436
|
+
info(`${bold(relativePath)} ${dim(`(${formatBytes(stat.size)})`)}`);
|
|
437
|
+
|
|
438
|
+
let html = readFileSync(filePath, "utf8");
|
|
439
|
+
|
|
440
|
+
if (!skipAssets) {
|
|
441
|
+
html = await processAssets(html, dirname(filePath), assetCache);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
pagePayloads.push({
|
|
445
|
+
path: pagePath,
|
|
446
|
+
html,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Deploy all pages as one multi-page site
|
|
451
|
+
const body = { pages: pagePayloads };
|
|
452
|
+
if (ttl) body.ttl = ttl;
|
|
453
|
+
if (existingSlug) body.slug = existingSlug;
|
|
454
|
+
if (title) body.title = title;
|
|
455
|
+
|
|
456
|
+
info(existingSlug ? `Re-deploying to ${cyan(existingSlug)}…` : "Deploying new multi-page site…");
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
const data = await post("/api/sites", body);
|
|
460
|
+
|
|
461
|
+
// Save link
|
|
462
|
+
writeLink(dirPath, {
|
|
463
|
+
multipage: {
|
|
464
|
+
slug: data.slug,
|
|
465
|
+
url: data.url,
|
|
466
|
+
pageCount: data.pageCount || pagePayloads.length,
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
console.log("");
|
|
471
|
+
console.log(` ${green("━".repeat(40))}`);
|
|
472
|
+
ok(`${bold("Live")} at ${cyan(`https://${data.url}`)}`);
|
|
473
|
+
console.log(` ${dim(`${data.pageCount || pagePayloads.length} pages · ${data.ttl} TTL`)}`);
|
|
474
|
+
console.log("");
|
|
475
|
+
|
|
476
|
+
for (const { pagePath } of htmlFiles) {
|
|
477
|
+
const urlPath = pagePath === "/" ? "" : pagePath;
|
|
478
|
+
console.log(` ${cyan(`https://${data.url}${urlPath}`)}`);
|
|
479
|
+
}
|
|
480
|
+
console.log("");
|
|
481
|
+
console.log(` ${dim("Linked → .htmlhost (multipage)")}`);
|
|
482
|
+
console.log("");
|
|
483
|
+
} catch (e) {
|
|
484
|
+
err(`Deploy failed: ${e.message}`);
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Recursively find all .html files and derive their page paths.
|
|
491
|
+
* Returns [{ filePath, relativePath, pagePath }]
|
|
492
|
+
*/
|
|
493
|
+
function findHtmlFiles(baseDir, currentDir) {
|
|
494
|
+
const entries = readdirSync(currentDir, { withFileTypes: true });
|
|
495
|
+
const results = [];
|
|
496
|
+
|
|
497
|
+
for (const entry of entries) {
|
|
498
|
+
if (entry.name.startsWith(".")) continue;
|
|
499
|
+
|
|
500
|
+
const fullPath = join(currentDir, entry.name);
|
|
501
|
+
|
|
502
|
+
if (entry.isDirectory()) {
|
|
503
|
+
results.push(...findHtmlFiles(baseDir, fullPath));
|
|
504
|
+
} else if (entry.name.endsWith(".html")) {
|
|
505
|
+
const relativePath = fullPath.slice(baseDir.length + 1); // e.g. "about.html" or "blog/post.html"
|
|
506
|
+
|
|
507
|
+
// Derive page path from file path
|
|
508
|
+
let pagePath;
|
|
509
|
+
if (entry.name === "index.html") {
|
|
510
|
+
// index.html → parent directory path (or "/" for root)
|
|
511
|
+
const dirRel = dirname(relativePath);
|
|
512
|
+
pagePath = dirRel === "." ? "/" : `/${dirRel}`;
|
|
513
|
+
} else {
|
|
514
|
+
// about.html → /about, blog/post.html → /blog/post
|
|
515
|
+
pagePath = "/" + relativePath.replace(/\.html$/, "");
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
results.push({ filePath: fullPath, relativePath, pagePath });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Sort: root index first, then alphabetical
|
|
523
|
+
results.sort((a, b) => {
|
|
524
|
+
if (a.pagePath === "/") return -1;
|
|
525
|
+
if (b.pagePath === "/") return 1;
|
|
526
|
+
return a.pagePath.localeCompare(b.pagePath);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
return results;
|
|
530
|
+
}
|
|
531
|
+
|
|
358
532
|
function getFlag(args, flag) {
|
|
359
533
|
const idx = args.indexOf(flag);
|
|
360
534
|
if (idx === -1 || idx >= args.length - 1) return null;
|