@the-smithy/create-hearth-site 0.30.0 → 0.33.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/bin/create-hearth-site.js +1 -1
- package/package.json +2 -2
- package/template/AGENTS.md +84 -14
- package/template/README.md +16 -5
- package/template/SITE.md +2 -2
- package/template/_gitattributes +2 -0
- package/template/_gitignore +2 -1
- package/template/package.json +1 -0
- package/template/scripts/hearth.mjs +124 -5
- package/template/src/hearth/blocks/collections.tsx +42 -35
- package/template/src/hearth/blocks/content.tsx +140 -99
- package/template/src/hearth/blocks/events.tsx +25 -10
- package/template/src/hearth/blocks/giving.tsx +29 -14
- package/template/src/hearth/blocks/index.tsx +20 -9
- package/template/src/hearth/blocks/info.tsx +43 -30
- package/template/src/hearth/blocks/media.tsx +36 -21
- package/template/src/hearth/components/EventView.tsx +8 -2
- package/template/src/hearth/components/FundraiserView.tsx +8 -2
- package/template/src/hearth/forms/DonationForm.tsx +9 -2
- package/template/src/hearth/layout/SiteFrame.tsx +8 -2
- package/template/src/hearth/overrides.ts +97 -15
- package/template/src/hearth/parts.tsx +17 -0
- package/template/src/theme/overrides.tsx +29 -14
|
@@ -21,7 +21,7 @@ const self = JSON.parse(await readFile(path.join(here, "..", "package.json"), "u
|
|
|
21
21
|
// The template's lockfile is the Hearth workspace's (the SDK as a workspace link); a new site makes its own.
|
|
22
22
|
const SKIP = new Set(["node_modules", ".next", ".netlify", ".vercel", ".env.local", "next-env.d.ts", "tsconfig.tsbuildinfo", "pnpm-lock.yaml"]);
|
|
23
23
|
/** Dotfiles npm won't publish (or that would confuse the monorepo) are stored with a leading underscore. */
|
|
24
|
-
const RENAMES = { _gitignore: ".gitignore", "_pnpm-workspace.yaml": "pnpm-workspace.yaml" };
|
|
24
|
+
const RENAMES = { _gitignore: ".gitignore", _gitattributes: ".gitattributes", "_pnpm-workspace.yaml": "pnpm-workspace.yaml" };
|
|
25
25
|
|
|
26
26
|
const usage = `Usage: pnpm create @the-smithy/hearth-site@latest <folder> --site <address> [--skip-install]
|
|
27
27
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@the-smithy/create-hearth-site",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "Start a new client website powered by Hearth: pnpm create @the-smithy/hearth-site",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
],
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"vitest": "^5.0.3",
|
|
33
|
-
"@the-smithy/hearth-schema": "^0.
|
|
33
|
+
"@the-smithy/hearth-schema": "^0.33.0"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"test": "node --test \"test/**/*.test.js\" && vitest run test/blocks.test.ts test/overrides.test.ts",
|
package/template/AGENTS.md
CHANGED
|
@@ -83,8 +83,9 @@ Work in this order, and stop at the first step that gets the result:
|
|
|
83
83
|
(`[data-page="events"]`, so the same blocks elsewhere stay as they are) or one card (`[data-card="our-history"]`, from
|
|
84
84
|
its title).
|
|
85
85
|
5. **A page in Hearth** for words the code draws on its own pages: see "Pages at the code's addresses" below.
|
|
86
|
-
6. **Overrides**, last: `src/theme/overrides.tsx` (see "Overrides" below).
|
|
87
|
-
|
|
86
|
+
6. **Overrides**, last: `src/theme/overrides.tsx` (see "Overrides" below). Use one only when everything above can't
|
|
87
|
+
get there, and the smallest that does: add to Hearth's component rather than replace it, and replace one part of a
|
|
88
|
+
block rather than the whole. What the site replaces is its own code, which updates don't reach. Build it from
|
|
88
89
|
`src/hearth/ui.tsx` and the theme, and say why in `SITE.md`.
|
|
89
90
|
|
|
90
91
|
To style blocks without setting up pages for them, open `/hearth/blocks` while `pnpm dev` is running: each block with
|
|
@@ -94,9 +95,9 @@ It shows one block at a time: `/hearth/blocks?block=hero` goes to one, and `?blo
|
|
|
94
95
|
Without a browser, the unstyled page is `node_modules/@the-smithy/hearth/block-reference.html`, but it's large: for slots,
|
|
95
96
|
use `src/hearth/theme/defaults.ts`.
|
|
96
97
|
|
|
97
|
-
Never copy a component out of `src/hearth/` to restyle
|
|
98
|
-
|
|
99
|
-
override can bridge the gap meanwhile.
|
|
98
|
+
Never copy a component out of `src/hearth/` to restyle or extend it: an override is given Hearth's own as `children`
|
|
99
|
+
for that. If a design needs a slot or an option Hearth doesn't have, say so: it belongs in Hearth's template for every
|
|
100
|
+
site (tell the developer to raise it with The Smithy), and an override can bridge the gap meanwhile.
|
|
100
101
|
|
|
101
102
|
### Pages at the code's addresses
|
|
102
103
|
|
|
@@ -112,19 +113,83 @@ never serves at `/not-found`, and redirects run before it.
|
|
|
112
113
|
### Overrides
|
|
113
114
|
|
|
114
115
|
Everything is optional. A site's file only exports what it uses, and an update never replaces it. They can import
|
|
115
|
-
Hearth's components, `Blocks` too. The types are in `src/hearth/overrides.ts`.
|
|
116
|
-
|
|
116
|
+
Hearth's components, `Blocks` too. The types are in `src/hearth/overrides.ts`.
|
|
117
|
+
|
|
118
|
+
**Each override is given Hearth's own version as `children`.** Keep it and add to it, and Hearth's stays up to date
|
|
119
|
+
underneath the site's; leave it out, and the site's replaces it and falls behind. Reach for them in this order:
|
|
120
|
+
|
|
121
|
+
1. **Add to a part**, keeping Hearth's: `({ children }) => <>{children}<Extra /></>`.
|
|
122
|
+
2. **Replace a part**: the part's own markup, the rest of the block still Hearth's.
|
|
123
|
+
3. **Add to a whole block**: something before or after it, inside its anchor.
|
|
124
|
+
4. **Replace a whole block**, last.
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
// src/theme/overrides.tsx
|
|
128
|
+
import type { Overrides } from "@/hearth/overrides";
|
|
129
|
+
import { theme } from "@/theme";
|
|
130
|
+
|
|
131
|
+
export const parts: Overrides["parts"] = {
|
|
132
|
+
// A third link beside the hero's two, which stay Hearth's.
|
|
133
|
+
"hero.actions": ({ children }) => (
|
|
134
|
+
<>
|
|
135
|
+
{children}
|
|
136
|
+
<a href="tel:+15551234567" className={theme.button.secondary}>Call us</a>
|
|
137
|
+
</>
|
|
138
|
+
),
|
|
139
|
+
// Each card as Hearth draws it, with a note under the ones without a link.
|
|
140
|
+
"cardGrid.item": ({ item, children }) => (
|
|
141
|
+
<>
|
|
142
|
+
{children}
|
|
143
|
+
{item.link ? null : <p className={theme.cardGrid.body}>Coming soon</p>}
|
|
144
|
+
</>
|
|
145
|
+
),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const blocks: Overrides["blocks"] = {
|
|
149
|
+
// Hearth's Stats block, with a source line under it.
|
|
150
|
+
stats: ({ children }) => (
|
|
151
|
+
<>
|
|
152
|
+
{children}
|
|
153
|
+
<p className={theme.section.inner}>Figures audited in 2026.</p>
|
|
154
|
+
</>
|
|
155
|
+
),
|
|
156
|
+
};
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Parts** (`parts`, by name; every one is in `PartProps` in `src/hearth/overrides.ts`, and `get_block_type` on Hearth's
|
|
160
|
+
MCP server lists a block's). A part is what's inside its element, which stays Hearth's with its slot and attributes:
|
|
161
|
+
|
|
162
|
+
| Part | Is | Blocks |
|
|
163
|
+
| --- | --- | --- |
|
|
164
|
+
| `<type>.heading` | The block's heading: eyebrow, heading and intro (the hero's and a CTA's: the text above the buttons) | Most blocks |
|
|
165
|
+
| `<type>.item` | One item's contents, inside its `<li>` (a stat's `<div>`, a question's `<div>` or `<details>`). Given `item`, `index` and, for ones with a title, `heading` (`"h2"` or `"h3"`) | `stats`, `cardGrid`, `projects`, `pricing`, `faq`, `gallery`, `people`, `logos`, `locations`, `schedule`, `collectionList` |
|
|
166
|
+
| `<type>.actions` | The links inside the button row. The row is drawn for the site's part even with no links in Hearth | `hero`, `mediaText`, `cta`, `eventSpotlight`, `fundraiserSpotlight` |
|
|
167
|
+
| `<type>.media` | The image (children is null without one, and the part can still draw something) | `hero`, `mediaText`, `cta`, `eventSpotlight`, `fundraiserSpotlight` |
|
|
168
|
+
| `location.details` | The name, address, hours and directions beside the map | `location` |
|
|
169
|
+
|
|
170
|
+
Every part is given the `block` too.
|
|
171
|
+
|
|
172
|
+
**The rest**, used everywhere the template draws them:
|
|
117
173
|
|
|
118
|
-
| Export |
|
|
174
|
+
| Export | Is | Given |
|
|
119
175
|
| --- | --- | --- |
|
|
120
|
-
| `blocks` | A whole block
|
|
121
|
-
| `Logo` | The logo in the header, and in the footer once the `footer.logo` slot has classes | `{ site, place }` |
|
|
176
|
+
| `blocks` | A whole block, by type. An anchor from Hearth goes around it, so Hearth's in `children` has none | `{ block }` |
|
|
177
|
+
| `Logo` | The logo in the header, and in the footer once the `footer.logo` slot has classes. Hearth's has no component, so no `children` | `{ site, place }` |
|
|
122
178
|
| `Footer` | The footer | `{ site }` |
|
|
123
179
|
| `EventPage` | An event's page, at `/events/…` and in collections, so a site needs no route of its own | `{ event }` |
|
|
124
180
|
| `FundraiserPage` | A fundraiser's page | `{ fundraiser }` |
|
|
125
|
-
| `EventCard`, `FundraiserCard` | The cards in lists; each draws an `<li
|
|
181
|
+
| `EventCard`, `FundraiserCard` | The cards in lists; each draws an `<li>` (Hearth's in `children` is one too), and may be async | `{ event }` or `{ fundraiser }`, and `heading`: `"h2"` or `"h3"`, for its title |
|
|
126
182
|
| `DonateForm` | The donation form | The amounts, `fundraiser`, `fundraisers` (active ones to choose from) and `submitLabel` |
|
|
127
183
|
|
|
184
|
+
`children` works in client components too: it's Hearth's version already drawn, not a function. To draw Hearth's with
|
|
185
|
+
different content, import its component and give it changed data:
|
|
186
|
+
`import { Stats } from "@/hearth/blocks/content"`, then `<Stats block={{ ...block, data: { ...block.data, heading: "…" } }} />`.
|
|
187
|
+
Not through `Blocks`, which would draw the site's override again.
|
|
188
|
+
|
|
189
|
+
**After an update**, `pnpm hearth:update` lists what's overridden here whose Hearth version changed, and parts or
|
|
190
|
+
blocks Hearth no longer has. `pnpm hearth:overrides` checks against the last commit any time (`--since <git ref>` for
|
|
191
|
+
another). Look at each in `pnpm dev`.
|
|
192
|
+
|
|
128
193
|
`EventPage` and `FundraiserPage` have their structured data drawn around them. To build on Hearth's: `EventContent`,
|
|
129
194
|
`EventRegisterSection` and `EventVolunteerSection` from `components/EventView.tsx`, `FundraiserContent` from
|
|
130
195
|
`components/FundraiserView.tsx`. A site's own routes are for pages Hearth has no address for, not for an event's or
|
|
@@ -153,8 +218,8 @@ each site stays on the template and on current releases:
|
|
|
153
218
|
file, or write a site's own data layer, SEO or forms. A bug or a gap in Hearth's code gets fixed in Hearth's
|
|
154
219
|
template and reaches the site by an update; say so to the developer, and if it can't wait, bridge it with an override
|
|
155
220
|
and note it in `SITE.md`.
|
|
156
|
-
- **After an update,
|
|
157
|
-
|
|
221
|
+
- **After an update, check the overrides** that `pnpm hearth:update` lists as changed in Hearth (`pnpm hearth:overrides`
|
|
222
|
+
any time), and look through `SITE.md`'s. Delete any that the theme or a smaller override can now do.
|
|
158
223
|
|
|
159
224
|
## First steps in a new checkout
|
|
160
225
|
|
|
@@ -166,10 +231,15 @@ Do these before writing code, and tell the developer what you found:
|
|
|
166
231
|
it. If it fails, its message says what to fix (missing key, wrong `HEARTH_SITE`, revoked key). Ask the developer to
|
|
167
232
|
fix `.env.local`; don't guess values.
|
|
168
233
|
2. **Get the full reference:** `pnpm hearth:docs` saves Hearth's API guide (every block and its fields, events,
|
|
169
|
-
forms, previews, SEO) to `hearth-api.md
|
|
234
|
+
forms, previews, SEO) to `hearth-api.md`, and its onboarding guide to `hearth-onboarding.md`: how to set up,
|
|
235
|
+
design and build a site, step by step, with code samples. Read the parts you need from them, and from
|
|
170
236
|
`node_modules/@the-smithy/hearth/llms.md` for the SDK. It also installs Drift and any skills Hearth offers this site
|
|
171
237
|
(see "Skills" below), so run it again when they're missing. These aren't committed, so some assistants' search skips
|
|
172
238
|
them: open them by their exact path, or from the terminal if your tool won't.
|
|
239
|
+
Hearth's MCP server (`/api/mcp` on Hearth, read-only, with the site's API key) answers the same questions without
|
|
240
|
+
the files, if the developer has connected it: each block's fields, slots and starting classes, the tokens, and the
|
|
241
|
+
published content. For design work, its `get_design_workflow` tool is the way to carry a design from Figma, and
|
|
242
|
+
`read_onboarding_guide` reads the onboarding guide.
|
|
173
243
|
3. **"Nothing is published"** means every page is a 404 until editors publish in Hearth. That's not a code
|
|
174
244
|
problem: say so, and don't add placeholder content to work around it.
|
|
175
245
|
4. **Then style the site** as above, checking with `pnpm dev` against the published pages, on a phone width too.
|
package/template/README.md
CHANGED
|
@@ -51,9 +51,10 @@ Once your API key is in `.env.local`:
|
|
|
51
51
|
|
|
52
52
|
1. **Check the connection yourself, or have the agent do it:** `pnpm hearth:check` confirms the key works and
|
|
53
53
|
lists the published pages and the block types they use. It never prints the key.
|
|
54
|
-
2. **Give the agent the full reference:** `pnpm hearth:docs` saves Hearth's API guide to `hearth-api.md`
|
|
55
|
-
committed), and installs Drift and any coding-assistant skills
|
|
56
|
-
`.claude/skills/`. The agent's instructions already point it at
|
|
54
|
+
2. **Give the agent the full reference:** `pnpm hearth:docs` saves Hearth's API guide to `hearth-api.md` and its
|
|
55
|
+
onboarding guide to `hearth-onboarding.md` (neither committed), and installs Drift and any coding-assistant skills
|
|
56
|
+
Hearth offers your site in `.agents/skills/` and `.claude/skills/`. The agent's instructions already point it at
|
|
57
|
+
them.
|
|
57
58
|
3. **Then tell it what to build.** Prompts that work well:
|
|
58
59
|
|
|
59
60
|
- *"Run the first steps in AGENTS.md and tell me what's published and which blocks the site uses."*
|
|
@@ -64,6 +65,11 @@ Once your API key is in `.env.local`:
|
|
|
64
65
|
- *"The gallery looks plain. Make it a masonry grid using its slots in the theme, keeping it accessible."*
|
|
65
66
|
- *"Run `pnpm hearth:update`, then check every published page still looks right."*
|
|
66
67
|
|
|
68
|
+
**Without the code, or for design work:** Hearth also has an MCP server, so an agent can read the site's blocks, design
|
|
69
|
+
system and published content from anywhere, read-only. Hearth's **API & webhooks** page shows how to connect it. Ask a
|
|
70
|
+
connected agent to start with `get_design_workflow` to design the site in Figma and bring the design back into
|
|
71
|
+
`src/theme/`.
|
|
72
|
+
|
|
67
73
|
Keep the API key out of chat: the agent never needs to see it, only `.env.local` does. If it asks for content
|
|
68
74
|
(page text, menus, contact details), the answer is to add or publish it in Hearth, not to write it into the code.
|
|
69
75
|
|
|
@@ -101,7 +107,7 @@ Keep the API key out of chat: the agent never needs to see it, only `.env.local`
|
|
|
101
107
|
| `/hearth/blocks` | Not a file: while `pnpm dev` runs, this address shows each block with your theme, one at a time, without setting up pages for them. `?block=hero` goes to one, and `?block=all` shows the whole list. Its **Unstyled** switch shows them with no design, and each element's slot name |
|
|
102
108
|
| `src/theme/fonts.ts` | Web fonts (next/font) |
|
|
103
109
|
| `src/theme/custom-blocks.tsx` | Components for the site's own block types (Settings → Custom blocks in Hearth) |
|
|
104
|
-
| `src/theme/overrides.tsx` | Last resort: replace a
|
|
110
|
+
| `src/theme/overrides.tsx` | Last resort: add to or replace a part of a block (its heading, items, buttons or image), a whole block, the logo, the footer, the event or fundraiser page, their cards or the donation form. Each is given Hearth's own as `children`, to keep and add to (see `AGENTS.md`) |
|
|
105
111
|
| `SITE.md` | Notes about this site's design, for the next person or coding agent |
|
|
106
112
|
|
|
107
113
|
A slot holds the classes of one element, e.g. `hero.title` or `cardGrid.card`. Set a string to replace the default,
|
|
@@ -169,6 +175,10 @@ packages are public on npm, so nothing else is needed to install them.
|
|
|
169
175
|
|
|
170
176
|
## Documentation
|
|
171
177
|
|
|
178
|
+
**New to Hearth? Start with the onboarding guide**, in Hearth under **Onboarding guide** (`/admin/docs/onboarding`):
|
|
179
|
+
setting up this site, designing it in code or in Figma, building on the SDK, and prompting coding agents at each stage,
|
|
180
|
+
with sample prompts and the skills above. `pnpm hearth:docs` saves it as `hearth-onboarding.md` for your agent.
|
|
181
|
+
|
|
172
182
|
The full reference (API, SDK, every block and its fields, previews, forms) is in Hearth under **API docs**,
|
|
173
183
|
with a Markdown version to give an AI coding assistant. The SDK also ships `node_modules/@the-smithy/hearth/llms.md`.
|
|
174
184
|
Every block is at `/hearth/blocks` while `pnpm dev` runs, with your theme or unstyled. Hearth has the unstyled page under **Block library**.
|
|
@@ -186,4 +196,5 @@ with `pnpm add @the-smithy/<name>@latest`.
|
|
|
186
196
|
|
|
187
197
|
Keep the site on the template: design goes in `src/theme/`, and bugs or gaps in `src/hearth/` get fixed in Hearth's
|
|
188
198
|
template rather than here. If a site has to bridge a gap, its override goes in `src/theme/overrides.tsx` with a note
|
|
189
|
-
in `SITE.md`.
|
|
199
|
+
in `SITE.md`. `src/hearth/` is collapsed in pull requests (`.gitattributes`), and after an update `pnpm hearth:update`
|
|
200
|
+
lists the overrides whose Hearth version changed (`pnpm hearth:overrides` any time).
|
package/template/SITE.md
CHANGED
|
@@ -11,7 +11,7 @@ site's and stays as it is.
|
|
|
11
11
|
|
|
12
12
|
## Overrides
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
`
|
|
14
|
+
What `src/theme/overrides.tsx` adds to or replaces, and why slots, tokens and CSS couldn't do it. After an update,
|
|
15
|
+
check the ones `pnpm hearth:update` lists as changed in Hearth.
|
|
16
16
|
|
|
17
17
|
- None.
|
package/template/_gitignore
CHANGED
|
@@ -11,8 +11,9 @@ next-env.d.ts
|
|
|
11
11
|
.env*
|
|
12
12
|
!.env.example
|
|
13
13
|
|
|
14
|
-
# Hearth's API
|
|
14
|
+
# Hearth's API and onboarding guides, downloaded by pnpm hearth:docs for AI assistants.
|
|
15
15
|
hearth-api.md
|
|
16
|
+
hearth-onboarding.md
|
|
16
17
|
|
|
17
18
|
# Drift's code index, rebuilt as needed.
|
|
18
19
|
.drift-index.json
|
package/template/package.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"hearth:check": "node scripts/hearth.mjs check",
|
|
12
12
|
"hearth:docs": "node scripts/hearth.mjs docs",
|
|
13
13
|
"hearth:drift": "node scripts/hearth.mjs drift",
|
|
14
|
+
"hearth:overrides": "node scripts/hearth.mjs overrides",
|
|
14
15
|
"hearth:update": "node scripts/hearth.mjs update"
|
|
15
16
|
},
|
|
16
17
|
"dependencies": {
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// Setup helpers for this site, for you or your coding agent. Both read .env.local and never print the API key.
|
|
2
2
|
// pnpm hearth:check says whether The Smithy has released anything newer than this site uses, checks .env.local and
|
|
3
3
|
// the connection to Hearth, then lists what's published
|
|
4
|
-
// pnpm hearth:docs saves Hearth's full API guide to hearth-api.md
|
|
5
|
-
// and installs any coding-assistant skills
|
|
4
|
+
// pnpm hearth:docs saves Hearth's full API guide to hearth-api.md and its onboarding guide to hearth-onboarding.md
|
|
5
|
+
// (neither committed) for an AI assistant to read, and installs any coding-assistant skills
|
|
6
|
+
// Hearth offers this site, and Drift
|
|
6
7
|
// pnpm hearth:drift installs or updates Drift, which finds code for coding agents, from its latest GitHub release
|
|
7
8
|
// pnpm hearth:update updates Hearth's code in this site (src/hearth and the other files its manifest lists) to the
|
|
8
9
|
// latest release, and the packages it needs; src/theme and your own files stay as they are
|
|
10
|
+
// pnpm hearth:overrides lists what src/theme/overrides.tsx overrides whose Hearth version changed since the last
|
|
11
|
+
// commit (or --since <git ref>), which hearth:update runs too
|
|
9
12
|
import { spawnSync } from "node:child_process";
|
|
10
13
|
import { createHash } from "node:crypto";
|
|
11
14
|
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -158,11 +161,25 @@ async function docs() {
|
|
|
158
161
|
if (!res.ok) fail(`Hearth didn't return the API guide (${res.status}). Check the API key with pnpm hearth:check.`);
|
|
159
162
|
writeFileSync("hearth-api.md", await res.text());
|
|
160
163
|
console.log("✓ Saved Hearth's full API guide to hearth-api.md (not committed). Point your coding assistant at it.");
|
|
164
|
+
const guide = await fetch(`${env.baseUrl}/admin/docs/onboarding.md`, { headers: { Authorization: `Bearer ${env.apiKey}` } });
|
|
165
|
+
if (guide.ok) {
|
|
166
|
+
writeFileSync("hearth-onboarding.md", await guide.text());
|
|
167
|
+
ignore("hearth-onboarding.md");
|
|
168
|
+
console.log("✓ Saved Hearth's onboarding guide to hearth-onboarding.md (not committed): setup, design, the SDK, prompts and skills.");
|
|
169
|
+
} else console.log(` Hearth didn't return the onboarding guide (${guide.status}); skipped it.`);
|
|
161
170
|
await skills(env);
|
|
162
171
|
// Hearth's guide and skills are what this command is for, so Drift failing doesn't fail it.
|
|
163
172
|
await drift().catch((err) => console.log(`! Skipped Drift: ${err.message}`));
|
|
164
173
|
}
|
|
165
174
|
|
|
175
|
+
/** Adds a downloaded file to the site's .gitignore if it isn't there (sites made before it was downloaded lack it). */
|
|
176
|
+
function ignore(file) {
|
|
177
|
+
const text = existsSync(".gitignore") ? readFileSync(".gitignore", "utf8") : "";
|
|
178
|
+
if (text.split(/\r?\n/).some((l) => l.trim().replace(/^\//, "") === file)) return;
|
|
179
|
+
const gap = text && !text.endsWith("\n") ? "\n\n" : text ? "\n" : "";
|
|
180
|
+
writeFileSync(".gitignore", `${text}${gap}# Downloaded by pnpm hearth:docs for AI assistants.\n${file}\n`);
|
|
181
|
+
}
|
|
182
|
+
|
|
166
183
|
/** The skills a folder's .gitignore lists, which are the installed ones; anything else there is the site's own. */
|
|
167
184
|
function listedSkills(dir) {
|
|
168
185
|
const listFile = path.join(dir, ".gitignore");
|
|
@@ -374,6 +391,14 @@ async function update() {
|
|
|
374
391
|
}
|
|
375
392
|
writeFileSync(MANIFEST, JSON.stringify({ ...next, version, ...(own.length ? { own } : {}) }, null, 2) + "\n");
|
|
376
393
|
|
|
394
|
+
// Hearth's code collapsed in pull requests, as a new site has it; the rest of .gitattributes is the site's.
|
|
395
|
+
const attributes = existsSync(".gitattributes") ? readFileSync(".gitattributes", "utf8") : "";
|
|
396
|
+
if (!attributes.split(/\r?\n/).some((l) => l.startsWith("src/hearth/**"))) {
|
|
397
|
+
const gap = attributes && !attributes.endsWith("\n") ? "\n\n" : attributes ? "\n" : "";
|
|
398
|
+
writeFileSync(".gitattributes", `${attributes}${gap}${lf(files.get("_gitattributes")?.toString("utf8") ?? "src/hearth/** linguist-generated=true\n")}`);
|
|
399
|
+
changed.push(`${attributes ? "~" : "+"} .gitattributes: src/hearth/ collapsed in pull requests`);
|
|
400
|
+
}
|
|
401
|
+
|
|
377
402
|
// The packages the code needs, at least at the release's versions, and its hearth:* scripts. The site's own stay.
|
|
378
403
|
const pkg = JSON.parse(readFileSync("package.json", "utf8"));
|
|
379
404
|
const theirs = JSON.parse(files.get("package.json").toString("utf8"));
|
|
@@ -395,16 +420,110 @@ async function update() {
|
|
|
395
420
|
for (const line of changed) console.log(` ${line}`);
|
|
396
421
|
console.log(`\nNext: pnpm install, then pnpm typecheck and pnpm build, and check the site with pnpm dev.`);
|
|
397
422
|
console.log(` A type error in src/theme means a slot was renamed or removed: see src/hearth/theme/defaults.ts.`);
|
|
398
|
-
console.log(`
|
|
399
|
-
|
|
423
|
+
console.log(` What changed: CHANGELOG.md in ${PACKAGE}@${version}.\n`);
|
|
424
|
+
// The managed files were committed before the update (it checks), so the last commit has the old ones.
|
|
425
|
+
overridesChanged("HEAD");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const OVERRIDES = "src/theme/overrides.tsx";
|
|
429
|
+
/** Hearth's own version of each of the other overrides: its file and component. */
|
|
430
|
+
const HEARTHS = {
|
|
431
|
+
Footer: ["src/hearth/layout/SiteFooter.tsx", "SiteFooter"],
|
|
432
|
+
EventPage: ["src/hearth/components/EventView.tsx", "EventContent"],
|
|
433
|
+
FundraiserPage: ["src/hearth/components/FundraiserView.tsx", "FundraiserContent"],
|
|
434
|
+
EventCard: ["src/hearth/blocks/events.tsx", "EventCard"],
|
|
435
|
+
FundraiserCard: ["src/hearth/blocks/giving.tsx", "FundraiserCard"],
|
|
436
|
+
DonateForm: ["src/hearth/forms/DonateForm.tsx", "DonateForm"],
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const lf = (text) => text.replace(/\r\n/g, "\n");
|
|
440
|
+
|
|
441
|
+
/** A file at a git ref, from where this site is in the repo (null if it wasn't there or there's no git). */
|
|
442
|
+
function atRef(ref, file) {
|
|
443
|
+
const res = spawnSync("git", ["show", `${ref}:./${file}`], { encoding: "utf8" });
|
|
444
|
+
return res.status === 0 ? lf(res.stdout) : null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** A top-level function's source, to its closing brace at the start of a line. */
|
|
448
|
+
function component(source, name) {
|
|
449
|
+
const start = source?.search(new RegExp(`^(?:export )?(?:async )?function ${name}\\b`, "m")) ?? -1;
|
|
450
|
+
if (start < 0) return null;
|
|
451
|
+
const end = source.indexOf("\n}\n", start);
|
|
452
|
+
return source.slice(start, end < 0 ? undefined : end + 3);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** The keys of an exported object (`export const parts = { "hero.actions": …, stats, … }`): what's at its top level. */
|
|
456
|
+
function exportedKeys(source, name) {
|
|
457
|
+
const at = source.search(new RegExp(`^export const ${name}\\b[^=]*=\\s*\\{`, "m"));
|
|
458
|
+
if (at < 0) return [];
|
|
459
|
+
let depth = 0;
|
|
460
|
+
let top = "";
|
|
461
|
+
for (let i = source.indexOf("{", source.indexOf("=", at)); i < source.length; i++) {
|
|
462
|
+
const c = source[i];
|
|
463
|
+
if ("([{".includes(c)) depth++;
|
|
464
|
+
else if (")]}".includes(c) && --depth === 0) break;
|
|
465
|
+
if (depth === 1 && c !== "{") top += c;
|
|
466
|
+
else if (depth === 2 && "([{".includes(c)) top += " ";
|
|
467
|
+
}
|
|
468
|
+
// A key starts a line or the object (`stats: …` or shorthand `stats,`), or follows a comma with a colon after it.
|
|
469
|
+
// That leaves out most words in JSX text, which sits at this level too.
|
|
470
|
+
return [...top.matchAll(/(?:(?:^|\n)\s*(?:"([\w.]+)"|'([\w.]+)'|([A-Za-z_]\w*))\s*(?=[:,]|$)|,\s*(?:"([\w.]+)"|'([\w.]+)'|([A-Za-z_]\w*))\s*(?=:))/g)].map(
|
|
471
|
+
(m) => m[1] ?? m[2] ?? m[3] ?? m[4] ?? m[5] ?? m[6],
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* What src/theme/overrides.tsx overrides whose Hearth version changed since a git ref: the site's code adds to it or
|
|
477
|
+
* replaces it, so check each still fits. Also names parts and blocks Hearth no longer has.
|
|
478
|
+
*/
|
|
479
|
+
function overridesChanged(ref) {
|
|
480
|
+
if (!existsSync(OVERRIDES)) return;
|
|
481
|
+
const source = lf(readFileSync(OVERRIDES, "utf8"));
|
|
482
|
+
const map = lf(readFileSync("src/hearth/blocks/index.tsx", "utf8"));
|
|
483
|
+
const typesOf = Object.fromEntries([...map.slice(map.indexOf("export const blocks")).matchAll(/^\s+(\w+): (\w+),$/gm)].map((m) => [m[1], m[2]]));
|
|
484
|
+
const partNames = new Set([...lf(readFileSync("src/hearth/overrides.ts", "utf8")).matchAll(/^\s+"(\w+\.\w+)":/gm)].map((m) => m[1]));
|
|
485
|
+
const blockFiles = readdirSync("src/hearth/blocks").map((f) => `src/hearth/blocks/${f}`);
|
|
486
|
+
const where = (name) => blockFiles.find((f) => component(lf(readFileSync(f, "utf8")), name)) ?? "src/hearth/blocks/index.tsx";
|
|
487
|
+
|
|
488
|
+
/** [what the site overrides, Hearth's file, Hearth's component] */
|
|
489
|
+
const checks = [];
|
|
490
|
+
const gone = [];
|
|
491
|
+
for (const type of exportedKeys(source, "blocks")) {
|
|
492
|
+
if (!typesOf[type]) gone.push(`blocks.${type}`);
|
|
493
|
+
else checks.push([`blocks.${type}`, where(typesOf[type]), typesOf[type]]);
|
|
494
|
+
}
|
|
495
|
+
for (const part of exportedKeys(source, "parts")) {
|
|
496
|
+
const name = typesOf[part.split(".")[0]];
|
|
497
|
+
if (!partNames.has(part) || !name) gone.push(`parts["${part}"]`);
|
|
498
|
+
else checks.push([`parts["${part}"]`, where(name), name]);
|
|
499
|
+
}
|
|
500
|
+
for (const [name, [file, hearths]] of Object.entries(HEARTHS)) {
|
|
501
|
+
const line = source.match(new RegExp(`^export (?:const|function|async function) ${name}\\b.*$`, "m"))?.[0];
|
|
502
|
+
if (line && !/=\s*null;$/.test(line)) checks.push([name, file, hearths]);
|
|
503
|
+
}
|
|
504
|
+
if (!checks.length && !gone.length) return console.log(`✓ ${OVERRIDES} overrides nothing of Hearth's.`);
|
|
505
|
+
if (spawnSync("git", ["rev-parse", "--verify", "--quiet", ref]).status !== 0) {
|
|
506
|
+
return console.log(`Couldn't read ${ref} from git, so compare what ${OVERRIDES} overrides with src/hearth yourself.`);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const changed = checks.filter(([, file, name]) => component(atRef(ref, file), name) !== component(lf(readFileSync(file, "utf8")), name));
|
|
510
|
+
if (gone.length) console.log(`✗ Hearth has none of these any more, so ${OVERRIDES} overrides nothing with them: ${gone.join(", ")}.`);
|
|
511
|
+
if (!changed.length) return console.log(`✓ Nothing ${OVERRIDES} overrides has changed in Hearth since ${ref}.`);
|
|
512
|
+
console.log(`Changed in Hearth since ${ref}, and overridden in ${OVERRIDES}. Check each still fits (pnpm dev):`);
|
|
513
|
+
for (const [what, file, name] of changed) console.log(` ${what}: ${name} in ${file}`);
|
|
514
|
+
console.log(` See the changes: git diff ${ref} -- ${[...new Set(changed.map(([, file]) => file))].join(" ")}`);
|
|
400
515
|
}
|
|
401
516
|
|
|
402
517
|
try {
|
|
403
518
|
if (command === "check") await check();
|
|
404
519
|
else if (command === "docs") await docs();
|
|
405
520
|
else if (command === "update") await update();
|
|
521
|
+
else if (command === "overrides") {
|
|
522
|
+
const args = process.argv.slice(3);
|
|
523
|
+
overridesChanged(args.includes("--since") ? args[args.indexOf("--since") + 1] || fail("Give a git ref after --since, e.g. --since HEAD~1.") : "HEAD");
|
|
524
|
+
}
|
|
406
525
|
else if (command === "drift") await drift().catch((err) => fail(err instanceof Stop ? err.message : `Couldn't install Drift: ${err.message}`));
|
|
407
|
-
else fail("Usage: node scripts/hearth.mjs check | docs | drift | update [--version x.y.z] [--force]");
|
|
526
|
+
else fail("Usage: node scripts/hearth.mjs check | docs | drift | update [--version x.y.z] [--force] | overrides [--since <git ref>]");
|
|
408
527
|
} catch (err) {
|
|
409
528
|
if (!(err instanceof Stop)) throw err;
|
|
410
529
|
console.error(`\n✗ ${err.message}\n`);
|
|
@@ -2,33 +2,36 @@ import Link from "next/link";
|
|
|
2
2
|
import type { BlockOf, CollectionEntry } from "@the-smithy/hearth";
|
|
3
3
|
import { theme } from "@/theme";
|
|
4
4
|
import { formatEntryDate } from "../format";
|
|
5
|
+
import { Part } from "../parts";
|
|
5
6
|
import { fill, Img, Section, SectionHeading } from "../ui";
|
|
6
7
|
|
|
7
8
|
const t = theme.collectionList;
|
|
8
9
|
|
|
9
|
-
function Card({ entry,
|
|
10
|
-
const Title = heading ? "h3" : "h2";
|
|
10
|
+
function Card({ block, entry, index }: { block: BlockOf<"collectionList">; entry: CollectionEntry; index: number }) {
|
|
11
|
+
const Title = block.data.heading ? "h3" : "h2";
|
|
11
12
|
return (
|
|
12
13
|
<li className={t.card}>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<
|
|
18
|
-
{entry.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
{
|
|
25
|
-
|
|
26
|
-
{tag}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
14
|
+
<Part name="collectionList.item" props={{ block, item: entry, index, heading: Title }}>
|
|
15
|
+
{entry.image ? <Img image={entry.image} crop={16 / 10} className={t.image} /> : null}
|
|
16
|
+
<div className={t.body}>
|
|
17
|
+
{entry.date ? <p className={t.date}>{formatEntryDate(entry.date)}</p> : null}
|
|
18
|
+
<Title className={t.title}>
|
|
19
|
+
<Link href={entry.path} className={t.link}>
|
|
20
|
+
{entry.title}
|
|
21
|
+
</Link>
|
|
22
|
+
</Title>
|
|
23
|
+
{entry.summary ? <p className={t.summary}>{entry.summary}</p> : null}
|
|
24
|
+
{entry.tags.length ? (
|
|
25
|
+
<ul className={t.tags}>
|
|
26
|
+
{entry.tags.map((tag) => (
|
|
27
|
+
<li key={tag} className={t.tag}>
|
|
28
|
+
{tag}
|
|
29
|
+
</li>
|
|
30
|
+
))}
|
|
31
|
+
</ul>
|
|
32
|
+
) : null}
|
|
33
|
+
</div>
|
|
34
|
+
</Part>
|
|
32
35
|
</li>
|
|
33
36
|
);
|
|
34
37
|
}
|
|
@@ -38,30 +41,34 @@ export function CollectionList({ block }: { block: BlockOf<"collectionList"> })
|
|
|
38
41
|
const { entries, collection, layout } = block.data;
|
|
39
42
|
return (
|
|
40
43
|
<Section block="collectionList" id={block.anchor}>
|
|
41
|
-
<
|
|
44
|
+
<Part name="collectionList.heading" props={{ block }}>
|
|
45
|
+
<SectionHeading eyebrow={block.data.eyebrow} heading={block.data.heading} intro={block.data.intro} />
|
|
46
|
+
</Part>
|
|
42
47
|
{!entries.length ? (
|
|
43
48
|
<p className={t.empty}>{theme.text.noEntries}</p>
|
|
44
49
|
) : layout === "list" ? (
|
|
45
50
|
<ul className={t.list}>
|
|
46
|
-
{entries.map((e) => (
|
|
51
|
+
{entries.map((e, i) => (
|
|
47
52
|
<li key={e.path} className={t.listItem}>
|
|
48
|
-
<
|
|
49
|
-
{e.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
<Part name="collectionList.item" props={{ block, item: e, index: i, heading: block.data.heading ? "h3" : "h2" }}>
|
|
54
|
+
<Link href={e.path} className={t.listLink}>
|
|
55
|
+
{e.title}
|
|
56
|
+
</Link>
|
|
57
|
+
{e.date || e.summary ? (
|
|
58
|
+
<p className={t.listMeta}>
|
|
59
|
+
{e.date ? <span className={t.listDate}>{formatEntryDate(e.date)}</span> : null}
|
|
60
|
+
{e.date && e.summary ? " · " : null}
|
|
61
|
+
{e.summary}
|
|
62
|
+
</p>
|
|
63
|
+
) : null}
|
|
64
|
+
</Part>
|
|
58
65
|
</li>
|
|
59
66
|
))}
|
|
60
67
|
</ul>
|
|
61
68
|
) : (
|
|
62
69
|
<ul className={t.cards}>
|
|
63
|
-
{entries.map((e) => (
|
|
64
|
-
<Card key={e.path} entry={e}
|
|
70
|
+
{entries.map((e, i) => (
|
|
71
|
+
<Card key={e.path} block={block} entry={e} index={i} />
|
|
65
72
|
))}
|
|
66
73
|
</ul>
|
|
67
74
|
)}
|