foss-design 0.9.7 → 0.9.8

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/dist/cli.js CHANGED
@@ -3033,8 +3033,16 @@ import fs20 from "fs";
3033
3033
  import os3 from "os";
3034
3034
 
3035
3035
  // src/server/snapshots.ts
3036
+ import { createHash as createHash5 } from "crypto";
3036
3037
  import fs17 from "fs";
3037
3038
  import path17 from "path";
3039
+ function sourceHash(file) {
3040
+ try {
3041
+ return createHash5("sha1").update(fs17.readFileSync(file)).digest("hex").slice(0, 16);
3042
+ } catch {
3043
+ return null;
3044
+ }
3045
+ }
3038
3046
  var SAFE = /^@?[a-z0-9][a-z0-9_-]*$/i;
3039
3047
  function isSafeSegment(value) {
3040
3048
  return SAFE.test(value);
@@ -3050,6 +3058,12 @@ var SnapshotStore = class {
3050
3058
  metaFile(canvas) {
3051
3059
  return path17.join(this.dir, canvas, "meta.json");
3052
3060
  }
3061
+ /** Written aside and renamed: a run stopped mid-write leaves the old file, not half of one. */
3062
+ writeMeta(canvas, data) {
3063
+ const temp = `${this.metaFile(canvas)}.${process.pid}.tmp`;
3064
+ fs17.writeFileSync(temp, JSON.stringify(data));
3065
+ fs17.renameSync(temp, this.metaFile(canvas));
3066
+ }
3053
3067
  load(canvas) {
3054
3068
  const cached = this.meta.get(canvas);
3055
3069
  if (cached) return cached;
@@ -3084,7 +3098,21 @@ var SnapshotStore = class {
3084
3098
  if (!isSafeSegment(canvas) || !isSafeSegment(id2)) return null;
3085
3099
  return this.load(canvas)[id2]?.[theme2] ?? null;
3086
3100
  }
3087
- save(canvas, id2, theme2, png, height) {
3101
+ /** The source hash recorded with the snapshot; undefined for snapshots taken before hashes were kept. */
3102
+ source(canvas, id2, theme2) {
3103
+ if (!isSafeSegment(canvas) || !isSafeSegment(id2)) return void 0;
3104
+ return this.load(canvas)[id2]?.sources?.[theme2];
3105
+ }
3106
+ /** Record `hash` for a snapshot taken before hashes were kept: from now on it counts as that content's. */
3107
+ adopt(canvas, id2, theme2, hash) {
3108
+ if (!isSafeSegment(canvas) || !isSafeSegment(id2)) return;
3109
+ const data = this.load(canvas);
3110
+ const entry = data[id2];
3111
+ if (!entry?.[theme2]) return;
3112
+ entry.sources = { ...entry.sources, [theme2]: hash };
3113
+ this.writeMeta(canvas, data);
3114
+ }
3115
+ save(canvas, id2, theme2, png, height, source) {
3088
3116
  if (!isSafeSegment(canvas) || !isSafeSegment(id2)) throw new Error("bad snapshot key");
3089
3117
  const dir = path17.join(this.dir, canvas);
3090
3118
  fs17.mkdirSync(dir, { recursive: true });
@@ -3093,10 +3121,12 @@ var SnapshotStore = class {
3093
3121
  fs17.renameSync(`${file}.${process.pid}.tmp`, file);
3094
3122
  const data = this.load(canvas);
3095
3123
  const version = Date.now();
3096
- data[id2] = { ...data[id2], [theme2]: version, ...height ? { height } : {} };
3097
- const temp = `${this.metaFile(canvas)}.${process.pid}.tmp`;
3098
- fs17.writeFileSync(temp, JSON.stringify(data));
3099
- fs17.renameSync(temp, this.metaFile(canvas));
3124
+ const previous = data[id2];
3125
+ const sources = { ...previous?.sources };
3126
+ if (source) sources[theme2] = source;
3127
+ else delete sources[theme2];
3128
+ data[id2] = { ...previous, [theme2]: version, ...height ? { height } : {}, sources };
3129
+ this.writeMeta(canvas, data);
3100
3130
  return { url: this.url(canvas, id2, theme2, version) };
3101
3131
  }
3102
3132
  /** Absolute path of `<id>.<theme>.png`, or of its thumbnail for `<id>.<theme>.thumb.png` (made on first use); null when missing. */
@@ -3353,13 +3383,18 @@ function staleSnapshots(paths, canvasIds, filter = {}) {
3353
3383
  if (filter.screens && !filter.screens.has(job.id)) continue;
3354
3384
  const file = files.get(job.id);
3355
3385
  if (!file || !fs20.existsSync(file)) continue;
3356
- const changed = fs20.statSync(file).mtimeMs;
3386
+ const hash = sourceHash(file);
3357
3387
  const pinned = inDark.get(job.id) === job.theme;
3358
3388
  const themes = pinned ? [job.theme] : THEMES2;
3359
- const screen = { id: job.id };
3360
3389
  for (const theme2 of themes) {
3361
- const taken = store.version(id2, screen.id, theme2);
3362
- if (!taken || taken < changed) stale.push({ canvas: id2, screen: screen.id, theme: theme2 });
3390
+ if (!store.version(id2, job.id, theme2)) {
3391
+ stale.push({ canvas: id2, screen: job.id, theme: theme2, source: hash });
3392
+ continue;
3393
+ }
3394
+ const recorded = store.source(id2, job.id, theme2);
3395
+ if (recorded === void 0) {
3396
+ if (hash) store.adopt(id2, job.id, theme2, hash);
3397
+ } else if (recorded !== hash) stale.push({ canvas: id2, screen: job.id, theme: theme2, source: hash });
3363
3398
  }
3364
3399
  }
3365
3400
  }
@@ -3391,7 +3426,7 @@ async function takeSnapshots(paths, targets, preview, progress2) {
3391
3426
  const line = () => `Snapshots ${done}/${targets.length} (both themes, as the viewer and the cloud show them)`;
3392
3427
  progress2.update(line());
3393
3428
  try {
3394
- await mapLimit([...targets], TABS, async ({ canvas, screen, theme: theme2 }) => {
3429
+ await mapLimit([...targets], TABS, async ({ canvas, screen, theme: theme2, source }) => {
3395
3430
  const job = canvasFrames(docOf(canvas), url, theme2, { id: screen })[0];
3396
3431
  if (job) {
3397
3432
  const page = await browser.newPage({
@@ -3404,7 +3439,7 @@ async function takeSnapshots(paths, targets, preview, progress2) {
3404
3439
  if (report.ready) {
3405
3440
  const png = await page.screenshot({ fullPage: job.fullPage });
3406
3441
  const height = job.fullPage ? await page.evaluate(() => document.documentElement.scrollHeight) : void 0;
3407
- store.save(canvas, screen, theme2, png, height);
3442
+ store.save(canvas, screen, theme2, png, height, source);
3408
3443
  } else failed.push(`${canvas}/${screen} (${theme2})`);
3409
3444
  } catch {
3410
3445
  failed.push(`${canvas}/${screen} (${theme2})`);
@@ -4236,7 +4271,8 @@ async function handleApi(ctx, req, res, url) {
4236
4271
  try {
4237
4272
  const body = await readBody(req);
4238
4273
  if (body.subarray(0, 8).toString("hex") !== "89504e470d0a1a0a") throw new Error("not a PNG");
4239
- const { url: snapshotUrl } = snapshots.save(canvas, id2, theme2, body, height);
4274
+ const file = project.source(canvas, id2)?.file;
4275
+ const { url: snapshotUrl } = snapshots.save(canvas, id2, theme2, body, height, file ? sourceHash(file) : null);
4240
4276
  project.invalidateCanvas(canvas);
4241
4277
  events.send({ type: "snapshot", canvas, id: id2, theme: theme2, url: snapshotUrl, height });
4242
4278
  sendJson(res, 200, { url: snapshotUrl });