htmlhost-cli 1.4.0 → 1.5.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/assets.mjs +233 -75
- package/src/cli.mjs +1 -1
- package/src/commands/deploy.mjs +6 -2
- package/src/ui.mjs +3 -0
package/package.json
CHANGED
package/src/assets.mjs
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Asset
|
|
3
|
-
*
|
|
2
|
+
* Asset processing — scan HTML for local asset references, inline CSS/JS,
|
|
3
|
+
* upload binary assets (images, fonts, PDFs, SVGs), and rewrite the HTML.
|
|
4
|
+
*
|
|
5
|
+
* Strategy:
|
|
6
|
+
* CSS/JS files (< 512 KB) → inlined directly into the HTML
|
|
7
|
+
* CSS/JS files (≥ 512 KB) → uploaded to media library, URL rewritten
|
|
8
|
+
* Binary assets → uploaded to media library, URL rewritten
|
|
9
|
+
*
|
|
10
|
+
* Before inlining CSS, all url() references inside the CSS are resolved,
|
|
11
|
+
* uploaded, and rewritten so the inlined stylesheet is fully self-contained.
|
|
4
12
|
*
|
|
5
13
|
* Handles: <script src>, <link href>, <img src>, <img srcset>,
|
|
6
14
|
* <source src/srcset>, <video src/poster>, <audio src>,
|
|
@@ -10,15 +18,19 @@
|
|
|
10
18
|
* anchors (#), and files > 10 MB.
|
|
11
19
|
*/
|
|
12
20
|
import { readFileSync, statSync, existsSync } from "node:fs";
|
|
13
|
-
import { resolve, dirname, basename } from "node:path";
|
|
21
|
+
import { resolve, dirname, basename, extname } from "node:path";
|
|
14
22
|
import { uploadFile } from "./api.mjs";
|
|
15
23
|
import { getApi } from "./config.mjs";
|
|
16
24
|
import { ok, warn, dim, cyan, formatBytes, mimeFromExt } from "./ui.mjs";
|
|
17
25
|
|
|
18
26
|
const MAX_ASSET_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
27
|
+
const INLINE_THRESHOLD = 512 * 1024; // 512 KB — inline CSS/JS below this
|
|
28
|
+
|
|
29
|
+
/** File extensions that should be inlined into the HTML instead of uploaded. */
|
|
30
|
+
const INLINE_EXTENSIONS = new Set([".css", ".js", ".mjs"]);
|
|
19
31
|
|
|
20
32
|
/**
|
|
21
|
-
* Returns true for references we should NOT try to
|
|
33
|
+
* Returns true for references we should NOT try to process.
|
|
22
34
|
*/
|
|
23
35
|
function isExternal(ref) {
|
|
24
36
|
if (!ref || !ref.trim()) return true;
|
|
@@ -37,8 +49,6 @@ function isExternal(ref) {
|
|
|
37
49
|
/**
|
|
38
50
|
* Given a base directory and a relative path, resolve it and check
|
|
39
51
|
* it exists, is a file, and is under the size limit.
|
|
40
|
-
*
|
|
41
|
-
* Returns { filePath, skip, reason } — if skip is true, reason explains why.
|
|
42
52
|
*/
|
|
43
53
|
function resolveAsset(baseDir, ref) {
|
|
44
54
|
const cleaned = ref.split("?")[0].split("#")[0]; // strip query/hash
|
|
@@ -68,35 +78,121 @@ function resolveAsset(baseDir, ref) {
|
|
|
68
78
|
return { filePath, size: stat.size, skip: false };
|
|
69
79
|
}
|
|
70
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Check if a file should be inlined based on extension and size.
|
|
83
|
+
*/
|
|
84
|
+
function shouldInline(filePath, fileSize) {
|
|
85
|
+
const ext = extname(filePath).toLowerCase();
|
|
86
|
+
return INLINE_EXTENSIONS.has(ext) && fileSize < INLINE_THRESHOLD;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Extract url(…) references from a CSS string.
|
|
91
|
+
*/
|
|
92
|
+
function extractCssUrls(css) {
|
|
93
|
+
const urlRe = /url\(\s*(?:"([^"]*)"|'([^']*)'|([^)]*?))\s*\)/gi;
|
|
94
|
+
const refs = [];
|
|
95
|
+
let m;
|
|
96
|
+
while ((m = urlRe.exec(css)) !== null) {
|
|
97
|
+
const val = (m[1] ?? m[2] ?? m[3] ?? "").trim();
|
|
98
|
+
if (val && !isExternal(val)) {
|
|
99
|
+
refs.push(val);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return refs;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Process a CSS file's url() references — upload assets and rewrite URLs.
|
|
107
|
+
* Returns the rewritten CSS content.
|
|
108
|
+
*/
|
|
109
|
+
async function processCssContent(css, cssDir, sharedCache, urlMap) {
|
|
110
|
+
const refs = extractCssUrls(css);
|
|
111
|
+
if (refs.length === 0) return css;
|
|
112
|
+
|
|
113
|
+
const api = getApi();
|
|
114
|
+
|
|
115
|
+
for (const ref of refs) {
|
|
116
|
+
if (urlMap.has(ref)) continue;
|
|
117
|
+
|
|
118
|
+
const asset = resolveAsset(cssDir, ref);
|
|
119
|
+
if (asset.skip) {
|
|
120
|
+
warn(` CSS url() ${cyan(ref)} — ${asset.reason}`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check shared cache
|
|
125
|
+
if (sharedCache && sharedCache.has(asset.filePath)) {
|
|
126
|
+
urlMap.set(ref, sharedCache.get(asset.filePath));
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const fileName = basename(asset.filePath);
|
|
131
|
+
const mime = mimeFromExt(fileName);
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const data = await uploadFile("/api/media", asset.filePath, fileName, mime);
|
|
135
|
+
const hostedUrl = `${api}${data.url}`;
|
|
136
|
+
urlMap.set(ref, hostedUrl);
|
|
137
|
+
if (sharedCache) sharedCache.set(asset.filePath, hostedUrl);
|
|
138
|
+
ok(` ${cyan(ref)} ${dim(`(${formatBytes(asset.size)})`)} → ${hostedUrl}`);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
warn(` Failed to upload ${cyan(ref)}: ${e.message}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Rewrite all url() references in the CSS
|
|
145
|
+
let result = css;
|
|
146
|
+
for (const [ref, hostedUrl] of urlMap) {
|
|
147
|
+
result = replaceAll(result, `"${ref}"`, `"${hostedUrl}"`);
|
|
148
|
+
result = replaceAll(result, `'${ref}'`, `'${hostedUrl}'`);
|
|
149
|
+
result = replaceAll(result, `url(${ref})`, `url(${hostedUrl})`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
71
155
|
/**
|
|
72
156
|
* Scan HTML string for local asset references.
|
|
73
|
-
* Returns an array of { original, ref } entries
|
|
74
|
-
* exact attribute value substring and `ref` is the cleaned path.
|
|
157
|
+
* Returns an array of { original, ref, tagType } entries.
|
|
75
158
|
*/
|
|
76
159
|
function findAssetRefs(html) {
|
|
77
160
|
const refs = new Set();
|
|
78
161
|
const entries = [];
|
|
79
162
|
|
|
80
|
-
function add(original) {
|
|
163
|
+
function add(original, tagType = "attr") {
|
|
81
164
|
const ref = original.trim();
|
|
82
165
|
if (isExternal(ref)) return;
|
|
83
166
|
if (refs.has(ref)) return; // dedupe
|
|
84
167
|
refs.add(ref);
|
|
85
|
-
entries.push({ original, ref });
|
|
168
|
+
entries.push({ original, ref, tagType });
|
|
86
169
|
}
|
|
87
170
|
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
// <video src="x" poster="x">, <audio src="x">,
|
|
91
|
-
// <link href="x"> (only stylesheets/icons/preloads)
|
|
92
|
-
// We intentionally use a broad regex and then filter.
|
|
93
|
-
|
|
94
|
-
// --- Tag attributes: src, href, poster ---
|
|
95
|
-
const attrRe = /<(?:script|img|source|video|audio|link|embed|track)\b[^>]*?\b(?:src|href|poster)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
|
|
171
|
+
// --- <script src="..."> ---
|
|
172
|
+
const scriptRe = /<script\b[^>]*?\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
|
|
96
173
|
let m;
|
|
174
|
+
while ((m = scriptRe.exec(html)) !== null) {
|
|
175
|
+
const val = m[1] ?? m[2];
|
|
176
|
+
if (val) add(val, "script");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// --- <link href="..." rel="stylesheet"> ---
|
|
180
|
+
const linkRe = /<link\b[^>]*?\bhref\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
|
|
181
|
+
while ((m = linkRe.exec(html)) !== null) {
|
|
182
|
+
const val = m[1] ?? m[2];
|
|
183
|
+
const fullTag = m[0];
|
|
184
|
+
if (val) {
|
|
185
|
+
// Only treat as CSS if it's a stylesheet link
|
|
186
|
+
const isStylesheet = /rel\s*=\s*["']stylesheet["']/i.test(fullTag);
|
|
187
|
+
add(val, isStylesheet ? "css" : "attr");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// --- Other tag attributes: src, href, poster (non-script, non-link) ---
|
|
192
|
+
const attrRe = /<(?:img|source|video|audio|embed|track)\b[^>]*?\b(?:src|href|poster)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
|
|
97
193
|
while ((m = attrRe.exec(html)) !== null) {
|
|
98
194
|
const val = m[1] ?? m[2];
|
|
99
|
-
if (val) add(val);
|
|
195
|
+
if (val) add(val, "attr");
|
|
100
196
|
}
|
|
101
197
|
|
|
102
198
|
// --- srcset attributes (img, source) ---
|
|
@@ -104,48 +200,39 @@ function findAssetRefs(html) {
|
|
|
104
200
|
while ((m = srcsetRe.exec(html)) !== null) {
|
|
105
201
|
const srcset = m[1] ?? m[2];
|
|
106
202
|
if (!srcset) continue;
|
|
107
|
-
// srcset is comma-separated: "path 2x, path2 300w"
|
|
108
203
|
for (const part of srcset.split(",")) {
|
|
109
204
|
const src = part.trim().split(/\s+/)[0];
|
|
110
|
-
if (src) add(src);
|
|
205
|
+
if (src) add(src, "attr");
|
|
111
206
|
}
|
|
112
207
|
}
|
|
113
208
|
|
|
114
209
|
// --- CSS url() in <style> blocks ---
|
|
115
210
|
const styleBlockRe = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
|
116
211
|
while ((m = styleBlockRe.exec(html)) !== null) {
|
|
117
|
-
extractCssUrls(m[1]
|
|
212
|
+
const urls = extractCssUrls(m[1]);
|
|
213
|
+
for (const u of urls) add(u, "css-url");
|
|
118
214
|
}
|
|
119
215
|
|
|
120
216
|
// --- CSS url() in inline style="…" attributes ---
|
|
121
217
|
const inlineStyleRe = /\bstyle\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
|
|
122
218
|
while ((m = inlineStyleRe.exec(html)) !== null) {
|
|
123
219
|
const css = m[1] ?? m[2];
|
|
124
|
-
if (css)
|
|
220
|
+
if (css) {
|
|
221
|
+
const urls = extractCssUrls(css);
|
|
222
|
+
for (const u of urls) add(u, "css-url");
|
|
223
|
+
}
|
|
125
224
|
}
|
|
126
225
|
|
|
127
226
|
return entries;
|
|
128
227
|
}
|
|
129
228
|
|
|
130
229
|
/**
|
|
131
|
-
*
|
|
132
|
-
*/
|
|
133
|
-
function extractCssUrls(css, add) {
|
|
134
|
-
const urlRe = /url\(\s*(?:"([^"]*)"|'([^']*)'|([^)]*?))\s*\)/gi;
|
|
135
|
-
let m;
|
|
136
|
-
while ((m = urlRe.exec(css)) !== null) {
|
|
137
|
-
const val = (m[1] ?? m[2] ?? m[3] ?? "").trim();
|
|
138
|
-
if (val) add(val);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Main entry: process HTML, upload local assets, return rewritten HTML.
|
|
230
|
+
* Main entry: process HTML, inline CSS/JS, upload binary assets, return rewritten HTML.
|
|
144
231
|
*
|
|
145
232
|
* @param {string} html The original HTML content
|
|
146
|
-
* @param {string} baseDir Directory the HTML file lives in
|
|
147
|
-
* @param {Map<string, string>} [sharedCache] Optional
|
|
148
|
-
* @returns {Promise<string>} Rewritten HTML with hosted
|
|
233
|
+
* @param {string} baseDir Directory the HTML file lives in
|
|
234
|
+
* @param {Map<string, string>} [sharedCache] Optional filePath → hostedUrl cache for batch deploys
|
|
235
|
+
* @returns {Promise<string>} Rewritten HTML with inlined/hosted assets
|
|
149
236
|
*/
|
|
150
237
|
export async function processAssets(html, baseDir, sharedCache) {
|
|
151
238
|
const entries = findAssetRefs(html);
|
|
@@ -154,48 +241,82 @@ export async function processAssets(html, baseDir, sharedCache) {
|
|
|
154
241
|
|
|
155
242
|
const api = getApi();
|
|
156
243
|
|
|
157
|
-
|
|
158
|
-
/** @type {Map<string, string>} ref → hosted URL */
|
|
244
|
+
/** @type {Map<string, string>} ref → hosted URL (for binary uploads) */
|
|
159
245
|
const urlMap = new Map();
|
|
160
246
|
|
|
161
|
-
|
|
162
|
-
|
|
247
|
+
/** @type {Map<string, {content: string, size: number}>} ref → inline content (for CSS/JS) */
|
|
248
|
+
const inlineMap = new Map();
|
|
249
|
+
|
|
250
|
+
// --- Phase 1: Categorize and process each reference ---
|
|
251
|
+
|
|
252
|
+
let uploadCount = 0;
|
|
253
|
+
let inlineCount = 0;
|
|
254
|
+
let cachedCount = 0;
|
|
255
|
+
|
|
256
|
+
// Pre-scan to count
|
|
163
257
|
for (const { ref } of entries) {
|
|
164
|
-
if (urlMap.has(ref)) continue;
|
|
258
|
+
if (urlMap.has(ref) || inlineMap.has(ref)) continue;
|
|
165
259
|
const asset = resolveAsset(baseDir, ref);
|
|
166
260
|
if (asset.skip) continue;
|
|
167
261
|
if (sharedCache && sharedCache.has(asset.filePath)) {
|
|
168
|
-
|
|
169
|
-
} else {
|
|
170
|
-
needsUpload++;
|
|
262
|
+
cachedCount++;
|
|
171
263
|
}
|
|
172
264
|
}
|
|
173
265
|
|
|
174
|
-
const
|
|
175
|
-
|
|
266
|
+
const totalRefs = entries.filter((e) => {
|
|
267
|
+
const asset = resolveAsset(baseDir, e.ref);
|
|
268
|
+
return !asset.skip;
|
|
269
|
+
}).length;
|
|
176
270
|
|
|
177
|
-
if (
|
|
271
|
+
if (totalRefs > 0) {
|
|
178
272
|
console.log("");
|
|
179
|
-
|
|
180
|
-
if (cachedCount > 0 && needsUpload > 0) {
|
|
181
|
-
ok(`Found ${cyan(String(totalLocal))} ${label} (${cachedCount} cached, ${needsUpload} to upload)`);
|
|
182
|
-
} else if (cachedCount > 0) {
|
|
183
|
-
ok(`Found ${cyan(String(totalLocal))} ${label} (all cached)`);
|
|
184
|
-
} else {
|
|
185
|
-
ok(`Found ${cyan(String(totalLocal))} ${label} to upload`);
|
|
186
|
-
}
|
|
273
|
+
ok(`Found ${cyan(String(totalRefs))} local asset${totalRefs === 1 ? "" : "s"} to process`);
|
|
187
274
|
console.log("");
|
|
188
275
|
}
|
|
189
276
|
|
|
190
|
-
for (const { ref } of entries) {
|
|
191
|
-
if (urlMap.has(ref)
|
|
277
|
+
for (const { ref, tagType } of entries) {
|
|
278
|
+
if (urlMap.has(ref) || inlineMap.has(ref)) continue;
|
|
192
279
|
|
|
193
280
|
const asset = resolveAsset(baseDir, ref);
|
|
194
281
|
if (asset.skip) {
|
|
195
|
-
warn(`Skipping ${cyan(ref)} — ${asset.reason}`);
|
|
282
|
+
if (asset.reason !== "empty") warn(`Skipping ${cyan(ref)} — ${asset.reason}`);
|
|
196
283
|
continue;
|
|
197
284
|
}
|
|
198
285
|
|
|
286
|
+
// Check shared cache first
|
|
287
|
+
if (sharedCache && sharedCache.has(asset.filePath)) {
|
|
288
|
+
urlMap.set(ref, sharedCache.get(asset.filePath));
|
|
289
|
+
ok(`${cyan(ref)} ${dim("(cached)")}`);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const ext = extname(asset.filePath).toLowerCase();
|
|
294
|
+
|
|
295
|
+
// --- CSS file: inline (with nested url() processing) ---
|
|
296
|
+
if (tagType === "css" && shouldInline(asset.filePath, asset.size)) {
|
|
297
|
+
let cssContent = readFileSync(asset.filePath, "utf8");
|
|
298
|
+
const cssDir = dirname(asset.filePath);
|
|
299
|
+
|
|
300
|
+
// Process url() references inside the CSS file
|
|
301
|
+
const cssUrlMap = new Map();
|
|
302
|
+
cssContent = await processCssContent(cssContent, cssDir, sharedCache, cssUrlMap);
|
|
303
|
+
|
|
304
|
+
inlineMap.set(ref, { content: cssContent, size: asset.size });
|
|
305
|
+
inlineCount++;
|
|
306
|
+
ok(`${cyan(ref)} ${dim(`(${formatBytes(asset.size)})`)} → ${dim("inlined as <style>")}`);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// --- JS file: inline ---
|
|
311
|
+
if (tagType === "script" && shouldInline(asset.filePath, asset.size)) {
|
|
312
|
+
const jsContent = readFileSync(asset.filePath, "utf8");
|
|
313
|
+
inlineMap.set(ref, { content: jsContent, size: asset.size });
|
|
314
|
+
inlineCount++;
|
|
315
|
+
ok(`${cyan(ref)} ${dim(`(${formatBytes(asset.size)})`)} → ${dim("inlined as <script>")}`);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// --- Binary asset or large CSS/JS: upload to media library ---
|
|
199
320
|
const fileName = basename(asset.filePath);
|
|
200
321
|
const mime = mimeFromExt(fileName);
|
|
201
322
|
|
|
@@ -203,35 +324,66 @@ export async function processAssets(html, baseDir, sharedCache) {
|
|
|
203
324
|
const data = await uploadFile("/api/media", asset.filePath, fileName, mime);
|
|
204
325
|
const hostedUrl = `${api}${data.url}`;
|
|
205
326
|
urlMap.set(ref, hostedUrl);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
sharedCache.set(asset.filePath, hostedUrl);
|
|
209
|
-
}
|
|
327
|
+
if (sharedCache) sharedCache.set(asset.filePath, hostedUrl);
|
|
328
|
+
uploadCount++;
|
|
210
329
|
ok(`${cyan(ref)} ${dim(`(${formatBytes(asset.size)})`)} → ${hostedUrl}`);
|
|
211
330
|
} catch (e) {
|
|
212
331
|
warn(`Failed to upload ${cyan(ref)}: ${e.message}`);
|
|
213
332
|
}
|
|
214
333
|
}
|
|
215
334
|
|
|
216
|
-
if (urlMap.size === 0) return html;
|
|
335
|
+
if (urlMap.size === 0 && inlineMap.size === 0) return html;
|
|
336
|
+
|
|
337
|
+
// --- Phase 2: Rewrite the HTML ---
|
|
217
338
|
|
|
218
|
-
// Replace all occurrences.
|
|
219
|
-
// We do a simple, safe string replacement — find all attribute values matching
|
|
220
|
-
// our refs and swap them with the hosted URL.
|
|
221
339
|
let result = html;
|
|
222
340
|
|
|
341
|
+
// 2a. Inline CSS: replace <link rel="stylesheet" href="ref"> with <style>content</style>
|
|
342
|
+
for (const [ref, { content }] of inlineMap) {
|
|
343
|
+
const ext = extname(ref).toLowerCase();
|
|
344
|
+
|
|
345
|
+
if (ext === ".css") {
|
|
346
|
+
// Match <link ... href="ref" ...> (handles both quote styles, any attribute order)
|
|
347
|
+
const linkPattern = new RegExp(
|
|
348
|
+
`<link\\b[^>]*?\\bhref\\s*=\\s*(?:"${escapeRegex(ref)}"|'${escapeRegex(ref)}')[^>]*/?>`,
|
|
349
|
+
"gi"
|
|
350
|
+
);
|
|
351
|
+
result = result.replace(linkPattern, `<style>\n${content}\n</style>`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (ext === ".js" || ext === ".mjs") {
|
|
355
|
+
// Match <script src="ref" ...></script> (handles attributes before/after src)
|
|
356
|
+
const scriptPattern = new RegExp(
|
|
357
|
+
`<script\\b([^>]*?)\\bsrc\\s*=\\s*(?:"${escapeRegex(ref)}"|'${escapeRegex(ref)}')([^>]*)>\\s*</script>`,
|
|
358
|
+
"gi"
|
|
359
|
+
);
|
|
360
|
+
result = result.replace(scriptPattern, (_, before, after) => {
|
|
361
|
+
// Remove type="module" if present since inlined scripts don't need it for basic cases
|
|
362
|
+
// But keep it if the user explicitly set it — they might need ES module behavior
|
|
363
|
+
const attrs = (before + after).trim();
|
|
364
|
+
const cleanedAttrs = attrs
|
|
365
|
+
.replace(/\bsrc\s*=\s*(?:"[^"]*"|'[^']*')/gi, "")
|
|
366
|
+
.trim();
|
|
367
|
+
return `<script${cleanedAttrs ? " " + cleanedAttrs : ""}>\n${content}\n</script>`;
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 2b. Rewrite URL references for uploaded assets
|
|
223
373
|
for (const [ref, hostedUrl] of urlMap) {
|
|
224
|
-
// Replace in attribute values: ="ref" and ='ref'
|
|
225
|
-
// Use a function replacer so we handle all occurrences correctly.
|
|
226
374
|
result = replaceAll(result, `"${ref}"`, `"${hostedUrl}"`);
|
|
227
375
|
result = replaceAll(result, `'${ref}'`, `'${hostedUrl}'`);
|
|
228
|
-
|
|
229
|
-
// Replace in CSS url(): url(ref), url("ref"), url('ref')
|
|
230
|
-
// The quoted forms are handled above. Handle the unquoted form too.
|
|
231
376
|
result = replaceAll(result, `url(${ref})`, `url(${hostedUrl})`);
|
|
232
377
|
}
|
|
233
378
|
|
|
379
|
+
// Summary
|
|
234
380
|
console.log("");
|
|
381
|
+
if (inlineCount > 0) {
|
|
382
|
+
ok(`Inlined ${cyan(String(inlineCount))} file${inlineCount === 1 ? "" : "s"} into HTML`);
|
|
383
|
+
}
|
|
384
|
+
if (uploadCount > 0) {
|
|
385
|
+
ok(`Uploaded ${cyan(String(uploadCount))} asset${uploadCount === 1 ? "" : "s"} to media library`);
|
|
386
|
+
}
|
|
235
387
|
|
|
236
388
|
return result;
|
|
237
389
|
}
|
|
@@ -240,6 +392,12 @@ export async function processAssets(html, baseDir, sharedCache) {
|
|
|
240
392
|
* Replace all occurrences of `search` in `str` with `replacement`.
|
|
241
393
|
*/
|
|
242
394
|
function replaceAll(str, search, replacement) {
|
|
243
|
-
// Use split/join for safe literal replacement (no regex escaping needed)
|
|
244
395
|
return str.split(search).join(replacement);
|
|
245
396
|
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Escape a string for use in a RegExp.
|
|
400
|
+
*/
|
|
401
|
+
function escapeRegex(str) {
|
|
402
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
403
|
+
}
|
package/src/cli.mjs
CHANGED
package/src/commands/deploy.mjs
CHANGED
|
@@ -330,10 +330,14 @@ async function deployDirectory(dirPath, { ttl, forceNew, skipAssets }) {
|
|
|
330
330
|
}
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
-
// Save all links to .htmlhost
|
|
333
|
+
// Save all links to .htmlhost — re-read to pick up any preference saved
|
|
334
|
+
// during the prompt flow (the original `onRedeploy` var was captured before
|
|
335
|
+
// the user's choice was written).
|
|
336
|
+
const freshLink = readLink(dirPath);
|
|
337
|
+
const finalOnRedeploy = freshLink?.onRedeploy || onRedeploy;
|
|
334
338
|
writeLink(dirPath, {
|
|
335
339
|
sites: { ...existingSites, ...results },
|
|
336
|
-
...(
|
|
340
|
+
...(finalOnRedeploy ? { onRedeploy: finalOnRedeploy } : {}),
|
|
337
341
|
});
|
|
338
342
|
|
|
339
343
|
// Summary
|
package/src/ui.mjs
CHANGED
|
@@ -99,6 +99,7 @@ export function mimeFromExt(filename) {
|
|
|
99
99
|
gif: "image/gif",
|
|
100
100
|
svg: "image/svg+xml",
|
|
101
101
|
webp: "image/webp",
|
|
102
|
+
avif: "image/avif",
|
|
102
103
|
ico: "image/x-icon",
|
|
103
104
|
woff: "font/woff",
|
|
104
105
|
woff2: "font/woff2",
|
|
@@ -106,10 +107,12 @@ export function mimeFromExt(filename) {
|
|
|
106
107
|
otf: "font/otf",
|
|
107
108
|
css: "text/css",
|
|
108
109
|
js: "text/javascript",
|
|
110
|
+
mjs: "text/javascript",
|
|
109
111
|
json: "application/json",
|
|
110
112
|
mp4: "video/mp4",
|
|
111
113
|
webm: "video/webm",
|
|
112
114
|
mp3: "audio/mpeg",
|
|
115
|
+
ogg: "audio/ogg",
|
|
113
116
|
wav: "audio/wav",
|
|
114
117
|
pdf: "application/pdf",
|
|
115
118
|
};
|