@shevky/core 0.0.8 → 0.0.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shevky/core",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "A minimal, dependency-light static site generator.",
5
5
  "type": "module",
6
6
  "main": "shevky.js",
@@ -1,3 +1,4 @@
1
+ import crypto from "node:crypto";
1
2
  import { io as _io, config as _cfg, log as _log } from "@shevky/base";
2
3
  import matter from "gray-matter";
3
4
 
@@ -334,7 +335,8 @@ export class ContentRegistry {
334
335
  }
335
336
 
336
337
  /**
337
- * Adds content only if required fields exist and id+lang is unique.
338
+ * Adds or replaces content by id+lang.
339
+ * If duplicate payload is same -> ignore, if different -> replace with latest.
338
340
  * @param {ContentFile} contentFile
339
341
  */
340
342
  #_addUniqueContent(contentFile) {
@@ -362,18 +364,83 @@ export class ContentRegistry {
362
364
  return false;
363
365
  }
364
366
 
365
- const hasExisting = this.#_cache.some((entry) => {
367
+ const existingIndex = this.#_cache.findIndex((entry) => {
366
368
  const existingId = typeof entry.id === "string" ? entry.id.trim() : "";
367
369
  const existingLang =
368
370
  typeof entry.lang === "string" ? entry.lang.trim() : "";
369
371
  return existingId === id && existingLang === lang;
370
372
  });
371
373
 
372
- if (hasExisting) {
373
- return false;
374
+ if (existingIndex !== -1) {
375
+ const currentEntry = this.#_cache[existingIndex];
376
+ const currentHash = this.#_buildContentHash(currentEntry);
377
+ const incomingHash = this.#_buildContentHash(contentFile);
378
+ if (currentHash === incomingHash) {
379
+ return false;
380
+ }
381
+
382
+ this.#_cache[existingIndex] = contentFile;
383
+ return true;
374
384
  }
375
385
 
376
386
  this.#_cache.push(contentFile);
377
387
  return true;
378
388
  }
389
+
390
+ /**
391
+ * @param {ContentFile} contentFile
392
+ */
393
+ #_buildContentHash(contentFile) {
394
+ const payload = {
395
+ header: contentFile.header?.raw ?? {},
396
+ content: typeof contentFile.content === "string" ? contentFile.content : "",
397
+ isValid: Boolean(contentFile.isValid),
398
+ };
399
+
400
+ return crypto
401
+ .createHash("sha1")
402
+ .update(this.#_stableStringify(payload))
403
+ .digest("hex");
404
+ }
405
+
406
+ /**
407
+ * Stable stringify to avoid key-order differences in hash computation.
408
+ * @param {unknown} value
409
+ * @returns {string}
410
+ */
411
+ #_stableStringify(value) {
412
+ if (value === undefined) {
413
+ return '"__undefined__"';
414
+ }
415
+
416
+ if (value === null) {
417
+ return "null";
418
+ }
419
+
420
+ if (value instanceof Date) {
421
+ return JSON.stringify(value.toISOString());
422
+ }
423
+
424
+ if (Array.isArray(value)) {
425
+ return `[${value.map((item) => this.#_stableStringify(item)).join(",")}]`;
426
+ }
427
+
428
+ if (typeof value !== "object") {
429
+ if (typeof value === "number" && !Number.isFinite(value)) {
430
+ return JSON.stringify(String(value));
431
+ }
432
+ return JSON.stringify(value);
433
+ }
434
+
435
+ const entries = Object.keys(value)
436
+ .sort()
437
+ .map(
438
+ (key) =>
439
+ `${JSON.stringify(key)}:${this.#_stableStringify(
440
+ /** @type {Record<string, unknown>} */ (value)[key],
441
+ )}`,
442
+ );
443
+
444
+ return `{${entries.join(",")}}`;
445
+ }
379
446
  }
package/scripts/main.js CHANGED
@@ -4,7 +4,7 @@ import _cli from "./cli.js";
4
4
  import _build from "./build.js";
5
5
  import _init from "./init.js";
6
6
 
7
- const VERSION = "0.0.8";
7
+ const VERSION = "0.0.9";
8
8
 
9
9
  const __filename = _io.url.toPath(import.meta.url);
10
10
  const __dirname = _io.path.name(__filename);