hubspot-cms-sync 0.3.0 → 0.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hubspot-cms-sync",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Git-backed bidirectional sync for HubSpot CMS themes, content, blogs, forms, and assets.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,12 +45,14 @@ import {
45
45
  mkdirSync,
46
46
  readdirSync,
47
47
  existsSync,
48
+ rmSync,
48
49
  } from 'node:fs';
49
50
  import { join, resolve as resolvePath, basename, extname } from 'node:path';
50
51
  import { createHash } from 'node:crypto';
51
52
 
52
53
  import { hub, getAll } from '../lib/hub.mjs';
53
54
  import { stableStringify } from '../lib/canonical.mjs';
55
+ import { wireToFile, fileToWire } from '../lib/posts-format.mjs';
54
56
  import { resolve as resolveRefs, canonicalize as canonicalizeRefs } from '../lib/refs.mjs';
55
57
  import { resolveCtaEmbeds, loadInventory } from '../cta-inventory.mjs';
56
58
 
@@ -317,10 +319,13 @@ export async function pull(acct, { contentDir, registry }) {
317
319
  // (codex #7). It is content here, not a volatile timestamp to strip.
318
320
  publishDate: p.publishDate || null,
319
321
  };
320
- writeFileSync(
321
- join(postsOut, `${postFileFor(p.slug)}.json`),
322
- stableStringify(portable),
323
- );
322
+ // Canonical post format is frontmatter + HTML body (.md). Reshaping is lossless
323
+ // to the wire object (lib/posts-format.mjs round-trip), so the push payload is
324
+ // byte-identical to the old .json path. Drop any stale sibling .json from the
325
+ // pre-frontmatter format so push never sees the same post twice.
326
+ const base = join(postsOut, postFileFor(p.slug));
327
+ writeFileSync(`${base}.md`, wireToFile(portable));
328
+ if (existsSync(`${base}.json`)) rmSync(`${base}.json`);
324
329
  pulled++;
325
330
  }
326
331
 
@@ -372,6 +377,10 @@ export async function push(
372
377
  registry,
373
378
  publish = false,
374
379
  limit,
380
+ // only: restrict the push to specific posts by file base name (no extension),
381
+ // e.g. ['blog__hello']. Enables a scoped sample push without touching the rest
382
+ // of the blog — used by verification harnesses; undefined means "all posts".
383
+ only,
375
384
  dryRun = false,
376
385
  hubFn = hub,
377
386
  // Injectable clock + sleep so the "wait past every scheduled publish" gate
@@ -393,12 +402,29 @@ export async function push(
393
402
  // then replaces @asset tokens below. (The old blog-local rehostAssets path is
394
403
  // retired: one upload location, no /blog-migrated vs /synced-assets split.)
395
404
 
396
- let files = readdirSync(postsDir).filter((f) => f.endsWith('.json'));
397
- files.sort();
405
+ // Accept the canonical frontmatter format (.md) and the legacy .json. If both
406
+ // exist for one post, .md wins; dedup by base name so a post is never pushed
407
+ // twice during the transition.
408
+ const byBase = new Map();
409
+ for (const f of readdirSync(postsDir)) {
410
+ const m = /^(.*)\.(md|json)$/.exec(f);
411
+ if (!m) continue;
412
+ const [, base, ext] = m;
413
+ if (ext === 'md' || !byBase.has(base)) byBase.set(base, f);
414
+ }
415
+ let files = [...byBase.values()].sort();
416
+ if (only) {
417
+ const want = new Set(only);
418
+ files = files.filter((f) => want.has(f.replace(/\.(md|json)$/, '')));
419
+ }
398
420
  if (limit) files = files.slice(0, limit);
399
421
 
400
- // Group posts by their blogSlug and resolve each container exactly once.
401
- const posts = files.map((f) => JSON.parse(readFileSync(join(postsDir, f), 'utf8')));
422
+ // Group posts by their blogSlug and resolve each container exactly once. The
423
+ // frontmatter codec yields the same wire object JSON.parse would have.
424
+ const posts = files.map((f) => {
425
+ const raw = readFileSync(join(postsDir, f), 'utf8');
426
+ return f.endsWith('.md') ? fileToWire(raw) : JSON.parse(raw);
427
+ });
402
428
  const containerCache = new Map();
403
429
  async function containerIdFor(blogSlug) {
404
430
  if (containerCache.has(blogSlug)) return containerCache.get(blogSlug);