@forumone/create-throughline 0.3.1 → 0.4.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 +40 -0
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +4 -0
- package/dist/generator.js.map +1 -1
- package/dist/post-install.d.ts.map +1 -1
- package/dist/post-install.js +10 -3
- package/dist/post-install.js.map +1 -1
- package/dist/templates/base/.env.example +9 -13
- package/dist/templates/base/README.md +55 -2
- package/dist/templates/base/apps/web/package.json.template +11 -2
- package/dist/templates/base/apps/web/scripts/check-migrations.ts +147 -0
- package/dist/templates/base/apps/web/scripts/lib/target.ts +95 -0
- package/dist/templates/base/apps/web/scripts/write-migration-snapshot.ts +119 -0
- package/dist/templates/base/apps/web/src/app/api/inngest/route.ts.template +8 -1
- package/dist/templates/base/apps/web/src/lib/database.ts +158 -0
- package/dist/templates/base/apps/web/src/payload.config.ts.template +73 -16
- package/dist/templates/base/github/scripts/check-generated.sh +44 -0
- package/dist/templates/base/github/workflows/ci.yml +131 -0
- package/dist/templates/base/package.json.template +6 -1
- package/dist/templates/base/scripts/check-audit.ts +117 -0
- package/dist/templates/base/scripts/check-single-instance.ts +113 -0
- package/dist/templates/base/scripts/check-tested-packages.ts +165 -0
- package/dist/templates/base/turbo.json +16 -1
- package/dist/templates/with-reference-ds/design-system/scripts/build-manifest.ts +59 -29
- package/dist/templates/with-reference-ds/design-system/scripts/validate.ts +26 -1
- package/dist/versions.json +1 -1
- package/package.json +2 -2
|
@@ -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,165 @@
|
|
|
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
|
+
'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
|
+
{{#if useReferenceDs}}{{else}} 'design-system':
|
|
31
|
+
'Placeholder until the design system is written. Delete this entry with its first component.',
|
|
32
|
+
{{/if}}}
|
|
33
|
+
|
|
34
|
+
/** Directories that never hold source worth testing. */
|
|
35
|
+
const IGNORED_DIRS = new Set([
|
|
36
|
+
'node_modules',
|
|
37
|
+
'dist',
|
|
38
|
+
'build',
|
|
39
|
+
'.next',
|
|
40
|
+
'.turbo',
|
|
41
|
+
'storybook-static',
|
|
42
|
+
'coverage',
|
|
43
|
+
'__snapshots__',
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']
|
|
47
|
+
|
|
48
|
+
const isTest = (name: string): boolean => /\.(test|spec)\.[cm]?[jt]sx?$/.test(name)
|
|
49
|
+
|
|
50
|
+
const isSource = (name: string): boolean =>
|
|
51
|
+
SOURCE_EXTENSIONS.some((extension) => name.endsWith(extension)) &&
|
|
52
|
+
!isTest(name) &&
|
|
53
|
+
!name.endsWith('.d.ts')
|
|
54
|
+
|
|
55
|
+
interface Counts {
|
|
56
|
+
source: number
|
|
57
|
+
tests: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Source and test files under a package's own `src`. `src` rather than the
|
|
62
|
+
* package root, so a package cannot pass on the strength of its own build
|
|
63
|
+
* tooling. A package with no `src` ships no code and is not asked.
|
|
64
|
+
*/
|
|
65
|
+
function countUnder(packagePath: string): Counts | undefined {
|
|
66
|
+
const src = join(packagePath, 'src')
|
|
67
|
+
if (!existsSync(src)) return undefined
|
|
68
|
+
|
|
69
|
+
const counts: Counts = { source: 0, tests: 0 }
|
|
70
|
+
const walk = (dir: string): void => {
|
|
71
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
72
|
+
if (IGNORED_DIRS.has(entry.name)) continue
|
|
73
|
+
const full = join(dir, entry.name)
|
|
74
|
+
if (entry.isDirectory()) walk(full)
|
|
75
|
+
else if (isTest(entry.name)) counts.tests += 1
|
|
76
|
+
else if (isSource(entry.name)) counts.source += 1
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
walk(src)
|
|
80
|
+
return counts
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface WorkspacePackage {
|
|
84
|
+
name: string
|
|
85
|
+
path: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function workspacePackages(): WorkspacePackage[] {
|
|
89
|
+
const stdout = execFileSync('pnpm', ['ls', '--recursive', '--depth', '-1', '--json'], {
|
|
90
|
+
encoding: 'utf8',
|
|
91
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
92
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
93
|
+
})
|
|
94
|
+
return (JSON.parse(stdout) as WorkspacePackage[]).filter((pkg) => Boolean(pkg.name && pkg.path))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function main(): void {
|
|
98
|
+
const root = process.cwd()
|
|
99
|
+
const untested: string[] = []
|
|
100
|
+
const staleExceptions: string[] = []
|
|
101
|
+
let checked = 0
|
|
102
|
+
|
|
103
|
+
for (const pkg of workspacePackages()) {
|
|
104
|
+
const counts = countUnder(pkg.path)
|
|
105
|
+
if (!counts || counts.source === 0) continue
|
|
106
|
+
|
|
107
|
+
checked += 1
|
|
108
|
+
const where = relative(root, pkg.path) || '.'
|
|
109
|
+
const accepted = ACCEPTED[where]
|
|
110
|
+
|
|
111
|
+
if (counts.tests > 0) {
|
|
112
|
+
// An exception that has stopped applying reads, in review, as though
|
|
113
|
+
// somebody decided this package needs no tests.
|
|
114
|
+
if (accepted) staleExceptions.push(where)
|
|
115
|
+
console.log(
|
|
116
|
+
` = ${where.padEnd(32)} ${String(counts.tests).padStart(3)} test files ` +
|
|
117
|
+
`${String(counts.source).padStart(4)} source`,
|
|
118
|
+
)
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (accepted) {
|
|
123
|
+
console.log(` - ${where.padEnd(32)} no tests, accepted`)
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
untested.push(where)
|
|
128
|
+
console.log(` ! ${where.padEnd(32)} 0 test files ${String(counts.source).padStart(4)} source`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (staleExceptions.length > 0) {
|
|
132
|
+
console.log(
|
|
133
|
+
[
|
|
134
|
+
'',
|
|
135
|
+
`${staleExceptions.length} package(s) are on the accepted list and now have tests:`,
|
|
136
|
+
'',
|
|
137
|
+
...staleExceptions.map((where) => ` ${where}`),
|
|
138
|
+
'',
|
|
139
|
+
'Delete the entry from ACCEPTED in scripts/check-tested-packages.ts.',
|
|
140
|
+
].join('\n'),
|
|
141
|
+
)
|
|
142
|
+
process.exitCode = 1
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (untested.length === 0) {
|
|
146
|
+
if (!process.exitCode) console.log(`\nAll ${checked} packages that ship code have tests.`)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log(
|
|
151
|
+
[
|
|
152
|
+
'',
|
|
153
|
+
`${untested.length} package(s) ship code with no test file:`,
|
|
154
|
+
'',
|
|
155
|
+
...untested.map((where) => ` ${where}`),
|
|
156
|
+
'',
|
|
157
|
+
'One test is the bar, not a coverage number. If the package genuinely holds no',
|
|
158
|
+
'runtime behaviour, add it to ACCEPTED in scripts/check-tested-packages.ts with',
|
|
159
|
+
'the reason.',
|
|
160
|
+
].join('\n'),
|
|
161
|
+
)
|
|
162
|
+
process.exitCode = 1
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
main()
|
|
@@ -5,7 +5,22 @@
|
|
|
5
5
|
"build": {
|
|
6
6
|
"dependsOn": ["^build"],
|
|
7
7
|
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
|
|
8
|
-
"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"],
|
|
@@ -10,42 +10,77 @@ import { getTokenList } from '../src/tokens/index.ts'
|
|
|
10
10
|
|
|
11
11
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
12
12
|
const packageRoot = resolve(__dirname, '..')
|
|
13
|
-
const componentsDir = resolve(packageRoot, 'src/components')
|
|
14
13
|
const outputDir = resolve(packageRoot, 'dist')
|
|
15
14
|
const outputFile = resolve(outputDir, 'manifest.json')
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Every directory, relative to the package, whose subdirectories are
|
|
18
|
+
* components. A design system that splits components across layers (atoms and
|
|
19
|
+
* molecules, or a Gesso-style `02-layouts` / `03-components`) lists each one.
|
|
20
|
+
*/
|
|
21
|
+
const CONTRACT_LAYERS = ['src/components']
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The manifest names the design system from its own `package.json`. This file
|
|
25
|
+
* is vendored into every scaffolded project, and a hard-coded name made each of
|
|
26
|
+
* them publish a manifest claiming to be the reference design system.
|
|
27
|
+
*/
|
|
28
|
+
async function readPackage(): Promise<{ name: string; version: string; description?: string }> {
|
|
18
29
|
const raw = await readFile(resolve(packageRoot, 'package.json'), 'utf8')
|
|
19
|
-
const parsed = JSON.parse(raw) as { version?: string }
|
|
20
|
-
|
|
30
|
+
const parsed = JSON.parse(raw) as { name?: string; version?: string; description?: string }
|
|
31
|
+
if (!parsed.name) throw new Error('package.json has no "name"; the manifest needs one.')
|
|
32
|
+
return {
|
|
33
|
+
name: parsed.name,
|
|
34
|
+
version: parsed.version ?? '0.0.0',
|
|
35
|
+
...(parsed.description ? { description: parsed.description } : {}),
|
|
36
|
+
}
|
|
21
37
|
}
|
|
22
38
|
|
|
23
39
|
async function collectContracts(): Promise<Record<string, ComponentContract>> {
|
|
24
40
|
const components: Record<string, ComponentContract> = {}
|
|
25
41
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
for (const layer of CONTRACT_LAYERS) {
|
|
43
|
+
let entries: string[]
|
|
44
|
+
try {
|
|
45
|
+
entries = await readdir(resolve(packageRoot, layer))
|
|
46
|
+
} catch {
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
32
49
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (!dirStat?.isDirectory()) continue
|
|
50
|
+
for (const name of entries.sort()) {
|
|
51
|
+
// Underscore-prefixed directories are private implementation details.
|
|
52
|
+
if (name.startsWith('_') || name.startsWith('.')) continue
|
|
37
53
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
54
|
+
const dir = resolve(packageRoot, layer, name)
|
|
55
|
+
const dirStat = await stat(dir).catch(() => null)
|
|
56
|
+
if (!dirStat?.isDirectory()) continue
|
|
41
57
|
|
|
42
|
-
|
|
43
|
-
contract
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
/*
|
|
59
|
+
A component with no contract is refused, not skipped. Skipped, it is
|
|
60
|
+
absent from the manifest — and every other gate reads the manifest or the
|
|
61
|
+
contract, so nothing would notice that the CMS cannot offer it.
|
|
62
|
+
*/
|
|
63
|
+
const contractPath = join(dir, `${name}.contract.ts`)
|
|
64
|
+
const contractStat = await stat(contractPath).catch(() => null)
|
|
65
|
+
if (!contractStat?.isFile()) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`${layer}/${name} has no ${name}.contract.ts, so the CMS cannot offer it. ` +
|
|
68
|
+
`Write one beside the component. If the directory is not a component, ` +
|
|
69
|
+
`prefix it with an underscore.`,
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const module = (await import(pathToFileURL(contractPath).href)) as {
|
|
74
|
+
contract?: ComponentContract
|
|
75
|
+
}
|
|
76
|
+
if (!module.contract) {
|
|
77
|
+
throw new Error(`${name}.contract.ts does not export "contract"`)
|
|
78
|
+
}
|
|
79
|
+
if (components[name]) {
|
|
80
|
+
throw new Error(`Duplicate component contract for "${name}"`)
|
|
81
|
+
}
|
|
82
|
+
components[name] = module.contract
|
|
47
83
|
}
|
|
48
|
-
components[name] = module.contract
|
|
49
84
|
}
|
|
50
85
|
|
|
51
86
|
return components
|
|
@@ -56,12 +91,7 @@ async function main() {
|
|
|
56
91
|
|
|
57
92
|
const manifest = {
|
|
58
93
|
contractVersion: CONTRACT_VERSION,
|
|
59
|
-
designSystem:
|
|
60
|
-
name: '@forumone/throughline-reference-ds',
|
|
61
|
-
version: await readPackageVersion(),
|
|
62
|
-
description:
|
|
63
|
-
'A brand-neutral reference design system demonstrating contract compliance for Throughline.',
|
|
64
|
-
},
|
|
94
|
+
designSystem: await readPackage(),
|
|
65
95
|
tokens: getTokenList(),
|
|
66
96
|
components,
|
|
67
97
|
build: {
|
|
@@ -32,6 +32,24 @@ async function collectStoryIds(): Promise<Set<string> | undefined> {
|
|
|
32
32
|
return ids
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/*
|
|
36
|
+
Lint rules this design system treats as failures, though the shared linter
|
|
37
|
+
reports them as warnings.
|
|
38
|
+
|
|
39
|
+
`design-contract` sets both to `warning` because it serves every project, and a
|
|
40
|
+
project migrating an existing design system onto the contract should not be
|
|
41
|
+
blocked by prose. A design system written against the contract from the start is
|
|
42
|
+
never mid-migration, so it starts strict. Relax a rule here, deliberately, if
|
|
43
|
+
yours is.
|
|
44
|
+
|
|
45
|
+
Both are promoted because of who reads them. `intent` and `antiExamples` are the
|
|
46
|
+
two fields the composing agent reasons about hardest: `intent` is how it chooses
|
|
47
|
+
between components that could both fit, and `antiExamples` is how it rules one
|
|
48
|
+
out. A component with neither still renders, and still gets composed into the
|
|
49
|
+
wrong place, silently.
|
|
50
|
+
*/
|
|
51
|
+
const REQUIRED_RULES = new Set(['antiExamples.empty', 'intent.brevity'])
|
|
52
|
+
|
|
35
53
|
async function main() {
|
|
36
54
|
const raw = await readJson<unknown>(manifestPath)
|
|
37
55
|
const loaded = loadManifest(raw)
|
|
@@ -39,7 +57,14 @@ async function main() {
|
|
|
39
57
|
const availableStoryIds = await collectStoryIds()
|
|
40
58
|
const lintOptions = availableStoryIds ? { availableStoryIds } : {}
|
|
41
59
|
|
|
42
|
-
|
|
60
|
+
/*
|
|
61
|
+
Severity is rewritten, not just re-bucketed: `formatLintIssues` groups by the
|
|
62
|
+
field itself, so a promoted rule left as a warning would print under
|
|
63
|
+
"Warnings" immediately before the command exits 1.
|
|
64
|
+
*/
|
|
65
|
+
const issues = lintManifest(loaded.raw, lintOptions).map((issue) =>
|
|
66
|
+
REQUIRED_RULES.has(issue.rule) ? { ...issue, severity: 'error' as const } : issue,
|
|
67
|
+
)
|
|
43
68
|
const errors = issues.filter((i) => i.severity === 'error')
|
|
44
69
|
const warnings = issues.filter((i) => i.severity === 'warning')
|
|
45
70
|
|
package/dist/versions.json
CHANGED
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
"@forumone/throughline-integrations": "0.8.5",
|
|
10
10
|
"@forumone/throughline-plugin-contract": "0.4.1",
|
|
11
11
|
"@forumone/throughline-publishing": "0.10.0",
|
|
12
|
-
"@forumone/throughline-reference-ds": "0.3.
|
|
12
|
+
"@forumone/throughline-reference-ds": "0.3.6",
|
|
13
13
|
"@forumone/throughline-workflows": "0.4.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.4.0",
|
|
4
4
|
"description": "Scaffolder for new Throughline projects (Payload CMS + MCP servers + Inngest workflows).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@clack/prompts": "^0.
|
|
43
|
+
"@clack/prompts": "^0.11.0",
|
|
44
44
|
"picocolors": "^1.1.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|