ferst-core 0.7.11 → 0.7.13
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/bin/ferst-update.mjs +72 -17
- package/components/recipes/Hero.astro +19 -10
- package/package.json +2 -2
package/bin/ferst-update.mjs
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
// npx --no-install ferst-update --dry-run # fetch + show what WOULD change; no writes
|
|
12
12
|
// npx --no-install ferst-update --no-core # scaffold sync only; skip the npm bump
|
|
13
13
|
//
|
|
14
|
-
// The
|
|
15
|
-
//
|
|
14
|
+
// The git upstream comes from the repo's own `ferst-template.json` (the ownership
|
|
15
|
+
// manifest); the paths to sync come from the TEMPLATE's manifest at its latest version, so
|
|
16
|
+
// a path it newly owns arrives in the same run (falling back to the repo's list). Client paths are NEVER touched —
|
|
16
17
|
// the scaffold sync is a per-path OVERWRITE from upstream, conflict-free by
|
|
17
18
|
// construction (see docs/CLIENT-TEMPLATE-ARCHITECTURE.md, Workstream D). It never
|
|
18
19
|
// merges/rebases (the CMS bot commits to client repos continuously; a history merge
|
|
@@ -87,6 +88,35 @@ export function resolveOwnedPaths(manifest) {
|
|
|
87
88
|
return owned;
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
/**
|
|
92
|
+
* The paths to sync: the TEMPLATE's own list at its latest version, so a path it newly
|
|
93
|
+
* owns arrives in the same run. Falls back to the site's list when the template's
|
|
94
|
+
* manifest can't be read or declares none (an older template, a fetch problem).
|
|
95
|
+
*/
|
|
96
|
+
export function ownedPathsToSync(localOwned, upstreamManifestRaw) {
|
|
97
|
+
try {
|
|
98
|
+
return resolveOwnedPaths(parseManifest(upstreamManifestRaw));
|
|
99
|
+
} catch {
|
|
100
|
+
return localOwned;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The manifest text to build on after the sync. The manifest is itself template-owned, so
|
|
106
|
+
* the sync has just replaced it: build on that, or the site's old ownedPaths would be
|
|
107
|
+
* written back over the template's new ones. Falls back to the pre-sync text when the file
|
|
108
|
+
* is missing or unreadable.
|
|
109
|
+
*/
|
|
110
|
+
export function manifestAfterSync(preSyncRaw, postSyncRaw) {
|
|
111
|
+
if (typeof postSyncRaw !== 'string') return preSyncRaw;
|
|
112
|
+
try {
|
|
113
|
+
parseManifest(postSyncRaw);
|
|
114
|
+
return postSyncRaw;
|
|
115
|
+
} catch {
|
|
116
|
+
return preSyncRaw;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
90
120
|
/**
|
|
91
121
|
* The template git URL to sync the scaffold from. Uses the manifest's `upstream`
|
|
92
122
|
* when present; otherwise falls back to the canonical template repo and flags it so
|
|
@@ -280,11 +310,27 @@ function readSiteMeta(cwd) {
|
|
|
280
310
|
}
|
|
281
311
|
}
|
|
282
312
|
|
|
283
|
-
/**
|
|
284
|
-
|
|
313
|
+
/**
|
|
314
|
+
* The git remote the template is fetched through. Never `upstream`, `github` or `origin`:
|
|
315
|
+
* `gh` takes the first of those it finds for "this repository", so a template remote named
|
|
316
|
+
* `upstream` made `gh` act on the public template instead of the site.
|
|
317
|
+
*/
|
|
318
|
+
export const TEMPLATE_REMOTE = 'ferst-template';
|
|
319
|
+
|
|
320
|
+
/** Is this an older run's `upstream` remote that points at the template (safe to drop)? */
|
|
321
|
+
export function isLeftoverTemplateRemote(name, remoteUrl, templateUrl) {
|
|
322
|
+
const norm = (u) => String(u ?? '').trim().replace(/\/+$/, '').replace(/\.git$/, '').toLowerCase();
|
|
323
|
+
return name === 'upstream' && norm(remoteUrl) !== '' && norm(remoteUrl) === norm(templateUrl);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Ensure TEMPLATE_REMOTE points at `url`, and drop an older run's `upstream` template remote. */
|
|
327
|
+
function ensureTemplateRemote(url, cwd) {
|
|
285
328
|
const remotes = (tryGit('remote', cwd) || '').split(/\r?\n/).map((s) => s.trim());
|
|
286
|
-
if (remotes.includes('upstream')
|
|
287
|
-
|
|
329
|
+
if (remotes.includes('upstream') && isLeftoverTemplateRemote('upstream', tryGit('remote get-url upstream', cwd), url)) {
|
|
330
|
+
git('remote remove upstream', cwd);
|
|
331
|
+
}
|
|
332
|
+
if (remotes.includes(TEMPLATE_REMOTE)) git(`remote set-url ${TEMPLATE_REMOTE} ${q(url)}`, cwd);
|
|
333
|
+
else git(`remote add ${TEMPLATE_REMOTE} ${q(url)}`, cwd);
|
|
288
334
|
}
|
|
289
335
|
|
|
290
336
|
function printHelp() {
|
|
@@ -330,23 +376,26 @@ function main() {
|
|
|
330
376
|
// 2. Git scaffold sync — overwrite the template-owned paths from upstream's default
|
|
331
377
|
// branch. Never a merge/rebase; path-checkout works across unrelated histories.
|
|
332
378
|
try {
|
|
333
|
-
|
|
379
|
+
ensureTemplateRemote(upstream.url, cwd);
|
|
334
380
|
console.log(`ferst-update: fetching ${upstream.url} ...`);
|
|
335
|
-
git(
|
|
381
|
+
git(`fetch ${TEMPLATE_REMOTE}`, cwd);
|
|
336
382
|
} catch (e) {
|
|
337
383
|
console.error(`ferst-update: git fetch failed — ${tail(e.stderr || e.message, 10)}`);
|
|
338
384
|
process.exit(1);
|
|
339
385
|
}
|
|
340
|
-
const branch = parseDefaultBranch(tryGit(
|
|
386
|
+
const branch = parseDefaultBranch(tryGit(`ls-remote --symref ${TEMPLATE_REMOTE} HEAD`, cwd)) || 'main';
|
|
341
387
|
console.log(`ferst-update: upstream default branch = ${branch}`);
|
|
342
388
|
// The exact template commit this run syncs from (for the provenance stamp / #18 ledger).
|
|
343
|
-
const templateCommit = (tryGit(`rev-parse
|
|
389
|
+
const templateCommit = (tryGit(`rev-parse ${TEMPLATE_REMOTE}/${branch}`, cwd) || '').trim() || null;
|
|
390
|
+
// The template's own manifest decides what it owns now (see ownedPathsToSync).
|
|
391
|
+
const upstreamManifestRaw = tryGit(`show ${TEMPLATE_REMOTE}/${branch}:${MANIFEST}`, cwd) || '';
|
|
392
|
+
const syncPaths = ownedPathsToSync(ownedPaths, upstreamManifestRaw);
|
|
344
393
|
|
|
345
394
|
let templateVersion = null; // the template version this sync brought (for the manifest + stamp)
|
|
346
395
|
let scaffoldCount = 0; // files the scaffold sync touched (counted BEFORE the npm churn)
|
|
347
396
|
if (dryRun) {
|
|
348
|
-
const spec =
|
|
349
|
-
const diff = (tryGit(`diff --stat
|
|
397
|
+
const spec = syncPaths.map(q).join(' ');
|
|
398
|
+
const diff = (tryGit(`diff --stat ${TEMPLATE_REMOTE}/${branch} -- ${spec}`, cwd) || '').trim();
|
|
350
399
|
console.log('ferst-update: --dry-run — scaffold changes that WOULD apply:');
|
|
351
400
|
console.log(diff ? diff : ' (no scaffold changes)');
|
|
352
401
|
} else {
|
|
@@ -354,10 +403,10 @@ function main() {
|
|
|
354
403
|
const pkgPath = resolve(cwd, 'package.json');
|
|
355
404
|
let clientPkg = null;
|
|
356
405
|
try { clientPkg = readFileSync(pkgPath, 'utf8'); } catch { /* none yet */ }
|
|
357
|
-
for (const p of
|
|
406
|
+
for (const p of syncPaths) {
|
|
358
407
|
// Per-path so one path that upstream no longer ships (pathspec no-match) can't
|
|
359
408
|
// abort the whole sync — warn and continue.
|
|
360
|
-
if (tryGit(`checkout
|
|
409
|
+
if (tryGit(`checkout ${TEMPLATE_REMOTE}/${branch} -- ${q(p)}`, cwd) === null) {
|
|
361
410
|
console.warn(`ferst-update: could not sync "${p}" (not present upstream?) — skipped.`);
|
|
362
411
|
}
|
|
363
412
|
}
|
|
@@ -371,9 +420,15 @@ function main() {
|
|
|
371
420
|
}
|
|
372
421
|
} catch { /* package.json not synced — nothing to restore */ }
|
|
373
422
|
}
|
|
374
|
-
// The manifest
|
|
375
|
-
//
|
|
376
|
-
|
|
423
|
+
// The manifest is itself template-owned and was just synced: build on that text (see
|
|
424
|
+
// manifestAfterSync), so the coreRange step and the stamp see the template's manifest.
|
|
425
|
+
let syncedManifest = null;
|
|
426
|
+
try { syncedManifest = readFileSync(manifestPath, 'utf8'); } catch { /* not synced */ }
|
|
427
|
+
raw = manifestAfterSync(raw, syncedManifest);
|
|
428
|
+
manifest = parseManifest(raw);
|
|
429
|
+
// Carry the template's version across explicitly too, for a site whose manifest isn't
|
|
430
|
+
// among the synced paths; `raw` is what the coreRange step builds on.
|
|
431
|
+
const upstreamVersion = readTemplateVersion(upstreamManifestRaw);
|
|
377
432
|
if (upstreamVersion) {
|
|
378
433
|
const bumped = replaceTemplateVersion(raw, upstreamVersion);
|
|
379
434
|
if (bumped && bumped !== raw) {
|
|
@@ -46,7 +46,10 @@ const provider = bgLink ? videoEmbed(bgLink)?.provider : undefined;
|
|
|
46
46
|
const watchLabel = media?.linkLabel || (provider ? `Watch the full video on ${provider}` : 'Watch the full video');
|
|
47
47
|
// The still: the image (no motion), or an embed's poster until the player may load.
|
|
48
48
|
const stillSrc = embed ? media?.poster ?? still : !media?.video && !slides.length ? still : undefined;
|
|
49
|
-
|
|
49
|
+
// Drawn as a real <img>, never a CSS background: a picture set through a CSS variable is
|
|
50
|
+
// found late (never by the preload scanner), so the hero sat grey until it arrived, and
|
|
51
|
+
// some browsers kept it grey. A real image, loaded first, is found early and painted.
|
|
52
|
+
const heroImg = isMedia && stillSrc ? stillSrc : undefined;
|
|
50
53
|
---
|
|
51
54
|
|
|
52
55
|
<section
|
|
@@ -58,10 +61,11 @@ const bgImage = isMedia && stillSrc ? `--hero-image: url('${stillSrc}')` : '';
|
|
|
58
61
|
data-linked={bgLink ? 'true' : undefined}
|
|
59
62
|
data-parallax={isMedia && parallax ? 'true' : undefined}
|
|
60
63
|
data-full-bleed={isMedia && fullBleed ? 'true' : undefined}
|
|
61
|
-
style={isMedia ? `--hero-overlay: ${overlay}
|
|
64
|
+
style={isMedia ? `--hero-overlay: ${overlay}${focus ? `; --hero-focus: ${focus}` : ''}` : undefined}
|
|
62
65
|
>
|
|
63
66
|
{isMedia && (
|
|
64
67
|
<div class="b-hero__bg" aria-hidden="true">
|
|
68
|
+
{heroImg && <img class="b-hero__img" src={heroImg} alt="" loading="eager" fetchpriority="high" decoding="async" />}
|
|
65
69
|
{embed && <div class="b-hero__embed" data-embed-url={embed.embedUrl} data-provider={embed.provider}></div>}
|
|
66
70
|
{media?.video && !embed ? (
|
|
67
71
|
<video autoplay muted loop playsinline poster={media.poster} preload="metadata">
|
|
@@ -69,7 +73,7 @@ const bgImage = isMedia && stillSrc ? `--hero-image: url('${stillSrc}')` : '';
|
|
|
69
73
|
</video>
|
|
70
74
|
) : null}
|
|
71
75
|
{slides.map((src, i) => (
|
|
72
|
-
<
|
|
76
|
+
<img class:list={['b-hero__slide', { 'is-on': i === 0 }]} src={src} alt="" loading={i === 0 ? 'eager' : 'lazy'} fetchpriority={i === 0 ? 'high' : undefined} decoding="async" />
|
|
73
77
|
))}
|
|
74
78
|
<span class="b-hero__scrim"></span>
|
|
75
79
|
</div>
|
|
@@ -346,11 +350,20 @@ const bgImage = isMedia && stillSrc ? `--hero-image: url('${stillSrc}')` : '';
|
|
|
346
350
|
position: absolute;
|
|
347
351
|
inset: 0;
|
|
348
352
|
z-index: -1;
|
|
349
|
-
background-image: var(--hero-image, none);
|
|
350
|
-
background-size: cover;
|
|
351
|
-
background-position: var(--hero-focus, center);
|
|
352
353
|
background-color: var(--bg-section);
|
|
353
354
|
}
|
|
355
|
+
/* The picture (and each rotating slide) covers the band, framed by `focus`. */
|
|
356
|
+
.b-hero__img,
|
|
357
|
+
.b-hero__slide {
|
|
358
|
+
position: absolute;
|
|
359
|
+
inset: 0;
|
|
360
|
+
width: 100%;
|
|
361
|
+
height: 100%;
|
|
362
|
+
object-fit: cover;
|
|
363
|
+
object-position: var(--hero-focus, center);
|
|
364
|
+
display: block;
|
|
365
|
+
max-width: none;
|
|
366
|
+
}
|
|
354
367
|
/* Parallax: give the background room to drift without uncovering an edge. */
|
|
355
368
|
.b-hero.is-parallax .b-hero__bg {
|
|
356
369
|
inset: 0 0 -35% 0;
|
|
@@ -390,10 +403,6 @@ const bgImage = isMedia && stillSrc ? `--hero-image: url('${stillSrc}')` : '';
|
|
|
390
403
|
display: none;
|
|
391
404
|
}
|
|
392
405
|
.b-hero__slide {
|
|
393
|
-
position: absolute;
|
|
394
|
-
inset: 0;
|
|
395
|
-
background-size: cover;
|
|
396
|
-
background-position: var(--hero-focus, center);
|
|
397
406
|
opacity: 0;
|
|
398
407
|
transition: opacity 1.6s ease-in-out;
|
|
399
408
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ferst-core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.13",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Ferst Core
|
|
5
|
+
"description": "Ferst Core — the shared, brand-agnostic client-site system: Astro components, layouts, content schemas, the layered config resolver, and a neutral styling architecture that every client overrides.",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
7
7
|
"author": "MyAI4 Ltd (https://ferst.co.uk)",
|
|
8
8
|
"homepage": "https://ferst.co.uk",
|