@forumone/create-throughline 0.3.1 → 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.
Files changed (39) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/dist/generator.d.ts.map +1 -1
  3. package/dist/generator.js +5 -0
  4. package/dist/generator.js.map +1 -1
  5. package/dist/post-install.d.ts.map +1 -1
  6. package/dist/post-install.js +10 -3
  7. package/dist/post-install.js.map +1 -1
  8. package/dist/templates/base/.env.example +32 -18
  9. package/dist/templates/base/CLAUDE.md +112 -0
  10. package/dist/templates/base/README.md +92 -7
  11. package/dist/templates/base/apps/web/e2e/site.ts +36 -0
  12. package/dist/templates/base/apps/web/e2e/smoke.spec.ts +52 -0
  13. package/dist/templates/base/apps/web/package.json.template +23 -7
  14. package/dist/templates/base/apps/web/playwright.config.ts +63 -0
  15. package/dist/templates/base/apps/web/scripts/check-migrations.ts +147 -0
  16. package/dist/templates/base/apps/web/scripts/lib/target.ts +95 -0
  17. package/dist/templates/base/apps/web/scripts/write-migration-snapshot.ts +119 -0
  18. package/dist/templates/base/apps/web/src/access/anonymousAccess.test.ts +36 -0
  19. package/dist/templates/base/apps/web/src/app/api/inngest/route.ts.template +41 -4
  20. package/dist/templates/base/apps/web/src/instrumentation.ts +35 -0
  21. package/dist/templates/base/apps/web/src/lib/cache-tags.ts +23 -0
  22. package/dist/templates/base/apps/web/src/lib/database.ts +158 -0
  23. package/dist/templates/base/apps/web/src/payload.config.ts.template +136 -19
  24. package/dist/templates/base/apps/web/vitest.config.ts +57 -0
  25. package/dist/templates/base/claude/hooks/gate.sh +45 -0
  26. package/dist/templates/base/claude/settings.json +16 -0
  27. package/dist/templates/base/github/scripts/check-generated.sh +44 -0
  28. package/dist/templates/base/github/workflows/ci.yml +143 -0
  29. package/dist/templates/base/gitignore +9 -0
  30. package/dist/templates/base/package.json.template +7 -1
  31. package/dist/templates/base/scripts/check-audit.ts +117 -0
  32. package/dist/templates/base/scripts/check-single-instance.ts +113 -0
  33. package/dist/templates/base/scripts/check-tested-packages.ts +163 -0
  34. package/dist/templates/base/turbo.json +16 -1
  35. package/dist/templates/with-reference-ds/design-system/scripts/build-manifest.ts +59 -29
  36. package/dist/templates/with-reference-ds/design-system/scripts/validate.ts +26 -1
  37. package/dist/templates/with-reference-ds/design-system/src/components/Divider/Divider.contract.ts +1 -0
  38. package/dist/versions.json +11 -11
  39. package/package.json +5 -4
@@ -0,0 +1,143 @@
1
+ name: CI
2
+
3
+ # Two tiers.
4
+ #
5
+ # fast every pull request and every push to main. No database: every gate
6
+ # here needs the config to parse, not to connect.
7
+ # verify pushes to main, and on demand. A Postgres service, the migrations,
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.
11
+ #
12
+ # A green `fast` is what a pull request is reviewed on; `verify` is what says
13
+ # main still builds and migrates. Add a path filter to run `verify` on pull
14
+ # requests that touch the config, the migrations or the dependencies, once this
15
+ # project has enough of all three to make it worth the minutes.
16
+
17
+ on:
18
+ pull_request:
19
+ push:
20
+ branches: [main]
21
+ workflow_dispatch:
22
+
23
+ concurrency:
24
+ group: ci-${{ github.ref }}
25
+ cancel-in-progress: true
26
+
27
+ # What payload.config.ts and the plugins check at load, as placeholders. The
28
+ # config parses the connection string and never connects in `fast`; secrets only
29
+ # have to be long enough to pass the plugins' own checks. Never a real value.
30
+ env:
31
+ DATABASE_URI: postgres://ci:ci@localhost:5432/ci
32
+ PAYLOAD_SECRET: ci-placeholder-not-a-secret-000000000000
33
+ APPROVAL_TOKEN_SECRET: ci-placeholder-not-a-secret-000000000000
34
+ FORMS_IP_HASH_SECRET: ci-placeholder-not-a-secret-000000000000
35
+ RESEND_API_KEY: re_ci_placeholder
36
+ EMAIL_FROM_ADDRESS: ci@example.com
37
+ NEXT_PUBLIC_SERVER_URL: http://localhost:3000
38
+
39
+ jobs:
40
+ fast:
41
+ runs-on: ubuntu-latest
42
+ timeout-minutes: 20
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+
46
+ - uses: pnpm/action-setup@v4
47
+
48
+ - uses: actions/setup-node@v4
49
+ with:
50
+ node-version: 22
51
+ cache: pnpm
52
+
53
+ # First, because a lockfile missing an entry passes every other gate
54
+ # locally and fails here.
55
+ - name: Install
56
+ run: pnpm install --frozen-lockfile
57
+
58
+ - name: Typecheck
59
+ run: pnpm typecheck
60
+
61
+ - name: Lint
62
+ run: pnpm lint
63
+
64
+ - name: Check singletons resolve once
65
+ run: pnpm check:instances
66
+
67
+ - name: Check every package that ships code has tests
68
+ run: pnpm check:tested
69
+
70
+ - name: Check the migration chain
71
+ run: pnpm check:migrations
72
+
73
+ # Builds the design system's manifest and lints it against the contract:
74
+ # an unknown token, a composition rule naming a missing component, an
75
+ # example pointing at a story that does not exist.
76
+ - name: Validate the design-system manifest
77
+ run: pnpm validate
78
+
79
+ - name: Check generated Payload artifacts are current
80
+ run: bash .github/scripts/check-generated.sh
81
+
82
+ - name: Test
83
+ run: pnpm test
84
+
85
+ # Non-blocking: a new advisory is news, not necessarily a defect, and
86
+ # blocking every open pull request the morning a CVE is published is how
87
+ # a gate gets removed rather than fixed. Read it.
88
+ - name: Audit production dependencies
89
+ run: pnpm check:audit
90
+ continue-on-error: true
91
+
92
+ verify:
93
+ if: github.event_name != 'pull_request'
94
+ runs-on: ubuntu-latest
95
+ timeout-minutes: 25
96
+
97
+ services:
98
+ postgres:
99
+ image: postgres:17-alpine
100
+ env:
101
+ POSTGRES_USER: ci
102
+ POSTGRES_PASSWORD: ci
103
+ POSTGRES_DB: ci
104
+ ports:
105
+ - 5432:5432
106
+ options: >-
107
+ --health-cmd pg_isready
108
+ --health-interval 10s
109
+ --health-timeout 5s
110
+ --health-retries 5
111
+
112
+ steps:
113
+ - uses: actions/checkout@v4
114
+
115
+ - uses: pnpm/action-setup@v4
116
+
117
+ - uses: actions/setup-node@v4
118
+ with:
119
+ node-version: 22
120
+ cache: pnpm
121
+
122
+ - name: Install
123
+ run: pnpm install --frozen-lockfile
124
+
125
+ # A fresh database, migrated exactly as a deploy would migrate it. A
126
+ # migration that does not apply cleanly from empty fails here rather than
127
+ # on the first environment somebody creates.
128
+ - name: Migrate
129
+ run: pnpm --dir apps/web migrate
130
+
131
+ - name: Build
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/
@@ -7,14 +7,20 @@
7
7
  "dev": "{{#if useReferenceDs}}pnpm --filter \"./design-system\" build && {{/if}}turbo run dev",
8
8
  "storybook": "turbo run storybook",
9
9
  "build-storybook": "turbo run build-storybook",
10
- "validate": "turbo run validate",
10
+ "validate": "turbo run validate --filter=./design-system",
11
11
  "lint": "turbo run lint",
12
12
  "typecheck": "turbo run typecheck",
13
13
  "test": "turbo run test",
14
+ "check:instances": "tsx scripts/check-single-instance.ts",
15
+ "check:tested": "tsx scripts/check-tested-packages.ts",
16
+ "check:audit": "tsx scripts/check-audit.ts",
17
+ "check:migrations": "pnpm --dir apps/web run check:migrations",
18
+ "payload:reap": "pnpm --dir apps/web run payload:reap",
14
19
  "format": "prettier --write ."
15
20
  },
16
21
  "devDependencies": {
17
22
  "prettier": "^3.4.0",
23
+ "tsx": "^4.21.0",
18
24
  "turbo": "^2.3.0",
19
25
  "typescript": "^5.6.0"
20
26
  },
@@ -0,0 +1,117 @@
1
+ import { execFileSync } from 'node:child_process'
2
+
3
+ /*
4
+ `pnpm audit` over the production tree, with an allowlist that cannot rot.
5
+
6
+ pnpm check:audit
7
+
8
+ **Why not `pnpm audit --prod --audit-level=high` on its own.** A Payload + Next
9
+ production tree is rarely clean: there are usually high advisories in it that no
10
+ request the site serves can reach. A gate that is red on every run is a gate
11
+ people stop reading.
12
+
13
+ **Why not `pnpm.auditConfig.ignoreGhsas`.** It filters, and that is all it does.
14
+ A list of ids in `package.json` carries no reason, and nothing notices when one
15
+ stops applying — the dependency is dropped or patched, and the entry sits there
16
+ suppressing a finding it no longer describes.
17
+
18
+ So this asserts the allowlist in both directions. An advisory that is not on the
19
+ list is reported. An entry that no longer appears in the audit is *also*
20
+ reported, as something to delete. That second half is why this is a script.
21
+
22
+ It is non-blocking in CI. A new advisory is news, not necessarily a defect, and
23
+ blocking every open pull request the morning a CVE is published is how a gate
24
+ gets removed rather than fixed.
25
+ */
26
+
27
+ interface Allowed {
28
+ ghsa: string
29
+ module: string
30
+ /** Which path it is on, and why that path cannot reach a request. */
31
+ why: string
32
+ }
33
+
34
+ /**
35
+ * High and critical advisories in the production tree that are accepted.
36
+ *
37
+ * "Not exploitable" is not a reason. Say which dependency path it arrives on
38
+ * and what would have to be true for a request to reach it.
39
+ */
40
+ const ALLOWED: Allowed[] = []
41
+
42
+ interface Advisory {
43
+ severity: string
44
+ module_name: string
45
+ title: string
46
+ github_advisory_id?: string
47
+ }
48
+
49
+ function audit(): Advisory[] {
50
+ let stdout: string
51
+ try {
52
+ // Exits non-zero whenever it finds anything at or above the level, so the
53
+ // throw is the normal case and the output is on it.
54
+ stdout = execFileSync('pnpm', ['audit', '--prod', '--audit-level=high', '--json'], {
55
+ encoding: 'utf8',
56
+ stdio: ['ignore', 'pipe', 'ignore'],
57
+ })
58
+ } catch (error) {
59
+ stdout = (error as { stdout?: string }).stdout ?? ''
60
+ }
61
+
62
+ if (stdout.trim() === '') return []
63
+ const parsed = JSON.parse(stdout) as { advisories?: Record<string, Advisory> }
64
+ // One advisory reached through several dependency paths is reported once
65
+ // per path; it is one finding.
66
+ const unique = new Map<string, Advisory>()
67
+ for (const advisory of Object.values(parsed.advisories ?? {})) {
68
+ if (advisory.severity !== 'high' && advisory.severity !== 'critical') continue
69
+ unique.set(advisory.github_advisory_id ?? `${advisory.module_name}:${advisory.title}`, advisory)
70
+ }
71
+ return [...unique.values()]
72
+ }
73
+
74
+ function main(): void {
75
+ const found = audit()
76
+ const seen = new Set(found.map((a) => a.github_advisory_id).filter(Boolean) as string[])
77
+ const allowed = new Map(ALLOWED.map((entry) => [entry.ghsa, entry]))
78
+
79
+ const unexpected = found.filter(
80
+ (a) => !a.github_advisory_id || !allowed.has(a.github_advisory_id),
81
+ )
82
+ const stale = ALLOWED.filter((entry) => !seen.has(entry.ghsa))
83
+
84
+ for (const advisory of found) {
85
+ if (advisory.github_advisory_id && allowed.has(advisory.github_advisory_id)) {
86
+ console.log(` = ${advisory.module_name.padEnd(18)} ${advisory.github_advisory_id} accepted`)
87
+ }
88
+ }
89
+
90
+ if (unexpected.length > 0) {
91
+ console.log(`\n${unexpected.length} advisory/advisories not on the allowlist:\n`)
92
+ for (const advisory of unexpected) {
93
+ console.log(` ! ${advisory.severity.padEnd(8)} ${advisory.module_name}`)
94
+ console.log(` ${advisory.github_advisory_id ?? '(no GHSA id)'} — ${advisory.title}`)
95
+ }
96
+ console.log(
97
+ '\nEither upgrade past it, or add it to ALLOWED in scripts/check-audit.ts with the\n' +
98
+ 'dependency path it arrives on and why no request can reach it.',
99
+ )
100
+ }
101
+
102
+ if (stale.length > 0) {
103
+ console.log(
104
+ `\n${stale.length} allowlist entry/entries no longer apply and should be deleted:\n`,
105
+ )
106
+ for (const entry of stale) console.log(` - ${entry.ghsa} (${entry.module})`)
107
+ }
108
+
109
+ if (unexpected.length === 0 && stale.length === 0) {
110
+ console.log(`\nNothing new. ${found.length} known advisory/advisories, each with a reason.`)
111
+ return
112
+ }
113
+
114
+ process.exitCode = 1
115
+ }
116
+
117
+ main()
@@ -0,0 +1,113 @@
1
+ import { execFileSync } from 'node:child_process'
2
+
3
+ /*
4
+ One copy of each package that carries React context or is a singleton.
5
+
6
+ pnpm check:instances
7
+
8
+ **The failure this exists for.** pnpm keys a package's identity on its version
9
+ *and* its resolved peers. When two workspace packages resolve a peer differently
10
+ — one on `@types/node@^20`, another on `^24`, say — pnpm installs the same
11
+ version of `@payloadcms/ui` twice, in two directories. Two directories are two
12
+ module instances, two instances are two React contexts, and a component that
13
+ reads the context the provider did not populate gets `undefined`. In the admin
14
+ that is an intermittent 500 on one view while every other view works.
15
+
16
+ **Nothing else can see it.** `pnpm install` succeeds, typecheck, lint and every
17
+ unit test pass, because the two copies are byte-identical: the problem exists
18
+ only at module-resolution time, and only matters at render time. So this asserts
19
+ what no other gate can — that each package below resolves to exactly one
20
+ instance across the workspace. A version range is not the thing to check; the
21
+ instance is.
22
+
23
+ When it fails, compare the peers of the packages that pull in each copy and pin
24
+ the offender in `pnpm.overrides` in the root `package.json`.
25
+ */
26
+
27
+ /**
28
+ * Packages where a second copy is a bug rather than waste: each holds React
29
+ * context or is a singleton by construction, so two copies fail at runtime.
30
+ */
31
+ const SINGLETONS = ['@payloadcms/ui', 'payload', 'react', 'react-dom']
32
+
33
+ /**
34
+ * Copies that are real but cannot reach a bundle, by version, with the reason.
35
+ * An entry here is a claim that nothing you ship imports that copy.
36
+ */
37
+ const ACCEPTED: Record<string, string[]> = {}
38
+
39
+ interface Instance {
40
+ version: string
41
+ path: string
42
+ }
43
+
44
+ /** Every resolved copy of `name`, by reading pnpm's own resolution. */
45
+ function instances(name: string): Instance[] {
46
+ const stdout = execFileSync(
47
+ 'pnpm',
48
+ ['ls', name, '--recursive', '--depth', 'Infinity', '--json'],
49
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 64 * 1024 * 1024 },
50
+ )
51
+
52
+ const found = new Map<string, Instance>()
53
+ const visit = (deps: Record<string, unknown> | undefined): void => {
54
+ for (const [depName, raw] of Object.entries(deps ?? {})) {
55
+ const info = raw as {
56
+ version?: string
57
+ path?: string
58
+ dependencies?: Record<string, unknown>
59
+ }
60
+ if (!info || typeof info !== 'object') continue
61
+ if (depName === name && info.path) {
62
+ found.set(info.path, { version: info.version ?? '?', path: info.path })
63
+ }
64
+ visit(info.dependencies)
65
+ }
66
+ }
67
+
68
+ for (const pkg of JSON.parse(stdout) as Record<string, unknown>[]) {
69
+ visit(pkg['dependencies'] as Record<string, unknown> | undefined)
70
+ visit(pkg['devDependencies'] as Record<string, unknown> | undefined)
71
+ }
72
+ return [...found.values()]
73
+ }
74
+
75
+ function main(): void {
76
+ let failed = false
77
+
78
+ for (const name of SINGLETONS) {
79
+ const all = instances(name)
80
+ const accepted = ACCEPTED[name] ?? []
81
+ const live = all.filter((i) => !accepted.includes(i.version))
82
+
83
+ if (live.length <= 1) {
84
+ const note = all.length > live.length ? ` (+${all.length - live.length} accepted)` : ''
85
+ console.log(` = ${name.padEnd(20)} one instance${note}`)
86
+ continue
87
+ }
88
+
89
+ failed = true
90
+ console.log(`\n ! ${name} resolves to ${live.length} instances:\n`)
91
+ for (const instance of live) console.log(` ${instance.version} ${instance.path}`)
92
+ }
93
+
94
+ if (!failed) {
95
+ console.log('\nEvery singleton resolves once.')
96
+ return
97
+ }
98
+
99
+ console.log(
100
+ [
101
+ '',
102
+ 'Two copies of the same version are still two modules, and two modules holding',
103
+ 'React context is a runtime failure no type or test can see.',
104
+ '',
105
+ 'The usual cause is a peer that differs between workspace packages, which changes',
106
+ "pnpm's identity hash for the dependency. Compare the peers and pin the offender",
107
+ 'in `pnpm.overrides`.',
108
+ ].join('\n'),
109
+ )
110
+ process.exitCode = 1
111
+ }
112
+
113
+ main()
@@ -0,0 +1,163 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { existsSync, readdirSync } from 'node:fs'
3
+ import { join, relative } from 'node:path'
4
+
5
+ /*
6
+ Every workspace package that ships code has at least one test.
7
+
8
+ pnpm check:tested
9
+
10
+ Deliberately not a coverage percentage. A number invites a target, and a target
11
+ invites tests written to move it. What this asserts is the one bit a coverage
12
+ report hides: a package with source and **no test file at all**, which looks
13
+ exactly like a package whose tests pass. Defects in a code generator, a
14
+ serialiser or a set of access rules then surface downstream, one at a time, long
15
+ after the package that caused them.
16
+ */
17
+
18
+ /**
19
+ * Packages with source files and no tests, keyed by their path from the repo
20
+ * root, with the reason.
21
+ *
22
+ * This is the whole exception list. An entry is a claim that the package holds
23
+ * no behaviour worth asserting, not that testing it is inconvenient. An entry
24
+ * whose package has since gained a test fails the check, so each one is removed
25
+ * by the same change that makes it untrue.
26
+ */
27
+ const ACCEPTED: Record<string, string> = {
28
+ {{#if useReferenceDs}}{{else}} 'design-system':
29
+ 'Placeholder until the design system is written. Delete this entry with its first component.',
30
+ {{/if}}}
31
+
32
+ /** Directories that never hold source worth testing. */
33
+ const IGNORED_DIRS = new Set([
34
+ 'node_modules',
35
+ 'dist',
36
+ 'build',
37
+ '.next',
38
+ '.turbo',
39
+ 'storybook-static',
40
+ 'coverage',
41
+ '__snapshots__',
42
+ ])
43
+
44
+ const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']
45
+
46
+ const isTest = (name: string): boolean => /\.(test|spec)\.[cm]?[jt]sx?$/.test(name)
47
+
48
+ const isSource = (name: string): boolean =>
49
+ SOURCE_EXTENSIONS.some((extension) => name.endsWith(extension)) &&
50
+ !isTest(name) &&
51
+ !name.endsWith('.d.ts')
52
+
53
+ interface Counts {
54
+ source: number
55
+ tests: number
56
+ }
57
+
58
+ /**
59
+ * Source and test files under a package's own `src`. `src` rather than the
60
+ * package root, so a package cannot pass on the strength of its own build
61
+ * tooling. A package with no `src` ships no code and is not asked.
62
+ */
63
+ function countUnder(packagePath: string): Counts | undefined {
64
+ const src = join(packagePath, 'src')
65
+ if (!existsSync(src)) return undefined
66
+
67
+ const counts: Counts = { source: 0, tests: 0 }
68
+ const walk = (dir: string): void => {
69
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
70
+ if (IGNORED_DIRS.has(entry.name)) continue
71
+ const full = join(dir, entry.name)
72
+ if (entry.isDirectory()) walk(full)
73
+ else if (isTest(entry.name)) counts.tests += 1
74
+ else if (isSource(entry.name)) counts.source += 1
75
+ }
76
+ }
77
+ walk(src)
78
+ return counts
79
+ }
80
+
81
+ interface WorkspacePackage {
82
+ name: string
83
+ path: string
84
+ }
85
+
86
+ function workspacePackages(): WorkspacePackage[] {
87
+ const stdout = execFileSync('pnpm', ['ls', '--recursive', '--depth', '-1', '--json'], {
88
+ encoding: 'utf8',
89
+ stdio: ['ignore', 'pipe', 'ignore'],
90
+ maxBuffer: 64 * 1024 * 1024,
91
+ })
92
+ return (JSON.parse(stdout) as WorkspacePackage[]).filter((pkg) => Boolean(pkg.name && pkg.path))
93
+ }
94
+
95
+ function main(): void {
96
+ const root = process.cwd()
97
+ const untested: string[] = []
98
+ const staleExceptions: string[] = []
99
+ let checked = 0
100
+
101
+ for (const pkg of workspacePackages()) {
102
+ const counts = countUnder(pkg.path)
103
+ if (!counts || counts.source === 0) continue
104
+
105
+ checked += 1
106
+ const where = relative(root, pkg.path) || '.'
107
+ const accepted = ACCEPTED[where]
108
+
109
+ if (counts.tests > 0) {
110
+ // An exception that has stopped applying reads, in review, as though
111
+ // somebody decided this package needs no tests.
112
+ if (accepted) staleExceptions.push(where)
113
+ console.log(
114
+ ` = ${where.padEnd(32)} ${String(counts.tests).padStart(3)} test files ` +
115
+ `${String(counts.source).padStart(4)} source`,
116
+ )
117
+ continue
118
+ }
119
+
120
+ if (accepted) {
121
+ console.log(` - ${where.padEnd(32)} no tests, accepted`)
122
+ continue
123
+ }
124
+
125
+ untested.push(where)
126
+ console.log(` ! ${where.padEnd(32)} 0 test files ${String(counts.source).padStart(4)} source`)
127
+ }
128
+
129
+ if (staleExceptions.length > 0) {
130
+ console.log(
131
+ [
132
+ '',
133
+ `${staleExceptions.length} package(s) are on the accepted list and now have tests:`,
134
+ '',
135
+ ...staleExceptions.map((where) => ` ${where}`),
136
+ '',
137
+ 'Delete the entry from ACCEPTED in scripts/check-tested-packages.ts.',
138
+ ].join('\n'),
139
+ )
140
+ process.exitCode = 1
141
+ }
142
+
143
+ if (untested.length === 0) {
144
+ if (!process.exitCode) console.log(`\nAll ${checked} packages that ship code have tests.`)
145
+ return
146
+ }
147
+
148
+ console.log(
149
+ [
150
+ '',
151
+ `${untested.length} package(s) ship code with no test file:`,
152
+ '',
153
+ ...untested.map((where) => ` ${where}`),
154
+ '',
155
+ 'One test is the bar, not a coverage number. If the package genuinely holds no',
156
+ 'runtime behaviour, add it to ACCEPTED in scripts/check-tested-packages.ts with',
157
+ 'the reason.',
158
+ ].join('\n'),
159
+ )
160
+ process.exitCode = 1
161
+ }
162
+
163
+ main()
@@ -5,7 +5,22 @@
5
5
  "build": {
6
6
  "dependsOn": ["^build"],
7
7
  "outputs": ["dist/**", ".next/**", "!.next/cache/**"],
8
- "env": ["NODE_ENV"]
8
+ "env": [
9
+ "NODE_ENV",
10
+ "DATABASE_URI",
11
+ "DATABASE_URL",
12
+ "DATABASE_URL_UNPOOLED",
13
+ "POSTGRES_URL",
14
+ "POSTGRES_URL_NON_POOLING",
15
+ "PAYLOAD_SECRET",
16
+ "APPROVAL_TOKEN_SECRET",
17
+ "FORMS_IP_HASH_SECRET",
18
+ "RESEND_API_KEY",
19
+ "EMAIL_*",
20
+ "INNGEST_*",
21
+ "BLOB_READ_WRITE_TOKEN",
22
+ "NEXT_PUBLIC_*"
23
+ ]
9
24
  },
10
25
  "build-storybook": {
11
26
  "dependsOn": ["^build"],