@forumone/create-throughline 0.4.0 → 0.5.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/CHANGELOG.md +85 -0
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +4 -3
- package/dist/generator.js.map +1 -1
- package/dist/templates/base/.env.example +23 -5
- package/dist/templates/base/CLAUDE.md +112 -0
- package/dist/templates/base/README.md +38 -6
- package/dist/templates/base/apps/web/e2e/site.ts +36 -0
- package/dist/templates/base/apps/web/e2e/smoke.spec.ts +52 -0
- package/dist/templates/base/apps/web/package.json.template +21 -14
- package/dist/templates/base/apps/web/playwright.config.ts +63 -0
- package/dist/templates/base/apps/web/src/access/anonymousAccess.test.ts +36 -0
- package/dist/templates/base/apps/web/src/app/api/inngest/route.ts.template +33 -3
- package/dist/templates/base/apps/web/src/instrumentation.ts +35 -0
- package/dist/templates/base/apps/web/src/lib/cache-tags.ts +23 -0
- package/dist/templates/base/apps/web/src/payload.config.ts.template +76 -16
- package/dist/templates/base/apps/web/vitest.config.ts +57 -0
- package/dist/templates/base/claude/hooks/gate.sh +45 -0
- package/dist/templates/base/claude/settings.json +16 -0
- package/dist/templates/base/github/workflows/ci.yml +14 -2
- package/dist/templates/base/gitignore +9 -0
- package/dist/templates/base/package.json.template +1 -0
- package/dist/templates/base/scripts/check-tested-packages.ts +0 -2
- package/dist/templates/with-reference-ds/design-system/src/components/Divider/Divider.contract.ts +1 -0
- package/dist/versions.json +11 -11
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,90 @@
|
|
|
1
1
|
# @forumone/create-throughline
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 006ae30: A missing environment variable is reported together with every other one, in a
|
|
8
|
+
single error, instead of one plugin at a time.
|
|
9
|
+
|
|
10
|
+
The plugins that read the environment at init now declare what they cannot
|
|
11
|
+
start without, as data: `approvalsEnv` (`APPROVAL_TOKEN_SECRET`, 32+
|
|
12
|
+
characters), `emailEnv` (`RESEND_API_KEY`, `EMAIL_FROM_ADDRESS`) and `formsEnv`
|
|
13
|
+
(`FORMS_IP_HASH_SECRET`, 32+ characters). Each is a list of `EnvRequirement`
|
|
14
|
+
(`{ name, minLength?, why }`), a new type in `plugin-contract`, and each
|
|
15
|
+
plugin's own init check now reads the same entries, so the two cannot drift.
|
|
16
|
+
An empty or whitespace-only value now counts as missing in those checks.
|
|
17
|
+
|
|
18
|
+
Core exports `assertEnvironment(...checks)`. Call it first in
|
|
19
|
+
`payload.config.ts` with the plugins' lists and your own variables; it throws
|
|
20
|
+
one `EnvironmentError` whose first line reads "Configuration problem: N
|
|
21
|
+
environment variables are missing or invalid", followed by every missing or
|
|
22
|
+
too-short variable and why it is needed. Values are never printed. An argument
|
|
23
|
+
can also be a function, for a rule that is not "this name, this long", such as
|
|
24
|
+
a database URL accepted under several names. `checkEnvValue` is the one-value
|
|
25
|
+
check behind it, for a plugin's own backstop.
|
|
26
|
+
|
|
27
|
+
New projects call `assertEnvironment` at the top of `payload.config.ts` with
|
|
28
|
+
`approvalsEnv`, `emailEnv`, `formsEnv`, `PAYLOAD_SECRET` (32+ characters),
|
|
29
|
+
`NEXT_PUBLIC_SERVER_URL` and the database resolver, and `.env.example` marks
|
|
30
|
+
which variables are checked. An existing site can do the same and delete any
|
|
31
|
+
hand-kept copy of the plugins' requirements.
|
|
32
|
+
|
|
33
|
+
- 833c1bd: A scaffolded project ships the rules an agent, or a new developer, needs:
|
|
34
|
+
|
|
35
|
+
- **`CLAUDE.md`** covers the layout, the gate in CI's own order, which files are generated and committed, how migrations and snapshots work, and the contract rule for the design system.
|
|
36
|
+
- **A Stop hook** (`.claude/hooks/gate.sh`) runs typecheck and lint when a session ends with pending changes. It reports, never blocks, and is silenced by `THROUGHLINE_SKIP_GATE=1`.
|
|
37
|
+
|
|
38
|
+
The README's project layout now matches what the scaffold generates.
|
|
39
|
+
|
|
40
|
+
- 549d292: A scaffolded project runs every Payload CLI script — `payload`, `generate:types`, `generate:importmap`, `migrate`, `migrate:create`, `migrate:status` — through core's `throughline-payload`, so a hung run is killed rather than left spinning after its shell dies. It adds `payload:reap` to `apps/web` and the root, and ignores `.payload-cli-pids`.
|
|
41
|
+
- 70385c4: A scaffolded project reports its failures. `apps/web/src/instrumentation.ts` sends unhandled request errors to `ERROR_WEBHOOK_URL` through core's reporter, and warns at boot when that variable is unset. The Inngest route passes `createTerminalFailureHandler` to every framework workflow and `createHealthcheckFailureHandler` to the healthcheck. `payload.config.ts` registers `jobFailuresPlugin`. `.env.example` lists `ERROR_WEBHOOK_URL` as optional.
|
|
42
|
+
|
|
43
|
+
A new project's first migration includes the `job-failures` table. An existing project that copies these changes needs `payload migrate:create`.
|
|
44
|
+
|
|
45
|
+
- c8a86bf: A scaffolded project now starts with tests in `apps/web`:
|
|
46
|
+
|
|
47
|
+
- **Vitest.** `vitest.config.ts` loads `payload.config.ts` with placeholder variables and no database. `src/access/anonymousAccess.test.ts` uses `describeAnonymousAccess` to put every collection the project has into an access bucket. `pnpm test` runs it in CI's `fast` job. `check:tested` drops its exception for `apps/web`.
|
|
48
|
+
- **Playwright smoke pack.** `e2e/` runs against `next start`, or against `E2E_BASE_URL`. It checks the front door, a 404, the admin sign-in screen, and anonymous REST reads. The paths are in `e2e/site.ts`, along with the checks a new project cannot pass yet because it does not serve the routes they test. It runs in CI's `verify` job after the build. The new scripts are `test`, `test:smoke` and `test:smoke:install`.
|
|
49
|
+
- **`pages` read rule.** The example `pages` collection gains a published-or-signed-in `read` rule. Before, Payload's default refused every anonymous read, so the public site could never read a page.
|
|
50
|
+
|
|
51
|
+
### Patch Changes
|
|
52
|
+
|
|
53
|
+
- c8a86bf: The `Divider` contract now declares `decorative` with `defaultValue: true`, matching the component. Before, the default was written only in prose, so a divider added in the CMS started unticked and screen readers announced it.
|
|
54
|
+
- ab623e1: The scaffolded `payload.config.ts` uses core's `mcpApiKeyAccess(isAdmin)` to make the MCP key collection admin-only, replacing the inline override. It now also covers `unlock` and refuses a key principal outright. The scaffold depends on `payload` and `@payloadcms/*` `^3.89.0`.
|
|
55
|
+
- 653817e: Revalidation no longer guesses paths, and covers the changes a publish event never announces.
|
|
56
|
+
|
|
57
|
+
- **workflows (breaking)**: `createRevalidateOnPublishFunction` has no built-in URL
|
|
58
|
+
builders. They mapped `pages` to `/<slug>`, `posts` to `/blog/<slug>` and any other
|
|
59
|
+
collection to `/<slug>`, so a site whose routes differed revalidated the wrong path
|
|
60
|
+
without a word. `urlBuilders` is now required; a collection with no entry has its
|
|
61
|
+
tags dropped and no path revalidated, and the run logs a warning. To keep the old
|
|
62
|
+
behaviour, pass the old builders:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
urlBuilders: {
|
|
66
|
+
pages: (slug) => (slug === 'home' || slug === '' ? '/' : `/${slug}`),
|
|
67
|
+
posts: (slug) => `/blog/${slug}`,
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
and then check them against your routes.
|
|
72
|
+
|
|
73
|
+
- **workflows**: `createTagRevalidationHooks` — collection `afterChange` and
|
|
74
|
+
`afterDelete`, and global `afterChange`, hooks that call
|
|
75
|
+
`revalidateTag(tag, { expire: 0 })`. Draft saves and autosave drop nothing (via
|
|
76
|
+
publishing's `isDraftWrite`); an unpublish still does. A hook never throws: outside a
|
|
77
|
+
Next request (seeds, migrations, the CLI) it logs at `debug`, and any other failure
|
|
78
|
+
at `error` with the tag and the cause.
|
|
79
|
+
- **workflows**: `createCacheTags` builds every tag string, so the hooks, the publish
|
|
80
|
+
workflow (new `cacheTags` option) and cached reads cannot name different tags.
|
|
81
|
+
Defaults: the bare slug for a collection, `global_<slug>` for a global. Also on the
|
|
82
|
+
dependency-free `@forumone/throughline-workflows/cache-tags` subpath for frontend code.
|
|
83
|
+
`@forumone/throughline-publishing` is now a dependency of this package.
|
|
84
|
+
- **create-throughline**: the scaffold adds `apps/web/src/lib/cache-tags.ts`, attaches
|
|
85
|
+
the tag hooks to `Pages`, and passes explicit `urlBuilders` and the shared
|
|
86
|
+
`cacheTags` to the publish workflow.
|
|
87
|
+
|
|
3
88
|
## 0.4.0
|
|
4
89
|
|
|
5
90
|
### Minor Changes
|
package/dist/generator.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAgC,MAAM,EAAE,EAAE,EAAE,IAAI,EAAa,MAAM,kBAAkB,CAAA;AAK5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAK3C,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,qEAAqE;IACrE,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsC7F;AAED,UAAU,UAAU;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC9B;AAED;;;;;;;;;GASG;AACH,iBAAe,eAAe,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAmClF;AAiBD,iBAAS,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAI3C;
|
|
1
|
+
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAgC,MAAM,EAAE,EAAE,EAAE,IAAI,EAAa,MAAM,kBAAkB,CAAA;AAK5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAK3C,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,qEAAqE;IACrE,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsC7F;AAED,UAAU,UAAU;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC9B;AAED;;;;;;;;;GASG;AACH,iBAAe,eAAe,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAmClF;AAiBD,iBAAS,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAI3C;AAeD,iBAAS,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AA6BD,eAAO,MAAM,SAAS;;;;;;;CAAsE,CAAA"}
|
package/dist/generator.js
CHANGED
|
@@ -117,13 +117,14 @@ function isBinary(filename) {
|
|
|
117
117
|
/**
|
|
118
118
|
* npm strips files literally named `.gitignore` from a published tarball, so
|
|
119
119
|
* we author them as `gitignore` in templates and restore the dot on output.
|
|
120
|
-
* `.github`
|
|
121
|
-
* whichever ignore rules the publishing npm applies, and
|
|
122
|
-
*
|
|
120
|
+
* `.github` and `.claude` go the same way: a dot-directory in a tarball is at
|
|
121
|
+
* the mercy of whichever ignore rules the publishing npm applies, and neither
|
|
122
|
+
* the scaffold's CI nor its agent setup is something to lose silently.
|
|
123
123
|
*/
|
|
124
124
|
const DOTFILE_RENAMES = {
|
|
125
125
|
gitignore: '.gitignore',
|
|
126
126
|
github: '.github',
|
|
127
|
+
claude: '.claude',
|
|
127
128
|
};
|
|
128
129
|
function applyDotfileRename(name) {
|
|
129
130
|
return DOTFILE_RENAMES[name] ?? name;
|
package/dist/generator.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.js","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"generator.js","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Every variable marked "checked at startup" is listed in the assertEnvironment
|
|
2
|
+
# call at the top of apps/web/src/payload.config.ts, which reports all the
|
|
3
|
+
# missing or too-short ones in a single error before anything else runs. Add a
|
|
4
|
+
# variable there when you add one here that the site cannot start without.
|
|
5
|
+
|
|
6
|
+
# --- Required (checked at startup) ---
|
|
7
|
+
# The database may come from DATABASE_URI or, on Vercel + Neon, the
|
|
8
|
+
# integration's DATABASE_URL and friends — see apps/web/src/lib/database.ts.
|
|
2
9
|
DATABASE_URI=postgres://user:pass@host/dbname
|
|
3
10
|
# Optional: a schema-owner credential used only by `pnpm migrate`, so the app
|
|
4
11
|
# itself can run as a role that reads and writes rows and no more. See
|
|
5
12
|
# apps/web/src/lib/database.ts.
|
|
6
13
|
# MIGRATION_DATABASE_URL=
|
|
14
|
+
# 32+ characters. Generate: openssl rand -base64 48
|
|
7
15
|
PAYLOAD_SECRET=generate-with-openssl-rand-base64-48
|
|
8
16
|
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
|
|
9
17
|
|
|
@@ -14,17 +22,19 @@ INNGEST_EVENT_KEY=
|
|
|
14
22
|
INNGEST_SIGNING_KEY=
|
|
15
23
|
|
|
16
24
|
# --- Resend (for approval + form notification emails) ---
|
|
25
|
+
# RESEND_API_KEY and EMAIL_FROM_ADDRESS are checked at startup (emailEnv).
|
|
17
26
|
RESEND_API_KEY=
|
|
18
27
|
EMAIL_FROM_ADDRESS=notifications@example.com
|
|
28
|
+
# Optional.
|
|
19
29
|
EMAIL_FROM_NAME={{projectName}}
|
|
20
30
|
EMAIL_REPLY_TO=
|
|
21
31
|
|
|
22
|
-
# --- Approval tokens ---
|
|
23
|
-
# Generate: openssl rand -base64 48
|
|
32
|
+
# --- Approval tokens (checked at startup: approvalsEnv) ---
|
|
33
|
+
# 32+ characters. Generate: openssl rand -base64 48
|
|
24
34
|
APPROVAL_TOKEN_SECRET=
|
|
25
35
|
|
|
26
|
-
# --- Forms ---
|
|
27
|
-
# Generate: openssl rand -base64 48
|
|
36
|
+
# --- Forms (checked at startup: formsEnv) ---
|
|
37
|
+
# 32+ characters. Generate: openssl rand -base64 48
|
|
28
38
|
FORMS_IP_HASH_SECRET=
|
|
29
39
|
|
|
30
40
|
# --- MCP ---
|
|
@@ -33,6 +43,14 @@ FORMS_IP_HASH_SECRET=
|
|
|
33
43
|
# API Keys and give it to your MCP client, not to this file. Scheduled
|
|
34
44
|
# publishes run in process and need no key either.
|
|
35
45
|
|
|
46
|
+
# --- Optional: error reporting ---
|
|
47
|
+
# Where unhandled request errors, workflows that ran out of retries and failing
|
|
48
|
+
# healthchecks are POSTed as JSON (with a one-line `text`, so a Slack incoming
|
|
49
|
+
# webhook works as-is). A log drain, an alerting endpoint or a proxy in front of
|
|
50
|
+
# your error tracker. Unset: errors reach the log only, and the server says so
|
|
51
|
+
# at boot. Job failures are also recorded in the admin under Job Failures.
|
|
52
|
+
ERROR_WEBHOOK_URL=
|
|
53
|
+
|
|
36
54
|
# --- Optional: storage ---
|
|
37
55
|
# Vercel Blob token if deploying to Vercel; remove this and the storage
|
|
38
56
|
# adapter from payload.config.ts if you're using a different provider.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
A **Throughline** project: a Payload CMS application (`apps/web`) wired to the
|
|
4
|
+
Throughline plugin suite, plus the design system that supplies its component
|
|
5
|
+
vocabulary. Throughline docs: https://github.com/forumone/throughline
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
apps/web/ Next.js + Payload host
|
|
9
|
+
design-system/ The design system and its Storybook
|
|
10
|
+
scripts/ Workspace gates, each explained at the top of its file
|
|
11
|
+
.github/ CI: `fast` on every pull request, `verify` on main
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## The gate
|
|
15
|
+
|
|
16
|
+
This is what CI's `fast` job runs, in its order. Run it before calling work
|
|
17
|
+
done:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm install --frozen-lockfile \
|
|
21
|
+
&& pnpm typecheck && pnpm lint \
|
|
22
|
+
&& pnpm check:instances && pnpm check:tested && pnpm check:migrations \
|
|
23
|
+
&& pnpm validate \
|
|
24
|
+
&& bash .github/scripts/check-generated.sh \
|
|
25
|
+
&& pnpm test
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`--frozen-lockfile` leads because a lockfile missing an entry passes every
|
|
29
|
+
other gate locally and fails in CI. `check-generated.sh` needs the environment
|
|
30
|
+
`payload.config.ts` validates; the placeholders at the top of
|
|
31
|
+
`.github/workflows/ci.yml` are enough — it parses the connection string and
|
|
32
|
+
never connects.
|
|
33
|
+
|
|
34
|
+
`verify` adds a migrated Postgres and `pnpm build`; it runs on main.
|
|
35
|
+
|
|
36
|
+
A Stop hook (`.claude/hooks/gate.sh`) runs typecheck and lint when a session
|
|
37
|
+
ends with pending changes. It reports and never blocks. Set
|
|
38
|
+
`THROUGHLINE_SKIP_GATE=1` to silence it.
|
|
39
|
+
|
|
40
|
+
## Generated files are committed — never hand-edit them
|
|
41
|
+
|
|
42
|
+
After any change to `payload.config.ts`, a collection or a plugin version:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pnpm --dir apps/web generate:types # -> src/payload-types.ts
|
|
46
|
+
pnpm --dir apps/web generate:importmap # -> src/app/(payload)/admin/importMap.js
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Commit both. CI regenerates them and fails on a difference. A stale import map
|
|
50
|
+
is a 500 on any admin screen that renders a component it has never heard of;
|
|
51
|
+
stale types make everything downstream compile against a schema that no longer
|
|
52
|
+
exists.
|
|
53
|
+
|
|
54
|
+
## Schema changes go through migrations
|
|
55
|
+
|
|
56
|
+
`push` is off, in development too: `apps/web/src/migrations` is the only way the
|
|
57
|
+
schema changes.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pnpm --dir apps/web migrate:create <name> # diffs the config against the newest snapshot
|
|
61
|
+
pnpm --dir apps/web migrate # apply
|
|
62
|
+
pnpm --dir apps/web migrate:snapshot # only after hand-writing a migration
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`migrate:create` diffs against the `.json` snapshot beside the newest migration,
|
|
66
|
+
**not the database**. A hand-written migration without its snapshot leaves the
|
|
67
|
+
generator a step behind, re-proposing changes that already shipped.
|
|
68
|
+
`check:migrations` fails on a missing or orphaned snapshot, and on a drop that a
|
|
69
|
+
`CASCADE` has already carried out.
|
|
70
|
+
|
|
71
|
+
The `payload` CLI loads `.env.local`; `tsx` scripts load nothing. For anything
|
|
72
|
+
that writes to a database, pass the variables on the command line and use
|
|
73
|
+
`requireTarget` from `apps/web/scripts/lib/target.ts`, which prints the target
|
|
74
|
+
and demands `--confirm` before overwriting.
|
|
75
|
+
|
|
76
|
+
## The design system reaches the CMS through its manifest
|
|
77
|
+
|
|
78
|
+
`payload.config.ts` hands the design system's manifest to `componentsPlugin`,
|
|
79
|
+
and that manifest is how the CMS knows which components exist and how they may
|
|
80
|
+
be composed. It is built from a `<Name>.contract.ts` beside every component.
|
|
81
|
+
{{#if useReferenceDs}}
|
|
82
|
+
- **A new component needs a contract.** `build:manifest` refuses a component
|
|
83
|
+
directory without one; prefix a directory with `_` if it is not a component.
|
|
84
|
+
- **`pnpm validate` must pass.** It catches an unknown token, a composition rule
|
|
85
|
+
naming a component that does not exist, and an example pointing at a story
|
|
86
|
+
that is not there.
|
|
87
|
+
{{else}}
|
|
88
|
+
The design system here is a placeholder. When you build it, give every
|
|
89
|
+
component a contract, publish a manifest, and point `componentsPlugin` at it —
|
|
90
|
+
see the design-contract package in the Throughline repository.
|
|
91
|
+
{{/if}}
|
|
92
|
+
## Tests
|
|
93
|
+
|
|
94
|
+
`check:tested` requires at least one test in every package that ships code.
|
|
95
|
+
|
|
96
|
+
`apps/web` starts with one: `src/access/anonymousAccess.test.ts`, which
|
|
97
|
+
`pnpm test` runs through a vitest config that loads `payload.config.ts` without
|
|
98
|
+
a database. It puts every collection in one of two buckets: read by the
|
|
99
|
+
rendered site, so anonymous reads must be allowed, or private, so they must be
|
|
100
|
+
refused. A new collection fails it until you add it to a bucket. Decide which,
|
|
101
|
+
and don't open a collection to anonymous reads to make the test pass. More good
|
|
102
|
+
first tests need no database: field validations, and hooks as functions.
|
|
103
|
+
|
|
104
|
+
`pnpm test:smoke` runs the Playwright smoke pack in `e2e/` against
|
|
105
|
+
`next start`, so build first, or against a deployed site when `E2E_BASE_URL`
|
|
106
|
+
is set. Every check is a GET. CI runs it in `verify`, after the production
|
|
107
|
+
build. Add routes to `e2e/site.ts` as the site gains them.
|
|
108
|
+
|
|
109
|
+
## Conventions
|
|
110
|
+
|
|
111
|
+
- pnpm only. The lockfile is at the root.
|
|
112
|
+
- Open a pull request for review; do not merge your own.
|
|
@@ -30,7 +30,13 @@ openssl rand -base64 48
|
|
|
30
30
|
# - APPROVAL_TOKEN_SECRET — generated above
|
|
31
31
|
# - FORMS_IP_HASH_SECRET — generated above
|
|
32
32
|
# - RESEND_API_KEY — from Resend dashboard
|
|
33
|
+
# - EMAIL_FROM_ADDRESS — a verified sender on your Resend domain
|
|
33
34
|
# - INNGEST_EVENT_KEY/SIGNING — from Inngest (optional in local dev)
|
|
35
|
+
#
|
|
36
|
+
# Anything missing or too short is reported all at once, in one error that
|
|
37
|
+
# starts "Configuration problem", by the assertEnvironment call at the top
|
|
38
|
+
# of apps/web/src/payload.config.ts — the first command that loads the
|
|
39
|
+
# config (step 5) tells you everything left to fill in.
|
|
34
40
|
|
|
35
41
|
# 5. Create and apply the first migration. The config sets `push: false`, so
|
|
36
42
|
# migrations are the only way the schema changes, from the first table on.
|
|
@@ -75,6 +81,27 @@ pnpm dev
|
|
|
75
81
|
- `pnpm typecheck` — TypeScript check across the monorepo
|
|
76
82
|
- `pnpm lint` — ESLint
|
|
77
83
|
- `pnpm test` — run all package tests
|
|
84
|
+
- `pnpm payload:reap` — kill any Payload CLI run a killed shell left behind (the next Payload command does this too)
|
|
85
|
+
|
|
86
|
+
Every script that runs the Payload CLI — `payload`, `generate:*`, `migrate*` —
|
|
87
|
+
goes through `throughline-payload` from `@forumone/throughline-core`. pnpm does
|
|
88
|
+
not forward signals to the node process it starts, so a bare `payload` whose
|
|
89
|
+
shell is killed keeps running, and a hung one spins on a core indefinitely. The
|
|
90
|
+
runner gives Payload its own process group, forwards Ctrl-C and friends to it,
|
|
91
|
+
and kills it after five minutes (`PAYLOAD_CLI_TIMEOUT_MS`, `0` to disable;
|
|
92
|
+
`migrate*` has no limit unless you set one). Run the Payload CLI through these
|
|
93
|
+
scripts rather than `npx payload`.
|
|
94
|
+
|
|
95
|
+
- `pnpm --dir apps/web test:smoke` — the Playwright smoke pack, against a local `next start` (run `pnpm build` first) or `E2E_BASE_URL`
|
|
96
|
+
|
|
97
|
+
## Tests
|
|
98
|
+
|
|
99
|
+
`apps/web` has two kinds of test.
|
|
100
|
+
|
|
101
|
+
- **`pnpm test`** runs vitest in Node with no database and no browser, so it runs in CI's `fast` job on every pull request. `vitest.config.ts` gives `payload.config.ts` placeholder environment variables, so the config loads without connecting to anything. The first test, `src/access/anonymousAccess.test.ts`, puts every collection into one of two buckets: collections the public site reads with nobody signed in (`renderPath`), and collections it must not read (`private`). It then checks each collection's `read` rule against its bucket. **Adding a collection, or a plugin that brings collections, fails this test until you put each new collection in a bucket.** See the Throughline guide _Adding a collection_.
|
|
102
|
+
- **`pnpm --dir apps/web test:smoke`** is a Playwright smoke pack (`e2e/`) that sends real requests to `next start` on an empty, migrated database. It checks that the front door answers, that an unknown path returns 404, that the admin shows its sign-in screen, and that the REST API refuses anonymous reads of private collections while allowing reads of public ones. It runs in CI's `verify` job, which has a database. Set `E2E_BASE_URL` to run it against a deployed site. Every check is a GET request. The paths it checks are configured in `e2e/site.ts`. That file also lists checks a new project cannot pass yet because it does not serve the route: security headers, `robots.txt`, the sitemap, `llms.txt` and a draft-mode route. Add each check when you add its route.
|
|
103
|
+
|
|
104
|
+
Run `pnpm --dir apps/web test:smoke:install` once to download Chromium.
|
|
78
105
|
|
|
79
106
|
## Migrations
|
|
80
107
|
|
|
@@ -109,14 +136,15 @@ map has never heard of.
|
|
|
109
136
|
`.github/workflows/ci.yml` runs two tiers. `fast` runs on every pull request
|
|
110
137
|
and needs no database: install from the lockfile, typecheck, lint, the gates
|
|
111
138
|
below, manifest validation, the generated-files check and tests. `verify` runs
|
|
112
|
-
on pushes to main: it migrates a fresh Postgres
|
|
139
|
+
on pushes to main: it migrates a fresh Postgres, runs a production build, and
|
|
140
|
+
runs the smoke pack against it.
|
|
113
141
|
|
|
114
142
|
## Workspace gates
|
|
115
143
|
|
|
116
144
|
Each is explained at the top of its script in `scripts/`:
|
|
117
145
|
|
|
118
146
|
- `pnpm check:instances` — `payload`, `@payloadcms/ui`, `react` and `react-dom` each resolve to exactly one copy. Two byte-identical copies are two React contexts, and a runtime failure nothing else can see.
|
|
119
|
-
- `pnpm check:tested` — every workspace package that ships code has at least one test. Not a coverage number.
|
|
147
|
+
- `pnpm check:tested` — every workspace package that ships code has at least one test. Not a coverage number. A package with no tests can go on the exception list in the script, with a reason. An entry fails once that package has tests.
|
|
120
148
|
- `pnpm check:audit` — high and critical production advisories against an allowlist that carries a reason for each entry, and that fails when an entry stops applying.
|
|
121
149
|
|
|
122
150
|
## Project layout
|
|
@@ -125,10 +153,14 @@ Each is explained at the top of its script in `scripts/`:
|
|
|
125
153
|
{{projectName}}/
|
|
126
154
|
├── apps/
|
|
127
155
|
│ └── web/ # Next.js + Payload host
|
|
128
|
-
├──
|
|
129
|
-
│
|
|
130
|
-
|
|
131
|
-
|
|
156
|
+
│ ├── scripts/ # migration tooling, requireTarget
|
|
157
|
+
│ └── src/migrations/ # the only way the schema changes
|
|
158
|
+
├── design-system/ # components + contracts -> the manifest the CMS reads
|
|
159
|
+
├── packages/ # your own shared packages (optional)
|
|
160
|
+
├── scripts/ # workspace gates
|
|
161
|
+
├── .claude/ # agent settings and the Stop-hook gate
|
|
162
|
+
├── .github/ # CI: fast + verify
|
|
163
|
+
├── CLAUDE.md # the rules an agent (or a new developer) needs
|
|
132
164
|
├── .env.example
|
|
133
165
|
├── pnpm-workspace.yaml
|
|
134
166
|
├── turbo.json
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
What the smoke pack checks, and where. Site data: change the paths here, not in
|
|
3
|
+
the specs.
|
|
4
|
+
|
|
5
|
+
**What the pack does not check yet, because a fresh project does not serve it.**
|
|
6
|
+
Each is a check worth adding with the route that makes it true:
|
|
7
|
+
|
|
8
|
+
- security headers — `next.config.mjs` sets none. Add `headers()` there, then
|
|
9
|
+
assert them per route: HSTS, `nosniff`, a referrer policy, and framing
|
|
10
|
+
(`DENY` on `/admin`, `SAMEORIGIN` where live preview frames the site).
|
|
11
|
+
- `robots.txt` and `sitemap.xml` — add `app/robots.ts` and `app/sitemap.ts`;
|
|
12
|
+
assert a 200, the content type, and that the sitemap lists each URL once.
|
|
13
|
+
- `llms.txt` — a route serving `text/markdown`, with a heading and links.
|
|
14
|
+
- draft mode — a preview route must refuse a request without its secret
|
|
15
|
+
(401) and one naming no document (400).
|
|
16
|
+
*/
|
|
17
|
+
export const site = {
|
|
18
|
+
/**
|
|
19
|
+
* A deployed site to point at, from `E2E_BASE_URL`. Unset, the pack starts
|
|
20
|
+
* `next start` locally on `port`.
|
|
21
|
+
*/
|
|
22
|
+
baseURL: process.env.E2E_BASE_URL?.trim() || undefined,
|
|
23
|
+
port: 3100,
|
|
24
|
+
|
|
25
|
+
/** The front door. */
|
|
26
|
+
home: '/',
|
|
27
|
+
/** The Payload admin. On an empty database it asks for the first user. */
|
|
28
|
+
admin: '/admin',
|
|
29
|
+
/** A path nothing serves, which must be a 404 and never a 500. */
|
|
30
|
+
missing: '/a-page-that-was-never-written',
|
|
31
|
+
|
|
32
|
+
/** REST collections an anonymous request must be refused. */
|
|
33
|
+
privateCollections: ['users', 'payload-mcp-api-keys'],
|
|
34
|
+
/** REST collections an anonymous request may read — published documents only. */
|
|
35
|
+
publicCollections: ['pages'],
|
|
36
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test'
|
|
2
|
+
import { site } from './site'
|
|
3
|
+
|
|
4
|
+
test.describe('the front door', () => {
|
|
5
|
+
test('answers with a document and a main landmark', async ({ page }) => {
|
|
6
|
+
const response = await page.goto(site.home)
|
|
7
|
+
expect(response?.status()).toBe(200)
|
|
8
|
+
await expect(page.locator('main')).toBeAttached()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('404s a path that does not exist, rather than erroring', async ({ request }) => {
|
|
12
|
+
const response = await request.get(site.missing, { maxRedirects: 0 })
|
|
13
|
+
expect(response.status()).toBe(404)
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test.describe('the admin', () => {
|
|
18
|
+
/*
|
|
19
|
+
The whole admin is one client bundle and the import map it reads. A stale
|
|
20
|
+
map, a missing environment variable or a plugin that throws at init all
|
|
21
|
+
surface here as a 500 or a blank screen — and as nothing at all in a test
|
|
22
|
+
that does not load the page.
|
|
23
|
+
*/
|
|
24
|
+
test('serves a sign-in screen', async ({ page }) => {
|
|
25
|
+
const response = await page.goto(site.admin)
|
|
26
|
+
expect(response?.status()).toBeLessThan(500)
|
|
27
|
+
await expect(page.getByRole('textbox', { name: /email/i })).toBeVisible({ timeout: 30_000 })
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test.describe('the REST API, to a request with nobody signed in', () => {
|
|
32
|
+
/*
|
|
33
|
+
The same rules `src/access/anonymousAccess.test.ts` checks as values, here as
|
|
34
|
+
responses: proof that nothing between the rule and the wire — a plugin, an
|
|
35
|
+
endpoint, a framework upgrade — reopened a collection.
|
|
36
|
+
*/
|
|
37
|
+
for (const slug of site.privateCollections) {
|
|
38
|
+
test(`refuses to list ${slug}`, async ({ request }) => {
|
|
39
|
+
const response = await request.get(`/api/${slug}`)
|
|
40
|
+
expect(response.status()).toBe(403)
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const slug of site.publicCollections) {
|
|
45
|
+
test(`lists ${slug}`, async ({ request }) => {
|
|
46
|
+
const response = await request.get(`/api/${slug}`)
|
|
47
|
+
expect(response.status()).toBe(200)
|
|
48
|
+
const body = (await response.json()) as { docs?: unknown }
|
|
49
|
+
expect(Array.isArray(body.docs)).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
})
|
|
@@ -9,14 +9,19 @@
|
|
|
9
9
|
"start": "next start",
|
|
10
10
|
"clean": "rm -rf .next .turbo",
|
|
11
11
|
"typecheck": "tsc --noEmit",
|
|
12
|
-
"payload": "payload",
|
|
13
|
-
"
|
|
14
|
-
"generate:
|
|
15
|
-
"
|
|
16
|
-
"migrate
|
|
17
|
-
"migrate:
|
|
12
|
+
"payload": "throughline-payload",
|
|
13
|
+
"payload:reap": "throughline-payload --reap",
|
|
14
|
+
"generate:types": "throughline-payload generate:types",
|
|
15
|
+
"generate:importmap": "throughline-payload generate:importmap",
|
|
16
|
+
"migrate": "PAYLOAD_MIGRATING=1 throughline-payload migrate",
|
|
17
|
+
"migrate:create": "throughline-payload migrate:create",
|
|
18
|
+
"migrate:status": "throughline-payload migrate:status",
|
|
18
19
|
"migrate:snapshot": "tsx scripts/write-migration-snapshot.ts",
|
|
19
|
-
"check:migrations": "tsx scripts/check-migrations.ts"
|
|
20
|
+
"check:migrations": "tsx scripts/check-migrations.ts",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"test:smoke": "playwright test",
|
|
24
|
+
"test:smoke:install": "playwright install --with-deps chromium"
|
|
20
25
|
},
|
|
21
26
|
"dependencies": {
|
|
22
27
|
"@forumone/throughline-approvals": "^{{version_approvals}}",
|
|
@@ -30,23 +35,25 @@
|
|
|
30
35
|
"@forumone/throughline-workflows": "^{{version_workflows}}",
|
|
31
36
|
{{#if useReferenceDs}}"@forumone/throughline-design-contract": "^{{version_design_contract}}",
|
|
32
37
|
"{{designSystemName}}": "workspace:*",
|
|
33
|
-
{{/if}}"@payloadcms/db-postgres": "^3.
|
|
34
|
-
"@payloadcms/next": "^3.
|
|
35
|
-
"@payloadcms/plugin-form-builder": "^3.
|
|
36
|
-
"@payloadcms/plugin-mcp": "^3.
|
|
37
|
-
"@payloadcms/richtext-lexical": "^3.
|
|
38
|
+
{{/if}}"@payloadcms/db-postgres": "^3.89.0",
|
|
39
|
+
"@payloadcms/next": "^3.89.0",
|
|
40
|
+
"@payloadcms/plugin-form-builder": "^3.89.0",
|
|
41
|
+
"@payloadcms/plugin-mcp": "^3.89.0",
|
|
42
|
+
"@payloadcms/richtext-lexical": "^3.89.0",
|
|
38
43
|
"graphql": "^16.8.1",
|
|
39
44
|
"inngest": "^4.2.0",
|
|
40
45
|
"next": "^16.2.4",
|
|
41
|
-
"payload": "^3.
|
|
46
|
+
"payload": "^3.89.0",
|
|
42
47
|
"react": "^19.2.0",
|
|
43
48
|
"react-dom": "^19.2.0"
|
|
44
49
|
},
|
|
45
50
|
"devDependencies": {
|
|
51
|
+
"@playwright/test": "^1.63.0",
|
|
46
52
|
"@types/node": "^20.17.0",
|
|
47
53
|
"@types/react": "^19.2.0",
|
|
48
54
|
"@types/react-dom": "^19.2.0",
|
|
49
55
|
"tsx": "^4.21.0",
|
|
50
|
-
"typescript": "^5.6.0"
|
|
56
|
+
"typescript": "^5.6.0",
|
|
57
|
+
"vitest": "^4.1.10"
|
|
51
58
|
}
|
|
52
59
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test'
|
|
2
|
+
import { site } from './e2e/site'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
The smoke pack: a real server answering real requests.
|
|
6
|
+
|
|
7
|
+
The vitest suite (the `*.test.ts` files under `src`) checks values — access rules, field
|
|
8
|
+
validations — and connects to nothing. What it cannot reach is anything true
|
|
9
|
+
only of a *response*: that the admin serves a login screen, that an unknown
|
|
10
|
+
path is a 404 rather than a 500, that the REST API refuses an anonymous read of
|
|
11
|
+
`users` in practice and not only on paper. That is this.
|
|
12
|
+
|
|
13
|
+
**Deliberately not a functional suite.** It runs against an empty, migrated
|
|
14
|
+
database — CI's `verify` job creates one — so nothing here needs a document to
|
|
15
|
+
exist. Anything that does belongs in a seeded environment.
|
|
16
|
+
|
|
17
|
+
pnpm --dir apps/web build && pnpm --dir apps/web test:smoke # local build
|
|
18
|
+
E2E_BASE_URL=https://example.org pnpm --dir apps/web test:smoke # deployed
|
|
19
|
+
|
|
20
|
+
With `E2E_BASE_URL` set the pack runs against that URL and starts no server.
|
|
21
|
+
Every check is a GET, which is what makes pointing it at production defensible;
|
|
22
|
+
adding one that writes would not be. Which routes it checks is in `e2e/site.ts`.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
`next start`, not `next dev`: headers, caching and rendering are properties of
|
|
27
|
+
the built output, and a pack against `dev` would assert something nobody
|
|
28
|
+
deploys. So this needs `.next/` — in CI the build step has made it; locally,
|
|
29
|
+
run `pnpm build` first.
|
|
30
|
+
|
|
31
|
+
Hoisted, and spread in only when there is no URL to point at: with
|
|
32
|
+
`exactOptionalPropertyTypes` an explicit `webServer: undefined` is a type
|
|
33
|
+
error, and omitting the key is the only way to say "start nothing".
|
|
34
|
+
*/
|
|
35
|
+
const webServer = {
|
|
36
|
+
command: `pnpm exec next start --port ${String(site.port)}`,
|
|
37
|
+
url: `http://127.0.0.1:${String(site.port)}${site.home}`,
|
|
38
|
+
reuseExistingServer: !process.env.CI,
|
|
39
|
+
timeout: 120_000,
|
|
40
|
+
stdout: 'pipe' as const,
|
|
41
|
+
stderr: 'pipe' as const,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default defineConfig({
|
|
45
|
+
testDir: './e2e',
|
|
46
|
+
outputDir: './e2e/.results',
|
|
47
|
+
|
|
48
|
+
// A cold `next start` can lose the first request to a route while it warms.
|
|
49
|
+
// Nothing here is a visual comparison, so one retry cannot hide a real failure.
|
|
50
|
+
retries: process.env.CI ? 1 : 0,
|
|
51
|
+
forbidOnly: Boolean(process.env.CI),
|
|
52
|
+
reporter: process.env.CI ? [['github'], ['list']] : [['list']],
|
|
53
|
+
workers: 1,
|
|
54
|
+
|
|
55
|
+
use: {
|
|
56
|
+
baseURL: site.baseURL ?? `http://127.0.0.1:${String(site.port)}`,
|
|
57
|
+
trace: 'retain-on-failure',
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
|
61
|
+
|
|
62
|
+
...(site.baseURL ? {} : { webServer }),
|
|
63
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describeAnonymousAccess } from '@forumone/throughline-core/testing'
|
|
2
|
+
import config from '../payload.config'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
What a reader with no account can read, as values — no database.
|
|
6
|
+
|
|
7
|
+
Every query the public site makes runs with nobody signed in, so a collection's
|
|
8
|
+
own `read` rule decides whether a page can see its content. Close one by
|
|
9
|
+
mistake and nothing errors: the page renders empty. Open the wrong one and the
|
|
10
|
+
audit log, the approval queue or the MCP keys are on the internet.
|
|
11
|
+
|
|
12
|
+
Every collection in the config — yours and the ones the plugins bring — has to
|
|
13
|
+
be in exactly one bucket below. **Adding a collection fails this test until you
|
|
14
|
+
put it in one.** Say where the site reads it, or why it is private.
|
|
15
|
+
*/
|
|
16
|
+
describeAnonymousAccess(config, {
|
|
17
|
+
renderPath: {
|
|
18
|
+
// Anonymous reads see published pages only; signed-in users see drafts.
|
|
19
|
+
pages: 'the page routes, once the frontend renders them',
|
|
20
|
+
// formsPlugin opens form definitions so a page can render the fields.
|
|
21
|
+
forms: 'any page that embeds a form',
|
|
22
|
+
},
|
|
23
|
+
private: {
|
|
24
|
+
users: 'accounts',
|
|
25
|
+
'payload-mcp-api-keys': 'MCP keys are credentials; narrowed to admins in payload.config.ts',
|
|
26
|
+
'audit-events': 'who did what',
|
|
27
|
+
approvals: 'who signed off on what',
|
|
28
|
+
'form-submissions': 'what readers typed into a form',
|
|
29
|
+
integrations: 'connection settings and sync state',
|
|
30
|
+
'job-failures': 'background jobs that ran out of retries, with their errors',
|
|
31
|
+
'payload-kv': 'the key-value store Payload keeps',
|
|
32
|
+
'payload-locked-documents': 'who is editing what',
|
|
33
|
+
'payload-preferences': 'admin UI state',
|
|
34
|
+
'payload-migrations': 'the migration history',
|
|
35
|
+
},
|
|
36
|
+
})
|
|
@@ -2,15 +2,18 @@ import type { InngestFunction } from 'inngest'
|
|
|
2
2
|
import { serve } from 'inngest/next'
|
|
3
3
|
import { getPayload } from 'payload'
|
|
4
4
|
import config from '@/payload.config'
|
|
5
|
+
import { cacheTags } from '@/lib/cache-tags'
|
|
5
6
|
import { createInngestClient } from '@forumone/throughline-core'
|
|
6
7
|
import {
|
|
7
8
|
createAuditEventEchoFunction,
|
|
8
9
|
createExecuteScheduledPublishesFunction,
|
|
9
10
|
createExpireStaleApprovalsFunction,
|
|
11
|
+
createHealthcheckFailureHandler,
|
|
10
12
|
createHealthcheckFunction,
|
|
11
13
|
createPayloadReachableCheck,
|
|
12
14
|
createPublishAtScheduledTimeFunction,
|
|
13
15
|
createRevalidateOnPublishFunction,
|
|
16
|
+
createTerminalFailureHandler,
|
|
14
17
|
type ScheduledPublishRequest,
|
|
15
18
|
} from '@forumone/throughline-workflows'
|
|
16
19
|
import { getPublishingService } from '@forumone/throughline-publishing'
|
|
@@ -24,6 +27,13 @@ import {
|
|
|
24
27
|
const inngest = createInngestClient({ id: '{{projectName}}' })
|
|
25
28
|
const payload = await getPayload({ config })
|
|
26
29
|
|
|
30
|
+
/*
|
|
31
|
+
A workflow that runs out of retries is logged, recorded in the `job-failures`
|
|
32
|
+
collection, and posted to ERROR_WEBHOOK_URL if that is set. Passed to every
|
|
33
|
+
factory below: a factory without it fails silently into Inngest's dashboard.
|
|
34
|
+
*/
|
|
35
|
+
const onTerminalFailure = createTerminalFailureHandler({ payload })
|
|
36
|
+
|
|
27
37
|
const emailFunctions = getEmailFunctions(payload) ?? []
|
|
28
38
|
const formsFunctions = getFormsFunctions(payload) ?? []
|
|
29
39
|
/*
|
|
@@ -43,6 +53,16 @@ async function publishScheduled({ collection, id, reasoning }: ScheduledPublishR
|
|
|
43
53
|
|
|
44
54
|
const scheduledCollections = [{ slug: 'pages' }]
|
|
45
55
|
|
|
56
|
+
/*
|
|
57
|
+
Where each publishable collection's documents are served, so a publish
|
|
58
|
+
revalidates the right path. There are no defaults: a collection missing here
|
|
59
|
+
has its cache tags dropped but no path revalidated, and the run logs a warning.
|
|
60
|
+
Add an entry for every collection you give publishingPlugin.
|
|
61
|
+
*/
|
|
62
|
+
const urlBuilders = {
|
|
63
|
+
pages: (slug: string) => (slug === 'home' ? '/' : `/${slug}`),
|
|
64
|
+
}
|
|
65
|
+
|
|
46
66
|
/*
|
|
47
67
|
Typed with this app's own `InngestFunction.Any`. The registry is generic in what
|
|
48
68
|
`createFunctions` returns and defaults to `unknown`, because the platform never
|
|
@@ -61,13 +81,20 @@ const integrationFunctions =
|
|
|
61
81
|
export const { GET, POST, PUT } = serve({
|
|
62
82
|
client: inngest,
|
|
63
83
|
functions: [
|
|
64
|
-
createRevalidateOnPublishFunction({
|
|
84
|
+
createRevalidateOnPublishFunction({
|
|
85
|
+
inngest,
|
|
86
|
+
payload,
|
|
87
|
+
urlBuilders,
|
|
88
|
+
cacheTags,
|
|
89
|
+
onTerminalFailure,
|
|
90
|
+
}),
|
|
65
91
|
// On time: sleeps until each document's scheduled time.
|
|
66
92
|
createPublishAtScheduledTimeFunction({
|
|
67
93
|
inngest,
|
|
68
94
|
payload,
|
|
69
95
|
collections: scheduledCollections,
|
|
70
96
|
publish: publishScheduled,
|
|
97
|
+
onTerminalFailure,
|
|
71
98
|
}),
|
|
72
99
|
// The backstop, for a schedule whose wake-up event never arrived.
|
|
73
100
|
createExecuteScheduledPublishesFunction({
|
|
@@ -77,13 +104,16 @@ export const { GET, POST, PUT } = serve({
|
|
|
77
104
|
publish: publishScheduled,
|
|
78
105
|
schedule: '17 3 * * *',
|
|
79
106
|
overdueByMs: 60 * 60 * 1000,
|
|
107
|
+
onTerminalFailure,
|
|
80
108
|
}),
|
|
81
|
-
createExpireStaleApprovalsFunction({ inngest, payload }),
|
|
82
|
-
createAuditEventEchoFunction({ inngest }),
|
|
109
|
+
createExpireStaleApprovalsFunction({ inngest, payload, onTerminalFailure }),
|
|
110
|
+
createAuditEventEchoFunction({ inngest, onTerminalFailure }),
|
|
83
111
|
createHealthcheckFunction({
|
|
84
112
|
inngest,
|
|
85
113
|
payload,
|
|
86
114
|
checks: [createPayloadReachableCheck()],
|
|
115
|
+
onFailure: createHealthcheckFailureHandler({ payload }),
|
|
116
|
+
onTerminalFailure,
|
|
87
117
|
}),
|
|
88
118
|
...emailFunctions,
|
|
89
119
|
...formsFunctions,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Instrumentation } from 'next'
|
|
2
|
+
import {
|
|
3
|
+
buildRequestErrorReport,
|
|
4
|
+
describeErrorReporting,
|
|
5
|
+
reportError,
|
|
6
|
+
} from '@forumone/throughline-core/observability'
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
Where a server-side error goes.
|
|
10
|
+
|
|
11
|
+
`onRequestError` fires when Next captures an error in a Server Component
|
|
12
|
+
render, a Route Handler, a Server Action or the proxy — the one place that sees
|
|
13
|
+
all four. It does not see client-side errors; `error.tsx` and
|
|
14
|
+
`global-error.tsx` are the boundaries for those.
|
|
15
|
+
|
|
16
|
+
The report is logged first, always, and then posted to `ERROR_WEBHOOK_URL` if
|
|
17
|
+
it is set — a log drain, an alerting endpoint, a Slack incoming webhook, a
|
|
18
|
+
proxy in front of whichever tracker you choose. Request headers are copied
|
|
19
|
+
from an allowlist: `cookie` carries the Payload session, and must never leave.
|
|
20
|
+
|
|
21
|
+
The subpath import keeps the CMS out of this file. This hook runs when
|
|
22
|
+
something has already gone wrong, and loading Payload here would put the whole
|
|
23
|
+
CMS in the one path that has to work regardless.
|
|
24
|
+
*/
|
|
25
|
+
export const onRequestError: Instrumentation.onRequestError = async (error, request, context) => {
|
|
26
|
+
const report = buildRequestErrorReport(error, request, context)
|
|
27
|
+
console.error('[error]', JSON.stringify(report))
|
|
28
|
+
await reportError(report)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Once per server instance: say so if error reports are going nowhere. */
|
|
32
|
+
export function register(): void {
|
|
33
|
+
const status = describeErrorReporting()
|
|
34
|
+
if (!status.startsWith('error reporting: on')) console.warn('[boot]', status)
|
|
35
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Next cache tags, named in one place.
|
|
3
|
+
|
|
4
|
+
A tag has two ends: a reader that caches under it and a hook that drops it.
|
|
5
|
+
Nothing checks that they match, so both build their tags from this object —
|
|
6
|
+
`payload.config.ts` hands it to the revalidation hooks, the Inngest route hands
|
|
7
|
+
it to `createRevalidateOnPublishFunction`, and frontend code that caches a read
|
|
8
|
+
tags it from here too:
|
|
9
|
+
|
|
10
|
+
import { unstable_cache } from 'next/cache'
|
|
11
|
+
import { cacheTags } from '@/lib/cache-tags'
|
|
12
|
+
|
|
13
|
+
const getNavigation = unstable_cache(loadNavigation, ['navigation'], {
|
|
14
|
+
tags: [cacheTags.global('navigation')],
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
Change the scheme here and every end moves with it. Imported from the
|
|
18
|
+
package's `/cache-tags` subpath, which imports nothing, so frontend code that
|
|
19
|
+
uses it does not pull in Payload.
|
|
20
|
+
*/
|
|
21
|
+
import { createCacheTags } from '@forumone/throughline-workflows/cache-tags'
|
|
22
|
+
|
|
23
|
+
export const cacheTags = createCacheTags()
|
|
@@ -5,27 +5,68 @@ import type { Access, CollectionConfig } from 'payload'
|
|
|
5
5
|
import { postgresAdapter } from '@payloadcms/db-postgres'
|
|
6
6
|
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
|
7
7
|
import {
|
|
8
|
+
assertEnvironment,
|
|
8
9
|
auditPlugin,
|
|
9
10
|
createInngestClient,
|
|
10
11
|
createMcpToolCollector,
|
|
12
|
+
mcpApiKeyAccess,
|
|
11
13
|
} from '@forumone/throughline-core'
|
|
12
14
|
import { mcpPlugin } from '@payloadcms/plugin-mcp'
|
|
13
15
|
import { componentsPlugin } from '@forumone/throughline-components'
|
|
14
16
|
import { publishingPlugin } from '@forumone/throughline-publishing'
|
|
15
17
|
import {
|
|
18
|
+
approvalsEnv,
|
|
16
19
|
approvalsPlugin,
|
|
17
20
|
buildActionUrl,
|
|
18
21
|
generateActionToken,
|
|
19
22
|
} from '@forumone/throughline-approvals'
|
|
20
23
|
import { auditQueryPlugin } from '@forumone/throughline-audit'
|
|
21
24
|
import { integrationsPlugin } from '@forumone/throughline-integrations'
|
|
22
|
-
import { emailPlugin } from '@forumone/throughline-email'
|
|
23
|
-
import { formsPlugin } from '@forumone/throughline-forms'
|
|
25
|
+
import { emailEnv, emailPlugin } from '@forumone/throughline-email'
|
|
26
|
+
import { formsEnv, formsPlugin } from '@forumone/throughline-forms'
|
|
27
|
+
import { createTagRevalidationHooks } from '@forumone/throughline-workflows'
|
|
28
|
+
import { cacheTags } from './lib/cache-tags'
|
|
29
|
+
import { jobFailuresPlugin } from '@forumone/throughline-core/observability'
|
|
24
30
|
import { databaseConnectionString } from './lib/database'
|
|
25
31
|
{{#if useReferenceDs}}import type { Manifest } from '@forumone/throughline-design-contract'
|
|
26
32
|
import designSystemManifest from '{{designSystemName}}/manifest' with { type: 'json' }
|
|
27
33
|
{{/if}}
|
|
28
34
|
|
|
35
|
+
/*
|
|
36
|
+
Every variable this site needs, checked once and reported together, before any
|
|
37
|
+
plugin runs. Without it each plugin throws on the first thing it finds, in
|
|
38
|
+
registration order, inside `next build`'s "Failed to collect page data" — one
|
|
39
|
+
missing variable per deploy, reading like a build failure.
|
|
40
|
+
|
|
41
|
+
The plugins' lists are their own declarations of what they refuse to start
|
|
42
|
+
without, so a requirement a plugin adds arrives here with the upgrade. Only the
|
|
43
|
+
site's own variables are written out. A value passed to a plugin as an option
|
|
44
|
+
rather than read from the environment takes that plugin's list out of this call.
|
|
45
|
+
|
|
46
|
+
Anything that loads this file — `next build`, `payload generate:types`,
|
|
47
|
+
`payload migrate` — needs these set. Where only parsing matters (CI's typecheck
|
|
48
|
+
and type generation), placeholders of the right length do; see
|
|
49
|
+
.github/workflows/ci.yml.
|
|
50
|
+
*/
|
|
51
|
+
assertEnvironment(
|
|
52
|
+
approvalsEnv,
|
|
53
|
+
emailEnv,
|
|
54
|
+
formsEnv,
|
|
55
|
+
{
|
|
56
|
+
name: 'PAYLOAD_SECRET',
|
|
57
|
+
minLength: 32,
|
|
58
|
+
why: 'Signs Payload sessions and auth tokens. Generate with `openssl rand -base64 48`.',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'NEXT_PUBLIC_SERVER_URL',
|
|
62
|
+
why:
|
|
63
|
+
'The origin approval links are built against, e.g. http://localhost:3000. On a ' +
|
|
64
|
+
'Vercel preview, set it to https://$VERCEL_URL.',
|
|
65
|
+
},
|
|
66
|
+
// Accepts several names (see lib/database.ts), so it is a check, not a name.
|
|
67
|
+
() => databaseConnectionString(),
|
|
68
|
+
)
|
|
69
|
+
|
|
29
70
|
const __filename = fileURLToPath(import.meta.url)
|
|
30
71
|
const __dirname = path.dirname(__filename)
|
|
31
72
|
|
|
@@ -40,6 +81,14 @@ something nobody fills.
|
|
|
40
81
|
*/
|
|
41
82
|
const mcpTools = createMcpToolCollector()
|
|
42
83
|
|
|
84
|
+
/*
|
|
85
|
+
Drops a Next cache tag when content changes, named by the same scheme your
|
|
86
|
+
cached reads use (see lib/cache-tags.ts). Give every collection and global that
|
|
87
|
+
a cached read renders a pair of these: afterChange and afterDelete on a
|
|
88
|
+
collection, afterChange on a global. Draft saves and autosave drop nothing.
|
|
89
|
+
*/
|
|
90
|
+
const revalidation = createTagRevalidationHooks({ cacheTags })
|
|
91
|
+
|
|
43
92
|
// A person in `users` with the admin role. `req.user` can also be an MCP key
|
|
44
93
|
// document, which has no roles and is never an admin, so the collection is
|
|
45
94
|
// checked first — once payload-types.ts exists, TypeScript insists on it.
|
|
@@ -71,13 +120,27 @@ const Users: CollectionConfig = {
|
|
|
71
120
|
],
|
|
72
121
|
}
|
|
73
122
|
|
|
123
|
+
// The public site reads with nobody signed in, so a collection it renders needs
|
|
124
|
+
// a read rule that says yes to that. Without one, Payload's default refuses —
|
|
125
|
+
// and the page renders empty rather than erroring. Anonymous readers get a
|
|
126
|
+
// query, so they see published documents only; anybody signed in sees drafts
|
|
127
|
+
// too, which the admin and live preview need. `src/access/anonymousAccess.test.ts`
|
|
128
|
+
// holds every collection to the bucket it is listed in.
|
|
129
|
+
const publishedOrSignedIn: Access = ({ req: { user } }) =>
|
|
130
|
+
user ? true : { _status: { equals: 'published' } }
|
|
131
|
+
|
|
74
132
|
// Example Pages collection. Replace this with your real content model;
|
|
75
133
|
// keep the `policy` group on collections that should require approval —
|
|
76
134
|
// the approvals plugin reads that field shape.
|
|
77
135
|
const Pages: CollectionConfig = {
|
|
78
136
|
slug: 'pages',
|
|
79
137
|
admin: { useAsTitle: 'title' },
|
|
138
|
+
access: { read: publishedOrSignedIn },
|
|
80
139
|
versions: { drafts: true },
|
|
140
|
+
hooks: {
|
|
141
|
+
afterChange: [revalidation.afterCollectionChange()],
|
|
142
|
+
afterDelete: [revalidation.afterCollectionDelete()],
|
|
143
|
+
},
|
|
81
144
|
fields: [
|
|
82
145
|
{ name: 'title', type: 'text', required: true },
|
|
83
146
|
{ name: 'slug', type: 'text', required: true, unique: true },
|
|
@@ -138,6 +201,11 @@ export default buildConfig({
|
|
|
138
201
|
// denied to every key with no error anywhere.
|
|
139
202
|
auditPlugin({ inngest }),
|
|
140
203
|
|
|
204
|
+
// Where a workflow that ran out of retries, or a failing healthcheck, leaves
|
|
205
|
+
// a row — see the failure handlers in app/api/inngest/route.ts. Not the
|
|
206
|
+
// audit log: that records who did what through an MCP tool.
|
|
207
|
+
jobFailuresPlugin({}),
|
|
208
|
+
|
|
141
209
|
componentsPlugin({
|
|
142
210
|
{{#if useReferenceDs}}manifest: { type: 'object', manifest: designSystemManifest as unknown as Manifest },
|
|
143
211
|
{{else}}// TODO: point at your design system manifest
|
|
@@ -197,7 +265,7 @@ export default buildConfig({
|
|
|
197
265
|
|
|
198
266
|
formsPlugin({
|
|
199
267
|
inngest,
|
|
200
|
-
ipHashSecret:
|
|
268
|
+
// ipHashSecret: read from FORMS_IP_HASH_SECRET, checked above.
|
|
201
269
|
// TODO: list every place a form is allowed to send to, and delete the
|
|
202
270
|
// placeholder. The plugin refuses to start with an empty list — the
|
|
203
271
|
// allowlist is the security perimeter — and refuses to deliver anywhere
|
|
@@ -216,21 +284,13 @@ export default buildConfig({
|
|
|
216
284
|
|
|
217
285
|
integrationsPlugin({ inngest, mcpTools }),
|
|
218
286
|
|
|
219
|
-
// Last of the tool-bearing chain. An MCP key is a credential
|
|
220
|
-
//
|
|
221
|
-
// is
|
|
287
|
+
// Last of the tool-bearing chain. An MCP key is a credential that runs
|
|
288
|
+
// every tool as the person it is bound to, so who may mint, read and
|
|
289
|
+
// revoke one is an admin's call: `mcpApiKeyAccess` narrows the key
|
|
290
|
+
// collection to `isAdmin`, and refuses a key principal outright.
|
|
222
291
|
mcpPlugin({
|
|
223
292
|
mcp: { tools: mcpTools.tools },
|
|
224
|
-
overrideApiKeyCollection: (
|
|
225
|
-
...collection,
|
|
226
|
-
access: {
|
|
227
|
-
...collection.access,
|
|
228
|
-
read: isAdmin,
|
|
229
|
-
create: isAdmin,
|
|
230
|
-
update: isAdmin,
|
|
231
|
-
delete: isAdmin,
|
|
232
|
-
},
|
|
233
|
-
}),
|
|
293
|
+
overrideApiKeyCollection: mcpApiKeyAccess(isAdmin),
|
|
234
294
|
}),
|
|
235
295
|
],
|
|
236
296
|
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { defineConfig } from 'vitest/config'
|
|
4
|
+
|
|
5
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
The cheap tier: vitest in node, with no database and no browser.
|
|
9
|
+
|
|
10
|
+
Everything here is an invariant over values — the access rules, field
|
|
11
|
+
validations, hooks as functions — and `payload.config.ts` is imported, never
|
|
12
|
+
connected. That is what lets these run on every pull request, in CI's `fast`
|
|
13
|
+
job, which has no database. Anything that needs one belongs in `verify`, and
|
|
14
|
+
anything true only of a real response belongs in the smoke pack (`e2e/`).
|
|
15
|
+
|
|
16
|
+
`environment: 'node'`, deliberately: nothing in `apps/web` renders here. The
|
|
17
|
+
components live in the design system, whose own suite runs them.
|
|
18
|
+
*/
|
|
19
|
+
export default defineConfig({
|
|
20
|
+
/*
|
|
21
|
+
The two aliases `tsconfig.json` declares. Next resolves them from that file
|
|
22
|
+
and vitest does not, so a module importing `@/lib/…` or `@payload-config`
|
|
23
|
+
cannot be loaded here unless they are repeated.
|
|
24
|
+
*/
|
|
25
|
+
resolve: {
|
|
26
|
+
alias: {
|
|
27
|
+
'@payload-config': path.join(here, 'src/payload.config.ts'),
|
|
28
|
+
'@': path.join(here, 'src'),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
test: {
|
|
32
|
+
environment: 'node',
|
|
33
|
+
include: ['src/**/*.test.ts'],
|
|
34
|
+
/*
|
|
35
|
+
Enough for `payload.config.ts` to load, and nothing more.
|
|
36
|
+
|
|
37
|
+
The plugins check their secrets when the config is built, so importing it
|
|
38
|
+
needs every value present. None is a credential: the secrets are padded
|
|
39
|
+
placeholders, the address is under `.invalid`, which RFC 2606 reserves so
|
|
40
|
+
it can never resolve, and the connection string names a database nobody
|
|
41
|
+
has. That is safe because the config *parses* the string and never
|
|
42
|
+
connects — the same fact CI's `fast` job relies on.
|
|
43
|
+
|
|
44
|
+
Set here rather than left to the shell, so `pnpm test` needs no setup and a
|
|
45
|
+
`DATABASE_URI` somebody exported for a migration cannot reach a test.
|
|
46
|
+
*/
|
|
47
|
+
env: {
|
|
48
|
+
DATABASE_URI: 'postgres://test:test@localhost:5432/does-not-exist',
|
|
49
|
+
PAYLOAD_SECRET: 'test-only-not-a-real-secret-000000000000',
|
|
50
|
+
APPROVAL_TOKEN_SECRET: 'test-only-not-a-real-secret-000000000000',
|
|
51
|
+
FORMS_IP_HASH_SECRET: 'test-only-not-a-real-secret-000000000000',
|
|
52
|
+
RESEND_API_KEY: 're_test_only_not_a_real_key',
|
|
53
|
+
EMAIL_FROM_ADDRESS: 'test-only@example.invalid',
|
|
54
|
+
NEXT_PUBLIC_SERVER_URL: 'http://localhost:3000',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Stop hook: run the cheap half of CI's gate — typecheck and lint — when a
|
|
4
|
+
# session ends with pending changes under apps/ or design-system/.
|
|
5
|
+
#
|
|
6
|
+
# Why a hook and not only CI: these failures are cheap to fix in the session
|
|
7
|
+
# that caused them and expensive later. A session that touched nothing exits in
|
|
8
|
+
# milliseconds, which is what keeps this cheap enough to leave switched on.
|
|
9
|
+
#
|
|
10
|
+
# Deliberately not here: tests, manifest validation, the generated-files check
|
|
11
|
+
# and the build. They take minutes or need a database; they are CI's job. See
|
|
12
|
+
# CLAUDE.md for the whole gate.
|
|
13
|
+
#
|
|
14
|
+
# This reports. It never blocks and never edits, so it cannot wedge a session on
|
|
15
|
+
# a failure it cannot resolve. Exit is always 0. Set THROUGHLINE_SKIP_GATE=1 to
|
|
16
|
+
# silence it, for instance in headless sessions that run their own gate.
|
|
17
|
+
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
|
|
20
|
+
[[ -z "${THROUGHLINE_SKIP_GATE:-}" ]] || exit 0
|
|
21
|
+
|
|
22
|
+
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
|
|
23
|
+
[[ -n "$ROOT" ]] || exit 0
|
|
24
|
+
|
|
25
|
+
if [[ -z "$(git -C "$ROOT" status --porcelain -- apps design-system 2>/dev/null)" ]]; then
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
cd "$ROOT" || exit 0
|
|
30
|
+
|
|
31
|
+
OUTPUT="$(pnpm typecheck 2>&1 && pnpm lint 2>&1)"
|
|
32
|
+
STATUS=$?
|
|
33
|
+
|
|
34
|
+
if [[ $STATUS -eq 0 ]]; then
|
|
35
|
+
MESSAGE="Gate clean: typecheck + lint. Tests, validate, check-generated and the build are CI's job — see CLAUDE.md."
|
|
36
|
+
else
|
|
37
|
+
# The failing rule or type error is at the end, and a systemMessage is read
|
|
38
|
+
# in a terminal, not scrolled.
|
|
39
|
+
MESSAGE="Gate FAILED (typecheck/lint) — CI will fail on this:
|
|
40
|
+
|
|
41
|
+
$(printf '%s' "$OUTPUT" | tail -25)"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
node -e 'process.stdout.write(JSON.stringify({ systemMessage: process.argv[1], suppressOutput: true }))' "$MESSAGE"
|
|
45
|
+
exit 0
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"Stop": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "GATE=\"$(git rev-parse --show-toplevel 2>/dev/null)/.claude/hooks/gate.sh\"; [ -f \"$GATE\" ] && exec bash \"$GATE\" || exit 0",
|
|
9
|
+
"timeout": 180,
|
|
10
|
+
"statusMessage": "Running the gate (typecheck + lint)…"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -5,8 +5,9 @@ name: CI
|
|
|
5
5
|
# fast every pull request and every push to main. No database: every gate
|
|
6
6
|
# here needs the config to parse, not to connect.
|
|
7
7
|
# verify pushes to main, and on demand. A Postgres service, the migrations,
|
|
8
|
-
#
|
|
9
|
-
# data, so it needs a migrated database
|
|
8
|
+
# a production build — `next build` boots Payload to collect page
|
|
9
|
+
# data, so it needs a migrated database — and the Playwright smoke
|
|
10
|
+
# pack against `next start` on that build.
|
|
10
11
|
#
|
|
11
12
|
# A green `fast` is what a pull request is reviewed on; `verify` is what says
|
|
12
13
|
# main still builds and migrates. Add a path filter to run `verify` on pull
|
|
@@ -129,3 +130,14 @@ jobs:
|
|
|
129
130
|
|
|
130
131
|
- name: Build
|
|
131
132
|
run: pnpm build
|
|
133
|
+
|
|
134
|
+
# The smoke pack (apps/web/e2e): `next start` on the build above, against
|
|
135
|
+
# the empty database migrated above, asserting only what answers without
|
|
136
|
+
# content — the front door, a 404, the admin sign-in screen, and the REST
|
|
137
|
+
# API refusing an anonymous read of private collections. Here and not in
|
|
138
|
+
# `fast`, because the server needs a database to answer at all.
|
|
139
|
+
- name: Install Chromium for the smoke pack
|
|
140
|
+
run: pnpm --dir apps/web test:smoke:install
|
|
141
|
+
|
|
142
|
+
- name: Smoke test
|
|
143
|
+
run: pnpm --dir apps/web test:smoke
|
|
@@ -13,3 +13,12 @@ dist/
|
|
|
13
13
|
.vscode/
|
|
14
14
|
|
|
15
15
|
coverage/
|
|
16
|
+
|
|
17
|
+
# Written by throughline-payload (@forumone/throughline-core): the process
|
|
18
|
+
# group of each in-flight Payload CLI run, so the next run can reap one whose
|
|
19
|
+
# runner was killed before it could clean up.
|
|
20
|
+
.payload-cli-pids
|
|
21
|
+
|
|
22
|
+
# Playwright: traces and screenshots from a failed smoke run
|
|
23
|
+
e2e/.results/
|
|
24
|
+
playwright-report/
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"check:tested": "tsx scripts/check-tested-packages.ts",
|
|
16
16
|
"check:audit": "tsx scripts/check-audit.ts",
|
|
17
17
|
"check:migrations": "pnpm --dir apps/web run check:migrations",
|
|
18
|
+
"payload:reap": "pnpm --dir apps/web run payload:reap",
|
|
18
19
|
"format": "prettier --write ."
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|
|
@@ -25,8 +25,6 @@ after the package that caused them.
|
|
|
25
25
|
* by the same change that makes it untrue.
|
|
26
26
|
*/
|
|
27
27
|
const ACCEPTED: Record<string, string> = {
|
|
28
|
-
'apps/web':
|
|
29
|
-
'Freshly scaffolded: the config and routes are generated wiring. Delete this entry with your first test here — an access rule, a hook or a field validation is the place to start.',
|
|
30
28
|
{{#if useReferenceDs}}{{else}} 'design-system':
|
|
31
29
|
'Placeholder until the design system is written. Delete this entry with its first component.',
|
|
32
30
|
{{/if}}}
|
package/dist/versions.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"@forumone/throughline-approvals": "0.
|
|
3
|
-
"@forumone/throughline-audit": "0.
|
|
4
|
-
"@forumone/throughline-components": "0.
|
|
5
|
-
"@forumone/throughline-core": "0.
|
|
2
|
+
"@forumone/throughline-approvals": "0.9.0",
|
|
3
|
+
"@forumone/throughline-audit": "0.6.0",
|
|
4
|
+
"@forumone/throughline-components": "0.6.0",
|
|
5
|
+
"@forumone/throughline-core": "0.10.0",
|
|
6
6
|
"@forumone/throughline-design-contract": "0.6.0",
|
|
7
|
-
"@forumone/throughline-email": "0.
|
|
8
|
-
"@forumone/throughline-forms": "0.
|
|
9
|
-
"@forumone/throughline-integrations": "0.
|
|
10
|
-
"@forumone/throughline-plugin-contract": "0.
|
|
11
|
-
"@forumone/throughline-publishing": "0.
|
|
12
|
-
"@forumone/throughline-reference-ds": "0.3.
|
|
13
|
-
"@forumone/throughline-workflows": "0.
|
|
7
|
+
"@forumone/throughline-email": "0.3.0",
|
|
8
|
+
"@forumone/throughline-forms": "0.7.0",
|
|
9
|
+
"@forumone/throughline-integrations": "0.9.0",
|
|
10
|
+
"@forumone/throughline-plugin-contract": "0.5.0",
|
|
11
|
+
"@forumone/throughline-publishing": "0.11.0",
|
|
12
|
+
"@forumone/throughline-reference-ds": "0.3.7",
|
|
13
|
+
"@forumone/throughline-workflows": "0.5.0"
|
|
14
14
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forumone/create-throughline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Scaffolder for new Throughline projects (Payload CMS + MCP servers + Inngest workflows).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -44,13 +44,14 @@
|
|
|
44
44
|
"picocolors": "^1.1.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
+
"@playwright/test": "^1.63.0",
|
|
47
48
|
"@types/node": "^20.17.0",
|
|
48
49
|
"eslint": "^10.11.0",
|
|
49
50
|
"tsx": "^4.21.0",
|
|
50
51
|
"typescript": "^5.6.0",
|
|
51
52
|
"vitest": "^4.1.10",
|
|
52
|
-
"@forumone/throughline-
|
|
53
|
-
"@forumone/throughline-
|
|
53
|
+
"@forumone/throughline-eslint-config": "0.0.0",
|
|
54
|
+
"@forumone/throughline-tsconfig": "0.0.0"
|
|
54
55
|
},
|
|
55
56
|
"engines": {
|
|
56
57
|
"node": ">=20.9.0"
|