@cocreate/file-server 1.18.8 → 1.18.10

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 (3) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +1 -1
  3. package/src/index.js +112 -48
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [1.18.10](https://github.com/CoCreate-app/CoCreate-file-server/compare/v1.18.9...v1.18.10) (2025-11-17)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * enhance font and data URI handling in file processing ([25b7e8c](https://github.com/CoCreate-app/CoCreate-file-server/commit/25b7e8c1976c12ea99f1806025c248e0fa865ef0))
7
+
8
+ ## [1.18.9](https://github.com/CoCreate-app/CoCreate-file-server/compare/v1.18.8...v1.18.9) (2025-10-11)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * add theme detection and handling for preferred color scheme in responses ([484a37c](https://github.com/CoCreate-app/CoCreate-file-server/commit/484a37c3f35b397a3baff0d9dc62c338a0135903))
14
+
1
15
  ## [1.18.8](https://github.com/CoCreate-app/CoCreate-file-server/compare/v1.18.7...v1.18.8) (2025-10-11)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/file-server",
3
- "version": "1.18.8",
3
+ "version": "1.18.10",
4
4
  "description": "A simple file-server component in vanilla javascript. Easily configured using HTML5 data-attributes and/or JavaScript API.",
5
5
  "keywords": [
6
6
  "file-server",
package/src/index.js CHANGED
@@ -30,6 +30,24 @@ class CoCreateFileSystem {
30
30
  try {
31
31
  const hostname = urlObject.hostname;
32
32
 
33
+ // --- determine preferred color scheme from browser headers ---
34
+ let theme;
35
+ const headerValue =
36
+ (req && req.headers && (
37
+ req.headers["sec-ch-prefers-color-scheme"] ||
38
+ req.headers["prefers-color-scheme"] ||
39
+ req.headers["x-prefers-color-scheme"] ||
40
+ req.headers["x-color-scheme"]
41
+ ));
42
+ if (typeof headerValue === "string") {
43
+ // take first token, strip quotes, normalize
44
+ const token = headerValue.split(",")[0].trim().replace(/^"|"$/g, "").toLowerCase();
45
+ if (token === "dark" || token === "light") theme = token;
46
+ }
47
+ // optional: expose detected theme to clients / downstream code
48
+ if (theme) res.setHeader("X-Preferred-Color-Scheme", theme);
49
+ // --- end theme detection ---
50
+
33
51
  let data = {
34
52
  method: "object.read",
35
53
  host: hostname,
@@ -199,67 +217,103 @@ class CoCreateFileSystem {
199
217
 
200
218
  let contentType = file["content-type"] || "text/html";
201
219
 
202
- // Remove redundant handling for `src.src` in font file processing
203
- if (contentType.startsWith("font/") || /\.(woff2?|ttf|otf)$/i.test(pathname)) {
204
- try {
205
- if (typeof src === "string") {
206
- if (src.startsWith("data:font/")) {
207
- src = src.substring(src.indexOf(",") + 1);
220
+ // Normalize and decode src based on content-type and pathname.
221
+ async function normalizeSrc(src, contentType, pathname) {
222
+ const isBase64Only = (s) =>
223
+ typeof s === "string" && /^[A-Za-z0-9+/]+={0,2}$/.test(s) && s.length % 4 === 0;
224
+
225
+ const parseDataUri = (s) => {
226
+ // returns { mime, isBase64, data } or null
227
+ const m = /^data:([^;]+)(;base64)?,(.*)$/is.exec(s);
228
+ if (!m) return null;
229
+ return { mime: m[1].toLowerCase(), isBase64: !!m[2], data: m[3] };
230
+ };
231
+
232
+ // Fonts: data:font/...;base64 or plain base64 -> Buffer
233
+ if (contentType.startsWith("font/") || /\.(woff2?|ttf|otf)$/i.test(pathname)) {
234
+ if (Buffer.isBuffer(src)) return src;
235
+ if (typeof src !== "string") throw new Error("Invalid font src");
236
+ const d = parseDataUri(src);
237
+ if (d && d.isBase64) return Buffer.from(d.data, "base64");
238
+ // maybe stored as bare base64
239
+ if (isBase64Only(src)) return Buffer.from(src, "base64");
240
+ throw new Error("Font data is not valid base64 or data URI");
241
+ }
242
+
243
+ // Data URIs
244
+ if (typeof src === "string") {
245
+ const d = parseDataUri(src);
246
+ if (d) {
247
+ // SVG: decode to utf8 string so browsers render SVG correctly
248
+ if (d.mime === "image/svg+xml") {
249
+ if (d.isBase64) return Buffer.from(d.data, "base64").toString("utf8");
250
+ // URI-encoded SVG payload
251
+ try {
252
+ return decodeURIComponent(d.data);
253
+ } catch (_) {
254
+ return d.data;
255
+ }
208
256
  }
209
- if (/^([A-Za-z0-9+/]+={0,2})$/.test(src)) {
210
- // Decode base64-encoded font data
211
- src = Buffer.from(src, "base64");
212
- } else {
213
- throw new Error("Font data is not valid base64");
257
+ // Raster images -> Buffer
258
+ if (/^image\/(png|jpe?g|webp|bmp|gif)$/i.test(d.mime)) {
259
+ if (d.isBase64) return Buffer.from(d.data, "base64");
260
+ }
261
+ // If it's a text data URI (e.g., xml) and not base64, return decoded text
262
+ if (!d.isBase64) {
263
+ try {
264
+ return decodeURIComponent(d.data);
265
+ } catch (_) {
266
+ return d.data;
267
+ }
214
268
  }
215
- } else {
216
- throw new Error("Font data is not a valid base64 string");
217
269
  }
218
- } catch (err) {
219
- console.error("Error processing font file:", {
220
- message: err.message,
221
- contentType,
222
- srcType: typeof src
223
- });
224
- let pageNotFound = await getDefaultFile("/404.html");
225
- return sendResponse(pageNotFound.object[0].src, 404, {
226
- "Content-Type": "text/html"
227
- });
228
270
  }
229
- } else if (
230
- /^data:image\/[a-zA-Z0-9+.-]+;base64,([A-Za-z0-9+/]+={0,2})$/.test(
231
- src
232
- )
233
- ) {
234
- src = src.replace(/^data:image\/(png|jpeg|jpg);base64,/, "");
235
- src = Buffer.from(src, "base64");
236
- } else if (/^([A-Za-z0-9+/]+={0,2})$/.test(src)) {
237
- src = Buffer.from(src, "base64");
238
- } else if (contentType === "text/html") {
239
- try {
240
- let data = {};
241
271
 
242
- src = await this.render.HTML(
272
+ // Plain base64-only string: decode to Buffer only for binary content types
273
+ if (isBase64Only(src)) {
274
+ if (
275
+ contentType.startsWith("image/") ||
276
+ contentType.startsWith("font/") ||
277
+ contentType === "application/octet-stream"
278
+ ) {
279
+ return Buffer.from(src, "base64");
280
+ }
281
+ // textual content kept as-is
282
+ return src;
283
+ }
284
+
285
+ // HTML rendering
286
+ if (contentType === "text/html") {
287
+ return await this.render.HTML(
243
288
  file,
244
289
  organization,
245
290
  urlObject,
246
291
  langRegion,
247
- lang
292
+ lang,
293
+ theme
248
294
  );
249
- } catch (err) {
250
- console.warn("server-side-render: " + err.message);
251
295
  }
252
- } else if (
253
- contentType === "text/xml" ||
254
- contentType === "application/xml"
255
- ) {
256
- const protocol = "https://";
257
- src = src.replaceAll("{{$host}}", `${protocol}${hostname}`);
258
- } else if (contentType !== "text/javascript" || contentType === "text/css") {
259
-
260
- console.warn(`Unknown content type: ${contentType}`);
296
+
297
+ // XML host replacement if src is string
298
+ if ((contentType === "text/xml" || contentType === "application/xml") && typeof src === "string") {
299
+ const protocol = "https://";
300
+ return src.replaceAll("{{$host}}", `${protocol}${hostname}`);
301
+ }
302
+
303
+ // Otherwise return as-is (string, Buffer, object)
304
+ return src;
261
305
  }
262
306
 
307
+ try {
308
+ src = await normalizeSrc.call(this, src, contentType, pathname);
309
+ } catch (err) {
310
+ console.error("Error processing file src:", err && err.message);
311
+ let pageNotFound = await getDefaultFile("/404.html");
312
+ return sendResponse(pageNotFound.object[0].src, 404, {
313
+ "Content-Type": "text/html"
314
+ });
315
+ }
316
+
263
317
  sendResponse(src, 200, { "Content-Type": contentType });
264
318
  this.sitemap.check(file, hostname);
265
319
 
@@ -289,6 +343,16 @@ class CoCreateFileSystem {
289
343
 
290
344
  // Always override/set Cache-Control header so the response aligns with the file metadata/defaults
291
345
  headers["Cache-Control"] = cacheControl;
346
+ // Advertise that we accept the Sec-CH-Prefers-Color-Scheme client hint.
347
+ // After the browser sees this in a response it may include
348
+ // the Sec-CH-Prefers-Color-Scheme header on subsequent requests.
349
+ headers["Accept-CH"] = "Sec-CH-Prefers-Color-Scheme";
350
+ // optional: tell browser how long to remember this preference (seconds)
351
+ headers["Accept-CH-Lifetime"] = "86400";
352
+ // ensure caches/proxies vary responses by this hint
353
+ headers["Vary"] = headers["Vary"]
354
+ ? headers["Vary"] + ", Sec-CH-Prefers-Color-Scheme"
355
+ : "Sec-CH-Prefers-Color-Scheme";
292
356
 
293
357
  if (src instanceof Uint8Array || Buffer.isBuffer(src)) {
294
358
  // Ensure binary data is sent as-is