@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.
Files changed (32) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/generator.d.ts.map +1 -1
  3. package/dist/generator.js +7 -1
  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 +9 -13
  9. package/dist/templates/base/README.md +63 -11
  10. package/dist/templates/base/apps/web/package.json.template +21 -12
  11. package/dist/templates/base/apps/web/scripts/check-migrations.ts +147 -0
  12. package/dist/templates/base/apps/web/scripts/lib/target.ts +95 -0
  13. package/dist/templates/base/apps/web/scripts/write-migration-snapshot.ts +119 -0
  14. package/dist/templates/base/apps/web/src/app/api/inngest/route.ts.template +40 -4
  15. package/dist/templates/base/apps/web/src/lib/database.ts +158 -0
  16. package/dist/templates/base/apps/web/src/payload.config.ts.template +90 -20
  17. package/dist/templates/base/github/scripts/check-generated.sh +44 -0
  18. package/dist/templates/base/github/workflows/ci.yml +131 -0
  19. package/dist/templates/base/package.json.template +6 -1
  20. package/dist/templates/base/scripts/check-audit.ts +117 -0
  21. package/dist/templates/base/scripts/check-single-instance.ts +113 -0
  22. package/dist/templates/base/scripts/check-tested-packages.ts +165 -0
  23. package/dist/templates/base/turbo.json +16 -1
  24. package/dist/templates/with-reference-ds/design-system/package.json.template +3 -3
  25. package/dist/templates/with-reference-ds/design-system/scripts/build-manifest.ts +59 -29
  26. package/dist/templates/with-reference-ds/design-system/scripts/validate.ts +26 -1
  27. package/dist/utils/versions.d.ts +20 -0
  28. package/dist/utils/versions.d.ts.map +1 -0
  29. package/dist/utils/versions.js +59 -0
  30. package/dist/utils/versions.js.map +1 -0
  31. package/dist/versions.json +14 -0
  32. 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
- async function readPackageVersion(): Promise<string> {
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
- return parsed.version ?? '0.0.0'
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
- let entries: string[]
27
- try {
28
- entries = await readdir(componentsDir)
29
- } catch {
30
- return components
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
- for (const name of entries) {
34
- const dir = resolve(componentsDir, name)
35
- const dirStat = await stat(dir).catch(() => null)
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
- const contractPath = join(dir, `${name}.contract.ts`)
39
- const contractStat = await stat(contractPath).catch(() => null)
40
- if (!contractStat?.isFile()) continue
54
+ const dir = resolve(packageRoot, layer, name)
55
+ const dirStat = await stat(dir).catch(() => null)
56
+ if (!dirStat?.isDirectory()) continue
41
57
 
42
- const module = (await import(pathToFileURL(contractPath).href)) as {
43
- contract?: ComponentContract
44
- }
45
- if (!module.contract) {
46
- throw new Error(`${name}.contract.ts does not export "contract"`)
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
- const issues = lintManifest(loaded.raw, lintOptions)
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.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.8.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": "^9.15.0",
48
+ "eslint": "^10.11.0",
49
49
  "tsx": "^4.21.0",
50
50
  "typescript": "^5.6.0",
51
- "vitest": "^2.1.0",
52
- "@forumone/throughline-eslint-config": "0.0.0",
53
- "@forumone/throughline-tsconfig": "0.0.0"
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"