@forumone/create-throughline 0.3.0 → 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 +47 -0
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +7 -1
- 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 +63 -11
- package/dist/templates/base/apps/web/package.json.template +21 -12
- 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 +40 -4
- package/dist/templates/base/apps/web/src/lib/database.ts +158 -0
- package/dist/templates/base/apps/web/src/payload.config.ts.template +90 -20
- 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/package.json.template +3 -3
- 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/utils/versions.d.ts +20 -0
- package/dist/utils/versions.d.ts.map +1 -0
- package/dist/utils/versions.js +59 -0
- package/dist/utils/versions.js.map +1 -0
- package/dist/versions.json +14 -0
- package/package.json +6 -6
|
@@ -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
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The version of every Throughline package the scaffold depends on, keyed for
|
|
3
|
+
* the template renderer: `@forumone/throughline-design-contract` becomes
|
|
4
|
+
* `version_design_contract`, rendered as `"^{{version_design_contract}}"`.
|
|
5
|
+
*
|
|
6
|
+
* The ranges used to be typed into the templates by hand, and nothing moved
|
|
7
|
+
* them: they sat at `^0.2.0` — which for a 0.x version admits patches only —
|
|
8
|
+
* while the packages reached 0.9 and 0.10. A scaffolded site installed the
|
|
9
|
+
* earliest releases, against a `payload.config.ts` written for today's APIs.
|
|
10
|
+
*
|
|
11
|
+
* So the versions are read, never typed. A published scaffolder reads
|
|
12
|
+
* `versions.json`, which `copy-templates` writes beside `templates/` at build
|
|
13
|
+
* time; `publish-packages` builds after `changeset version`, so that file names
|
|
14
|
+
* exactly the set being published. Running from source (tests, `tsx`), there is
|
|
15
|
+
* no such file and the workspace's own `package.json` files are the answer.
|
|
16
|
+
*/
|
|
17
|
+
export declare function loadPackageVersions(templatesDir: string): Promise<Record<string, string>>;
|
|
18
|
+
/** `{ '@forumone/throughline-core': '0.9.1', … }` from a `packages/` directory. */
|
|
19
|
+
export declare function readWorkspaceVersions(packagesDir: string): Promise<Record<string, string>>;
|
|
20
|
+
//# sourceMappingURL=versions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"versions.d.ts","sourceRoot":"","sources":["../../src/utils/versions.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAO/F;AAED,mFAAmF;AACnF,wBAAsB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CA0BhG"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
const SCOPE_PREFIX = '@forumone/throughline-';
|
|
5
|
+
/**
|
|
6
|
+
* The version of every Throughline package the scaffold depends on, keyed for
|
|
7
|
+
* the template renderer: `@forumone/throughline-design-contract` becomes
|
|
8
|
+
* `version_design_contract`, rendered as `"^{{version_design_contract}}"`.
|
|
9
|
+
*
|
|
10
|
+
* The ranges used to be typed into the templates by hand, and nothing moved
|
|
11
|
+
* them: they sat at `^0.2.0` — which for a 0.x version admits patches only —
|
|
12
|
+
* while the packages reached 0.9 and 0.10. A scaffolded site installed the
|
|
13
|
+
* earliest releases, against a `payload.config.ts` written for today's APIs.
|
|
14
|
+
*
|
|
15
|
+
* So the versions are read, never typed. A published scaffolder reads
|
|
16
|
+
* `versions.json`, which `copy-templates` writes beside `templates/` at build
|
|
17
|
+
* time; `publish-packages` builds after `changeset version`, so that file names
|
|
18
|
+
* exactly the set being published. Running from source (tests, `tsx`), there is
|
|
19
|
+
* no such file and the workspace's own `package.json` files are the answer.
|
|
20
|
+
*/
|
|
21
|
+
export async function loadPackageVersions(templatesDir) {
|
|
22
|
+
const baked = resolve(templatesDir, '..', 'versions.json');
|
|
23
|
+
if (existsSync(baked)) {
|
|
24
|
+
return templateKeys(JSON.parse(await readFile(baked, 'utf-8')));
|
|
25
|
+
}
|
|
26
|
+
// src/templates or dist/templates -> the package -> packages/
|
|
27
|
+
return templateKeys(await readWorkspaceVersions(resolve(templatesDir, '..', '..', '..')));
|
|
28
|
+
}
|
|
29
|
+
/** `{ '@forumone/throughline-core': '0.9.1', … }` from a `packages/` directory. */
|
|
30
|
+
export async function readWorkspaceVersions(packagesDir) {
|
|
31
|
+
const versions = {};
|
|
32
|
+
const entries = await readdir(packagesDir, { withFileTypes: true }).catch(() => []);
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (!entry.isDirectory())
|
|
35
|
+
continue;
|
|
36
|
+
const manifest = join(packagesDir, entry.name, 'package.json');
|
|
37
|
+
if (!existsSync(manifest))
|
|
38
|
+
continue;
|
|
39
|
+
const { name, version, private: isPrivate, } = JSON.parse(await readFile(manifest, 'utf-8'));
|
|
40
|
+
if (!name?.startsWith(SCOPE_PREFIX) || !version || isPrivate)
|
|
41
|
+
continue;
|
|
42
|
+
versions[name] = version;
|
|
43
|
+
}
|
|
44
|
+
if (Object.keys(versions).length === 0) {
|
|
45
|
+
throw new Error(`create-throughline: no package versions found. Expected versions.json beside the ` +
|
|
46
|
+
`templates, or @forumone/throughline-* packages in ${packagesDir}.`);
|
|
47
|
+
}
|
|
48
|
+
return versions;
|
|
49
|
+
}
|
|
50
|
+
function templateKeys(versions) {
|
|
51
|
+
const keys = {};
|
|
52
|
+
for (const [name, version] of Object.entries(versions)) {
|
|
53
|
+
if (!name.startsWith(SCOPE_PREFIX))
|
|
54
|
+
continue;
|
|
55
|
+
keys[`version_${name.slice(SCOPE_PREFIX.length).replace(/-/g, '_')}`] = version;
|
|
56
|
+
}
|
|
57
|
+
return keys;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=versions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"versions.js","sourceRoot":"","sources":["../../src/utils/versions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEzC,MAAM,YAAY,GAAG,wBAAwB,CAAA;AAE7C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,YAAoB;IAC5D,MAAM,KAAK,GAAG,OAAO,CAAC,YAAY,EAAE,IAAI,EAAE,eAAe,CAAC,CAAA;IAC1D,IAAI,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,CAA2B,CAAC,CAAA;IAC3F,CAAC;IACD,8DAA8D;IAC9D,OAAO,YAAY,CAAC,MAAM,qBAAqB,CAAC,OAAO,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;AAC3F,CAAC;AAED,mFAAmF;AACnF,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,WAAmB;IAC7D,MAAM,QAAQ,GAA2B,EAAE,CAAA;IAC3C,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,WAAW,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAA;IACnF,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE;YAAE,SAAQ;QAClC,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,IAAI,EAAE,cAAc,CAAC,CAAA;QAC9D,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,SAAQ;QACnC,MAAM,EACJ,IAAI,EACJ,OAAO,EACP,OAAO,EAAE,SAAS,GACnB,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAI/C,CAAA;QACD,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,IAAI,SAAS;YAAE,SAAQ;QACtE,QAAQ,CAAC,IAAI,CAAC,GAAG,OAAO,CAAA;IAC1B,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CACb,mFAAmF;YACjF,qDAAqD,WAAW,GAAG,CACtE,CAAA;IACH,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,SAAS,YAAY,CAAC,QAAgC;IACpD,MAAM,IAAI,GAA2B,EAAE,CAAA;IACvC,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACvD,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC;YAAE,SAAQ;QAC5C,IAAI,CAAC,WAAW,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAA;IACjF,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"@forumone/throughline-approvals": "0.8.8",
|
|
3
|
+
"@forumone/throughline-audit": "0.5.4",
|
|
4
|
+
"@forumone/throughline-components": "0.5.7",
|
|
5
|
+
"@forumone/throughline-core": "0.9.1",
|
|
6
|
+
"@forumone/throughline-design-contract": "0.6.0",
|
|
7
|
+
"@forumone/throughline-email": "0.2.12",
|
|
8
|
+
"@forumone/throughline-forms": "0.6.4",
|
|
9
|
+
"@forumone/throughline-integrations": "0.8.5",
|
|
10
|
+
"@forumone/throughline-plugin-contract": "0.4.1",
|
|
11
|
+
"@forumone/throughline-publishing": "0.10.0",
|
|
12
|
+
"@forumone/throughline-reference-ds": "0.3.6",
|
|
13
|
+
"@forumone/throughline-workflows": "0.4.0"
|
|
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,17 +40,17 @@
|
|
|
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": {
|
|
47
47
|
"@types/node": "^20.17.0",
|
|
48
|
-
"eslint": "^
|
|
48
|
+
"eslint": "^10.11.0",
|
|
49
49
|
"tsx": "^4.21.0",
|
|
50
50
|
"typescript": "^5.6.0",
|
|
51
|
-
"vitest": "^
|
|
52
|
-
"@forumone/throughline-
|
|
53
|
-
"@forumone/throughline-
|
|
51
|
+
"vitest": "^4.1.10",
|
|
52
|
+
"@forumone/throughline-tsconfig": "0.0.0",
|
|
53
|
+
"@forumone/throughline-eslint-config": "0.0.0"
|
|
54
54
|
},
|
|
55
55
|
"engines": {
|
|
56
56
|
"node": ">=20.9.0"
|