ferst-core 0.9.0 → 0.9.2
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 +44 -7
- package/bin/ferst-update.mjs +21 -2
- package/cms/config.template.yml +13 -1
- package/content/posts.ts +2 -2
- package/package.json +1 -1
package/bin/cms-config.mjs
CHANGED
|
@@ -108,18 +108,47 @@ export function deploymentBranch() {
|
|
|
108
108
|
return 'main';
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/** Ferst's managed GitHub org: its sites are on the Basic plan. */
|
|
112
|
+
const MANAGED_OWNER = 'myai4webdev';
|
|
113
|
+
|
|
114
|
+
/** The Pages collection's description on each plan. */
|
|
115
|
+
const PAGES_DESCRIPTION = {
|
|
116
|
+
basic: "See each page and request a change — our team makes it and sends it back for you to check. You don't edit the page layout here.",
|
|
117
|
+
free: "You're on the free plan: you look after this site yourself and change its pages in your own repository. Asking the Ferst team to make changes for you comes with the Basic plan.",
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The site's plan, for what its CMS offers. Requests (the Ferst team making a change for the
|
|
122
|
+
* site's people) come with the Basic plan: a managed site. A self-managed site, the client's
|
|
123
|
+
* own repo, is on the free plan and makes its own changes. The plan the platform records in
|
|
124
|
+
* `ferst-site.json` wins; otherwise the repo's owner decides. Unknown keeps the requests field:
|
|
125
|
+
* the platform refuses a free-plan site's request anyway.
|
|
126
|
+
* @param {string} repo owner/name
|
|
127
|
+
* @param {{ plan?: string }} [site] the parsed ferst-site.json
|
|
128
|
+
* @returns {'basic' | 'free'}
|
|
129
|
+
*/
|
|
130
|
+
export function planFor(repo, site = {}) {
|
|
131
|
+
if (site && (site.plan === 'basic' || site.plan === 'free')) return site.plan;
|
|
132
|
+
const owner = String(repo ?? '').split('/')[0].trim().toLowerCase();
|
|
133
|
+
if (!owner) return 'basic';
|
|
134
|
+
return owner === MANAGED_OWNER ? 'basic' : 'free';
|
|
135
|
+
}
|
|
136
|
+
|
|
111
137
|
/**
|
|
112
|
-
* Fill the config template's placeholders. Pure — unit-testable.
|
|
138
|
+
* Fill the config template's placeholders. Pure — unit-testable. On the free plan the
|
|
139
|
+
* `# ferst:requests` … `# /ferst:requests` block (the pages' "Requested changes") is left out.
|
|
113
140
|
* @param {string} tpl the template contents
|
|
114
|
-
* @param {{ repo: string, base: string, branch: string }} vars
|
|
141
|
+
* @param {{ repo: string, base: string, branch: string, plan?: 'basic' | 'free' }} vars
|
|
115
142
|
* @returns {string}
|
|
116
143
|
*/
|
|
117
|
-
export function fillConfig(tpl, { repo, base, branch }) {
|
|
118
|
-
const
|
|
144
|
+
export function fillConfig(tpl, { repo, base, branch, plan = 'basic' }) {
|
|
145
|
+
const forPlan = plan === 'free' ? tpl.replace(/^[ \t]*# ferst:requests\b[\s\S]*?^[ \t]*# \/ferst:requests[^\n]*\n/m, '') : tpl;
|
|
146
|
+
const out = forPlan
|
|
119
147
|
.split('__REPO__').join(repo)
|
|
120
148
|
.split('__API_ROOT__').join(`${base}/git`)
|
|
121
149
|
.split('__BASE_URL__').join(base)
|
|
122
|
-
.split('__BRANCH__').join(branch)
|
|
150
|
+
.split('__BRANCH__').join(branch)
|
|
151
|
+
.split('__PAGES_DESCRIPTION__').join(PAGES_DESCRIPTION[plan] ?? PAGES_DESCRIPTION.basic);
|
|
123
152
|
const leftover = out.match(/__[A-Z][A-Z_]*__/);
|
|
124
153
|
if (leftover) {
|
|
125
154
|
throw new Error(`ferst-cms-config: unfilled placeholder ${leftover[0]} — add it to the filler.`);
|
|
@@ -147,9 +176,17 @@ function main() {
|
|
|
147
176
|
process.exit(1);
|
|
148
177
|
}
|
|
149
178
|
|
|
179
|
+
let site = {};
|
|
180
|
+
try {
|
|
181
|
+
site = JSON.parse(readFileSync(resolve(process.cwd(), 'ferst-site.json'), 'utf8')) ?? {};
|
|
182
|
+
} catch {
|
|
183
|
+
/* no ferst-site.json: the repo's owner decides the plan */
|
|
184
|
+
}
|
|
185
|
+
const plan = planFor(repo, site);
|
|
186
|
+
|
|
150
187
|
let out;
|
|
151
188
|
try {
|
|
152
|
-
out = fillConfig(tpl, { repo, base, branch });
|
|
189
|
+
out = fillConfig(tpl, { repo, base, branch, plan });
|
|
153
190
|
} catch (e) {
|
|
154
191
|
console.error(String(e.message ?? e));
|
|
155
192
|
process.exit(1);
|
|
@@ -157,7 +194,7 @@ function main() {
|
|
|
157
194
|
|
|
158
195
|
mkdirSync(dirname(outPath), { recursive: true });
|
|
159
196
|
writeFileSync(outPath, out);
|
|
160
|
-
console.log(`ferst-cms-config: wrote ${outPath} (repo=${repo}, branch="${branch}", base=${base})`);
|
|
197
|
+
console.log(`ferst-cms-config: wrote ${outPath} (repo=${repo}, branch="${branch}", base=${base}, plan=${plan})`);
|
|
161
198
|
}
|
|
162
199
|
|
|
163
200
|
// Run when invoked directly as a bin — including via npm's node_modules/.bin symlink.
|
package/bin/ferst-update.mjs
CHANGED
|
@@ -142,6 +142,26 @@ export function coreRangeFor(version) {
|
|
|
142
142
|
return `^${m[1]}.${m[2]}.0`;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/** Whether `b` is a newer plain x.y.z version than `a`; anything unparsable is not newer. */
|
|
146
|
+
export function isNewerVersion(a, b) {
|
|
147
|
+
const parse = (v) => String(v ?? '').trim().replace(/^v/, '').match(/^(\d+)\.(\d+)\.(\d+)$/)?.slice(1).map(Number);
|
|
148
|
+
const [x, y] = [parse(a), parse(b)];
|
|
149
|
+
if (!x || !y) return false;
|
|
150
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return y[i] > x[i];
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The dry run's line about the engine. A site's scheduled run decides from the dry run
|
|
156
|
+
* whether to update, so a newer ferst-core has to read as a change that WOULD apply, like
|
|
157
|
+
* the scaffold changes above it; otherwise an engine-only release never reaches the site.
|
|
158
|
+
*/
|
|
159
|
+
export function coreUpdateNote(installed, latest) {
|
|
160
|
+
if (!latest) return `ferst-core is ${installed ?? '(not installed)'}; the latest version is unknown`;
|
|
161
|
+
if (!installed) return `ferst-core ${latest} WOULD be installed (none installed yet)`;
|
|
162
|
+
return isNewerVersion(installed, latest) ? `ferst-core ${installed} → ${latest} WOULD be installed` : `ferst-core is current (${installed})`;
|
|
163
|
+
}
|
|
164
|
+
|
|
145
165
|
/** Read the `version` out of a package.json's text. Throws if absent. */
|
|
146
166
|
export function extractVersion(pkgJsonText) {
|
|
147
167
|
let v;
|
|
@@ -451,8 +471,7 @@ function main() {
|
|
|
451
471
|
console.log('ferst-update: --no-core — skipping the ferst-core npm bump.');
|
|
452
472
|
} else if (dryRun) {
|
|
453
473
|
dryLatest = (tryExec('npm view ferst-core version', cwd) || '').trim() || null;
|
|
454
|
-
|
|
455
|
-
console.log(`ferst-update: ferst-core installed=${current ?? '(none)'} latest=${dryLatest || '(unknown)'}`);
|
|
474
|
+
console.log(`ferst-update: --dry-run — ${coreUpdateNote(readInstalledCoreVersion(cwd), dryLatest)}`);
|
|
456
475
|
} else {
|
|
457
476
|
const before = readInstalledCoreVersion(cwd);
|
|
458
477
|
console.log('ferst-update: npm install ferst-core@latest --save ...');
|
package/cms/config.template.yml
CHANGED
|
@@ -177,6 +177,16 @@ collections:
|
|
|
177
177
|
- { name: provider, label: Provider name, widget: string, default: Google Calendar }
|
|
178
178
|
- { name: weekStart, label: Week starts on, widget: select, default: monday, options: [{ label: "Monday", value: monday }, { label: "Sunday", value: sunday }] }
|
|
179
179
|
|
|
180
|
+
- name: posts-settings
|
|
181
|
+
label: News
|
|
182
|
+
file: src/content/postsSettings/index.json
|
|
183
|
+
icon: newspaper
|
|
184
|
+
fields:
|
|
185
|
+
- { name: title, label: Heading, widget: string, default: News }
|
|
186
|
+
- { name: intro, label: Intro, widget: text, required: false }
|
|
187
|
+
- { name: perPage, label: Posts per page, widget: number, default: 12, value_type: int, min: 1, max: 48, hint: "How many posts each news page shows. 12 fills rows of one, two, three or four evenly." }
|
|
188
|
+
- { name: excludeFromIndex, label: Tags kept off the main news page, widget: list, required: false, hint: "Tag names (for example newsletter) whose posts appear only on their own tag page." }
|
|
189
|
+
|
|
180
190
|
- name: posts
|
|
181
191
|
label: Posts
|
|
182
192
|
label_singular: Post
|
|
@@ -224,7 +234,7 @@ collections:
|
|
|
224
234
|
- name: pages
|
|
225
235
|
label: Pages
|
|
226
236
|
label_singular: Page
|
|
227
|
-
description: "
|
|
237
|
+
description: "__PAGES_DESCRIPTION__"
|
|
228
238
|
folder: src/content/pages
|
|
229
239
|
extension: json
|
|
230
240
|
format: json
|
|
@@ -234,6 +244,7 @@ collections:
|
|
|
234
244
|
identifier_field: title
|
|
235
245
|
fields:
|
|
236
246
|
- { name: title, label: Page title, widget: string }
|
|
247
|
+
# ferst:requests (the Basic plan only: bin/cms-config.mjs leaves it out on the free plan)
|
|
237
248
|
- name: requests
|
|
238
249
|
label: Requested changes
|
|
239
250
|
label_singular: Request
|
|
@@ -248,6 +259,7 @@ collections:
|
|
|
248
259
|
- { name: issue, widget: hidden, required: false }
|
|
249
260
|
- { name: by, widget: hidden, required: false }
|
|
250
261
|
- { name: at, widget: hidden, required: false }
|
|
262
|
+
# /ferst:requests
|
|
251
263
|
- { name: description, label: SEO description, widget: hidden, required: false }
|
|
252
264
|
- { name: heroImage, widget: hidden, required: false }
|
|
253
265
|
- { name: align, widget: hidden, required: false }
|
package/content/posts.ts
CHANGED
|
@@ -43,8 +43,8 @@ export const postsSettingsSchema = z.object({
|
|
|
43
43
|
title: z.string().default('News'),
|
|
44
44
|
/** Optional intro copy for the index. */
|
|
45
45
|
intro: z.string().default(''),
|
|
46
|
-
/** Posts per page in the paginated index. */
|
|
47
|
-
perPage: z.number().int().min(1).default(
|
|
46
|
+
/** Posts per page in the paginated index. 12 fills rows of 1, 2, 3 or 4 tiles evenly. */
|
|
47
|
+
perPage: z.number().int().min(1).default(12),
|
|
48
48
|
/** Tag slugs excluded from the MAIN index (e.g. a `newsletter` tag that has its
|
|
49
49
|
* own view). Kept generic — no capability knows what "newsletter" means. */
|
|
50
50
|
excludeFromIndex: z.array(z.string()).default([]),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ferst-core",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
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",
|