@the-smithy/create-hearth-site 0.19.0 → 0.21.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 +5 -0
- package/package.json +2 -2
- package/template/AGENTS.md +11 -2
- package/template/CLAUDE.md +87 -20
- package/template/README.md +55 -24
- package/template/SITE.md +17 -0
- package/template/package.json +3 -1
- package/template/scripts/hearth.mjs +194 -14
- package/template/src/app/[[...slug]]/page.tsx +2 -54
- package/template/src/app/api/preview/exit/route.ts +1 -1
- package/template/src/app/api/preview/route.ts +1 -1
- package/template/src/app/api/revalidate/route.ts +1 -1
- package/template/src/app/apple-icon.tsx +1 -1
- package/template/src/app/donate/page.tsx +2 -35
- package/template/src/app/donate/thank-you/page.tsx +2 -22
- package/template/src/app/events/[slug]/page.tsx +2 -26
- package/template/src/app/events/page.tsx +2 -29
- package/template/src/app/fundraisers/[slug]/page.tsx +2 -55
- package/template/src/app/fundraisers/page.tsx +2 -37
- package/template/src/app/globals.css +3 -74
- package/template/src/app/icon.tsx +1 -1
- package/template/src/app/layout.tsx +2 -36
- package/template/src/app/not-found.tsx +2 -14
- package/template/src/app/registered/page.tsx +2 -22
- package/template/src/app/robots.ts +1 -1
- package/template/src/app/sitemap.ts +2 -2
- package/template/src/{app → hearth}/actions.ts +15 -1
- package/template/src/hearth/blocks/OpenMap.tsx +104 -0
- package/template/src/hearth/blocks/collections.tsx +74 -0
- package/template/src/hearth/blocks/content.tsx +252 -0
- package/template/src/hearth/blocks/custom.tsx +12 -0
- package/template/src/hearth/blocks/events.tsx +130 -0
- package/template/src/hearth/blocks/giving.tsx +123 -0
- package/template/src/{components → hearth}/blocks/index.tsx +17 -11
- package/template/src/hearth/blocks/info.tsx +84 -0
- package/template/src/{components → hearth}/blocks/media.tsx +30 -27
- package/template/src/hearth/components/EventView.tsx +120 -0
- package/template/src/{components → hearth/components}/Registration.tsx +14 -14
- package/template/src/hearth/components/Share.tsx +102 -0
- package/template/src/hearth/format.ts +31 -0
- package/template/src/hearth/forms/ContactForm.tsx +27 -0
- package/template/src/hearth/forms/DonateForm.tsx +69 -0
- package/template/src/{components/blocks → hearth/forms}/EmberFormBlock.tsx +6 -5
- package/template/src/hearth/forms/RegistrationForm.tsx +68 -0
- package/template/src/hearth/forms/VolunteerForm.tsx +58 -0
- package/template/src/hearth/forms/fields.tsx +137 -0
- package/template/src/hearth/hearth.css +52 -0
- package/template/src/hearth/layout/MobileNav.tsx +49 -0
- package/template/src/hearth/layout/NavLink.tsx +16 -0
- package/template/src/hearth/layout/RootLayout.tsx +40 -0
- package/template/src/{components → hearth/layout}/SiteFooter.tsx +19 -14
- package/template/src/hearth/layout/SiteHeader.tsx +53 -0
- package/template/src/hearth/manifest.json +36 -0
- package/template/src/hearth/pages/donate.tsx +36 -0
- package/template/src/hearth/pages/event.tsx +26 -0
- package/template/src/hearth/pages/events.tsx +30 -0
- package/template/src/hearth/pages/fundraiser.tsx +77 -0
- package/template/src/hearth/pages/fundraisers.tsx +38 -0
- package/template/src/hearth/pages/not-found.tsx +15 -0
- package/template/src/hearth/pages/page.tsx +54 -0
- package/template/src/hearth/pages/registered.tsx +19 -0
- package/template/src/hearth/pages/thank-you.tsx +19 -0
- package/template/src/hearth/theme/defaults.ts +510 -0
- package/template/src/hearth/theme/index.ts +31 -0
- package/template/src/hearth/ui.tsx +144 -0
- package/template/src/theme/custom-blocks.tsx +11 -0
- package/template/src/theme/fonts.ts +9 -0
- package/template/src/theme/index.ts +13 -0
- package/template/src/theme/overrides.tsx +16 -0
- package/template/src/theme/theme.css +29 -0
- package/template/src/components/DonateForm.tsx +0 -104
- package/template/src/components/EventView.tsx +0 -48
- package/template/src/components/RegistrationForm.tsx +0 -103
- package/template/src/components/SiteHeader.tsx +0 -37
- package/template/src/components/blocks/Form.tsx +0 -92
- package/template/src/components/blocks/collections.tsx +0 -75
- package/template/src/components/blocks/content.tsx +0 -257
- package/template/src/components/blocks/custom.tsx +0 -20
- package/template/src/components/blocks/events.tsx +0 -104
- package/template/src/components/blocks/giving.tsx +0 -120
- package/template/src/components/blocks/info.tsx +0 -52
- package/template/src/components/ui.tsx +0 -128
- /package/template/src/{lib/hearth.ts → hearth/client.ts} +0 -0
- /package/template/src/{lib → hearth}/site.ts +0 -0
|
@@ -91,6 +91,11 @@ async function main() {
|
|
|
91
91
|
}
|
|
92
92
|
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
93
93
|
|
|
94
|
+
// Record which release Hearth's code in the site came from, for pnpm hearth:update.
|
|
95
|
+
const manifestPath = path.join(target, "src", "hearth", "manifest.json");
|
|
96
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
97
|
+
await writeFile(manifestPath, JSON.stringify({ ...manifest, version: self.version }, null, 2) + "\n");
|
|
98
|
+
|
|
94
99
|
const env = (await readFile(path.join(target, ".env.example"), "utf8")).replace(/^HEARTH_SITE=.*$/m, `HEARTH_SITE=${site ?? ""}`);
|
|
95
100
|
await writeFile(path.join(target, ".env.local"), env);
|
|
96
101
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@the-smithy/create-hearth-site",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
],
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"vitest": "^5.0.3",
|
|
32
|
-
"@the-smithy/hearth-schema": "^0.
|
|
32
|
+
"@the-smithy/hearth-schema": "^0.21.0"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"test": "node --test \"test/**/*.test.js\" && vitest run test/blocks.test.ts"
|
package/template/AGENTS.md
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
# Notes for coding agents
|
|
2
2
|
|
|
3
|
-
Read `CLAUDE.md` in this folder: it holds this project's instructions for every AI coding assistant,
|
|
4
|
-
|
|
3
|
+
Read `CLAUDE.md` in this folder: it holds this project's instructions for every AI coding assistant, then `SITE.md`
|
|
4
|
+
for notes about this site.
|
|
5
|
+
|
|
6
|
+
In short: Hearth's code (`src/hearth/`, and the files `src/hearth/manifest.json` lists) is shared by every site and
|
|
7
|
+
replaced by `pnpm hearth:update`, so never edit it. A site is styled in `src/theme/`: tokens in `theme.css`, and the
|
|
8
|
+
Tailwind classes of each element's slot in `index.ts` (every slot and its default is in
|
|
9
|
+
`src/hearth/theme/defaults.ts`).
|
|
10
|
+
|
|
11
|
+
In a new checkout, run `pnpm hearth:check`, then `pnpm hearth:docs`. The latter also installs the skills Hearth
|
|
12
|
+
offers this site in `.claude/skills/`. Read the `SKILL.md` of any that fits your task and follow it (see "Skills" in
|
|
13
|
+
`CLAUDE.md`).
|
package/template/CLAUDE.md
CHANGED
|
@@ -1,7 +1,70 @@
|
|
|
1
1
|
# Notes for Claude
|
|
2
2
|
|
|
3
|
-
This is a client website whose content comes from Hearth, The Smithy's headless CMS.
|
|
4
|
-
`
|
|
3
|
+
This is a client website whose content comes from Hearth, The Smithy's headless CMS. This file is Hearth's and is
|
|
4
|
+
replaced by `pnpm hearth:update`: notes about this particular site go in `SITE.md`, which you should read too.
|
|
5
|
+
|
|
6
|
+
## How this site is built: Hearth's code, the site's styling
|
|
7
|
+
|
|
8
|
+
Everything that works with Hearth is already here, and it's the same in every site Hearth runs: every block type,
|
|
9
|
+
the event, fundraiser and donation pages, forms (contact, volunteer sign-up, registration, donations), the
|
|
10
|
+
header and phone menu, the footer, previews, SEO, analytics, redirects and the sitemap. **A site is built by styling
|
|
11
|
+
it, not by writing code.** It has two parts:
|
|
12
|
+
|
|
13
|
+
| Folder | Whose | What's in it |
|
|
14
|
+
| -------------- | ---------- | ---------------------------------------------------------------------------------------------- |
|
|
15
|
+
| `src/hearth/` | Hearth's | All the components and pages, and the theme's slots and defaults. **Never edit it.** |
|
|
16
|
+
| `src/theme/` | The site's | Its look: tokens, slot classes, fonts, wording, custom blocks. **All design work goes here.** |
|
|
17
|
+
|
|
18
|
+
`src/app/` holds one-line route files that point at `src/hearth/pages/`; they're Hearth's too (see
|
|
19
|
+
`src/hearth/manifest.json` for every file Hearth manages). Pages the site needs that Hearth doesn't serve can be
|
|
20
|
+
added in `src/app/` as the site's own.
|
|
21
|
+
|
|
22
|
+
`pnpm hearth:update` brings in a new release of Hearth's code: it replaces the managed files and leaves `src/theme`
|
|
23
|
+
and the site's own files alone. That's how every site gets new blocks and fixes at once, so an edit to a managed file
|
|
24
|
+
is lost at the next update, and stops the update (it refuses to overwrite uncommitted changes there).
|
|
25
|
+
|
|
26
|
+
## Styling a site
|
|
27
|
+
|
|
28
|
+
Work in this order, and stop at the first step that gets the result:
|
|
29
|
+
|
|
30
|
+
1. **Tokens** in `src/theme/theme.css`: colors, fonts, content width. Every slot uses these names (`bg-accent`,
|
|
31
|
+
`text-highlight`, `bg-muted`, `border-field`…), so most of a brand is set here. Hearth's tones (default, muted,
|
|
32
|
+
inverse, accent) map onto them.
|
|
33
|
+
2. **Fonts** in `src/theme/fonts.ts` (next/font), used from the tokens.
|
|
34
|
+
3. **Slots** in `src/theme/index.ts`. Every element Hearth's code draws takes its Tailwind classes from one slot.
|
|
35
|
+
`src/hearth/theme/defaults.ts` lists every part (`hero`, `cardGrid`, `header`, `form`, `event`…), every slot
|
|
36
|
+
and its default classes: read it before styling. Set only the slots the design changes:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
export const theme = defineTheme({
|
|
40
|
+
hero: { title: "font-display text-7xl uppercase", inner: (base) => `${base} text-center` },
|
|
41
|
+
button: { primary: "inline-block bg-accent px-6 py-3 font-mono uppercase text-accent-ink hover:bg-ink" },
|
|
42
|
+
stats: { tone: "accent" },
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- A string **replaces** the slot's default; a function **extends** it (`(base) => …`). Replace when you change a
|
|
47
|
+
property the default already sets (a radius, a size), since two classes for the same property don't reliably
|
|
48
|
+
resolve in the order written.
|
|
49
|
+
- Each slot styles one element, so slots never fight. States are attributes on the element, so they're plain
|
|
50
|
+
Tailwind variants in the slot: `on-dark:` (on an inverse or accent ground), `data-[size=full]:`,
|
|
51
|
+
`data-highlight:`, `aria-invalid:`, `aria-[current=page]:`, `has-checked:`, `in-data-[…]:` and others; the top
|
|
52
|
+
of `defaults.ts` lists them and each slot's comment says which apply.
|
|
53
|
+
- Slots named `tone` pick a block's ground (default, muted, inverse or accent). `text` holds the words the code
|
|
54
|
+
shows ("Get directions", "Register"…). `map.style` is the map's MapLibre style; `emberForm` shapes Ember forms.
|
|
55
|
+
- Icons and marks drawn with CSS (`before:content-['✓']`, the FAQ's `+`, link arrows) live in slots too.
|
|
56
|
+
4. **Site CSS** under the tokens in `theme.css`: keyframes, background patterns, `@utility` classes your slots use,
|
|
57
|
+
and rules for Hearth's rich text (`.prose`) or a block (`[data-block="hero"]`).
|
|
58
|
+
5. **Overrides**, last: `src/theme/overrides.tsx` can replace a whole block's component or draw the header's logo.
|
|
59
|
+
An override is the site's own code: updates don't reach it, so it falls behind Hearth. Use one only when slots,
|
|
60
|
+
tokens and CSS can't get there, build it from `src/hearth/ui.tsx` and the theme, and say why in `SITE.md`.
|
|
61
|
+
|
|
62
|
+
Never copy a component out of `src/hearth/` to restyle it. If a design needs a slot or an option Hearth doesn't have,
|
|
63
|
+
say so: it belongs in Hearth's template for every site (tell the developer to raise it with The Smithy), and an
|
|
64
|
+
override can bridge the gap meanwhile.
|
|
65
|
+
|
|
66
|
+
**Custom blocks** (Settings → Custom blocks in Hearth) are the one place a site writes components: in
|
|
67
|
+
`src/theme/custom-blocks.tsx`, built from `src/hearth/ui.tsx` so they take the theme.
|
|
5
68
|
|
|
6
69
|
## First steps in a new checkout
|
|
7
70
|
|
|
@@ -13,39 +76,43 @@ Do these before writing code, and tell the developer what you found:
|
|
|
13
76
|
fix (missing key, wrong `HEARTH_SITE`, revoked key). Ask the developer to fix `.env.local`; don't guess values.
|
|
14
77
|
2. **Get the full reference:** `pnpm hearth:docs` saves Hearth's API guide (every block and its fields, events,
|
|
15
78
|
forms, previews, SEO) to `hearth-api.md`. Read the parts you need from it, and from
|
|
16
|
-
`node_modules/@the-smithy/hearth/llms.md` for the SDK.
|
|
79
|
+
`node_modules/@the-smithy/hearth/llms.md` for the SDK. It also installs any skills Hearth offers this site in
|
|
80
|
+
`.claude/skills/` (see "Skills" below), so run it again when they're missing.
|
|
17
81
|
3. **"Nothing is published"** means every page is a 404 until editors publish in Hearth. That's not a code
|
|
18
82
|
problem: say so, and don't add placeholder content to work around it.
|
|
19
|
-
4. **Then
|
|
20
|
-
|
|
83
|
+
4. **Then style the site** as above, checking with `pnpm dev` against the published pages, on a phone width too.
|
|
84
|
+
|
|
85
|
+
## Skills
|
|
86
|
+
|
|
87
|
+
`pnpm hearth:docs` installs the skills Hearth offers this site in `.claude/skills/` (which ones depends on the
|
|
88
|
+
site). Each `SKILL.md` says what it's for and when to use it. Claude Code loads them by itself; other assistants
|
|
89
|
+
should read the ones that fit the task and follow them. Use them for that work, design work in particular, rather
|
|
90
|
+
than starting from scratch. They aren't committed (`.claude/skills/.gitignore` lists them), so a fresh checkout gets
|
|
91
|
+
them by running `pnpm hearth:docs`; never edit them, because the next run replaces them.
|
|
21
92
|
|
|
22
93
|
## Rules
|
|
23
94
|
|
|
24
95
|
- **Content comes from Hearth.** Don't hard-code page text, menus, contact details or SEO tags. They're edited
|
|
25
|
-
in Hearth and arrive through `src/
|
|
26
|
-
- **The API key is secret.** Only import `@/
|
|
96
|
+
in Hearth and arrive through `src/hearth/client.ts`. The site's code owns design only.
|
|
97
|
+
- **The API key is secret.** Only import `@/hearth/client` from server components, route handlers and server
|
|
27
98
|
actions. Never from a `"use client"` file.
|
|
28
99
|
- **Use `hearthNext.*` for reads.** It caches and tags requests so the publish webhook can refresh them.
|
|
29
100
|
Calling `hearth.*` directly skips the cache.
|
|
30
|
-
- **
|
|
31
|
-
with
|
|
32
|
-
optional fields left empty.
|
|
33
|
-
- **Styling uses the tokens in `src/app/globals.css`** (`bg-accent`, `text-highlight`, …). Tones are semantic.
|
|
34
|
-
Check text contrast on every ground.
|
|
101
|
+
- **Keep every option working.** Blocks have tones, image positions, sizes and optional fields; check a restyle
|
|
102
|
+
with each, on light and dark grounds, and check text contrast on every ground (form field borders need 3:1).
|
|
35
103
|
- **No bundled content.** Every page comes from Hearth; an address with no published page is a 404. Don't add
|
|
36
104
|
fallback or sample content to the code.
|
|
37
|
-
- **Previews come from
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
page in Hearth: cookieless, so no consent banner. Don't add Google Analytics or other trackers. Marketing tags
|
|
42
|
-
(ads pixels) need a consent banner and are a separate, opt-in feature. Use `track("name")` from
|
|
43
|
-
`@the-smithy/hearth/analytics` for meaningful actions (a booking, a sign-up).
|
|
105
|
+
- **Previews, analytics and forms come from Hearth's code.** Don't build a preview banner, add Google Analytics or
|
|
106
|
+
other trackers (Hearth's analytics is cookieless; marketing tags need a consent banner and are a separate, opt-in
|
|
107
|
+
feature), or post forms anywhere else. Use `track("name")` from `@the-smithy/hearth/analytics` in the site's own
|
|
108
|
+
code for meaningful actions.
|
|
44
109
|
|
|
45
110
|
## Checks
|
|
46
111
|
|
|
47
112
|
`pnpm typecheck` and `pnpm build`. The build needs the `.env.local` keys, because it reads content from Hearth.
|
|
48
|
-
`pnpm hearth:check`
|
|
113
|
+
`pnpm hearth:check` shows any block type the published content uses without a component (run `pnpm hearth:update`).
|
|
114
|
+
After `pnpm hearth:update`: `pnpm install`, then the checks; a type error in `src/theme/index.ts` means a slot was
|
|
115
|
+
renamed or removed (see `defaults.ts`).
|
|
49
116
|
|
|
50
117
|
<!-- BEGIN:nextjs-agent-rules -->
|
|
51
118
|
|
package/template/README.md
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# Website powered by Hearth
|
|
2
2
|
|
|
3
3
|
A Next.js site whose content (pages, menu, footer, events and forms) comes from
|
|
4
|
-
[Hearth](https://hearth.thesmithy.io), The Smithy's content management system. **
|
|
5
|
-
|
|
4
|
+
[Hearth](https://hearth.thesmithy.io), The Smithy's content management system. **Hearth decides what's on the page;
|
|
5
|
+
this site decides how it looks.** Editors change content in Hearth, and you never hard-code it here.
|
|
6
|
+
|
|
7
|
+
Everything that works with Hearth (every block, the event and fundraiser pages, forms, previews, SEO) is already
|
|
8
|
+
built, in `src/hearth/`, and shared by every Hearth site. **You build the site by styling it** in `src/theme/`: brand
|
|
9
|
+
tokens, and Tailwind classes for each part of each block. `pnpm hearth:update` brings in new blocks and fixes later
|
|
10
|
+
without touching your styling. See "Styling the site" below.
|
|
6
11
|
|
|
7
12
|
## Before you start
|
|
8
13
|
|
|
@@ -42,16 +47,17 @@ The project comes with instructions for coding agents (`CLAUDE.md`, which Claude
|
|
|
42
47
|
1. **Check the connection yourself, or have the agent do it:** `pnpm hearth:check` confirms the key works and
|
|
43
48
|
lists the published pages and the block types they use. It never prints the key.
|
|
44
49
|
2. **Give the agent the full reference:** `pnpm hearth:docs` saves Hearth's API guide to `hearth-api.md` (not
|
|
45
|
-
committed)
|
|
50
|
+
committed), and installs any coding-assistant skills Hearth offers your site in `.claude/skills/`. The agent's
|
|
51
|
+
instructions already point it at both.
|
|
46
52
|
3. **Then tell it what to build.** Prompts that work well:
|
|
47
53
|
|
|
48
54
|
- *"Run the first steps in CLAUDE.md and tell me what's published and which blocks the site uses."*
|
|
49
55
|
- *"Restyle the site to match the brand: colors `#1F3A5F` and `#C8A15A`, font Lora for headings. Start with
|
|
50
|
-
the tokens in `
|
|
56
|
+
the tokens in `src/theme/theme.css`, then the hero, card grid and media-text slots in `src/theme/index.ts`."*
|
|
51
57
|
- *"Match the current website at https://example.org: compare each published page with the same page there
|
|
52
|
-
and restyle the blocks until they look alike. Content stays in Hearth."*
|
|
53
|
-
- *"The gallery
|
|
54
|
-
- *"Run `pnpm hearth:
|
|
58
|
+
and restyle the blocks through the theme until they look alike. Content stays in Hearth."*
|
|
59
|
+
- *"The gallery looks plain. Make it a masonry grid using its slots in the theme, keeping it accessible."*
|
|
60
|
+
- *"Run `pnpm hearth:update`, then check every published page still looks right."*
|
|
55
61
|
|
|
56
62
|
Keep the API key out of chat: the agent never needs to see it, only `.env.local` does. If it asks for content
|
|
57
63
|
(page text, menus, contact details), the answer is to add or publish it in Hearth, not to write it into the code.
|
|
@@ -60,36 +66,58 @@ Keep the API key out of chat: the agent never needs to see it, only `.env.local`
|
|
|
60
66
|
|
|
61
67
|
- **Seeing published changes locally.** Content is cached, and Hearth's publish webhook can't reach your
|
|
62
68
|
computer. After publishing, do a hard refresh (Ctrl+Shift+R, or Cmd+Shift+R on a Mac).
|
|
63
|
-
- **Making it the client's own.**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
- To leave a block type out, delete its line in `src/components/blocks/index.tsx` and ask for it to be turned
|
|
69
|
-
off for the site in Hearth.
|
|
69
|
+
- **Making it the client's own.** See "Styling the site" below: tokens first, then slots.
|
|
70
|
+
- Every block uses semantic tones (`default`, `muted`, `inverse`, `accent`). Map them to the brand in the tokens
|
|
71
|
+
rather than setting colors block by block.
|
|
72
|
+
- Headings can mark words with `*asterisks*` to highlight them (the `emphasis` slot, and `hero.emphasis`).
|
|
73
|
+
- To leave a block type out, ask for it to be turned off for the site in Hearth.
|
|
70
74
|
- **SEO comes from Hearth.** Titles, descriptions, canonical addresses, share images, structured data, the
|
|
71
75
|
sitemap, robots.txt, redirects for old addresses, Search Console verification and the page's language all come
|
|
72
76
|
from Hearth (`siteMetadata`, `generatePageMetadata`, `sitemapWith`, `robots`, `redirectOrNotFound`). Preview deploys
|
|
73
77
|
and sites hidden in Hearth are kept out of search engines. What's still yours:
|
|
74
78
|
- Pages your own code serves (like `/events`) need their own `generateMetadata` and a place in `src/app/sitemap.ts`.
|
|
75
79
|
- Use `<Img priority>` for the image at the top of a page, and keep one `<h1>` per page (the Hero block renders it).
|
|
80
|
+
- Give `<Img>` the `crop` of an image that fills a frame (`crop={4 / 3}` for `aspect-[4/3] object-cover`) so it keeps
|
|
81
|
+
the focal point set in Hearth's media library in view.
|
|
76
82
|
- Add both the bare domain and `www` to the site in Netlify, with the one Hearth uses as primary: Netlify then
|
|
77
83
|
redirects the other to it.
|
|
78
84
|
|
|
85
|
+
### Styling the site
|
|
86
|
+
|
|
87
|
+
| Where | What |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `src/theme/theme.css` | Brand tokens (colors, fonts, content width) and the site's own CSS. **Start here** |
|
|
90
|
+
| `src/theme/index.ts` | The Tailwind classes of any part of any block, page, form, header or footer, by slot |
|
|
91
|
+
| `src/hearth/theme/defaults.ts` | Every slot and its default classes: read it to find the slot to change |
|
|
92
|
+
| `src/theme/fonts.ts` | Web fonts (next/font) |
|
|
93
|
+
| `src/theme/custom-blocks.tsx` | Components for the site's own block types (Settings → Custom blocks in Hearth) |
|
|
94
|
+
| `src/theme/overrides.tsx` | Last resort: replace a whole block's component, or draw the logo |
|
|
95
|
+
| `SITE.md` | Notes about this site's design, for the next person or coding agent |
|
|
96
|
+
|
|
97
|
+
A slot holds the classes of one element, e.g. `hero.title` or `cardGrid.card`. Set a string to replace the default,
|
|
98
|
+
or a function to add to it:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
export const theme = defineTheme({
|
|
102
|
+
hero: { title: "font-display text-7xl uppercase", inner: (base) => `${base} text-center` },
|
|
103
|
+
cardGrid: { card: "flex flex-col gap-3 border-2 border-ink p-6" },
|
|
104
|
+
text: { directions: "Directions" },
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
States are Tailwind variants inside the slot: `on-dark:` for blocks on an inverse or accent ground,
|
|
109
|
+
`data-[size=full]:`, `aria-invalid:` and so on (listed at the top of `defaults.ts`).
|
|
110
|
+
|
|
79
111
|
### Where things live
|
|
80
112
|
|
|
81
113
|
| Where | What |
|
|
82
114
|
| --- | --- |
|
|
83
|
-
| `src/
|
|
84
|
-
| `src/
|
|
85
|
-
| `src/
|
|
86
|
-
| `src/app
|
|
87
|
-
| `src/app/events/` | Event list and detail pages (sites with Events) |
|
|
88
|
-
| `src/app/fundraisers/`, `src/app/donate/` | Fundraisers and donations (sites with Giving) |
|
|
89
|
-
| `src/components/RegistrationForm.tsx`, `src/app/registered/` | On-site event registration (sites with Tickets) |
|
|
115
|
+
| `src/hearth/` | Hearth's code: blocks, pages, forms, header and footer. Shared by every site; **don't edit it** |
|
|
116
|
+
| `src/hearth/client.ts` | The Hearth client. Server-side only, because the API key is secret |
|
|
117
|
+
| `src/hearth/manifest.json` | The files Hearth manages here, and the release they came from |
|
|
118
|
+
| `src/app/` | Route files that point at `src/hearth/pages/`, plus any pages of the site's own |
|
|
90
119
|
| `src/app/api/revalidate/route.ts` | Hearth calls this when something is published, so changes go live in seconds |
|
|
91
120
|
| `src/app/api/preview/route.ts` | Opens drafts from Hearth's "Preview on the website"; `/api/preview/exit` ends it |
|
|
92
|
-
| `<HearthPreview />` in `src/app/layout.tsx` | Refreshes the page while an editor previews and edits in Hearth |
|
|
93
121
|
| `src/app/sitemap.ts`, `robots.ts` | Generated from Hearth. Search engines stay out until the site's web address is set |
|
|
94
122
|
|
|
95
123
|
## Deploying to Netlify
|
|
@@ -128,6 +156,9 @@ Without the webhook, published changes still appear, but can take up to an hour.
|
|
|
128
156
|
The full reference (API, SDK, every block and its fields, previews, forms) is in Hearth under **API docs**,
|
|
129
157
|
with a Markdown version to give an AI coding assistant. The SDK also ships `node_modules/@the-smithy/hearth/llms.md`.
|
|
130
158
|
|
|
131
|
-
## Updating
|
|
159
|
+
## Updating
|
|
132
160
|
|
|
133
|
-
`pnpm
|
|
161
|
+
`pnpm hearth:update` updates Hearth's code in the site (`src/hearth/` and the other files `src/hearth/manifest.json`
|
|
162
|
+
lists) to the latest release, with the packages it needs. Your `src/theme/` and your own files aren't touched. Then
|
|
163
|
+
`pnpm install`, `pnpm typecheck`, `pnpm build`, and a look at the site. New blocks arrive styled by the defaults and
|
|
164
|
+
your tokens; give them slots in the theme if the design needs it.
|
package/template/SITE.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# This site
|
|
2
|
+
|
|
3
|
+
Notes for whoever works on this site next, people and coding agents alike: the design's intent, decisions, and
|
|
4
|
+
anything unusual. Hearth's own instructions are in `CLAUDE.md` (replaced by `pnpm hearth:update`); this file is the
|
|
5
|
+
site's and stays as it is.
|
|
6
|
+
|
|
7
|
+
## Design
|
|
8
|
+
|
|
9
|
+
- Brand: (colors, fonts, the look in a sentence)
|
|
10
|
+
- Reference: (the design file, the client's old site, design library boards)
|
|
11
|
+
|
|
12
|
+
## Overrides
|
|
13
|
+
|
|
14
|
+
Components replaced in `src/theme/overrides.tsx`, and why slots, tokens and CSS couldn't do it. Check each against
|
|
15
|
+
`src/hearth/blocks` after an update.
|
|
16
|
+
|
|
17
|
+
- None.
|
package/template/package.json
CHANGED
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
"start": "next start",
|
|
10
10
|
"typecheck": "tsc --noEmit",
|
|
11
11
|
"hearth:check": "node scripts/hearth.mjs check",
|
|
12
|
-
"hearth:docs": "node scripts/hearth.mjs docs"
|
|
12
|
+
"hearth:docs": "node scripts/hearth.mjs docs",
|
|
13
|
+
"hearth:update": "node scripts/hearth.mjs update"
|
|
13
14
|
},
|
|
14
15
|
"dependencies": {
|
|
15
16
|
"@the-smithy/ember": "^0.3.1",
|
|
16
17
|
"@the-smithy/hearth": "workspace:^",
|
|
18
|
+
"maplibre-gl": "^6.16.0",
|
|
17
19
|
"next": "16.3.8",
|
|
18
20
|
"react": "19.2.8",
|
|
19
21
|
"react-dom": "19.2.8",
|
|
@@ -1,8 +1,13 @@
|
|
|
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 checks .env.local and the connection to Hearth, then lists what's published
|
|
3
|
-
// pnpm hearth:docs saves Hearth's full API guide to hearth-api.md (not committed) for an AI assistant to read
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
// pnpm hearth:docs saves Hearth's full API guide to hearth-api.md (not committed) for an AI assistant to read,
|
|
4
|
+
// and installs any coding-assistant skills Hearth offers this site in .claude/skills/
|
|
5
|
+
// pnpm hearth:update updates Hearth's code in this site (src/hearth and the other files its manifest lists) to the
|
|
6
|
+
// latest release, and the packages it needs; src/theme and your own files stay as they are
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { gunzipSync } from "node:zlib";
|
|
6
11
|
|
|
7
12
|
const command = process.argv[2];
|
|
8
13
|
/** Stops with a message. Thrown rather than process.exit(), which can crash Node on Windows mid-request. */
|
|
@@ -11,7 +16,10 @@ const fail = (message) => {
|
|
|
11
16
|
throw new Stop(message);
|
|
12
17
|
};
|
|
13
18
|
|
|
14
|
-
|
|
19
|
+
/** The SDK, loaded only by the commands that talk to Hearth (so hearth:update works before pnpm install). */
|
|
20
|
+
const sdk = () => import("@the-smithy/hearth");
|
|
21
|
+
|
|
22
|
+
async function readEnv() {
|
|
15
23
|
if (!existsSync(".env.local")) fail("There's no .env.local. Copy .env.example to .env.local and fill it in (see README.md → First run).");
|
|
16
24
|
const env = Object.fromEntries(
|
|
17
25
|
readFileSync(".env.local", "utf8")
|
|
@@ -23,19 +31,23 @@ function readEnv() {
|
|
|
23
31
|
if (!env.HEARTH_SITE) fail("HEARTH_SITE is empty in .env.local. It's the site's address: the part after /admin/sites/ in Hearth's address bar.");
|
|
24
32
|
if (!env.HEARTH_API_KEY) fail("HEARTH_API_KEY is empty in .env.local. Create a key in Hearth → the site → API & webhooks → API keys.");
|
|
25
33
|
if (!env.HEARTH_API_KEY.startsWith("hk_")) fail("HEARTH_API_KEY doesn't look like a Hearth key (they start with hk_). Copy it again from API & webhooks.");
|
|
26
|
-
return { site: env.HEARTH_SITE, apiKey: env.HEARTH_API_KEY, baseUrl: (env.HEARTH_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, "") };
|
|
34
|
+
return { site: env.HEARTH_SITE, apiKey: env.HEARTH_API_KEY, baseUrl: (env.HEARTH_BASE_URL || (await sdk()).DEFAULT_BASE_URL).replace(/\/+$/, "") };
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
/** Block types the
|
|
37
|
+
/** Block types the site has a component for (the keys of `blocks` in src/hearth/blocks/index.tsx and src/theme/overrides.tsx). */
|
|
30
38
|
function designedBlocks() {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
const keys = (file) => {
|
|
40
|
+
if (!existsSync(file)) return [];
|
|
41
|
+
const map = readFileSync(file, "utf8").match(/export const blocks[^{]*\{([\s\S]*?)\n?\};/);
|
|
42
|
+
return map ? [...map[1].matchAll(/^\s*([a-zA-Z]+):/gm)].map((m) => m[1]) : [];
|
|
43
|
+
};
|
|
44
|
+
const found = [...keys("src/hearth/blocks/index.tsx"), ...keys("src/theme/overrides.tsx")];
|
|
45
|
+
return found.length ? new Set(found) : null;
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
async function check() {
|
|
38
|
-
const env = readEnv();
|
|
49
|
+
const env = await readEnv();
|
|
50
|
+
const { createHearthClient, isHearthError } = await sdk();
|
|
39
51
|
const hearth = createHearthClient(env);
|
|
40
52
|
let site;
|
|
41
53
|
try {
|
|
@@ -72,7 +84,7 @@ async function check() {
|
|
|
72
84
|
console.log(`\nBlock types in published pages (how often):`);
|
|
73
85
|
for (const [type, count] of [...used].sort((a, b) => b[1] - a[1])) {
|
|
74
86
|
const missing = designed && !designed.has(type);
|
|
75
|
-
console.log(` ${type.padEnd(22)} ${String(count).padStart(3)}${missing ? " ✗ no component
|
|
87
|
+
console.log(` ${type.padEnd(22)} ${String(count).padStart(3)}${missing ? " ✗ no component: run pnpm hearth:update" : ""}`);
|
|
76
88
|
}
|
|
77
89
|
if (site.features.includes("events")) {
|
|
78
90
|
const events = await hearth.events.list({ when: "upcoming" });
|
|
@@ -82,17 +94,185 @@ async function check() {
|
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
async function docs() {
|
|
85
|
-
const env = readEnv();
|
|
97
|
+
const env = await readEnv();
|
|
86
98
|
const res = await fetch(`${env.baseUrl}/admin/docs/api.md`, { headers: { Authorization: `Bearer ${env.apiKey}` } });
|
|
87
99
|
if (!res.ok) fail(`Hearth didn't return the API guide (${res.status}). Check the API key with pnpm hearth:check.`);
|
|
88
100
|
writeFileSync("hearth-api.md", await res.text());
|
|
89
101
|
console.log("✓ Saved Hearth's full API guide to hearth-api.md (not committed). Point your coding assistant at it.");
|
|
102
|
+
await skills(env);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Installs the coding-assistant skills Hearth offers this site into .claude/skills/, replacing the ones it
|
|
107
|
+
* installed before. Their names are listed in .claude/skills/.gitignore, so they're never committed and the
|
|
108
|
+
* site's own skills there are left alone.
|
|
109
|
+
*/
|
|
110
|
+
async function skills(env) {
|
|
111
|
+
const res = await fetch(`${env.baseUrl}/admin/docs/skills.json`, { headers: { Authorization: `Bearer ${env.apiKey}` } });
|
|
112
|
+
if (!res.ok) return console.log(` Hearth didn't return the coding-assistant skills (${res.status}); skipped them.`);
|
|
113
|
+
const { skills: offered = [] } = await res.json();
|
|
114
|
+
const dir = path.join(".claude", "skills");
|
|
115
|
+
const listFile = path.join(dir, ".gitignore");
|
|
116
|
+
const before = existsSync(listFile)
|
|
117
|
+
? readFileSync(listFile, "utf8").split(/\r?\n/).map((l) => l.replace(/^\/|\/$/g, "")).filter((l) => l && !l.startsWith("#") && l !== ".gitignore")
|
|
118
|
+
: [];
|
|
119
|
+
const safe = (p) => /^[a-z0-9][a-z0-9-]*$/.test(p);
|
|
120
|
+
const names = offered.map((s) => s.name).filter(safe);
|
|
121
|
+
for (const name of before) if (safe(name) && !names.includes(name)) rmSync(path.join(dir, name), { recursive: true, force: true });
|
|
122
|
+
for (const skill of offered.filter((s) => safe(s.name))) {
|
|
123
|
+
rmSync(path.join(dir, skill.name), { recursive: true, force: true });
|
|
124
|
+
for (const file of skill.files) {
|
|
125
|
+
if (!file.path.split("/").every((part) => /^[A-Za-z0-9._-]+$/.test(part) && part !== "..")) continue;
|
|
126
|
+
const target = path.join(dir, skill.name, ...file.path.split("/"));
|
|
127
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
128
|
+
writeFileSync(target, file.content);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (names.length || before.length) {
|
|
132
|
+
mkdirSync(dir, { recursive: true });
|
|
133
|
+
writeFileSync(listFile, ["# Installed by pnpm hearth:docs from Hearth; refreshed each run.", ".gitignore", ...names.map((n) => `/${n}/`)].join("\n") + "\n");
|
|
134
|
+
}
|
|
135
|
+
if (names.length) console.log(`✓ Installed ${names.length === 1 ? "a skill" : `${names.length} skills`} for your coding assistant in .claude/skills/: ${names.join(", ")}.`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const PACKAGE = "@the-smithy/create-hearth-site";
|
|
139
|
+
const MANIFEST = "src/hearth/manifest.json";
|
|
140
|
+
|
|
141
|
+
/** The files in an npm package tarball (.tgz), by their path inside it ("package/…"). */
|
|
142
|
+
function untar(tgz) {
|
|
143
|
+
const tar = gunzipSync(tgz);
|
|
144
|
+
const files = new Map();
|
|
145
|
+
let longName = null;
|
|
146
|
+
for (let at = 0; at + 512 <= tar.length; ) {
|
|
147
|
+
const header = tar.subarray(at, at + 512);
|
|
148
|
+
if (header.every((b) => b === 0)) break;
|
|
149
|
+
const text = (from, len) => header.subarray(from, from + len).toString("utf8").split("\0")[0];
|
|
150
|
+
const size = parseInt(text(124, 12).trim() || "0", 8);
|
|
151
|
+
const type = text(156, 1);
|
|
152
|
+
const body = tar.subarray(at + 512, at + 512 + size);
|
|
153
|
+
const name = longName ?? [text(345, 155), text(0, 100)].filter(Boolean).join("/");
|
|
154
|
+
longName = null;
|
|
155
|
+
if (type === "x") longName = body.toString("utf8").match(/\d+ path=([^\n]*)\n/)?.[1] ?? null;
|
|
156
|
+
else if (type === "0" || type === "") files.set(name, body);
|
|
157
|
+
at += 512 + Math.ceil(size / 512) * 512;
|
|
158
|
+
}
|
|
159
|
+
return files;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Every file at a manifest entry in this site (an entry ending in / is a folder), relative to the site. */
|
|
163
|
+
function localFiles(entry) {
|
|
164
|
+
if (!existsSync(entry)) return [];
|
|
165
|
+
if (!entry.endsWith("/")) return [entry];
|
|
166
|
+
return readdirSync(entry, { recursive: true })
|
|
167
|
+
.map((f) => path.join(entry, String(f)).split(path.sep).join("/"))
|
|
168
|
+
.filter((f) => statSync(f).isFile());
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const listed = (entries, file) => entries.some((e) => (e.endsWith("/") ? file.startsWith(e) : file === e));
|
|
172
|
+
|
|
173
|
+
/** Is version range a lower than b? (^1.2.3 against ^1.4.0) */
|
|
174
|
+
const lower = (a, b) => {
|
|
175
|
+
const v = (r) => (String(r).match(/(\d+)\.(\d+)\.(\d+)/) ?? []).slice(1).map(Number);
|
|
176
|
+
const [x, y] = [v(a), v(b)];
|
|
177
|
+
if (x.length < 3 || y.length < 3) return false;
|
|
178
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] < y[i];
|
|
179
|
+
return false;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Updates the code Hearth manages in this site to the latest release (or --version x.y.z): replaces the managed files
|
|
184
|
+
* listed in src/hearth/manifest.json, removes managed files the release dropped, adds seeded files that are new to
|
|
185
|
+
* it (never overwriting the site's), and raises the packages the code needs. src/theme and the site's own files
|
|
186
|
+
* aren't touched.
|
|
187
|
+
*/
|
|
188
|
+
async function update() {
|
|
189
|
+
const args = process.argv.slice(3);
|
|
190
|
+
const force = args.includes("--force");
|
|
191
|
+
const wanted = args.includes("--version") ? args[args.indexOf("--version") + 1] : "latest";
|
|
192
|
+
if (!wanted) fail("Give a version after --version, e.g. --version 0.21.0.");
|
|
193
|
+
if (!existsSync(MANIFEST)) fail(`There's no ${MANIFEST}, so this site predates hearth:update. Move it onto the template's structure first (see CLAUDE.md).`);
|
|
194
|
+
const old = JSON.parse(readFileSync(MANIFEST, "utf8"));
|
|
195
|
+
|
|
196
|
+
// Managed files edited here would be lost: styling belongs in src/theme.
|
|
197
|
+
const git = spawnSync("git", ["status", "--porcelain", "--", ...old.managed], { encoding: "utf8" });
|
|
198
|
+
if (git.status === 0 && git.stdout.trim() && !force) {
|
|
199
|
+
fail(`These managed files have uncommitted changes, which the update would replace:\n${git.stdout.trimEnd()}\nCommit them first (and move any styling into src/theme), or run with --force to replace them.`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// HEARTH_UPDATE_FROM=<a create-hearth-site .tgz> tries a release before it's published.
|
|
203
|
+
const prefix = "package/template/";
|
|
204
|
+
let tarball;
|
|
205
|
+
let version;
|
|
206
|
+
if (process.env.HEARTH_UPDATE_FROM) {
|
|
207
|
+
tarball = untar(readFileSync(process.env.HEARTH_UPDATE_FROM));
|
|
208
|
+
version = JSON.parse(tarball.get("package/package.json").toString("utf8")).version;
|
|
209
|
+
} else {
|
|
210
|
+
const res = await fetch(`https://registry.npmjs.org/${PACKAGE.replace("/", "%2f")}`);
|
|
211
|
+
if (!res.ok) fail(`npm didn't return ${PACKAGE} (${res.status}).`);
|
|
212
|
+
const meta = await res.json();
|
|
213
|
+
version = meta["dist-tags"]?.[wanted] ?? wanted;
|
|
214
|
+
const release = meta.versions?.[version];
|
|
215
|
+
if (!release) fail(`${PACKAGE} has no version ${wanted}.`);
|
|
216
|
+
if (version === old.version && !force) return console.log(`✓ Already on ${version}, the latest. (--force reinstalls it.)`);
|
|
217
|
+
const download = await fetch(release.dist.tarball);
|
|
218
|
+
if (!download.ok) fail(`Couldn't download ${PACKAGE}@${version} (${download.status}).`);
|
|
219
|
+
tarball = untar(Buffer.from(await download.arrayBuffer()));
|
|
220
|
+
}
|
|
221
|
+
const files = new Map([...tarball].filter(([name]) => name.startsWith(prefix)).map(([name, body]) => [name.slice(prefix.length), body]));
|
|
222
|
+
if (!files.has(MANIFEST)) fail(`${PACKAGE}@${version} has no ${MANIFEST}.`);
|
|
223
|
+
const next = JSON.parse(files.get(MANIFEST).toString("utf8"));
|
|
224
|
+
|
|
225
|
+
const changed = [];
|
|
226
|
+
// Managed files the release no longer has.
|
|
227
|
+
for (const file of old.managed.flatMap(localFiles)) {
|
|
228
|
+
if (files.has(file) || file === MANIFEST) continue;
|
|
229
|
+
rmSync(file);
|
|
230
|
+
changed.push(`- ${file}`);
|
|
231
|
+
}
|
|
232
|
+
for (const [file, body] of files) {
|
|
233
|
+
if (file === MANIFEST) continue;
|
|
234
|
+
const managed = listed(next.managed, file);
|
|
235
|
+
// Seeded files are the site's own: add only ones that are new to this release's list, and never over a file.
|
|
236
|
+
const newSeed = !managed && listed(next.seeded, file) && !listed(old.seeded ?? [], file) && !existsSync(file);
|
|
237
|
+
if (!managed && !newSeed) continue;
|
|
238
|
+
const existed = existsSync(file);
|
|
239
|
+
if (existed && readFileSync(file).equals(body)) continue;
|
|
240
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
241
|
+
writeFileSync(file, body);
|
|
242
|
+
changed.push(`${existed ? "~" : "+"} ${file}`);
|
|
243
|
+
}
|
|
244
|
+
writeFileSync(MANIFEST, JSON.stringify({ ...next, version }, null, 2) + "\n");
|
|
245
|
+
|
|
246
|
+
// The packages the code needs, at least at the release's versions, and its hearth:* scripts. The site's own stay.
|
|
247
|
+
const pkg = JSON.parse(readFileSync("package.json", "utf8"));
|
|
248
|
+
const theirs = JSON.parse(files.get("package.json").toString("utf8"));
|
|
249
|
+
for (const field of ["dependencies", "devDependencies"]) {
|
|
250
|
+
for (const [name, range] of Object.entries(theirs[field] ?? {})) {
|
|
251
|
+
const want = String(range).startsWith("workspace:") ? `^${version}` : range;
|
|
252
|
+
const have = pkg.dependencies?.[name] ?? pkg.devDependencies?.[name];
|
|
253
|
+
if (have && !lower(have, want)) continue;
|
|
254
|
+
pkg[field] = { ...pkg[field], [name]: want };
|
|
255
|
+
changed.push(`~ package.json: ${name} ${have ?? "(new)"} → ${want}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const [name, script] of Object.entries(theirs.scripts ?? {})) {
|
|
259
|
+
if (name.startsWith("hearth:") && pkg.scripts?.[name] !== script) pkg.scripts = { ...pkg.scripts, [name]: script };
|
|
260
|
+
}
|
|
261
|
+
writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n");
|
|
262
|
+
|
|
263
|
+
console.log(`✓ Updated Hearth's code from ${old.version} to ${version}${changed.length ? ":" : " (no files changed)."}`);
|
|
264
|
+
for (const line of changed) console.log(` ${line}`);
|
|
265
|
+
console.log(`\nNext: pnpm install, then pnpm typecheck and pnpm build, and check the site with pnpm dev.`);
|
|
266
|
+
console.log(` A type error in src/theme means a slot was renamed or removed: see src/hearth/theme/defaults.ts.`);
|
|
267
|
+
console.log(` Components replaced in src/theme/overrides.tsx are the site's own: compare them with src/hearth/blocks.`);
|
|
268
|
+
console.log(` What changed: CHANGELOG.md in ${PACKAGE}@${version}.`);
|
|
90
269
|
}
|
|
91
270
|
|
|
92
271
|
try {
|
|
93
272
|
if (command === "check") await check();
|
|
94
273
|
else if (command === "docs") await docs();
|
|
95
|
-
else
|
|
274
|
+
else if (command === "update") await update();
|
|
275
|
+
else fail("Usage: node scripts/hearth.mjs check | docs | update [--version x.y.z] [--force]");
|
|
96
276
|
} catch (err) {
|
|
97
277
|
if (!(err instanceof Stop)) throw err;
|
|
98
278
|
console.error(`\n✗ ${err.message}\n`);
|