@weft-js/create 0.1.1 → 0.1.2

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 (127) hide show
  1. package/README.md +60 -10
  2. package/bin/create-weft.mjs +62 -14
  3. package/package.json +26 -16
  4. package/scripts/tarballs.mjs +56 -6
  5. package/src/data.mjs +90 -18
  6. package/src/index.mjs +210 -51
  7. package/src/stack/pages.mjs +151 -0
  8. package/src/stack/schema.mjs +195 -0
  9. package/src/stack.mjs +102 -313
  10. package/src/templates.mjs +174 -0
  11. package/src/wizard.mjs +65 -16
  12. package/templates/admin/files/drizzle/.schema-hash +1 -0
  13. package/templates/admin/files/drizzle/0000_admin.sql +136 -0
  14. package/templates/admin/files/drizzle/meta/0000_snapshot.json +771 -0
  15. package/templates/admin/files/drizzle/meta/_journal.json +13 -0
  16. package/templates/admin/files/public/favicon.svg +1 -0
  17. package/templates/admin/files/src/auth.server.ts +24 -0
  18. package/templates/admin/files/src/components/AuthPage.tsx +33 -0
  19. package/templates/admin/files/src/components/Failed.tsx +19 -0
  20. package/templates/admin/files/src/components/Logo.tsx +16 -0
  21. package/templates/admin/files/src/components/OrderDetail.tsx +93 -0
  22. package/templates/admin/files/src/components/OrdersFilters.tsx +44 -0
  23. package/templates/admin/files/src/components/OrdersTable.tsx +144 -0
  24. package/templates/admin/files/src/components/Overview.tsx +113 -0
  25. package/templates/admin/files/src/components/StatusBadge.tsx +12 -0
  26. package/templates/admin/files/src/db/schema.ts +68 -0
  27. package/templates/admin/files/src/lib/orders.ts +60 -0
  28. package/templates/admin/files/src/routes/404.tsx +20 -0
  29. package/templates/admin/files/src/routes/_error.tsx +20 -0
  30. package/templates/admin/files/src/routes/_layout.tsx +6 -0
  31. package/templates/admin/files/src/routes/admin/_layout.tsx +89 -0
  32. package/templates/admin/files/src/routes/admin/index.tsx +45 -0
  33. package/templates/admin/files/src/routes/admin/nested/_layout.tsx +35 -0
  34. package/templates/admin/files/src/routes/admin/nested/index.tsx +77 -0
  35. package/templates/admin/files/src/routes/admin/orders/[id].tsx +24 -0
  36. package/templates/admin/files/src/routes/admin/orders/index.tsx +28 -0
  37. package/templates/admin/files/src/routes/admin/per-row.tsx +81 -0
  38. package/templates/admin/files/src/routes/admin/slow.tsx +74 -0
  39. package/templates/admin/files/src/routes/index.tsx +13 -0
  40. package/templates/admin/files/src/routes/login.tsx +53 -0
  41. package/templates/admin/files/src/routes/signup.tsx +60 -0
  42. package/templates/admin/files/src/server/orders.ts +193 -0
  43. package/templates/admin/files/src/server/reports.ts +92 -0
  44. package/templates/admin/files/src/server/session.ts +42 -0
  45. package/templates/admin/files/src/theme.css +43 -0
  46. package/templates/admin/files/weft.config.ts +31 -0
  47. package/templates/admin/template.json +22 -0
  48. package/templates/catalog/files/drizzle/.schema-hash +1 -0
  49. package/templates/catalog/files/drizzle/0000_catalog.sql +47 -0
  50. package/templates/catalog/files/drizzle/meta/0000_snapshot.json +263 -0
  51. package/templates/catalog/files/drizzle/meta/_journal.json +13 -0
  52. package/templates/catalog/files/public/favicon.svg +1 -0
  53. package/templates/catalog/files/seed.sql +13 -0
  54. package/templates/catalog/files/src/components/AddToCart.tsx +22 -0
  55. package/templates/catalog/files/src/components/Aisles.tsx +31 -0
  56. package/templates/catalog/files/src/components/CartLines.tsx +96 -0
  57. package/templates/catalog/files/src/components/CartLink.tsx +29 -0
  58. package/templates/catalog/files/src/components/Failed.tsx +19 -0
  59. package/templates/catalog/files/src/components/Featured.tsx +19 -0
  60. package/templates/catalog/files/src/components/Filters.tsx +40 -0
  61. package/templates/catalog/files/src/components/Logo.tsx +16 -0
  62. package/templates/catalog/files/src/components/ProductArt.tsx +42 -0
  63. package/templates/catalog/files/src/components/ProductCard.tsx +42 -0
  64. package/templates/catalog/files/src/components/ProductList.tsx +65 -0
  65. package/templates/catalog/files/src/components/Related.tsx +19 -0
  66. package/templates/catalog/files/src/components/SiteFooter.tsx +9 -0
  67. package/templates/catalog/files/src/components/SiteHeader.tsx +16 -0
  68. package/templates/catalog/files/src/db/schema.ts +53 -0
  69. package/templates/catalog/files/src/lib/catalog.ts +35 -0
  70. package/templates/catalog/files/src/routes/404.tsx +18 -0
  71. package/templates/catalog/files/src/routes/_error.tsx +18 -0
  72. package/templates/catalog/files/src/routes/_layout.tsx +14 -0
  73. package/templates/catalog/files/src/routes/c/[slug].tsx +53 -0
  74. package/templates/catalog/files/src/routes/cart.tsx +29 -0
  75. package/templates/catalog/files/src/routes/index.tsx +84 -0
  76. package/templates/catalog/files/src/routes/p/[slug].tsx +67 -0
  77. package/templates/catalog/files/src/server/cart.ts +125 -0
  78. package/templates/catalog/files/src/server/catalog.ts +107 -0
  79. package/templates/catalog/files/src/theme.css +159 -0
  80. package/templates/catalog/files/weft.config.ts +30 -0
  81. package/templates/catalog/template.json +20 -0
  82. package/templates/landing/files/.weft/plugins.lock.json +42 -0
  83. package/templates/landing/files/assets/fonts/LICENSE-figtree.txt +93 -0
  84. package/templates/landing/files/assets/fonts/figtree.woff2 +0 -0
  85. package/templates/landing/files/assets/hero.webp +0 -0
  86. package/templates/landing/files/drizzle/.schema-hash +1 -0
  87. package/templates/landing/files/drizzle/0000_landing.sql +6 -0
  88. package/templates/landing/files/drizzle/meta/0000_snapshot.json +60 -0
  89. package/templates/landing/files/drizzle/meta/_journal.json +13 -0
  90. package/templates/landing/files/public/favicon.svg +1 -0
  91. package/templates/landing/files/src/components/SiteFooter.tsx +24 -0
  92. package/templates/landing/files/src/components/SiteHeader.tsx +23 -0
  93. package/templates/landing/files/src/components/WaitlistForm.tsx +36 -0
  94. package/templates/landing/files/src/db/schema.ts +7 -0
  95. package/templates/landing/files/src/routes/404.tsx +24 -0
  96. package/templates/landing/files/src/routes/_error.tsx +24 -0
  97. package/templates/landing/files/src/routes/_layout.tsx +14 -0
  98. package/templates/landing/files/src/routes/index.tsx +130 -0
  99. package/templates/landing/files/src/routes/joined.tsx +30 -0
  100. package/templates/landing/files/src/routes/pricing.tsx +131 -0
  101. package/templates/landing/files/src/routes/privacy.tsx +56 -0
  102. package/templates/landing/files/src/server/waitlist.ts +13 -0
  103. package/templates/landing/files/src/theme.css +108 -0
  104. package/templates/landing/files/weft.config.ts +44 -0
  105. package/templates/landing/template.json +17 -0
  106. package/templates/todo/files/drizzle/.schema-hash +1 -0
  107. package/templates/todo/files/drizzle/0000_todo.sql +82 -0
  108. package/templates/todo/files/drizzle/meta/0000_snapshot.json +462 -0
  109. package/templates/todo/files/drizzle/meta/_journal.json +13 -0
  110. package/templates/todo/files/public/favicon.svg +1 -0
  111. package/templates/todo/files/src/components/AuthPage.tsx +33 -0
  112. package/templates/todo/files/src/components/Logo.tsx +14 -0
  113. package/templates/todo/files/src/components/NewTodo.tsx +27 -0
  114. package/templates/todo/files/src/components/TodoList.tsx +101 -0
  115. package/templates/todo/files/src/db/schema.ts +18 -0
  116. package/templates/todo/files/src/lib/views.ts +13 -0
  117. package/templates/todo/files/src/routes/404.tsx +20 -0
  118. package/templates/todo/files/src/routes/_error.tsx +20 -0
  119. package/templates/todo/files/src/routes/_layout.tsx +6 -0
  120. package/templates/todo/files/src/routes/index.tsx +13 -0
  121. package/templates/todo/files/src/routes/login.tsx +53 -0
  122. package/templates/todo/files/src/routes/signup.tsx +57 -0
  123. package/templates/todo/files/src/routes/todos.tsx +87 -0
  124. package/templates/todo/files/src/server/todos.ts +98 -0
  125. package/templates/todo/files/src/theme.css +71 -0
  126. package/templates/todo/files/weft.config.ts +30 -0
  127. package/templates/todo/template.json +18 -0
package/src/index.mjs CHANGED
@@ -1,16 +1,26 @@
1
- // create-weft: write a new app's files (research 26 §6, research 28 §6). `--ui`: `warp` adds @weft-js/warp and `warp()` in
2
- // weft.config.ts, `headless` adds @weft-js/warp-core's spec CSS under the app's own markup, `tailwind` adds Tailwind and its Vite plugin,
3
- // `none` adds nothing. `--db`, `--layer`
4
- // and `--auth` add a database, a todo app and a sign-in flow (stack.mjs); `--runtime` is what `weft build` targets.
5
- // AGENTS.md needs the installed weft, so bin/create-weft.mjs runs `weft agents-md` right after the install. Everything
6
- // else (.weft/, the slot modules) is generated by `weft` itself on the first `weft dev` or `weft check`, so a scaffold
7
- // never goes stale against the CLI.
1
+ // `--template` lays a real app over all that (templates.mjs). A short AGENTS.md is written here; the full one needs the
2
+ // installed weft, so bin/create-weft.mjs runs `weft agents-md` right after the install. Everything else (.weft/, the slot
3
+ // modules) is generated by `weft` itself on the first `weft dev` or `weft check`, so a scaffold never goes stale against
4
+ // the CLI.
8
5
 
9
6
  import { randomBytes } from 'node:crypto'
10
7
  import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs'
11
8
  import { basename, dirname, join } from 'node:path'
12
9
  import { customAuth } from './data.mjs'
13
- import { classes, context, loginPage, stackDeps, stackFiles, TAILWIND_CSS, TAILWIND_VITE, tailwindPackages, todosPage, VERSION } from './stack.mjs'
10
+ import {
11
+ classes,
12
+ context,
13
+ loginPage,
14
+ sharedAuthTables,
15
+ stackDeps,
16
+ stackFiles,
17
+ TAILWIND_CSS,
18
+ TAILWIND_VITE,
19
+ tailwindPackages,
20
+ todosPage,
21
+ VERSION,
22
+ } from './stack.mjs'
23
+ import { agentsFile, loadTemplate, withTemplate } from './templates.mjs'
14
24
 
15
25
  /** What `--ui` takes. */
16
26
  export const UI = ['warp', 'headless', 'tailwind', 'none']
@@ -22,52 +32,81 @@ export const LAYERS = ['drizzle', 'prisma', 'native']
22
32
  export const RUNTIMES = ['node', 'bun', 'deno']
23
33
  /** What `--config` takes. */
24
34
  export const CONFIGS = ['ts', 'json', 'yaml']
35
+ export { TEMPLATES, withTemplate } from './templates.mjs'
25
36
 
37
+ const json = (v) => JSON.stringify(v, null, 2) + '\n'
38
+ const title = (name) => name.replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
26
39
 
27
- const json = v => JSON.stringify(v, null, 2) + '\n'
28
- const title = name => name.replace(/[-_]+/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
29
-
30
- /** `weft.config.ts`, `.json` or `.yaml`: one schema, so the same app in each (research 34 §2). */
31
- const LAYER_PLUGIN = { prisma: { pkg: '@weft-js/db/plugin-prisma', name: 'prisma' }, native: { pkg: '@weft-js/db/plugin-native', name: 'native' } }
40
+ /** `weft.config.ts`, `.json` or `.yaml`: one schema, so the same app in each. */
41
+ const LAYER_PLUGIN = {
42
+ prisma: { pkg: '@weft-js/db/plugin-prisma', name: 'prisma' },
43
+ native: { pkg: '@weft-js/db/plugin-native', name: 'native' },
44
+ }
32
45
 
33
- const appPlugins = ({ ui, layer }) => [...(ui === 'warp' ? [{ pkg: '@weft-js/warp', name: 'warp' }] : []), ...(LAYER_PLUGIN[layer] ? [LAYER_PLUGIN[layer]] : [])]
46
+ const appPlugins = ({ ui, layer }) => [
47
+ ...(ui === 'warp' ? [{ pkg: '@weft-js/warp', name: 'warp' }] : []),
48
+ ...(LAYER_PLUGIN[layer] ? [LAYER_PLUGIN[layer]] : []),
49
+ ]
34
50
 
35
51
  /** What each plugin a scaffold picks declares. `weft check` fails a scaffold where these fall behind the plugin. */
36
52
  const GRANTS = {
37
- '@weft-js/warp': ['fs.read:node_modules/@weft-js/warp-core/**', 'fs.write:$TMPDIR/warp-seed-*', 'spawn:node'],
53
+ '@weft-js/warp': [
54
+ 'fs.read:node_modules/@weft-js/warp-core/**',
55
+ 'fs.write:$TMPDIR/warp-seed-*',
56
+ 'spawn:node',
57
+ ],
38
58
  '@weft-js/db/plugin-prisma': ['fs.read:prisma/**', 'fs.write:prisma/migrations/**', 'spawn:node'],
39
59
  '@weft-js/db/plugin-native': ['fs.read:migrations/**', 'fs.write:migrations/**'],
40
60
  }
41
61
 
42
62
  /** Choosing a plugin here is approving it. There's no install yet, so `weft plugins approve --all` pins the code later. */
43
63
  function pluginLock(options) {
44
- const plugins = Object.fromEntries(appPlugins(options).map(p => [p.pkg, { version: null, integrity: null, permissions: GRANTS[p.pkg], provides: [], undeclared: [] }]))
45
- return Object.keys(plugins).length ? { '.weft/plugins.lock.json': json({ version: 1, plugins }) } : {}
64
+ const plugins = Object.fromEntries(
65
+ appPlugins(options).map((p) => [
66
+ p.pkg,
67
+ { version: null, integrity: null, permissions: GRANTS[p.pkg], provides: [], undeclared: [] },
68
+ ]),
69
+ )
70
+ return Object.keys(plugins).length
71
+ ? { '.weft/plugins.lock.json': json({ version: 1, plugins }) }
72
+ : {}
46
73
  }
47
74
 
48
- function configFile(format, { ui, label, runtime, layer }) {
75
+ function configFile(format, { ui, label, runtime, layer, warp }) {
49
76
  const plugins = appPlugins({ ui, layer })
77
+ const entry = (p) => (p.pkg === '@weft-js/warp' && warp ? [p.pkg, warp] : p.pkg)
50
78
  const site = { name: label, title: `%s · ${label}`, description: 'Made with weft.' }
51
79
  if (format === 'json') {
52
- const config = { $schema: './.weft/config.schema.json', site, ...(plugins.length && { plugins: plugins.map(p => p.pkg) }), ...(runtime !== 'node' && { runtime }) }
80
+ const config = {
81
+ $schema: './.weft/config.schema.json',
82
+ site,
83
+ ...(plugins.length && { plugins: plugins.map(entry) }),
84
+ ...(runtime !== 'node' && { runtime }),
85
+ }
53
86
  return { 'weft.config.json': json(config) }
54
87
  }
55
88
  if (format === 'yaml') {
56
89
  return {
57
90
  'weft.config.yaml':
58
91
  `# yaml-language-server: $schema=./.weft/config.schema.json\nsite:\n` +
59
- Object.entries(site).map(([k, v]) => ` ${k}: ${JSON.stringify(v)}\n`).join('') +
60
- (plugins.length ? `plugins:\n${plugins.map(p => ` - "${p.pkg}"\n`).join('')}` : '') +
92
+ Object.entries(site)
93
+ .map(([k, v]) => ` ${k}: ${JSON.stringify(v)}\n`)
94
+ .join('') +
95
+ (plugins.length
96
+ ? `plugins:\n${plugins.map((p) => ` - ${JSON.stringify(entry(p))}\n`).join('')}`
97
+ : '') +
61
98
  (runtime !== 'node' ? `runtime: ${runtime}\n` : ''),
62
99
  }
63
100
  }
64
101
  return {
65
102
  'weft.config.ts':
66
103
  `import { defineConfig } from '@weft-js/core/config'\n` +
67
- plugins.map(p => `import { ${p.name} } from '${p.pkg}/plugin'\n`).join('') +
104
+ plugins.map((p) => `import { ${p.name} } from '${p.pkg}/plugin'\n`).join('') +
68
105
  `\nexport default defineConfig({\n` +
69
106
  ` site: { name: ${JSON.stringify(label)}, title: ${JSON.stringify(site.title)}, description: 'Made with weft.' },\n` +
70
- (plugins.length ? ` plugins: [${plugins.map(p => `${p.name}()`).join(', ')}],\n` : '') +
107
+ (plugins.length
108
+ ? ` plugins: [${plugins.map((p) => `${p.name}(${p.pkg === '@weft-js/warp' && warp ? JSON.stringify(warp) : ''})`).join(', ')}],\n`
109
+ : '') +
71
110
  (runtime !== 'node' ? ` runtime: '${runtime}',\n` : '') +
72
111
  `})\n`,
73
112
  }
@@ -75,36 +114,136 @@ function configFile(format, { ui, label, runtime, layer }) {
75
114
 
76
115
  /**
77
116
  * The files for an app, as `{ path: text }`.
78
- * @param {{ ui?: string, name?: string, version?: string, db?: string, layer?: string, auth?: boolean | 'custom', runtime?: string, config?: string, secret?: string }} [options]
117
+ * @param {{ ui?: string, name?: string, version?: string, db?: string, layer?: string, auth?: boolean | 'custom', runtime?: string, config?: string, secret?: string, template?: string, pm?: string }} [options]
79
118
  */
80
- export function template({ ui = 'warp', name = 'weft-app', version = VERSION, db = 'none', layer = 'drizzle', auth = false, runtime = 'node', config = 'ts', secret = randomBytes(32).toString('base64') } = {}) {
119
+ export function template(options = {}) {
120
+ const {
121
+ ui = 'warp',
122
+ name = 'weft-app',
123
+ version = VERSION,
124
+ db = 'none',
125
+ layer = 'drizzle',
126
+ auth = false,
127
+ runtime = 'node',
128
+ config = 'ts',
129
+ secret = randomBytes(32).toString('base64'),
130
+ template: chosen,
131
+ } = withTemplate(options)
81
132
  if (!UI.includes(ui)) throw new Error(`--ui must be one of ${UI.join(', ')} (got "${ui}")`)
82
133
  if (!DB.includes(db)) throw new Error(`--db must be one of ${DB.join(', ')} (got "${db}")`)
83
- if (!RUNTIMES.includes(runtime)) throw new Error(`--runtime must be one of ${RUNTIMES.join(', ')} (got "${runtime}")`)
84
- if (!CONFIGS.includes(config)) throw new Error(`--config must be one of ${CONFIGS.join(', ')} (got "${config}")`)
85
- if (!LAYERS.includes(layer)) throw new Error(`--layer must be one of ${LAYERS.join(', ')} (got "${layer}")`)
86
- if (auth && db === 'none') throw new Error('--auth keeps users in a database: add --db sqlite, postgres or mysql')
134
+ if (!RUNTIMES.includes(runtime))
135
+ throw new Error(`--runtime must be one of ${RUNTIMES.join(', ')} (got "${runtime}")`)
136
+ if (!CONFIGS.includes(config))
137
+ throw new Error(`--config must be one of ${CONFIGS.join(', ')} (got "${config}")`)
138
+ if (!LAYERS.includes(layer))
139
+ throw new Error(`--layer must be one of ${LAYERS.join(', ')} (got "${layer}")`)
140
+ if (auth && db === 'none')
141
+ throw new Error('--auth keeps users in a database: add --db sqlite, postgres or mysql')
142
+ const overlay = chosen && loadTemplate(chosen)
87
143
  const label = title(name)
88
144
  const stack = stackDeps({ db, auth: auth === true, layer, version })
89
145
  const deps = { '@weft-js/core': version, '@weft-js/start': version }
90
146
  if (ui === 'warp') deps['@weft-js/warp'] = version
91
147
  if (ui === 'headless') deps['@weft-js/warp-core'] = version
92
148
  if (ui === 'tailwind') Object.assign(stack.dev, tailwindPackages)
149
+ for (const [dep, wanted] of Object.entries(overlay?.dependencies ?? {}))
150
+ deps[dep] = dep.startsWith('@weft-js/') ? version : wanted
93
151
 
94
- const files = {
152
+ const files = baseFiles({
153
+ name,
154
+ version,
155
+ db,
156
+ layer,
157
+ ui,
158
+ config,
159
+ label,
160
+ runtime,
161
+ overlay,
162
+ stack,
163
+ deps,
164
+ auth,
165
+ })
166
+ Object.assign(files, pluginLock({ ui, layer }))
167
+ if (options.pm === 'pnpm')
168
+ files['pnpm-workspace.yaml'] = pnpmSettings(db !== 'none' && layer === 'prisma')
169
+ if (ui !== 'warp') files['src/app.css'] = css(ui)
170
+ if (ui === 'tailwind') files['vite.config.ts'] = TAILWIND_VITE
171
+ if (db !== 'none') {
172
+ Object.assign(files, stackFiles({ db, auth: auth === true, name, secret, layer }))
173
+ if (!overlay) files['src/routes/todos/index.tsx'] = todosPage(ui)
174
+ if (auth === true) files['src/routes/login.tsx'] = loginPage(ui)
175
+ if (auth === 'custom')
176
+ Object.assign(files, {
177
+ 'src/auth.server.ts': customAuth,
178
+ 'src/context.server.ts': context(true),
179
+ })
180
+ }
181
+ if (overlay) {
182
+ for (const path of Object.keys(files))
183
+ if (/^src\/(routes\/(about\.tsx|todos\/)|server\/todos\.ts)/.test(path)) delete files[path]
184
+ if (auth) files['src/db/auth.ts'] = sharedAuthTables()
185
+ Object.assign(files, overlay.files)
186
+ }
187
+ return files
188
+ }
189
+
190
+ // pnpm 11 and later stop an install at a dependency's build script that nobody has allowed or refused. None is needed:
191
+ // esbuild ships its binary as an optional package, and the app's own postinstall runs `prisma generate`.
192
+ const pnpmSettings = (prisma) =>
193
+ `allowBuilds:\n esbuild: false\n${prisma ? " '@prisma/engines': false\n prisma: false\n" : ''}`
194
+
195
+ function baseFiles({
196
+ name,
197
+ version,
198
+ db,
199
+ layer,
200
+ ui,
201
+ config,
202
+ label,
203
+ runtime,
204
+ overlay,
205
+ stack,
206
+ deps,
207
+ auth,
208
+ }) {
209
+ return {
95
210
  'package.json': json({
96
211
  name,
97
212
  private: true,
98
213
  type: 'module',
99
- scripts: { dev: 'weft dev', build: 'weft build', check: 'weft check', typecheck: 'weft tsc', start: 'weft start', ...(db !== 'none' && { 'db:generate': 'weft db generate' }), ...(db !== 'none' && layer === 'prisma' && { postinstall: 'prisma generate' }) },
214
+ scripts: {
215
+ dev: 'weft dev',
216
+ build: 'weft build',
217
+ check: 'weft check',
218
+ typecheck: 'weft tsc',
219
+ start: 'weft start',
220
+ ...(db !== 'none' && { 'db:generate': 'weft db generate' }),
221
+ ...(db !== 'none' && layer === 'prisma' && { postinstall: 'prisma generate' }),
222
+ },
100
223
  dependencies: { ...deps, ...stack.deps },
101
224
  devDependencies: { '@weft-js/cli': version, typescript: '7.0.2', ...stack.dev },
102
225
  }),
103
- ...configFile(config, { ui, label, runtime, layer: db === 'none' ? 'drizzle' : layer }),
226
+ ...configFile(config, {
227
+ ui,
228
+ label,
229
+ runtime,
230
+ layer: db === 'none' ? 'drizzle' : layer,
231
+ warp: overlay?.warp,
232
+ }),
104
233
  'tsconfig.json': json({
105
234
  compilerOptions: {
106
- target: 'es2022', module: 'esnext', moduleResolution: 'bundler', strict: true, noEmit: true, skipLibCheck: true,
107
- jsx: 'preserve', jsxImportSource: '@weft-js/core', lib: ['es2023', 'dom', 'dom.iterable'], paths: { '#/*': ['./src/*'] }, types: db === 'none' ? ['@weft-js/core/client'] : ['@weft-js/core/client', 'node'], plugins: [{ name: '@weft-js/cli/ts-plugin' }],
235
+ target: 'es2022',
236
+ module: 'esnext',
237
+ moduleResolution: 'bundler',
238
+ strict: true,
239
+ noEmit: true,
240
+ skipLibCheck: true,
241
+ jsx: 'preserve',
242
+ jsxImportSource: '@weft-js/core',
243
+ lib: ['es2023', 'dom', 'dom.iterable'],
244
+ paths: { '#/*': ['./src/*'] },
245
+ types: db === 'none' ? ['@weft-js/core/client'] : ['@weft-js/core/client', 'node'],
246
+ plugins: [{ name: '@weft-js/cli/ts-plugin' }],
108
247
  },
109
248
  include: ['src', ...(config === 'ts' ? ['weft.config.ts'] : []), '.weft/*.d.ts'],
110
249
  }),
@@ -113,23 +252,20 @@ export function template({ ui = 'warp', name = 'weft-app', version = VERSION, db
113
252
  '.gitignore': `node_modules/\n.weft/*\n!.weft/plugins.lock.json\n.output/\ndist/\n${db === 'none' ? '' : '.env\n.data/\n'}${layer === 'prisma' ? 'src/generated/\n' : ''}`,
114
253
  'src/routes/_layout.tsx': layout(ui, { db, auth }),
115
254
  'src/routes/index.tsx': home(ui, label),
255
+ 'AGENTS.md': agentsFile({ ui, db, layer, shipped: overlay?.pages, auth }),
116
256
  'src/routes/about.tsx': `export const meta = { title: 'About', description: 'What this app is.' }\n\nexport default function About() {\n return <h1>About</h1>\n}\n`,
117
257
  }
118
- Object.assign(files, pluginLock({ ui, layer }))
119
- if (ui !== 'warp') files['src/app.css'] = css(ui)
120
- if (ui === 'tailwind') files['vite.config.ts'] = TAILWIND_VITE
121
- if (db !== 'none') {
122
- Object.assign(files, stackFiles({ db, auth: auth === true, name, secret, layer }), { 'src/routes/todos/index.tsx': todosPage(ui) })
123
- if (auth === true) files['src/routes/login.tsx'] = loginPage(ui)
124
- if (auth === 'custom') Object.assign(files, { 'src/auth.server.ts': customAuth, 'src/context.server.ts': context(true) })
125
- }
126
- return files
127
258
  }
128
259
 
129
260
  /** Write the app into `dir` (which must be missing or empty). */
130
261
  export function scaffold(dir, options = {}) {
131
262
  if (existsSync(dir) && readdirSync(dir).length) throw new Error(`${dir} is not empty`)
132
- const files = template({ name: basename(dir).toLowerCase().replace(/[^a-z0-9-_.]/g, '-'), ...options })
263
+ const files = template({
264
+ name: basename(dir)
265
+ .toLowerCase()
266
+ .replace(/[^a-z0-9-_.]/g, '-'),
267
+ ...options,
268
+ })
133
269
  for (const [rel, text] of Object.entries(files)) {
134
270
  mkdirSync(dirname(join(dir, rel)), { recursive: true })
135
271
  writeFileSync(join(dir, rel), text)
@@ -139,13 +275,23 @@ export function scaffold(dir, options = {}) {
139
275
 
140
276
  function layout(ui, options) {
141
277
  const text = shell(ui, options)
142
- return ui === 'tailwind' ? text.replace('<nav>', '<nav class="flex gap-4 p-4">').replace('<main>', '<main class="p-4">') : text
278
+ return ui === 'tailwind'
279
+ ? text.replace('<nav>', '<nav class="flex gap-4 p-4">').replace('<main>', '<main class="p-4">')
280
+ : text
143
281
  }
144
282
 
145
283
  function shell(ui, { db, auth: which }) {
146
284
  const auth = which === true
147
- const imports = ui === 'warp' ? '' : (ui === 'headless' ? `import '@weft-js/warp-core/styles/warp.css'\n` : '') + `import '../app.css'\n`
285
+ const imports =
286
+ ui === 'warp'
287
+ ? ''
288
+ : (ui === 'headless' ? `import '@weft-js/warp-core/styles/warp.css'\n` : '') +
289
+ `import '../app.css'\n`
148
290
  const links = db === 'none' ? '' : `\n <Link href="/todos">Todos</Link>`
291
+ const signOut =
292
+ ui === 'warp'
293
+ ? '<Button emphasis="ghost" size="sm" onClick={() => signOut()}>Sign out</Button>'
294
+ : '<button type="button" onClick={() => signOut()}>Sign out</button>'
149
295
  if (!auth) {
150
296
  return `${imports}import { Link, type RouteProps } from '@weft-js/start'
151
297
 
@@ -162,7 +308,7 @@ export default function Root(props: RouteProps) {
162
308
  }
163
309
  `
164
310
  }
165
- return `${imports}import { Link, type LoadArgs, type RouteProps } from '@weft-js/start'
311
+ return `${imports}${ui === 'warp' ? "import { Button } from '@weft-js/warp'\n" : ''}import { Link, type LoadArgs, type RouteProps } from '@weft-js/start'
166
312
  import { me, signOut } from '#/server/session'
167
313
 
168
314
  export const load = ({ ctx }: LoadArgs) => me.fetch()
@@ -173,7 +319,7 @@ export default function Root(props: RouteProps<typeof load>) {
173
319
  <nav>
174
320
  <Link href="/">Home</Link>
175
321
  <Link href="/about">About</Link>${links}
176
- {props.data ? <button type="button" onClick={() => signOut()}>Sign out</button> : <Link href="/login">Sign in</Link>}
322
+ {props.data ? ${signOut} : <Link href="/login">Sign in</Link>}
177
323
  </nav>
178
324
  <main>{props.children}</main>
179
325
  </>
@@ -227,9 +373,22 @@ const named = (ui, part) => (classes(ui, part) ? ` class="${classes(ui, part)}"`
227
373
  function css(ui) {
228
374
  if (ui === 'tailwind') return TAILWIND_CSS
229
375
  // headless rides warp's tokens (warp-core's warp.css, the fluid title size included); none brings its own.
230
- const v = ui === 'headless'
231
- ? { bg: 'var(--warp-color-page)', fg: 'var(--warp-color-text)', accent: 'var(--warp-color-accent-solid)', radius: 'var(--warp-radius-surface)', title: 'var(--warp-text-title-size)' }
232
- : { bg: 'Canvas', fg: 'CanvasText', accent: 'LinkText', radius: '8px', title: 'clamp(1.5rem, 1.2rem + 2cqi, 2.5rem)' }
376
+ const v =
377
+ ui === 'headless'
378
+ ? {
379
+ bg: 'var(--warp-color-page)',
380
+ fg: 'var(--warp-color-text)',
381
+ accent: 'var(--warp-color-accent-solid)',
382
+ radius: 'var(--warp-radius-surface)',
383
+ title: 'var(--warp-text-title-size)',
384
+ }
385
+ : {
386
+ bg: 'Canvas',
387
+ fg: 'CanvasText',
388
+ accent: 'LinkText',
389
+ radius: '8px',
390
+ title: 'clamp(1.5rem, 1.2rem + 2cqi, 2.5rem)',
391
+ }
233
392
  return `:root { color-scheme: light dark; }
234
393
  body { margin: 0; font-family: system-ui, sans-serif; background: ${v.bg}; color: ${v.fg}; }
235
394
  nav { display: flex; gap: 1rem; padding: 1rem; }
@@ -0,0 +1,151 @@
1
+ // Tailwind: the same files `weft add tailwind` writes (packages/cli/src/tailwind.mjs, held equal by a test).
2
+ /** What `--ui tailwind` adds, pinned. */
3
+ export const tailwindPackages = { tailwindcss: '4.3.3', '@tailwindcss/vite': '4.3.3' }
4
+ /** `vite.config.ts` for `--ui tailwind`. */
5
+ export const TAILWIND_VITE = `import tailwindcss from '@tailwindcss/vite'
6
+
7
+ export default { plugins: [tailwindcss()] }
8
+ `
9
+ /** The stylesheet for `--ui tailwind`. */
10
+ export const TAILWIND_CSS = `@import "tailwindcss";
11
+ `
12
+ const tw = {
13
+ card: 'mx-auto max-w-2xl space-y-4 rounded-lg border border-current/15 p-6',
14
+ title: 'text-2xl font-semibold',
15
+ button: 'rounded-md border border-current/30 px-3 py-1.5 hover:bg-current/5',
16
+ input: 'rounded-md border border-current/30 px-3 py-1.5',
17
+ }
18
+ /** The class of a scaffold element: Tailwind's utilities, or the plain CSS in src/app.css. */
19
+ export const classes = (ui, part) => (ui === 'tailwind' ? tw[part] : part === 'card' ? 'card' : '')
20
+ const attr = (ui, part) => (ui === 'tailwind' ? ` class="${tw[part]}"` : '')
21
+
22
+ /** The page's controls in each UI kit: warp's components, or plain elements with the same roles and names. */
23
+ export const controls = (ui) =>
24
+ ui === 'warp'
25
+ ? {
26
+ imports: `import { Alert, Button, Card, Checkbox, Input } from '@weft-js/warp'\n`,
27
+ card: (title, body) => `<Card title=${JSON.stringify(title)}>${body}</Card>`,
28
+ input: (label, box, type = 'text') =>
29
+ `<Input label=${JSON.stringify(label)} type="${type}" value={${box}.value} onInput={e => (${box}.value = e.currentTarget.value)} />`,
30
+ submit: (label, pending) => `<Button type="submit" loading={${pending}}>${label}</Button>`,
31
+ button: (label, onClick, extra = '') =>
32
+ `<Button emphasis="ghost" ${extra}onClick={${onClick}}>${label}</Button>`,
33
+ error: (box) => `{${box}.value && <Alert tone="critical" live>{${box}.value}</Alert>}`,
34
+ row: `<Checkbox checked={t.done} onChange={() => toggleTodo({ id: t.id })}>{t.title}</Checkbox>
35
+ <Button emphasis="ghost" tone="neutral" size="sm" icon="x" aria-label={\`Delete \${t.title}\`} onClick={() => deleteTodo({ id: t.id })} />`,
36
+ }
37
+ : {
38
+ imports: '',
39
+ card: (title, body) =>
40
+ `<section class="${classes(ui, 'card')}"><h1${attr(ui, 'title')}>${title}</h1>${body}</section>`,
41
+ input: (label, box, type = 'text') =>
42
+ `<label>${label} <input${attr(ui, 'input')} type="${type}" value={${box}.value} onInput={e => (${box}.value = e.currentTarget.value)} /></label>`,
43
+ submit: (label, pending) =>
44
+ `<button${attr(ui, 'button')} type="submit" disabled={${pending}}>${label}</button>`,
45
+ button: (label, onClick, extra = '') =>
46
+ `<button${attr(ui, 'button')} type="button" ${extra}onClick={${onClick}}>${label}</button>`,
47
+ error: (box) => `{${box}.value && <p role="alert">{${box}.value}</p>}`,
48
+ row: `<label>
49
+ <input type="checkbox" checked={t.done} onChange={() => toggleTodo({ id: t.id })} /> {t.title}
50
+ </label>
51
+ <button type="button" aria-label={\`Delete \${t.title}\`} onClick={() => deleteTodo({ id: t.id })}>×</button>`,
52
+ }
53
+
54
+ /** `/todos`: the list on the server functions, written for this `--ui`. */
55
+ export function todosPage(ui) {
56
+ const c = controls(ui)
57
+ return `import type { Meta } from '@weft-js/start'
58
+ import { Boundary, state } from '@weft-js/core'
59
+ ${c.imports}import { addTodo, deleteTodo, listTodos, toggleTodo } from '#/server/todos'
60
+
61
+ export const meta: Meta = { title: 'Todos', description: 'Your todo list: add, tick off and delete.' }
62
+
63
+ // The query is made in here, inside the Boundary, so the Boundary sees it: it shows \`pending\` until the list arrives.
64
+ function TodoList() {
65
+ const todos = listTodos()
66
+ return (
67
+ <ul class="todos">
68
+ {(todos.value ?? []).map(t => (
69
+ <li key={t.id} class={t.done ? 'done' : 'open'}>
70
+ ${c.row}
71
+ </li>
72
+ ))}
73
+ </ul>
74
+ )
75
+ }
76
+
77
+ export default function Todos() {
78
+ const title = state('')
79
+ const problem = state('')
80
+ const add = async (e: SubmitEvent) => {
81
+ e.preventDefault()
82
+ const text = title.value.trim()
83
+ if (!text) return
84
+ title.value = ''
85
+ problem.value = ''
86
+ await addTodo({ title: text }).catch((err: Error) => (problem.value = err.message))
87
+ }
88
+ return (
89
+ ${c.card(
90
+ 'Todos',
91
+ `
92
+ <form onSubmit={add}>
93
+ ${c.input('New todo', 'title')}
94
+ ${c.submit('Add', 'addTodo.pending.value')}
95
+ </form>
96
+ ${c.error('problem')}
97
+ <Boundary pending={<p>Loading…</p>}>
98
+ <TodoList />
99
+ </Boundary>
100
+ `,
101
+ )}
102
+ )
103
+ }
104
+ `
105
+ }
106
+
107
+ /** `/login`: sign in and sign up, written for this `--ui`. */
108
+ export function loginPage(ui) {
109
+ const c = controls(ui)
110
+ return `import { redirect, type LoadArgs, type Meta } from '@weft-js/start'
111
+ import { state } from '@weft-js/core'
112
+ ${c.imports}import { signIn, signUp } from '#/server/session'
113
+
114
+ export const load = ({ ctx }: LoadArgs) => {
115
+ if (ctx.user) redirect('/todos')
116
+ }
117
+
118
+ export const meta: Meta = { title: 'Sign in', description: 'Sign in, or make an account.' }
119
+
120
+ export default function Login() {
121
+ const creating = state(false)
122
+ const name = state('')
123
+ const email = state('')
124
+ const password = state('')
125
+ const problem = state('')
126
+ const submit = async (e: SubmitEvent) => {
127
+ e.preventDefault()
128
+ problem.value = ''
129
+ const done = creating.value
130
+ ? signUp({ name: name.value, email: email.value, password: password.value })
131
+ : signIn({ email: email.value, password: password.value })
132
+ await done.catch((err: Error) => (problem.value = err.message))
133
+ }
134
+ return (
135
+ ${c.card(
136
+ 'Sign in',
137
+ `
138
+ <form onSubmit={submit}>
139
+ {creating.value && ${c.input('Name', 'name')}}
140
+ ${c.input('Email', 'email', 'email')}
141
+ ${c.input('Password', 'password', 'password')}
142
+ ${c.submit(`{creating.value ? 'Create account' : 'Sign in'}`, 'signIn.pending.value || signUp.pending.value')}
143
+ </form>
144
+ ${c.error('problem')}
145
+ ${c.button(`{creating.value ? 'I have an account' : 'Create an account'}`, '() => (creating.value = !creating.value)')}
146
+ `,
147
+ )}
148
+ )
149
+ }
150
+ `
151
+ }