ferst-core 0.9.5 → 0.9.7
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/cms-config.mjs +4 -47
- package/bin/ferst-modules.mjs +1 -1
- package/bin/ferst-update.mjs +58 -8
- package/components/recipes/Form.astro +1 -1
- package/lib/git-base.mjs +62 -0
- package/package.json +1 -1
package/bin/cms-config.mjs
CHANGED
|
@@ -60,53 +60,10 @@ export function repoSlug() {
|
|
|
60
60
|
return '';
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
* The platform git-proxy origin (CMS `base_url` / `api_root`) for THIS build:
|
|
68
|
-
* 1. `FERST_GIT_BASE` env — explicit override, always wins.
|
|
69
|
-
* 2. committed `ferst-site.json` `gitBase` — an explicit per-site pin (e.g. a
|
|
70
|
-
* custom proxy); a BYO/seeded site can hard-bind its origin this way.
|
|
71
|
-
* 3. otherwise DERIVE from the deploy environment: the production branch talks to
|
|
72
|
-
* the prod git-proxy, every other (preview/dev) branch talks to the dev
|
|
73
|
-
* git-proxy. So a templated repo self-configures per environment on deploy — a
|
|
74
|
-
* dev-branch build points its CMS at the dev worker and main at prod — with NO
|
|
75
|
-
* per-client config. (The production branch defaults to `main`; override via
|
|
76
|
-
* `FERST_PROD_BRANCH` env or `ferst-site.json` `productionBranch`.)
|
|
77
|
-
* This restores the "site respects the environment it's deployed in" binding the old
|
|
78
|
-
* per-branch site config gave, without a per-client file to maintain.
|
|
79
|
-
*/
|
|
80
|
-
export function resolveGitBase(cwd = process.cwd(), branch = deploymentBranch()) {
|
|
81
|
-
const fromEnv = env('FERST_GIT_BASE');
|
|
82
|
-
if (fromEnv) return fromEnv;
|
|
83
|
-
let prodBranch = env('FERST_PROD_BRANCH') || 'main';
|
|
84
|
-
try {
|
|
85
|
-
const site = JSON.parse(readFileSync(resolve(cwd, 'ferst-site.json'), 'utf8'));
|
|
86
|
-
if (site && typeof site.gitBase === 'string' && site.gitBase.trim()) return site.gitBase.trim();
|
|
87
|
-
if (!env('FERST_PROD_BRANCH') && site && typeof site.productionBranch === 'string' && site.productionBranch.trim()) {
|
|
88
|
-
prodBranch = site.productionBranch.trim();
|
|
89
|
-
}
|
|
90
|
-
} catch {
|
|
91
|
-
/* no ferst-site.json (e.g. the template repo itself) — derive from the branch */
|
|
92
|
-
}
|
|
93
|
-
return branch === prodBranch ? PROD_GIT_BASE : DEV_GIT_BASE;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** The deployment branch: CF Pages env → current git branch → main. */
|
|
97
|
-
export function deploymentBranch() {
|
|
98
|
-
const cf = env('CF_PAGES_BRANCH');
|
|
99
|
-
if (cf) return cf;
|
|
100
|
-
try {
|
|
101
|
-
const git = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
102
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
103
|
-
}).toString().trim();
|
|
104
|
-
if (git && git !== 'HEAD') return git;
|
|
105
|
-
} catch {
|
|
106
|
-
/* not a git checkout — fall through */
|
|
107
|
-
}
|
|
108
|
-
return 'main';
|
|
109
|
-
}
|
|
63
|
+
// The git-proxy origin lives in lib/git-base.mjs, which components can import: this file is a
|
|
64
|
+
// build-time script whose module scope needs a real filesystem (TEMPLATE_PATH).
|
|
65
|
+
export { PROD_GIT_BASE, DEV_GIT_BASE, resolveGitBase, deploymentBranch } from '../lib/git-base.mjs';
|
|
66
|
+
import { resolveGitBase, deploymentBranch } from '../lib/git-base.mjs';
|
|
110
67
|
|
|
111
68
|
/** Ferst's managed GitHub org: its sites are on the Basic plan. */
|
|
112
69
|
const MANAGED_OWNER = 'myai4webdev';
|
package/bin/ferst-modules.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { readFileSync, realpathSync } from 'node:fs';
|
|
13
13
|
import { resolve } from 'node:path';
|
|
14
14
|
import { pathToFileURL } from 'node:url';
|
|
15
|
-
import { resolveGitBase } from '
|
|
15
|
+
import { resolveGitBase } from '../lib/git-base.mjs';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* True when THIS file is run as a CLI (not imported). Each bin needs its own copy:
|
package/bin/ferst-update.mjs
CHANGED
|
@@ -294,6 +294,30 @@ export function mergeSiteMeta(existing, provenance) {
|
|
|
294
294
|
return { ...base, lastUpdate: provenance };
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
/**
|
|
298
|
+
* What the update would leave in one template-owned file: the template's copy, then the
|
|
299
|
+
* same adjustments the real run makes (package.json keeps the site's own name/description and
|
|
300
|
+
* takes the canonical engine range; the manifest takes the canonical coreRange). The dry run
|
|
301
|
+
* compares this, not the raw template copy, with the site's file, so it reports only real
|
|
302
|
+
* changes (CTK #20, OLR #23). Pure.
|
|
303
|
+
*/
|
|
304
|
+
export function scaffoldFileAfterUpdate(path, upstreamRaw, currentRaw, coreRange) {
|
|
305
|
+
let out = upstreamRaw;
|
|
306
|
+
if (path === 'package.json') {
|
|
307
|
+
if (currentRaw != null) out = keepClientPackageFields(out, currentRaw) ?? out;
|
|
308
|
+
if (coreRange) out = replacePackageCoreDep(out, coreRange) ?? out;
|
|
309
|
+
} else if (path === MANIFEST && coreRange) {
|
|
310
|
+
out = replaceCoreRange(out, coreRange) ?? out;
|
|
311
|
+
}
|
|
312
|
+
return out;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Whether a new drift stamp records a different state than the last (the time alone isn't). */
|
|
316
|
+
export function stampChanged(previous, next) {
|
|
317
|
+
if (!previous || typeof previous !== 'object') return true;
|
|
318
|
+
return ['templateUpstream', 'templateCommit', 'templateVersion', 'coreVersion'].some((k) => (previous[k] ?? null) !== (next[k] ?? null));
|
|
319
|
+
}
|
|
320
|
+
|
|
297
321
|
// ── thin side-effecting shell (kept out of the pure helpers above) ────────────────
|
|
298
322
|
|
|
299
323
|
const exec = (cmd, cwd) => execSync(cmd, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
@@ -413,11 +437,25 @@ function main() {
|
|
|
413
437
|
|
|
414
438
|
let templateVersion = null; // the template version this sync brought (for the manifest + stamp)
|
|
415
439
|
let scaffoldCount = 0; // files the scaffold sync touched (counted BEFORE the npm churn)
|
|
440
|
+
let dryScaffold = []; // dry-run only: the files the update would really change
|
|
416
441
|
if (dryRun) {
|
|
442
|
+
// Compare what the update would leave (the site's package identity, the canonical engine
|
|
443
|
+
// range), not the raw template copy, so only real changes count (CTK #20, OLR #23).
|
|
417
444
|
const spec = syncPaths.map(q).join(' ');
|
|
418
|
-
const
|
|
445
|
+
const target = noCore
|
|
446
|
+
? readInstalledCoreVersion(cwd)
|
|
447
|
+
: (tryExec('npm view ferst-core version', cwd) || '').trim() || readInstalledCoreVersion(cwd);
|
|
448
|
+
const range = target ? coreRangeFor(target) : null;
|
|
449
|
+
const candidates = (tryGit(`diff --name-only ${TEMPLATE_REMOTE}/${branch} -- ${spec}`, cwd) || '').split(/\r?\n/).filter(Boolean);
|
|
450
|
+
for (const f of candidates) {
|
|
451
|
+
const upstreamRaw = tryGit(`show ${TEMPLATE_REMOTE}/${branch}:${f}`, cwd);
|
|
452
|
+
let current = null;
|
|
453
|
+
try { current = readFileSync(resolve(cwd, f), 'utf8'); } catch { /* a file the site doesn't have yet */ }
|
|
454
|
+
const lf = (t) => t.replace(/\r\n/g, '\n'); // a checkout's line endings aren't a change
|
|
455
|
+
if (upstreamRaw === null || current === null || lf(scaffoldFileAfterUpdate(f, lf(upstreamRaw), lf(current), range)) !== lf(current)) dryScaffold.push(f);
|
|
456
|
+
}
|
|
419
457
|
console.log('ferst-update: --dry-run — scaffold changes that WOULD apply:');
|
|
420
|
-
console.log(
|
|
458
|
+
console.log(dryScaffold.length ? dryScaffold.map((f) => ` ${f}`).join('\n') : ' (no scaffold changes)');
|
|
421
459
|
} else {
|
|
422
460
|
// package.json is template-owned, but its identity fields are the client's — keep them.
|
|
423
461
|
const pkgPath = resolve(cwd, 'package.json');
|
|
@@ -430,6 +468,10 @@ function main() {
|
|
|
430
468
|
console.warn(`ferst-update: could not sync "${p}" (not present upstream?) — skipped.`);
|
|
431
469
|
}
|
|
432
470
|
}
|
|
471
|
+
// `git checkout <tree> -- path` also stages the template's copy. Unstage it, so the index
|
|
472
|
+
// never holds a copy that the steps below put back (OLR #23 saw `MM`), and a commit takes
|
|
473
|
+
// only what the update really changed.
|
|
474
|
+
tryGit(`reset -q -- ${syncPaths.map(q).join(' ')}`, cwd);
|
|
433
475
|
if (clientPkg) {
|
|
434
476
|
try {
|
|
435
477
|
const synced = readFileSync(pkgPath, 'utf8');
|
|
@@ -522,7 +564,10 @@ function main() {
|
|
|
522
564
|
if (dryRun) {
|
|
523
565
|
const wouldCommit = templateCommit ? templateCommit.slice(0, 7) : '(unknown)';
|
|
524
566
|
const wouldCore = (noCore ? readInstalledCoreVersion(cwd) : dryLatest) ?? '(unknown)';
|
|
525
|
-
|
|
567
|
+
const prev = readSiteMeta(cwd)?.lastUpdate;
|
|
568
|
+
const wouldMove = stampChanged(prev, { ...(prev ?? {}), templateCommit: templateCommit ?? null, coreVersion: wouldCore === '(unknown)' ? prev?.coreVersion ?? null : wouldCore });
|
|
569
|
+
if (!dryScaffold.length && !wouldMove) console.log('ferst-update: --dry-run — nothing to update (scaffold and engine current).');
|
|
570
|
+
else console.log(`ferst-update: --dry-run — WOULD stamp ${SITE_META}: template ${wouldCommit}, core ${wouldCore} (no write).`);
|
|
526
571
|
console.log('ferst-update: --dry-run complete (no writes, no verify).');
|
|
527
572
|
return;
|
|
528
573
|
}
|
|
@@ -542,10 +587,15 @@ function main() {
|
|
|
542
587
|
// and any other existing ferst-site.json fields.
|
|
543
588
|
const coreVersion = coreBump?.after ?? readInstalledCoreVersion(cwd);
|
|
544
589
|
const provenance = buildProvenance({ upstream: upstream.url, commit: templateCommit, coreVersion, templateVersion: templateVersion ?? readTemplateVersion(raw) });
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
);
|
|
590
|
+
// Only when the site actually moved: re-stamping just the time left a pointless commit (and
|
|
591
|
+
// a test-copy redeploy) on every scheduled run.
|
|
592
|
+
const existingMeta = readSiteMeta(cwd);
|
|
593
|
+
const restamp = stampChanged(existingMeta?.lastUpdate, provenance);
|
|
594
|
+
if (restamp) {
|
|
595
|
+
writeFileSync(resolve(cwd, SITE_META), `${JSON.stringify(mergeSiteMeta(existingMeta, provenance), null, 2)}\n`);
|
|
596
|
+
} else {
|
|
597
|
+
console.log(`ferst-update: ${SITE_META} already records this template and engine — not re-stamped.`);
|
|
598
|
+
}
|
|
549
599
|
const stampSha = templateCommit ? templateCommit.slice(0, 7) : '(unknown)';
|
|
550
600
|
|
|
551
601
|
// 6. Summary.
|
|
@@ -554,7 +604,7 @@ function main() {
|
|
|
554
604
|
: 'ferst-core unchanged (--no-core)';
|
|
555
605
|
console.log(
|
|
556
606
|
`ferst-update: DONE — scaffold files updated: ${scaffoldCount}; ${coreLine}; ` +
|
|
557
|
-
|
|
607
|
+
`${restamp ? 'stamped' : 'already recorded in'} ${SITE_META}: template ${stampSha}, core ${coreVersion ?? '(unknown)'}; build + check:thin green.`,
|
|
558
608
|
);
|
|
559
609
|
}
|
|
560
610
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import ContactForm from './ContactForm.astro';
|
|
9
9
|
import { loadFormDefinition } from '../../lib/forms';
|
|
10
|
-
import { resolveGitBase } from '../../
|
|
10
|
+
import { resolveGitBase } from '../../lib/git-base.mjs';
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
13
13
|
form: string;
|
package/lib/git-base.mjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The platform git-proxy origin for this build (the CMS `base_url`, and where the Form block
|
|
3
|
+
* reads a form's fields). Kept apart from bin/cms-config.mjs so components can import it: that
|
|
4
|
+
* file is a build-time script whose module scope needs a real filesystem, which an on-request
|
|
5
|
+
* renderer (Cloudflare Workers) doesn't have. Nothing here runs at import time.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import { resolve } from 'node:path';
|
|
10
|
+
|
|
11
|
+
function env(name) {
|
|
12
|
+
const v = typeof process !== 'undefined' ? process.env?.[name] : undefined;
|
|
13
|
+
return v && v.trim() ? v.trim() : '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const PROD_GIT_BASE = 'https://signin.ferst.co.uk';
|
|
17
|
+
export const DEV_GIT_BASE = 'https://signin-dev.ferst.co.uk';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The platform git-proxy origin (CMS `base_url` / `api_root`) for THIS build:
|
|
21
|
+
* 1. `FERST_GIT_BASE` env — explicit override, always wins.
|
|
22
|
+
* 2. committed `ferst-site.json` `gitBase` — an explicit per-site pin (e.g. a
|
|
23
|
+
* custom proxy); a BYO/seeded site can hard-bind its origin this way.
|
|
24
|
+
* 3. otherwise DERIVE from the deploy environment: the production branch talks to
|
|
25
|
+
* the prod git-proxy, every other (preview/dev) branch talks to the dev
|
|
26
|
+
* git-proxy. So a templated repo self-configures per environment on deploy — a
|
|
27
|
+
* dev-branch build points its CMS at the dev worker and main at prod — with NO
|
|
28
|
+
* per-client config. (The production branch defaults to `main`; override via
|
|
29
|
+
* `FERST_PROD_BRANCH` env or `ferst-site.json` `productionBranch`.)
|
|
30
|
+
* This restores the "site respects the environment it's deployed in" binding the old
|
|
31
|
+
* per-branch site config gave, without a per-client file to maintain.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveGitBase(cwd = process.cwd(), branch = deploymentBranch()) {
|
|
34
|
+
const fromEnv = env('FERST_GIT_BASE');
|
|
35
|
+
if (fromEnv) return fromEnv;
|
|
36
|
+
let prodBranch = env('FERST_PROD_BRANCH') || 'main';
|
|
37
|
+
try {
|
|
38
|
+
const site = JSON.parse(readFileSync(resolve(cwd, 'ferst-site.json'), 'utf8'));
|
|
39
|
+
if (site && typeof site.gitBase === 'string' && site.gitBase.trim()) return site.gitBase.trim();
|
|
40
|
+
if (!env('FERST_PROD_BRANCH') && site && typeof site.productionBranch === 'string' && site.productionBranch.trim()) {
|
|
41
|
+
prodBranch = site.productionBranch.trim();
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
/* no ferst-site.json (e.g. the template repo itself) — derive from the branch */
|
|
45
|
+
}
|
|
46
|
+
return branch === prodBranch ? PROD_GIT_BASE : DEV_GIT_BASE;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The deployment branch: CF Pages env → current git branch → main. */
|
|
50
|
+
export function deploymentBranch() {
|
|
51
|
+
const cf = env('CF_PAGES_BRANCH');
|
|
52
|
+
if (cf) return cf;
|
|
53
|
+
try {
|
|
54
|
+
const git = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
55
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
56
|
+
}).toString().trim();
|
|
57
|
+
if (git && git !== 'HEAD') return git;
|
|
58
|
+
} catch {
|
|
59
|
+
/* not a git checkout — fall through */
|
|
60
|
+
}
|
|
61
|
+
return 'main';
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ferst-core",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.7",
|
|
4
4
|
"type": "module",
|
|
5
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",
|