create-packkit 2.9.0 → 3.1.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.
@@ -54,7 +54,8 @@ export default {
54
54
  };
55
55
 
56
56
  function libraryEntry(cfg) {
57
- if (cfg.isReact) return reactEntry(cfg);
57
+ // Only reached for a plain library (the caller guards on !cfg.hasFramework),
58
+ // so framework entries never come through here — frameworks.js writes those.
58
59
  if (cfg.isTs) {
59
60
  return [
60
61
  `/** Greet someone by name. */`,
@@ -1,4 +1,5 @@
1
1
  import { toJson } from '../render.js';
2
+ import { V } from '../versions.js';
2
3
 
3
4
  export default {
4
5
  id: 'release',
@@ -21,7 +22,7 @@ export default {
21
22
  pkg.scripts.changeset = 'changeset';
22
23
  pkg.scripts.version = 'changeset version';
23
24
  pkg.scripts.release = `${buildThen(cfg)}changeset publish`;
24
- pkg.devDependencies['@changesets/cli'] = '^2.27.0';
25
+ pkg.devDependencies['@changesets/cli'] = V['@changesets/cli'];
25
26
  } else if (cfg.release === 'release-it') {
26
27
  files['.release-it.json'] = toJson({
27
28
  git: { commitMessage: 'chore: release v${version}' },
@@ -29,10 +30,10 @@ export default {
29
30
  npm: { publish: true },
30
31
  });
31
32
  pkg.scripts.release = 'release-it';
32
- pkg.devDependencies['release-it'] = '^20.0.0';
33
+ pkg.devDependencies['release-it'] = V['release-it'];
33
34
  } else if (cfg.release === 'np') {
34
35
  pkg.scripts.release = 'np';
35
- pkg.devDependencies.np = '^12.0.0';
36
+ pkg.devDependencies.np = V.np;
36
37
  }
37
38
 
38
39
  return { files, pkg };
@@ -1,3 +1,4 @@
1
+ import { V } from '../versions.js';
1
2
  // HTTP service target. Supports Hono (default), Fastify and Express. Splits the
2
3
  // app (testable) from the server entry (which listens), adds start/dev scripts,
3
4
  // and a production Dockerfile.
@@ -24,7 +25,7 @@ export default {
24
25
  },
25
26
  dependencies: deps(fw),
26
27
  devDependencies: {
27
- ...(cfg.isTs ? { tsx: '^4.0.0' } : {}),
28
+ ...(cfg.isTs ? { tsx: V.tsx } : {}),
28
29
  ...(cfg.isTs ? typeDeps(fw) : {}),
29
30
  },
30
31
  },
@@ -33,13 +34,13 @@ export default {
33
34
  };
34
35
 
35
36
  function deps(fw) {
36
- if (fw === 'fastify') return { fastify: '^5.0.0' };
37
- if (fw === 'express') return { express: '^5.0.0' };
38
- return { hono: '^4.5.0', '@hono/node-server': '^2.0.0' };
37
+ if (fw === 'fastify') return { fastify: V.fastify };
38
+ if (fw === 'express') return { express: V.express };
39
+ return { hono: V.hono, '@hono/node-server': V['@hono/node-server'] };
39
40
  }
40
41
 
41
42
  function typeDeps(fw) {
42
- if (fw === 'express') return { '@types/express': '^5.0.0' };
43
+ if (fw === 'express') return { '@types/express': V['@types/express'] };
43
44
  return {};
44
45
  }
45
46
 
@@ -2,6 +2,7 @@
2
2
  // brotli-compressed size of the built entry and fails when it exceeds the
3
3
  // limit — cheap insurance against a dependency silently bloating the package.
4
4
  import { toJson } from '../render.js';
5
+ import { V } from '../versions.js';
5
6
 
6
7
  export default {
7
8
  id: 'sizelimit',
@@ -14,8 +15,8 @@ export default {
14
15
  pkg: {
15
16
  scripts: { size: 'size-limit' },
16
17
  devDependencies: {
17
- 'size-limit': '^11.0.0',
18
- '@size-limit/preset-small-lib': '^11.0.0',
18
+ 'size-limit': V['size-limit'],
19
+ '@size-limit/preset-small-lib': V['@size-limit/preset-small-lib'],
19
20
  },
20
21
  },
21
22
  };
@@ -1,3 +1,4 @@
1
+ import { V } from '../versions.js';
1
2
  // Storybook for component libraries (React / Vue / Svelte), on the Vite builder.
2
3
 
3
4
  const FW = {
@@ -37,10 +38,10 @@ export default {
37
38
  'build-storybook': 'storybook build',
38
39
  },
39
40
  devDependencies: {
40
- storybook: '^10.0.0',
41
- [fw.builder]: '^10.0.0',
42
- [fw.renderer]: '^10.0.0',
43
- vite: '^8.0.0',
41
+ storybook: V.storybook,
42
+ [fw.builder]: V[fw.builder],
43
+ [fw.renderer]: V[fw.renderer],
44
+ vite: V.vite,
44
45
  },
45
46
  },
46
47
  };
@@ -1,4 +1,4 @@
1
- import { toJson } from '../render.js';
1
+ import { V } from '../versions.js';
2
2
 
3
3
  export default {
4
4
  id: 'test',
@@ -38,40 +38,40 @@ export default {
38
38
  ].filter((l) => l !== null).join('\n');
39
39
  pkg.scripts.test = 'vitest run';
40
40
  pkg.scripts['test:watch'] = 'vitest';
41
- pkg.devDependencies.vitest = '^4.0.0';
41
+ pkg.devDependencies.vitest = V.vitest;
42
42
  if (cfg.hasFramework) {
43
- pkg.devDependencies.jsdom = '^29.0.0';
44
- pkg.devDependencies['@testing-library/dom'] = '^10.0.0';
45
- if (cfg.isReact) pkg.devDependencies['@testing-library/react'] = '^16.0.0';
46
- if (cfg.isVue) pkg.devDependencies['@testing-library/vue'] = '^8.1.0';
47
- if (cfg.isSvelte) pkg.devDependencies['@testing-library/svelte'] = '^5.2.0';
43
+ pkg.devDependencies.jsdom = V.jsdom;
44
+ pkg.devDependencies['@testing-library/dom'] = V['@testing-library/dom'];
45
+ if (cfg.isReact) pkg.devDependencies['@testing-library/react'] = V['@testing-library/react'];
46
+ if (cfg.isVue) pkg.devDependencies['@testing-library/vue'] = V['@testing-library/vue'];
47
+ if (cfg.isSvelte) pkg.devDependencies['@testing-library/svelte'] = V['@testing-library/svelte'];
48
48
  }
49
49
  if (cfg.coverage) {
50
50
  pkg.scripts.coverage = 'vitest run --coverage';
51
- pkg.devDependencies['@vitest/coverage-v8'] = '^4.0.0';
51
+ pkg.devDependencies['@vitest/coverage-v8'] = V['@vitest/coverage-v8'];
52
52
  }
53
53
  files[`src/index.test.${testExt}`] = exampleTest('vitest', cfg);
54
54
  } else if (cfg.test === 'jest') {
55
55
  files['jest.config.js'] = jestConfig(cfg);
56
56
  pkg.scripts.test = 'jest';
57
57
  pkg.scripts['test:watch'] = 'jest --watch';
58
- pkg.devDependencies.jest = '^30.0.0';
58
+ pkg.devDependencies.jest = V.jest;
59
59
  if (cfg.isTs) {
60
- pkg.devDependencies['ts-jest'] = '^29.0.0';
61
- pkg.devDependencies['@types/jest'] = '^30.0.0';
60
+ pkg.devDependencies['ts-jest'] = V['ts-jest'];
61
+ pkg.devDependencies['@types/jest'] = V['@types/jest'];
62
62
  }
63
63
  if (cfg.coverage) pkg.scripts.coverage = 'jest --coverage';
64
64
  files[`src/index.test.${testExt}`] = exampleTest('jest', cfg);
65
65
  } else if (cfg.test === 'node') {
66
66
  pkg.scripts.test = cfg.isTs ? 'node --import tsx --test "src/**/*.test.ts"' : 'node --test';
67
- if (cfg.isTs) pkg.devDependencies.tsx = '^4.0.0';
67
+ if (cfg.isTs) pkg.devDependencies.tsx = V.tsx;
68
68
  files[`src/index.test.${testExt}`] = exampleTest('node', cfg);
69
69
  }
70
70
 
71
71
  // Express services test through supertest (no built-in inject).
72
72
  if (cfg.hasService && cfg.serviceFramework === 'express') {
73
- pkg.devDependencies.supertest = '^7.0.0';
74
- if (cfg.isTs) pkg.devDependencies['@types/supertest'] = '^6.0.0';
73
+ pkg.devDependencies.supertest = V.supertest;
74
+ if (cfg.isTs) pkg.devDependencies['@types/supertest'] = V['@types/supertest'];
75
75
  }
76
76
 
77
77
  return { files, pkg };
@@ -159,7 +159,6 @@ function exampleTest(runner, cfg) {
159
159
  ].join('\n');
160
160
  }
161
161
  const api = runner === 'jest' ? `` : `import { describe, it, expect } from 'vitest';\n`;
162
- const expectApi = runner === 'jest' ? '' : '';
163
162
  return [
164
163
  api + `import { greet } from '${imp}';`,
165
164
  ``,
@@ -1,4 +1,5 @@
1
1
  import { toJson } from '../render.js';
2
+ import { V } from '../versions.js';
2
3
 
3
4
  export default {
4
5
  id: 'typescript',
@@ -45,7 +46,7 @@ export default {
45
46
  },
46
47
  pkg: {
47
48
  devDependencies: {
48
- typescript: '^5.9.3',
49
+ typescript: V.typescript,
49
50
  '@types/node': `^${cfg.nodeVersion}.0.0`,
50
51
  },
51
52
  },
@@ -1,12 +1,13 @@
1
+ import { V } from '../versions.js';
1
2
  // Vite build path: powers Vite SPA apps (React/Vue/Svelte) and Vue component
2
3
  // libraries (SFCs need a real compiler). Owns the vite config, build scripts,
3
4
  // and — for libraries — the package entry points. index.html + source come
4
5
  // from frameworks.js. Svelte libraries ship source and don't come through here.
5
6
 
6
7
  const PLUGIN = {
7
- react: { import: `import react from '@vitejs/plugin-react';`, call: 'react()', dep: { '@vitejs/plugin-react': '^6.0.0' } },
8
- vue: { import: `import vue from '@vitejs/plugin-vue';`, call: 'vue()', dep: { '@vitejs/plugin-vue': '^6.0.0' } },
9
- svelte: { import: `import { svelte } from '@sveltejs/vite-plugin-svelte';`, call: 'svelte()', dep: { '@sveltejs/vite-plugin-svelte': '^7.0.0' } },
8
+ react: { import: `import react from '@vitejs/plugin-react';`, call: 'react()', dep: { '@vitejs/plugin-react': V['@vitejs/plugin-react'] } },
9
+ vue: { import: `import vue from '@vitejs/plugin-vue';`, call: 'vue()', dep: { '@vitejs/plugin-vue': V['@vitejs/plugin-vue'] } },
10
+ svelte: { import: `import { svelte } from '@sveltejs/vite-plugin-svelte';`, call: 'svelte()', dep: { '@sveltejs/vite-plugin-svelte': V['@sveltejs/vite-plugin-svelte'] } },
10
11
  };
11
12
 
12
13
  export default {
@@ -14,10 +15,10 @@ export default {
14
15
  active: (cfg) => cfg.viteBuild,
15
16
  apply(cfg) {
16
17
  const files = {};
17
- const pkg = { scripts: {}, devDependencies: { vite: '^8.0.0' } };
18
+ const pkg = { scripts: {}, devDependencies: { vite: V.vite } };
18
19
  const p = PLUGIN[cfg.framework];
19
20
  Object.assign(pkg.devDependencies, p.dep);
20
- if (cfg.isVue && cfg.isTs) pkg.devDependencies['vue-tsc'] = '^3.0.0';
21
+ if (cfg.isVue && cfg.isTs) pkg.devDependencies['vue-tsc'] = V['vue-tsc'];
21
22
 
22
23
  if (cfg.hasApp) {
23
24
  // Front-end SPA — not a published package.
@@ -48,8 +49,8 @@ export default {
48
49
  ].join('\n');
49
50
  pkg.scripts.build = 'vite build';
50
51
  pkg.scripts.dev = 'vite build --watch';
51
- pkg.devDependencies['vite-plugin-dts'] = '^5.0.0';
52
- if (cfg.isVue) pkg.devDependencies['vue-tsc'] = '^3.0.0';
52
+ pkg.devDependencies['vite-plugin-dts'] = V['vite-plugin-dts'];
53
+ if (cfg.isVue) pkg.devDependencies['vue-tsc'] = V['vue-tsc'];
53
54
  // entry points
54
55
  pkg.files = ['dist'];
55
56
  pkg.type = 'module';
@@ -60,7 +61,7 @@ export default {
60
61
  }
61
62
 
62
63
  pkg.scripts.clean = 'rimraf dist';
63
- pkg.devDependencies.rimraf = '^6.0.0';
64
+ pkg.devDependencies.rimraf = V.rimraf;
64
65
  return { files, pkg };
65
66
  },
66
67
  };
package/src/core/index.js CHANGED
@@ -22,23 +22,49 @@ export function fromPreset(name, overrides = {}) {
22
22
  return cfg;
23
23
  }
24
24
 
25
- /** Turn a config into a complete set of files. */
26
- export function generate(input) {
27
- const cfg = normalizeConfig(input);
28
- if (cfg.monorepo) return buildMonorepo(cfg);
29
-
25
+ /**
26
+ * Run every active feature, collecting its files and package.json fragment.
27
+ * Returns the raw material — file map, the fragments in contribution order,
28
+ * and which feature produced each file — so callers that need provenance (the
29
+ * embedded API's collision detection) get it without a second pass, while
30
+ * `generate` folds it into the same output as before.
31
+ */
32
+ export function assemble(cfg) {
30
33
  const files = {};
34
+ const fileSources = {};
35
+ const fragments = [];
31
36
  let pkg = {};
32
37
 
33
38
  for (const feat of features) {
34
39
  if (!feat.active(cfg)) continue;
35
40
  const out = feat.apply(cfg) || {};
36
41
  if (out.files) {
37
- for (const [path, contents] of Object.entries(out.files)) files[path] = contents;
42
+ for (const [path, contents] of Object.entries(out.files)) {
43
+ (fileSources[path] ||= []).push(feat.id);
44
+ files[path] = contents;
45
+ }
46
+ }
47
+ if (out.pkg) {
48
+ fragments.push({ source: feat.id, pkg: out.pkg });
49
+ pkg = deepMerge(pkg, out.pkg);
38
50
  }
39
- if (out.pkg) pkg = deepMerge(pkg, out.pkg);
40
51
  }
52
+ return { files, fileSources, fragments, pkg };
53
+ }
41
54
 
55
+ /**
56
+ * Generate, keeping the provenance assemble() produced. One assembly pass feeds
57
+ * both the public files and the embedded API's conflict diagnostics, so the
58
+ * bytes callers get and the provenance they inspect come from the same run.
59
+ */
60
+ export function generateTracked(input, diagnostics = null) {
61
+ const cfg = normalizeConfig(input, diagnostics);
62
+ if (cfg.monorepo) {
63
+ // The monorepo generator is a separate path with no per-feature fragments.
64
+ return { ...buildMonorepo(cfg), fileSources: {}, fragments: [] };
65
+ }
66
+
67
+ const { files, fileSources, fragments, pkg } = assemble(cfg);
42
68
  files['package.json'] = toJson(finalizePackageJson(pkg));
43
69
  files['packkit.json'] = provenance(cfg);
44
70
 
@@ -47,9 +73,17 @@ export function generate(input) {
47
73
  files,
48
74
  postCommands: postCommands(cfg),
49
75
  summary: summarize(cfg, files),
76
+ fileSources,
77
+ fragments,
50
78
  };
51
79
  }
52
80
 
81
+ /** Turn a config into a complete set of files. */
82
+ export function generate(input) {
83
+ const { config, files, postCommands, summary } = generateTracked(input);
84
+ return { config, files, postCommands, summary };
85
+ }
86
+
53
87
  function postCommands(cfg) {
54
88
  const install = {
55
89
  npm: 'npm install',
@@ -7,6 +7,7 @@ import community from './features/community.js';
7
7
  import agents from './features/agents.js';
8
8
  import gitfiles from './features/gitfiles.js';
9
9
  import { provenance } from './provenance.js';
10
+ import { V } from './versions.js';
10
11
 
11
12
  export function buildMonorepo(cfg) {
12
13
  // Two genuinely different shapes: a set of publishable libraries, or a
@@ -22,10 +23,7 @@ export function buildMonorepo(cfg) {
22
23
  const utils = `@${scope}/utils`;
23
24
  const wsProto = pm === 'pnpm' ? 'workspace:*' : '*';
24
25
 
25
- // Reuse the package-agnostic root files (LICENSE, community, AGENTS.md, gitignore).
26
- for (const feat of [community, agents, gitfiles]) {
27
- if (feat.active(cfg)) Object.assign(files, feat.apply(cfg).files);
28
- }
26
+ Object.assign(files, workspaceScaffold(cfg, { workspaceGlobs: ['packages/*'] }));
29
27
 
30
28
  // ---- root ----
31
29
  const rootPkg = {
@@ -46,22 +44,20 @@ export function buildMonorepo(cfg) {
46
44
  release: 'turbo build && changeset publish',
47
45
  },
48
46
  devDependencies: {
49
- turbo: '^2.0.0',
50
- typescript: '^5.9.3',
51
- tsup: '^8.0.0',
52
- vitest: '^4.0.0',
53
- eslint: '^10.0.0',
54
- '@eslint/js': '^10.0.0',
55
- 'typescript-eslint': '^8.0.0',
56
- prettier: '^3.3.0',
57
- '@changesets/cli': '^2.27.0',
47
+ turbo: V.turbo,
48
+ typescript: V.typescript,
49
+ tsup: V.tsup,
50
+ vitest: V.vitest,
51
+ eslint: V.eslint,
52
+ '@eslint/js': V['@eslint/js'],
53
+ 'typescript-eslint': V['typescript-eslint'],
54
+ prettier: V.prettier,
55
+ '@changesets/cli': V['@changesets/cli'],
58
56
  '@types/node': `^${cfg.nodeVersion}.0.0`,
59
57
  },
60
58
  };
61
59
  files['package.json'] = toJson(rootPkg);
62
60
 
63
- if (pm === 'pnpm') files['pnpm-workspace.yaml'] = 'packages:\n - "packages/*"\n';
64
-
65
61
  files['turbo.json'] = toJson({
66
62
  $schema: 'https://turbo.build/schema.json',
67
63
  tasks: {
@@ -73,21 +69,6 @@ export function buildMonorepo(cfg) {
73
69
  },
74
70
  });
75
71
 
76
- files['tsconfig.base.json'] = toJson({
77
- $schema: 'https://json.schemastore.org/tsconfig',
78
- compilerOptions: {
79
- target: 'ES2022',
80
- module: 'ESNext',
81
- moduleResolution: 'Bundler',
82
- lib: ['ES2022'],
83
- strict: true,
84
- esModuleInterop: true,
85
- skipLibCheck: true,
86
- declaration: true,
87
- noEmit: true,
88
- },
89
- });
90
-
91
72
  files['.changeset/config.json'] = toJson({
92
73
  $schema: 'https://unpkg.com/@changesets/config@3.0.0/schema.json',
93
74
  changelog: '@changesets/cli/changelog',
@@ -97,22 +78,7 @@ export function buildMonorepo(cfg) {
97
78
  });
98
79
  files['.changeset/README.md'] = '# Changesets\n\nRun `npx changeset` to record a version bump for your next release.\n';
99
80
 
100
- files['eslint.config.js'] = [
101
- `import js from '@eslint/js';`,
102
- `import tseslint from 'typescript-eslint';`,
103
- ``,
104
- `export default tseslint.config(`,
105
- `\tjs.configs.recommended,`,
106
- `\t...tseslint.configs.recommended,`,
107
- `\t{ ignores: ['**/dist'] },`,
108
- `);`,
109
- ``,
110
- ].join('\n');
111
- files['.prettierrc.json'] = toJson({ useTabs: true, singleQuote: true, semi: true, printWidth: 100, trailingComma: 'all' });
112
-
113
81
  files['README.md'] = rootReadme(cfg, pm, core, utils);
114
- files['packkit.json'] = provenance(cfg);
115
- files['.github/workflows/ci.yml'] = ciWorkflow(cfg, pm);
116
82
 
117
83
  // ---- packages ----
118
84
  addPackage(files, {
@@ -145,7 +111,6 @@ export function buildMonorepo(cfg) {
145
111
  deps: { [core]: wsProto },
146
112
  });
147
113
 
148
- const install = pm === 'npm' ? 'npm install' : `${pm} install`;
149
114
  return {
150
115
  config: cfg,
151
116
  files,
@@ -173,11 +138,9 @@ function buildFullstack(cfg) {
173
138
  const scope = cfg.name.replace(/^@/, '').split('/')[0];
174
139
  const shared = `@${scope}/shared`;
175
140
  const wsProto = pm === 'pnpm' ? 'workspace:*' : '*';
176
- const run = (s) => (pm === 'npm' ? `npm run ${s}` : `${pm} ${s}`);
177
141
 
178
- for (const feat of [community, agents, gitfiles]) {
179
- if (feat.active(cfg)) Object.assign(files, feat.apply(cfg).files);
180
- }
142
+ // apps/* + packages/*, and the DOM lib the web app's JSX needs.
143
+ Object.assign(files, workspaceScaffold(cfg, { workspaceGlobs: ['apps/*', 'packages/*'], tsconfigLib: ['ES2022', 'DOM'] }));
181
144
 
182
145
  files['package.json'] = toJson({
183
146
  name: cfg.name,
@@ -198,21 +161,17 @@ function buildFullstack(cfg) {
198
161
  typecheck: 'turbo typecheck',
199
162
  },
200
163
  devDependencies: {
201
- turbo: '^2.0.0',
202
- typescript: '^5.9.3',
203
- vitest: '^4.0.0',
204
- eslint: '^10.0.0',
205
- '@eslint/js': '^10.0.0',
206
- 'typescript-eslint': '^8.0.0',
207
- prettier: '^3.3.0',
164
+ turbo: V.turbo,
165
+ typescript: V.typescript,
166
+ vitest: V.vitest,
167
+ eslint: V.eslint,
168
+ '@eslint/js': V['@eslint/js'],
169
+ 'typescript-eslint': V['typescript-eslint'],
170
+ prettier: V.prettier,
208
171
  '@types/node': `^${cfg.nodeVersion}.0.0`,
209
172
  },
210
173
  });
211
174
 
212
- if (pm === 'pnpm') {
213
- files['pnpm-workspace.yaml'] = 'packages:\n - "apps/*"\n - "packages/*"\n';
214
- }
215
-
216
175
  files['turbo.json'] = toJson({
217
176
  $schema: 'https://turbo.build/schema.json',
218
177
  tasks: {
@@ -226,36 +185,7 @@ function buildFullstack(cfg) {
226
185
  },
227
186
  });
228
187
 
229
- files['tsconfig.base.json'] = toJson({
230
- $schema: 'https://json.schemastore.org/tsconfig',
231
- compilerOptions: {
232
- target: 'ES2022',
233
- module: 'ESNext',
234
- moduleResolution: 'Bundler',
235
- lib: ['ES2022', 'DOM'],
236
- strict: true,
237
- esModuleInterop: true,
238
- skipLibCheck: true,
239
- declaration: true,
240
- noEmit: true,
241
- },
242
- });
243
-
244
- files['eslint.config.js'] = [
245
- `import js from '@eslint/js';`,
246
- `import tseslint from 'typescript-eslint';`,
247
- ``,
248
- `export default tseslint.config(`,
249
- `\tjs.configs.recommended,`,
250
- `\t...tseslint.configs.recommended,`,
251
- `\t{ ignores: ['**/dist'] },`,
252
- `);`,
253
- ``,
254
- ].join('\n');
255
- files['.prettierrc.json'] = toJson({ useTabs: true, singleQuote: true, semi: true, printWidth: 100, trailingComma: 'all' });
256
- files['.github/workflows/ci.yml'] = ciWorkflow(cfg, pm);
257
188
  files['README.md'] = fullstackReadme(cfg, pm, shared);
258
- files['packkit.json'] = provenance(cfg);
259
189
 
260
190
  // ---- packages/shared: the contract both sides compile against ----
261
191
  files['packages/shared/package.json'] = toJson({
@@ -272,7 +202,7 @@ function buildFullstack(cfg) {
272
202
  typecheck: 'tsc --noEmit',
273
203
  lint: 'eslint .',
274
204
  },
275
- devDependencies: { tsup: '^8.0.0' },
205
+ devDependencies: { tsup: V.tsup },
276
206
  });
277
207
  files['packages/shared/tsconfig.json'] = toJson({ extends: '../../tsconfig.base.json', include: ['src'] });
278
208
  files['packages/shared/src/index.ts'] = [
@@ -307,8 +237,8 @@ function buildFullstack(cfg) {
307
237
  typecheck: 'tsc --noEmit',
308
238
  lint: 'eslint .',
309
239
  },
310
- dependencies: { hono: '^4.5.0', '@hono/node-server': '^2.0.0', [shared]: wsProto },
311
- devDependencies: { tsx: '^4.0.0', tsup: '^8.0.0' },
240
+ dependencies: { hono: V.hono, '@hono/node-server': V['@hono/node-server'], [shared]: wsProto },
241
+ devDependencies: { tsx: V.tsx, tsup: V.tsup },
312
242
  });
313
243
  files['apps/server/tsconfig.json'] = toJson({ extends: '../../tsconfig.base.json', include: ['src'] });
314
244
  files['apps/server/src/app.ts'] = [
@@ -368,17 +298,17 @@ function buildFullstack(cfg) {
368
298
  typecheck: 'tsc --noEmit',
369
299
  lint: 'eslint .',
370
300
  },
371
- dependencies: { react: '^19.0.0', 'react-dom': '^19.0.0', [shared]: wsProto },
301
+ dependencies: { react: V.react, 'react-dom': V['react-dom'], [shared]: wsProto },
372
302
  devDependencies: {
373
- vite: '^8.0.0',
374
- '@vitejs/plugin-react': '^6.0.0',
303
+ vite: V.vite,
304
+ '@vitejs/plugin-react': V['@vitejs/plugin-react'],
375
305
  // Without the React types, `turbo typecheck` fails on the very first run
376
306
  // with TS7016/TS7026 on every JSX element.
377
- '@types/react': '^19.0.0',
378
- '@types/react-dom': '^19.0.0',
379
- '@testing-library/react': '^16.0.0',
380
- '@testing-library/dom': '^10.0.0',
381
- jsdom: '^29.0.0',
307
+ '@types/react': V['@types/react'],
308
+ '@types/react-dom': V['@types/react-dom'],
309
+ '@testing-library/react': V['@testing-library/react'],
310
+ '@testing-library/dom': V['@testing-library/dom'],
311
+ jsdom: V.jsdom,
382
312
  },
383
313
  });
384
314
  files['apps/web/tsconfig.json'] = toJson({
@@ -515,6 +445,50 @@ function fullstackReadme(cfg, pm, shared) {
515
445
  ].join('\n');
516
446
  }
517
447
 
448
+ // Root-level scaffolding shared by both monorepo layouts: workspace globs, the
449
+ // TypeScript base config, lint/format config, community files, CI, and
450
+ // provenance. Each layout adds its own root package.json, turbo tasks, and
451
+ // packages on top. Extracted so these aren't maintained twice.
452
+ function workspaceScaffold(cfg, { workspaceGlobs, tsconfigLib = ['ES2022'] }) {
453
+ const files = {};
454
+ // Reuse the package-agnostic root files (LICENSE, community, AGENTS.md, gitignore).
455
+ for (const feat of [community, agents, gitfiles]) {
456
+ if (feat.active(cfg)) Object.assign(files, feat.apply(cfg).files);
457
+ }
458
+ if (cfg.packageManager === 'pnpm') {
459
+ files['pnpm-workspace.yaml'] = 'packages:\n' + workspaceGlobs.map((g) => ` - "${g}"\n`).join('');
460
+ }
461
+ files['tsconfig.base.json'] = toJson({
462
+ $schema: 'https://json.schemastore.org/tsconfig',
463
+ compilerOptions: {
464
+ target: 'ES2022',
465
+ module: 'ESNext',
466
+ moduleResolution: 'Bundler',
467
+ lib: tsconfigLib,
468
+ strict: true,
469
+ esModuleInterop: true,
470
+ skipLibCheck: true,
471
+ declaration: true,
472
+ noEmit: true,
473
+ },
474
+ });
475
+ files['eslint.config.js'] = [
476
+ `import js from '@eslint/js';`,
477
+ `import tseslint from 'typescript-eslint';`,
478
+ ``,
479
+ `export default tseslint.config(`,
480
+ `\tjs.configs.recommended,`,
481
+ `\t...tseslint.configs.recommended,`,
482
+ `\t{ ignores: ['**/dist'] },`,
483
+ `);`,
484
+ ``,
485
+ ].join('\n');
486
+ files['.prettierrc.json'] = toJson({ useTabs: true, singleQuote: true, semi: true, printWidth: 100, trailingComma: 'all' });
487
+ files['.github/workflows/ci.yml'] = ciWorkflow(cfg, cfg.packageManager);
488
+ files['packkit.json'] = provenance(cfg);
489
+ return files;
490
+ }
491
+
518
492
  function addPackage(files, { name, dir, src, test, deps }) {
519
493
  const pkg = {
520
494
  name,