ferst-core 0.6.2 → 0.7.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/ferst-update.mjs +73 -2
- package/components/BlockRenderer.astro +107 -103
- package/components/blocks/Prose.astro +56 -39
- package/components/recipes/ContactCard.astro +172 -170
- package/components/recipes/Documents.astro +146 -0
- package/components/recipes/Hero.astro +447 -191
- package/components/recipes/Tiles.astro +285 -252
- package/components/recipes/Video.astro +157 -0
- package/content/blocks.ts +775 -719
- package/content/cookies.ts +112 -103
- package/examples/primitives.ts +28 -28
- package/examples/recipes.ts +37 -32
- package/lib/documents.ts +62 -0
- package/lib/inline-markdown.ts +65 -52
- package/lib/prose.ts +29 -0
- package/lib/video.ts +75 -0
- package/package.json +1 -1
package/bin/ferst-update.mjs
CHANGED
|
@@ -99,11 +99,17 @@ export function resolveUpstream(manifest) {
|
|
|
99
99
|
return { url: DEFAULT_UPSTREAM, defaulted: true };
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* The caret range that pins an installed version to its major.minor, in the SAME
|
|
104
|
+
* canonical form the template ships (`^major.minor.0`, e.g. 0.6.3 → ^0.6.0). Emitting
|
|
105
|
+
* the `.0` matters: the template's manifest + package.json carry `^0.6.0`, so a matching
|
|
106
|
+
* canonical form makes the ownedPath resync + this rewrite idempotent — no per-update
|
|
107
|
+
* churn, and no `^0.6` vs `^0.6.0` drift between clients. (`^0.6.0` ≡ `^0.6` to npm.)
|
|
108
|
+
*/
|
|
103
109
|
export function coreRangeFor(version) {
|
|
104
110
|
const m = String(version).trim().replace(/^v/, '').match(/^(\d+)\.(\d+)/);
|
|
105
111
|
if (!m) throw new Error(`ferst-update: could not parse ferst-core version "${version}".`);
|
|
106
|
-
return `^${m[1]}.${m[2]}`;
|
|
112
|
+
return `^${m[1]}.${m[2]}.0`;
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
/** Read the `version` out of a package.json's text. Throws if absent. */
|
|
@@ -141,6 +147,42 @@ export function replaceCoreRange(raw, next) {
|
|
|
141
147
|
return raw.replace(RANGE, `$1${next}$2`);
|
|
142
148
|
}
|
|
143
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Set package.json's `ferst-core` dependency to the canonical range in place,
|
|
152
|
+
* formatting-preserving (same approach as replaceCoreRange). `npm install --save`
|
|
153
|
+
* would otherwise pin it to the exact installed patch (e.g. ^0.6.2), which churns
|
|
154
|
+
* against the template's `^0.6.0` on every resync; the exact version lives in the
|
|
155
|
+
* lockfile + the ferst-site.json drift stamp, so the dep range stays canonical.
|
|
156
|
+
* Returns the new text, or null when there is no `ferst-core` dependency to replace.
|
|
157
|
+
*/
|
|
158
|
+
export function replacePackageCoreDep(raw, next) {
|
|
159
|
+
const DEP = /("ferst-core"\s*:\s*")[^"]*(")/;
|
|
160
|
+
if (!DEP.test(raw)) return null;
|
|
161
|
+
return raw.replace(DEP, `$1${next}$2`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** package.json fields that describe the CLIENT's site, not the template's scaffold. */
|
|
165
|
+
export const CLIENT_PACKAGE_FIELDS = ['name', 'description', 'version', 'private', 'author', 'homepage', 'repository'];
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* After the scaffold sync overwrites package.json (a template-owned path), put the
|
|
169
|
+
* client's own identity fields back (name, description, …) so an update never renames
|
|
170
|
+
* the site. Keeps the synced file's indentation and key order. Returns the new text,
|
|
171
|
+
* or null when either side isn't valid JSON (then the synced file is left as is).
|
|
172
|
+
*/
|
|
173
|
+
export function keepClientPackageFields(syncedRaw, clientRaw) {
|
|
174
|
+
let synced, client;
|
|
175
|
+
try {
|
|
176
|
+
synced = JSON.parse(syncedRaw);
|
|
177
|
+
client = JSON.parse(clientRaw);
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
for (const k of CLIENT_PACKAGE_FIELDS) if (k in client) synced[k] = client[k];
|
|
182
|
+
const indent = (syncedRaw.match(/\n([ \t]+)"/) || [, ' '])[1];
|
|
183
|
+
return JSON.stringify(synced, null, indent) + (syncedRaw.endsWith('\n') ? '\n' : '');
|
|
184
|
+
}
|
|
185
|
+
|
|
144
186
|
/**
|
|
145
187
|
* The default branch name from `git ls-remote --symref <remote> HEAD` output
|
|
146
188
|
* (`ref: refs/heads/<branch>\tHEAD`). Null when it can't be read. Pure.
|
|
@@ -289,6 +331,10 @@ function main() {
|
|
|
289
331
|
console.log('ferst-update: --dry-run — scaffold changes that WOULD apply:');
|
|
290
332
|
console.log(diff ? diff : ' (no scaffold changes)');
|
|
291
333
|
} else {
|
|
334
|
+
// package.json is template-owned, but its identity fields are the client's — keep them.
|
|
335
|
+
const pkgPath = resolve(cwd, 'package.json');
|
|
336
|
+
let clientPkg = null;
|
|
337
|
+
try { clientPkg = readFileSync(pkgPath, 'utf8'); } catch { /* none yet */ }
|
|
292
338
|
for (const p of ownedPaths) {
|
|
293
339
|
// Per-path so one path that upstream no longer ships (pathspec no-match) can't
|
|
294
340
|
// abort the whole sync — warn and continue.
|
|
@@ -296,6 +342,16 @@ function main() {
|
|
|
296
342
|
console.warn(`ferst-update: could not sync "${p}" (not present upstream?) — skipped.`);
|
|
297
343
|
}
|
|
298
344
|
}
|
|
345
|
+
if (clientPkg) {
|
|
346
|
+
try {
|
|
347
|
+
const synced = readFileSync(pkgPath, 'utf8');
|
|
348
|
+
const kept = keepClientPackageFields(synced, clientPkg);
|
|
349
|
+
if (kept && kept !== synced) {
|
|
350
|
+
writeFileSync(pkgPath, kept);
|
|
351
|
+
console.log("ferst-update: kept the site's own package.json name/description.");
|
|
352
|
+
}
|
|
353
|
+
} catch { /* package.json not synced — nothing to restore */ }
|
|
354
|
+
}
|
|
299
355
|
const status = (tryGit('status --short', cwd) || '').trim();
|
|
300
356
|
scaffoldCount = status ? status.split(/\r?\n/).filter(Boolean).length : 0;
|
|
301
357
|
console.log('ferst-update: scaffold synced. git status --short:');
|
|
@@ -339,6 +395,21 @@ function main() {
|
|
|
339
395
|
console.warn(`ferst-update: no "coreRange" field in ${MANIFEST} to update (installed ${after}).`);
|
|
340
396
|
}
|
|
341
397
|
}
|
|
398
|
+
|
|
399
|
+
// Normalise package.json's ferst-core dep to the same canonical range, undoing
|
|
400
|
+
// `--save`'s exact-patch pin (^0.6.2 → ^0.6.0) so it never churns against the
|
|
401
|
+
// template's ownedPath value. The exact version stays in the lock + drift stamp.
|
|
402
|
+
const pkgPath = resolve(cwd, 'package.json');
|
|
403
|
+
try {
|
|
404
|
+
const pkgRaw = readFileSync(pkgPath, 'utf8');
|
|
405
|
+
const pkgNext = replacePackageCoreDep(pkgRaw, next);
|
|
406
|
+
if (pkgNext && pkgNext !== pkgRaw) {
|
|
407
|
+
writeFileSync(pkgPath, pkgNext);
|
|
408
|
+
console.log(`ferst-update: package.json ferst-core → ${next}`);
|
|
409
|
+
}
|
|
410
|
+
} catch {
|
|
411
|
+
/* no root package.json (shouldn't happen in a client) — skip silently */
|
|
412
|
+
}
|
|
342
413
|
}
|
|
343
414
|
|
|
344
415
|
// 4. Verify — a failed @latest bump is a real incompatibility to surface, so on
|
|
@@ -1,103 +1,107 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* Renders an ordered list of blocks by mapping each block's `type` to its
|
|
4
|
-
* component via a registry. Disabled blocks are skipped. The registry is typed
|
|
5
|
-
* `Record<BlockType, …>`, so adding a block type to the union without a renderer
|
|
6
|
-
* here is a compile error — no silent `undefined` component at runtime.
|
|
7
|
-
*/
|
|
8
|
-
import type { Block, BlockType } from '../content/blocks';
|
|
9
|
-
import Heading from './blocks/Heading.astro';
|
|
10
|
-
import Prose from './blocks/Prose.astro';
|
|
11
|
-
import ImageBlock from './blocks/ImageBlock.astro';
|
|
12
|
-
import ButtonBlock from './blocks/ButtonBlock.astro';
|
|
13
|
-
import Quote from './blocks/Quote.astro';
|
|
14
|
-
import Divider from './blocks/Divider.astro';
|
|
15
|
-
import Spacer from './blocks/Spacer.astro';
|
|
16
|
-
import Badge from './blocks/Badge.astro';
|
|
17
|
-
import Stat from './blocks/Stat.astro';
|
|
18
|
-
import List from './blocks/List.astro';
|
|
19
|
-
import Section from './blocks/Section.astro';
|
|
20
|
-
import Grid from './blocks/Grid.astro';
|
|
21
|
-
import Stack from './blocks/Stack.astro';
|
|
22
|
-
import Hero from './recipes/Hero.astro';
|
|
23
|
-
import FeatureCards from './recipes/FeatureCards.astro';
|
|
24
|
-
import Cta from './recipes/Cta.astro';
|
|
25
|
-
import Faq from './recipes/Faq.astro';
|
|
26
|
-
import Gallery from './recipes/Gallery.astro';
|
|
27
|
-
import Testimonial from './recipes/Testimonial.astro';
|
|
28
|
-
import ContactForm from './recipes/ContactForm.astro';
|
|
29
|
-
import Accordion from './recipes/Accordion.astro';
|
|
30
|
-
import Tiles from './recipes/Tiles.astro';
|
|
31
|
-
import Banner from './recipes/Banner.astro';
|
|
32
|
-
import Stats from './recipes/Stats.astro';
|
|
33
|
-
import Steps from './recipes/Steps.astro';
|
|
34
|
-
import Pricing from './recipes/Pricing.astro';
|
|
35
|
-
import Logos from './recipes/Logos.astro';
|
|
36
|
-
import Tabs from './recipes/Tabs.astro';
|
|
37
|
-
import Announcement from './recipes/Announcement.astro';
|
|
38
|
-
import Bento from './recipes/Bento.astro';
|
|
39
|
-
import Marquee from './recipes/Marquee.astro';
|
|
40
|
-
import LatestPosts from './recipes/LatestPosts.astro';
|
|
41
|
-
import MapEmbed from './recipes/MapEmbed.astro';
|
|
42
|
-
import Table from './recipes/Table.astro';
|
|
43
|
-
import Notice from './recipes/Notice.astro';
|
|
44
|
-
import MediaCards from './recipes/MediaCards.astro';
|
|
45
|
-
import ContactCard from './recipes/ContactCard.astro';
|
|
46
|
-
import Locations from './recipes/Locations.astro';
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Renders an ordered list of blocks by mapping each block's `type` to its
|
|
4
|
+
* component via a registry. Disabled blocks are skipped. The registry is typed
|
|
5
|
+
* `Record<BlockType, …>`, so adding a block type to the union without a renderer
|
|
6
|
+
* here is a compile error — no silent `undefined` component at runtime.
|
|
7
|
+
*/
|
|
8
|
+
import type { Block, BlockType } from '../content/blocks';
|
|
9
|
+
import Heading from './blocks/Heading.astro';
|
|
10
|
+
import Prose from './blocks/Prose.astro';
|
|
11
|
+
import ImageBlock from './blocks/ImageBlock.astro';
|
|
12
|
+
import ButtonBlock from './blocks/ButtonBlock.astro';
|
|
13
|
+
import Quote from './blocks/Quote.astro';
|
|
14
|
+
import Divider from './blocks/Divider.astro';
|
|
15
|
+
import Spacer from './blocks/Spacer.astro';
|
|
16
|
+
import Badge from './blocks/Badge.astro';
|
|
17
|
+
import Stat from './blocks/Stat.astro';
|
|
18
|
+
import List from './blocks/List.astro';
|
|
19
|
+
import Section from './blocks/Section.astro';
|
|
20
|
+
import Grid from './blocks/Grid.astro';
|
|
21
|
+
import Stack from './blocks/Stack.astro';
|
|
22
|
+
import Hero from './recipes/Hero.astro';
|
|
23
|
+
import FeatureCards from './recipes/FeatureCards.astro';
|
|
24
|
+
import Cta from './recipes/Cta.astro';
|
|
25
|
+
import Faq from './recipes/Faq.astro';
|
|
26
|
+
import Gallery from './recipes/Gallery.astro';
|
|
27
|
+
import Testimonial from './recipes/Testimonial.astro';
|
|
28
|
+
import ContactForm from './recipes/ContactForm.astro';
|
|
29
|
+
import Accordion from './recipes/Accordion.astro';
|
|
30
|
+
import Tiles from './recipes/Tiles.astro';
|
|
31
|
+
import Banner from './recipes/Banner.astro';
|
|
32
|
+
import Stats from './recipes/Stats.astro';
|
|
33
|
+
import Steps from './recipes/Steps.astro';
|
|
34
|
+
import Pricing from './recipes/Pricing.astro';
|
|
35
|
+
import Logos from './recipes/Logos.astro';
|
|
36
|
+
import Tabs from './recipes/Tabs.astro';
|
|
37
|
+
import Announcement from './recipes/Announcement.astro';
|
|
38
|
+
import Bento from './recipes/Bento.astro';
|
|
39
|
+
import Marquee from './recipes/Marquee.astro';
|
|
40
|
+
import LatestPosts from './recipes/LatestPosts.astro';
|
|
41
|
+
import MapEmbed from './recipes/MapEmbed.astro';
|
|
42
|
+
import Table from './recipes/Table.astro';
|
|
43
|
+
import Notice from './recipes/Notice.astro';
|
|
44
|
+
import MediaCards from './recipes/MediaCards.astro';
|
|
45
|
+
import ContactCard from './recipes/ContactCard.astro';
|
|
46
|
+
import Locations from './recipes/Locations.astro';
|
|
47
|
+
import Video from './recipes/Video.astro';
|
|
48
|
+
import Documents from './recipes/Documents.astro';
|
|
49
|
+
|
|
50
|
+
interface Props {
|
|
51
|
+
blocks: Block[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { blocks } = Astro.props;
|
|
55
|
+
|
|
56
|
+
const registry: Record<BlockType, any> = {
|
|
57
|
+
heading: Heading,
|
|
58
|
+
prose: Prose,
|
|
59
|
+
image: ImageBlock,
|
|
60
|
+
button: ButtonBlock,
|
|
61
|
+
quote: Quote,
|
|
62
|
+
divider: Divider,
|
|
63
|
+
spacer: Spacer,
|
|
64
|
+
badge: Badge,
|
|
65
|
+
stat: Stat,
|
|
66
|
+
list: List,
|
|
67
|
+
section: Section,
|
|
68
|
+
grid: Grid,
|
|
69
|
+
stack: Stack,
|
|
70
|
+
hero: Hero,
|
|
71
|
+
featureCards: FeatureCards,
|
|
72
|
+
cta: Cta,
|
|
73
|
+
faq: Faq,
|
|
74
|
+
gallery: Gallery,
|
|
75
|
+
testimonial: Testimonial,
|
|
76
|
+
contactForm: ContactForm,
|
|
77
|
+
accordion: Accordion,
|
|
78
|
+
tiles: Tiles,
|
|
79
|
+
banner: Banner,
|
|
80
|
+
stats: Stats,
|
|
81
|
+
steps: Steps,
|
|
82
|
+
pricing: Pricing,
|
|
83
|
+
logos: Logos,
|
|
84
|
+
tabs: Tabs,
|
|
85
|
+
announcement: Announcement,
|
|
86
|
+
bento: Bento,
|
|
87
|
+
marquee: Marquee,
|
|
88
|
+
latestPosts: LatestPosts,
|
|
89
|
+
mapEmbed: MapEmbed,
|
|
90
|
+
table: Table,
|
|
91
|
+
notice: Notice,
|
|
92
|
+
mediaCards: MediaCards,
|
|
93
|
+
contactCard: ContactCard,
|
|
94
|
+
locations: Locations,
|
|
95
|
+
video: Video,
|
|
96
|
+
documents: Documents,
|
|
97
|
+
};
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
blocks
|
|
102
|
+
.filter((block) => block.enabled !== false)
|
|
103
|
+
.map((block) => {
|
|
104
|
+
const Component = registry[block.type];
|
|
105
|
+
return <Component {...block} />;
|
|
106
|
+
})
|
|
107
|
+
}
|
|
@@ -1,39 +1,56 @@
|
|
|
1
|
-
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
</
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.b-prose
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.b-prose
|
|
27
|
-
margin
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
1
|
+
---
|
|
2
|
+
import { proseParts } from '../../lib/prose';
|
|
3
|
+
interface Props {
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
const { text } = Astro.props;
|
|
7
|
+
// Blank lines separate paragraphs; `- ` / `1. ` lines make lists; links, bold and
|
|
8
|
+
// italic are rendered safely (lib/prose.ts → inlineMarkdown).
|
|
9
|
+
const parts = proseParts(text);
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="b-prose">
|
|
13
|
+
{parts.map((part) =>
|
|
14
|
+
part.kind === 'p' ? <p set:html={part.html} />
|
|
15
|
+
: part.kind === 'ul' ? <ul>{part.items.map((i) => <li set:html={i} />)}</ul>
|
|
16
|
+
: <ol>{part.items.map((i) => <li set:html={i} />)}</ol>
|
|
17
|
+
)}
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<style>
|
|
21
|
+
.b-prose {
|
|
22
|
+
color: var(--fg);
|
|
23
|
+
}
|
|
24
|
+
.b-prose p,
|
|
25
|
+
.b-prose ul,
|
|
26
|
+
.b-prose ol {
|
|
27
|
+
margin: 0 0 1rem;
|
|
28
|
+
line-height: 1.7;
|
|
29
|
+
max-width: var(--max);
|
|
30
|
+
}
|
|
31
|
+
.b-prose ul,
|
|
32
|
+
.b-prose ol {
|
|
33
|
+
padding-inline-start: 1.4em;
|
|
34
|
+
}
|
|
35
|
+
.b-prose li + li {
|
|
36
|
+
margin-top: 0.25rem;
|
|
37
|
+
}
|
|
38
|
+
.b-prose > :last-child {
|
|
39
|
+
margin-bottom: 0;
|
|
40
|
+
}
|
|
41
|
+
.b-prose :global(a) {
|
|
42
|
+
color: var(--accent);
|
|
43
|
+
text-decoration: underline;
|
|
44
|
+
text-underline-offset: 0.15em;
|
|
45
|
+
}
|
|
46
|
+
/* The max-width reading column is left-anchored by default; when the page/
|
|
47
|
+
section is centred (or end-aligned) re-anchor it so the text reads as
|
|
48
|
+
deliberately centred, not centred inside a left column. Text-align itself
|
|
49
|
+
is inherited from the content-align cascade. */
|
|
50
|
+
:global([data-content-align='center']) .b-prose > * {
|
|
51
|
+
margin-inline: auto;
|
|
52
|
+
}
|
|
53
|
+
:global([data-content-align='end']) .b-prose > * {
|
|
54
|
+
margin-inline-start: auto;
|
|
55
|
+
}
|
|
56
|
+
</style>
|