@weft-js/create 0.1.0 → 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/stack.mjs CHANGED
@@ -1,29 +1,81 @@
1
- // The `--db` and `--auth` part of a scaffold (research 28 §6): a database, a todo app on it, and with `--auth` a
1
+ // The `--db` and `--auth` part of a scaffold: a database, a todo app on it, and with `--auth` a
2
2
  // sign-in/sign-up flow and a guarded `/todos`. Pinned versions, so a scaffold builds what the matrix tested.
3
3
  import { readFileSync } from 'node:fs'
4
- import { nativeAuthSql, nativeFiles, prismaAdapterPackage, prismaAuthModels, prismaFiles } from './data.mjs'
5
-
6
- const VERSIONS = {
4
+ import {
5
+ nativeAuthSql,
6
+ nativeFiles,
7
+ prismaAdapterPackage,
8
+ prismaAuthModels,
9
+ prismaFiles,
10
+ } from './data.mjs'
11
+ import { authTablesFile, sqliteSchema, pgSchema, mysqlSchema } from './stack/schema.mjs'
12
+ import { loginPage } from './stack/pages.mjs'
13
+ export { sharedAuthTables } from './stack/schema.mjs'
14
+ export {
15
+ tailwindPackages,
16
+ TAILWIND_VITE,
17
+ TAILWIND_CSS,
18
+ classes,
19
+ controls,
20
+ todosPage,
21
+ loginPage,
22
+ } from './stack/pages.mjs'
23
+
24
+ /** The version a scaffold pins for each package it adds that isn't weft's own: what the matrix tested. */
25
+ export const VERSIONS = {
7
26
  '@types/node': '26.6.5',
8
- '@t3-oss/env-core': '0.13.11', 'better-auth': '1.7.14', 'drizzle-kit': '0.31.11', 'drizzle-orm': '0.45.4', 'drizzle-zod': '0.8.3', kysely: '0.29.7', mysql2: '3.24.6', postgres: '4.0.0', zod: '4.6.7',
9
- prisma: '7.10.1', '@prisma/client': '7.10.1', '@prisma/adapter-libsql': '7.10.1', '@prisma/adapter-pg': '7.10.1', '@prisma/adapter-mariadb': '7.10.1',
27
+ '@t3-oss/env-core': '0.13.11',
28
+ 'better-auth': '1.7.14',
29
+ 'drizzle-kit': '0.31.11',
30
+ 'drizzle-orm': '0.45.4',
31
+ 'drizzle-zod': '0.8.3',
32
+ kysely: '0.29.7',
33
+ mysql2: '3.24.6',
34
+ postgres: '4.0.0',
35
+ zod: '4.6.7',
36
+ prisma: '7.10.1',
37
+ '@prisma/client': '7.10.1',
38
+ '@prisma/adapter-libsql': '7.10.1',
39
+ '@prisma/adapter-pg': '7.10.1',
40
+ '@prisma/adapter-mariadb': '7.10.1',
10
41
  }
11
42
 
12
- const pin = (...names) => Object.fromEntries(names.map(n => [n, VERSIONS[n]]))
43
+ const pin = (...names) => Object.fromEntries(names.map((n) => [n, VERSIONS[n]]))
13
44
 
14
- const drivers = db => (db === 'postgres' ? ['postgres'] : db === 'mysql' ? ['mysql2'] : [])
45
+ const drivers = (db) => (db === 'postgres' ? ['postgres'] : db === 'mysql' ? ['mysql2'] : [])
15
46
 
16
47
  /** Every scaffold pins what it shipped with: this package and weft's others go out at one version. */
17
- export const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version
48
+ export const VERSION = JSON.parse(
49
+ readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
50
+ ).version
18
51
 
19
52
  /** The pinned dependencies of a database, data layer and auth choice. */
20
53
  export function stackDeps({ db, auth, layer = 'drizzle', version = VERSION }) {
21
54
  if (db === 'none') return { deps: {}, dev: {} }
22
55
  const base = pin('@t3-oss/env-core', 'zod', ...(auth ? ['better-auth'] : []))
23
- if (layer === 'native') return { deps: { ...base, ...pin(...drivers(db), ...(auth ? ['kysely'] : [])), '@weft-js/db': version }, dev: pin('@types/node') }
24
- if (layer === 'prisma') return { deps: { ...base, ...pin('@prisma/client', prismaAdapterPackage(db)), '@weft-js/db': version }, dev: pin('prisma', '@types/node') }
56
+ if (layer === 'native')
57
+ return {
58
+ deps: {
59
+ ...base,
60
+ ...pin(...drivers(db), ...(auth ? ['kysely'] : [])),
61
+ '@weft-js/db': version,
62
+ },
63
+ dev: pin('@types/node'),
64
+ }
65
+ if (layer === 'prisma')
66
+ return {
67
+ deps: { ...base, ...pin('@prisma/client', prismaAdapterPackage(db)), '@weft-js/db': version },
68
+ dev: pin('prisma', '@types/node'),
69
+ }
25
70
  return {
26
- deps: pin('@t3-oss/env-core', 'drizzle-orm', 'drizzle-zod', 'zod', ...drivers(db), ...(auth ? ['better-auth'] : [])),
71
+ deps: pin(
72
+ '@t3-oss/env-core',
73
+ 'drizzle-orm',
74
+ 'drizzle-zod',
75
+ 'zod',
76
+ ...drivers(db),
77
+ ...(auth ? ['better-auth'] : []),
78
+ ),
27
79
  dev: pin('drizzle-kit', '@types/node'),
28
80
  }
29
81
  }
@@ -31,16 +83,22 @@ export function stackDeps({ db, auth, layer = 'drizzle', version = VERSION }) {
31
83
  /** Sign-in on an app with a database, for `weft add auth`: new files only, and what to add to the existing ones. */
32
84
  export function authFiles({ db, layer, ui }) {
33
85
  const files = stackFiles({ db, layer, auth: true, name: 'app', secret: '' })
34
- const out = { 'src/auth.server.ts': files['src/auth.server.ts'], 'src/server/session.ts': files['src/server/session.ts'], 'src/routes/login.tsx': loginPage(ui) }
35
- return { files: out, context: files['src/context.server.ts'], tables: { drizzle: authTablesFile(db), prisma: prismaAuthModels(db), native: nativeAuthSql(db) }[layer] }
86
+ const out = {
87
+ 'src/auth.server.ts': files['src/auth.server.ts'],
88
+ 'src/server/session.ts': files['src/server/session.ts'],
89
+ 'src/routes/login.tsx': loginPage(ui),
90
+ }
91
+ return {
92
+ files: out,
93
+ context: files['src/context.server.ts'],
94
+ tables: {
95
+ drizzle: authTablesFile(db),
96
+ prisma: prismaAuthModels(db),
97
+ native: nativeAuthSql(db),
98
+ }[layer],
99
+ }
36
100
  }
37
101
 
38
- const authTablesFile = db => ({
39
- sqlite: () => `import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'\n${authTablesSqlite()}`,
40
- postgres: () => `import { boolean, pgTable, text, timestamp } from 'drizzle-orm/pg-core'\n${authTablesPg()}`,
41
- mysql: () => `import { boolean, mysqlTable, text, timestamp, varchar } from 'drizzle-orm/mysql-core'\n${authTablesMysql()}`,
42
- }[db]())
43
-
44
102
  /** The files of a database, data layer and auth choice, as `{ path: text }`. */
45
103
  export function stackFiles({ db, auth, name, secret, layer = 'drizzle' }) {
46
104
  const shared = {
@@ -49,7 +107,10 @@ export function stackFiles({ db, auth, name, secret, layer = 'drizzle' }) {
49
107
  'src/env.server.ts': envModule({ db, auth }),
50
108
  'src/context.server.ts': context(auth),
51
109
  'src/routes/todos/index.tsx': '', // filled by the caller, which knows the UI kit
52
- ...(auth && { 'src/server/session.ts': sessionServer(), 'src/routes/todos/_layout.tsx': guard() }),
110
+ ...(auth && {
111
+ 'src/server/session.ts': sessionServer(),
112
+ 'src/routes/todos/_layout.tsx': guard(),
113
+ }),
53
114
  }
54
115
  if (layer === 'native') return { ...shared, ...nativeFiles({ db, auth }) }
55
116
  if (layer === 'prisma') return { ...shared, ...prismaFiles({ db, auth }) }
@@ -68,10 +129,13 @@ function drizzleFiles({ db, auth }) {
68
129
  }
69
130
  }
70
131
 
71
- const database = name => name.replace(/[^a-z0-9_]/g, '_')
132
+ const database = (name) => name.replace(/[^a-z0-9_]/g, '_')
72
133
  const envFile = ({ db, auth, name, secret }) =>
73
- ({ postgres: `DATABASE_URL=postgres://localhost:5432/${database(name)}\n`, mysql: `DATABASE_URL=mysql://root@localhost:3306/${database(name)}\n`, sqlite: 'DATABASE_URL=file:.data/app.db\n' }[db]) +
74
- (auth ? `BETTER_AUTH_SECRET=${secret}\nBETTER_AUTH_URL=http://localhost:5173\n` : '')
134
+ ({
135
+ postgres: `DATABASE_URL=postgres://localhost:5432/${database(name)}\n`,
136
+ mysql: `DATABASE_URL=mysql://root@localhost:3306/${database(name)}\n`,
137
+ sqlite: 'DATABASE_URL=file:.data/app.db\n',
138
+ })[db] + (auth ? `BETTER_AUTH_SECRET=${secret}\nBETTER_AUTH_URL=http://localhost:5173\n` : '')
75
139
 
76
140
  const envModule = ({ db, auth }) => `import { createEnv } from '@t3-oss/env-core'
77
141
  import { z } from 'zod'
@@ -87,155 +151,10 @@ export const env = createEnv({
87
151
  })
88
152
  `
89
153
 
90
- function sqliteSchema(auth) {
91
- const owner = auth ? `\n userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),` : ''
92
- return `import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
93
- ${auth ? authTablesSqlite() : ''}
94
- export const todos = sqliteTable('todos', {
95
- id: text('id').primaryKey(),${owner}
96
- title: text('title').notNull(),
97
- done: integer('done', { mode: 'boolean' }).notNull().default(false),
98
- createdAt: integer('created_at').notNull(),
99
- })
100
- `
101
- }
102
-
103
- function pgSchema(auth) {
104
- const owner = auth ? `\n userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),` : ''
105
- return `import { bigint, boolean, pgTable, text${auth ? ', timestamp' : ''} } from 'drizzle-orm/pg-core'
106
- ${auth ? authTablesPg() : ''}
107
- export const todos = pgTable('todos', {
108
- id: text('id').primaryKey(),${owner}
109
- title: text('title').notNull(),
110
- done: boolean('done').notNull().default(false),
111
- createdAt: bigint('created_at', { mode: 'number' }).notNull(),
112
- })
113
- `
114
- }
115
-
116
- // MySQL can't index a TEXT column, so ids, keys and anything unique are VARCHAR. Otherwise Postgres's tables hold.
117
- const authTablesMysql = () => authTablesPg()
118
- .replaceAll('pgTable', 'mysqlTable')
119
- .replace(/text\('(id|user_id|email|token|account_id|provider_id|identifier)'\)/g, "varchar('$1', { length: 255 })")
120
- .replace(/timestamp\('(\w+)'\)/g, "timestamp('$1', { fsp: 3 })")
121
-
122
- function mysqlSchema(auth) {
123
- const owner = auth ? `\n userId: varchar('user_id', { length: 255 }).notNull().references(() => user.id, { onDelete: 'cascade' }),` : ''
124
- return `import { bigint, boolean, mysqlTable, ${auth ? 'text, timestamp, ' : ''}varchar } from 'drizzle-orm/mysql-core'
125
- ${auth ? authTablesMysql() : ''}
126
- export const todos = mysqlTable('todos', {
127
- id: varchar('id', { length: 36 }).primaryKey(),${owner}
128
- title: varchar('title', { length: 200 }).notNull(),
129
- done: boolean('done').notNull().default(false),
130
- createdAt: bigint('created_at', { mode: 'number' }).notNull(),
131
- })
132
- `
133
- }
134
-
135
- const authTablesSqlite = () => {
136
- const when = (col, opts = '.notNull()') => `integer('${col}', { mode: 'timestamp_ms' })${opts}`
137
- return `
138
- // Better Auth's tables: it reads and writes them through Drizzle, with these names.
139
- export const user = sqliteTable('user', {
140
- id: text('id').primaryKey(),
141
- name: text('name').notNull(),
142
- email: text('email').notNull().unique(),
143
- emailVerified: integer('email_verified', { mode: 'boolean' }).notNull(),
144
- image: text('image'),
145
- createdAt: ${when('created_at')},
146
- updatedAt: ${when('updated_at')},
147
- })
148
-
149
- export const session = sqliteTable('session', {
150
- id: text('id').primaryKey(),
151
- expiresAt: ${when('expires_at')},
152
- token: text('token').notNull().unique(),
153
- createdAt: ${when('created_at')},
154
- updatedAt: ${when('updated_at')},
155
- ipAddress: text('ip_address'),
156
- userAgent: text('user_agent'),
157
- userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
158
- })
159
-
160
- export const account = sqliteTable('account', {
161
- id: text('id').primaryKey(),
162
- accountId: text('account_id').notNull(),
163
- providerId: text('provider_id').notNull(),
164
- userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
165
- accessToken: text('access_token'),
166
- refreshToken: text('refresh_token'),
167
- idToken: text('id_token'),
168
- accessTokenExpiresAt: ${when('access_token_expires_at', '')},
169
- refreshTokenExpiresAt: ${when('refresh_token_expires_at', '')},
170
- scope: text('scope'),
171
- password: text('password'),
172
- createdAt: ${when('created_at')},
173
- updatedAt: ${when('updated_at')},
174
- })
175
-
176
- export const verification = sqliteTable('verification', {
177
- id: text('id').primaryKey(),
178
- identifier: text('identifier').notNull(),
179
- value: text('value').notNull(),
180
- expiresAt: ${when('expires_at')},
181
- createdAt: ${when('created_at', '')},
182
- updatedAt: ${when('updated_at', '')},
183
- })
184
- `
185
- }
186
-
187
- const authTablesPg = () => `
188
- // Better Auth's tables: it reads and writes them through Drizzle, with these names.
189
- export const user = pgTable('user', {
190
- id: text('id').primaryKey(),
191
- name: text('name').notNull(),
192
- email: text('email').notNull().unique(),
193
- emailVerified: boolean('email_verified').notNull(),
194
- image: text('image'),
195
- createdAt: timestamp('created_at').notNull(),
196
- updatedAt: timestamp('updated_at').notNull(),
197
- })
198
-
199
- export const session = pgTable('session', {
200
- id: text('id').primaryKey(),
201
- expiresAt: timestamp('expires_at').notNull(),
202
- token: text('token').notNull().unique(),
203
- createdAt: timestamp('created_at').notNull(),
204
- updatedAt: timestamp('updated_at').notNull(),
205
- ipAddress: text('ip_address'),
206
- userAgent: text('user_agent'),
207
- userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
208
- })
209
-
210
- export const account = pgTable('account', {
211
- id: text('id').primaryKey(),
212
- accountId: text('account_id').notNull(),
213
- providerId: text('provider_id').notNull(),
214
- userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
215
- accessToken: text('access_token'),
216
- refreshToken: text('refresh_token'),
217
- idToken: text('id_token'),
218
- accessTokenExpiresAt: timestamp('access_token_expires_at'),
219
- refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
220
- scope: text('scope'),
221
- password: text('password'),
222
- createdAt: timestamp('created_at').notNull(),
223
- updatedAt: timestamp('updated_at').notNull(),
224
- })
225
-
226
- export const verification = pgTable('verification', {
227
- id: text('id').primaryKey(),
228
- identifier: text('identifier').notNull(),
229
- value: text('value').notNull(),
230
- expiresAt: timestamp('expires_at').notNull(),
231
- createdAt: timestamp('created_at'),
232
- updatedAt: timestamp('updated_at'),
233
- })
234
- `
235
-
236
154
  /** `src/context.server.ts`: the database on `ctx`, and the session with auth. */
237
- export const context = auth => auth
238
- ? `import { session } from '@weft-js/start/auth'
155
+ export const context = (auth) =>
156
+ auth
157
+ ? `import { session } from '@weft-js/start/auth'
239
158
  import auth from '#/auth.server'
240
159
  import { db } from '#/db/index.server'
241
160
 
@@ -251,7 +170,7 @@ declare module '@weft-js/start' {
251
170
  }
252
171
  }
253
172
  `
254
- : `import { db } from '#/db/index.server'
173
+ : `import { db } from '#/db/index.server'
255
174
 
256
175
  export default () => ({ db })
257
176
 
@@ -264,7 +183,9 @@ declare module '@weft-js/start' {
264
183
 
265
184
  function todosServer(auth, db) {
266
185
  const use = auth ? `\n auth: 'user',` : ''
267
- const mine = auth ? `and(eq(todos.id, input.id), eq(todos.userId, ctx.user.id))` : `eq(todos.id, input.id)`
186
+ const mine = auth
187
+ ? `and(eq(todos.id, input.id), eq(todos.userId, ctx.user.id))`
188
+ : `eq(todos.id, input.id)`
268
189
  // MySQL has no \`returning\`.
269
190
  const returning = db === 'mysql' ? '' : '.returning()'
270
191
  return `import { mutation, query } from '@weft-js/start'
@@ -288,10 +209,14 @@ export const addTodo = mutation({
288
209
  optimistic: ({ input }, cache) =>
289
210
  cache.update(listTodos, undefined, list => void list.push({ id: \`tmp:\${input.title}\`,${auth ? " userId: ''," : ''} title: input.title, done: false, createdAt: 0 })),
290
211
  run: async ({ input, ctx }) => {
291
- ${db === 'mysql' ? `const row = { id: crypto.randomUUID(),${auth ? ' userId: ctx.user.id,' : ''} title: input.title, createdAt: Date.now() }
212
+ ${
213
+ db === 'mysql'
214
+ ? `const row = { id: crypto.randomUUID(),${auth ? ' userId: ctx.user.id,' : ''} title: input.title, createdAt: Date.now() }
292
215
  await ctx.db.insert(todos).values(row)
293
- return { ...row, done: false }` : `const [row] = await ctx.db.insert(todos).values({ id: crypto.randomUUID(),${auth ? ' userId: ctx.user.id,' : ''} title: input.title, createdAt: Date.now() }).returning()
294
- return row`}
216
+ return { ...row, done: false }`
217
+ : `const [row] = await ctx.db.insert(todos).values({ id: crypto.randomUUID(),${auth ? ' userId: ctx.user.id,' : ''} title: input.title, createdAt: Date.now() }).returning()
218
+ return row`
219
+ }
295
220
  },
296
221
  })
297
222
 
@@ -363,139 +288,3 @@ export default function TodosLayout(props: RouteProps) {
363
288
  return <>{props.children}</>
364
289
  }
365
290
  `
366
-
367
- // Tailwind: the same files `weft add tailwind` writes (packages/cli/src/tailwind.mjs, held equal by a test).
368
- /** What `--ui tailwind` adds, pinned. */
369
- export const tailwindPackages = { tailwindcss: '4.3.3', '@tailwindcss/vite': '4.3.3' }
370
- /** `vite.config.ts` for `--ui tailwind`. */
371
- export const TAILWIND_VITE = `import tailwindcss from '@tailwindcss/vite'
372
-
373
- export default { plugins: [tailwindcss()] }
374
- `
375
- /** The stylesheet for `--ui tailwind`. */
376
- export const TAILWIND_CSS = `@import "tailwindcss";
377
- `
378
- const tw = {
379
- card: 'mx-auto max-w-2xl space-y-4 rounded-lg border border-current/15 p-6',
380
- title: 'text-2xl font-semibold',
381
- button: 'rounded-md border border-current/30 px-3 py-1.5 hover:bg-current/5',
382
- input: 'rounded-md border border-current/30 px-3 py-1.5',
383
- }
384
- /** The class of a scaffold element: Tailwind's utilities, or the plain CSS in src/app.css. */
385
- export const classes = (ui, part) => (ui === 'tailwind' ? tw[part] : part === 'card' ? 'card' : '')
386
- const attr = (ui, part) => (ui === 'tailwind' ? ` class="${tw[part]}"` : '')
387
-
388
- /** The page's controls in each UI kit: warp's components, or plain elements with the same roles and names. */
389
- export const controls = ui => ui === 'warp'
390
- ? {
391
- imports: `import { Alert, Button, Card, Checkbox, Input } from '@weft-js/warp'\n`,
392
- card: (title, body) => `<Card title=${JSON.stringify(title)}>${body}</Card>`,
393
- input: (label, box, type = 'text') => `<Input label=${JSON.stringify(label)} type="${type}" value={${box}.value} onInput={e => (${box}.value = e.currentTarget.value)} />`,
394
- submit: (label, pending) => `<Button type="submit" loading={${pending}}>${label}</Button>`,
395
- button: (label, onClick, extra = '') => `<Button emphasis="ghost" ${extra}onClick={${onClick}}>${label}</Button>`,
396
- error: box => `{${box}.value && <Alert tone="critical" live>{${box}.value}</Alert>}`,
397
- }
398
- : {
399
- imports: '',
400
- card: (title, body) => `<section class="${classes(ui, 'card')}"><h1${attr(ui, 'title')}>${title}</h1>${body}</section>`,
401
- input: (label, box, type = 'text') => `<label>${label} <input${attr(ui, 'input')} type="${type}" value={${box}.value} onInput={e => (${box}.value = e.currentTarget.value)} /></label>`,
402
- submit: (label, pending) => `<button${attr(ui, 'button')} type="submit" disabled={${pending}}>${label}</button>`,
403
- button: (label, onClick, extra = '') => `<button${attr(ui, 'button')} type="button" ${extra}onClick={${onClick}}>${label}</button>`,
404
- error: box => `{${box}.value && <p role="alert">{${box}.value}</p>}`,
405
- }
406
-
407
- /** `/todos`: the list on the server functions, written for this `--ui`. */
408
- export function todosPage(ui) {
409
- const c = controls(ui)
410
- return `import type { Meta } from '@weft-js/start'
411
- import { Boundary, state } from '@weft-js/core'
412
- ${c.imports}import { addTodo, deleteTodo, listTodos, toggleTodo } from '#/server/todos'
413
-
414
- export const meta: Meta = { title: 'Todos' }
415
-
416
- // The query is made in here, inside the Boundary, so the Boundary sees it: it shows \`pending\` until the list arrives.
417
- function TodoList() {
418
- const todos = listTodos()
419
- return (
420
- <ul class="todos">
421
- {(todos.value ?? []).map(t => (
422
- <li key={t.id} class={t.done ? 'done' : 'open'}>
423
- <label>
424
- <input type="checkbox" checked={t.done} onChange={() => toggleTodo({ id: t.id })} /> {t.title}
425
- </label>
426
- <button type="button" aria-label={\`Delete \${t.title}\`} onClick={() => deleteTodo({ id: t.id })}>×</button>
427
- </li>
428
- ))}
429
- </ul>
430
- )
431
- }
432
-
433
- export default function Todos() {
434
- const title = state('')
435
- const problem = state('')
436
- const add = async (e: SubmitEvent) => {
437
- e.preventDefault()
438
- const text = title.value.trim()
439
- if (!text) return
440
- title.value = ''
441
- problem.value = ''
442
- await addTodo({ title: text }).catch((err: Error) => (problem.value = err.message))
443
- }
444
- return (
445
- ${c.card('Todos', `
446
- <form onSubmit={add}>
447
- ${c.input('New todo', 'title')}
448
- ${c.submit('Add', 'addTodo.pending.value')}
449
- </form>
450
- ${c.error('problem')}
451
- <Boundary pending={<p>Loading…</p>}>
452
- <TodoList />
453
- </Boundary>
454
- `)}
455
- )
456
- }
457
- `
458
- }
459
-
460
- /** `/login`: sign in and sign up, written for this `--ui`. */
461
- export function loginPage(ui) {
462
- const c = controls(ui)
463
- return `import { redirect, type LoadArgs, type Meta } from '@weft-js/start'
464
- import { state } from '@weft-js/core'
465
- ${c.imports}import { signIn, signUp } from '#/server/session'
466
-
467
- export const load = ({ ctx }: LoadArgs) => {
468
- if (ctx.user) redirect('/todos')
469
- }
470
-
471
- export const meta: Meta = { title: 'Sign in' }
472
-
473
- export default function Login() {
474
- const creating = state(false)
475
- const name = state('')
476
- const email = state('')
477
- const password = state('')
478
- const problem = state('')
479
- const submit = async (e: SubmitEvent) => {
480
- e.preventDefault()
481
- problem.value = ''
482
- const done = creating.value
483
- ? signUp({ name: name.value, email: email.value, password: password.value })
484
- : signIn({ email: email.value, password: password.value })
485
- await done.catch((err: Error) => (problem.value = err.message))
486
- }
487
- return (
488
- ${c.card('Sign in', `
489
- <form onSubmit={submit}>
490
- {creating.value && ${c.input('Name', 'name')}}
491
- ${c.input('Email', 'email', 'email')}
492
- ${c.input('Password', 'password', 'password')}
493
- ${c.submit(`{creating.value ? 'Create account' : 'Sign in'}`, 'signIn.pending.value || signUp.pending.value')}
494
- </form>
495
- ${c.error('problem')}
496
- ${c.button(`{creating.value ? 'I have an account' : 'Create an account'}`, '() => (creating.value = !creating.value)')}
497
- `)}
498
- )
499
- }
500
- `
501
- }
@@ -0,0 +1,174 @@
1
+ // `--template`: a real app in `templates/<name>/`, laid over the scaffold. `template.json` holds what it
2
+ // `requires` (db, auth), its `pages`, its `warp()` options and any `dependencies` of its own; `files/` is copied as it is.
3
+ // A template's Drizzle schema is one dialect's, so it names that dialect, and its `drizzle/` ships with it
4
+ // (scripts/template-migrations.mjs): the app runs on a fresh database, and on one that already has these tables.
5
+
6
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
7
+ import { join, relative, sep } from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ /** What `--template` takes: the apps a scaffold can start as. */
11
+ export const TEMPLATES = ['landing', 'todo', 'admin', 'catalog']
12
+
13
+ const templates = fileURLToPath(new URL('../templates/', import.meta.url))
14
+ const utf8 = new TextDecoder('utf-8', { fatal: true })
15
+
16
+ /** Text as a string, and an image or a font as the bytes it is. */
17
+ function contents(file) {
18
+ const bytes = readFileSync(file)
19
+ try {
20
+ return utf8.decode(bytes)
21
+ } catch {
22
+ return bytes
23
+ }
24
+ }
25
+
26
+ function tree(dir, base = dir) {
27
+ return Object.fromEntries(
28
+ readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
29
+ const path = join(dir, entry.name)
30
+ return entry.isDirectory()
31
+ ? Object.entries(tree(path, base))
32
+ : [[relative(base, path).split(sep).join('/'), contents(path)]]
33
+ }),
34
+ )
35
+ }
36
+
37
+ const needed = ({ db }) => [
38
+ 'src/theme.css',
39
+ 'src/routes/_layout.tsx',
40
+ 'src/routes/index.tsx',
41
+ ...(db === 'none'
42
+ ? []
43
+ : ['src/db/schema.ts', 'drizzle/meta/_journal.json', 'drizzle/.schema-hash']),
44
+ ]
45
+
46
+ export function loadTemplate(name) {
47
+ if (!TEMPLATES.includes(name))
48
+ throw new Error(`--template must be one of ${TEMPLATES.join(', ')} (got "${name}")`)
49
+ const manifest = join(templates, name, 'template.json')
50
+ if (!existsSync(manifest)) throw new Error(`the ${name} template isn't written yet`)
51
+ const {
52
+ requires = {},
53
+ pages = [],
54
+ warp,
55
+ dependencies = {},
56
+ } = JSON.parse(readFileSync(manifest, 'utf8'))
57
+ const { db = 'none', auth = false } = requires
58
+ if (!['postgres', 'none'].includes(db))
59
+ throw new Error(
60
+ `the ${name} template's requires.db must be postgres or none (got ${JSON.stringify(db)})`,
61
+ )
62
+ const files = tree(join(templates, name, 'files'))
63
+ const missing = needed({ db }).filter((file) => !(file in files))
64
+ if (missing.length) throw new Error(`the ${name} template has no ${missing.join(', ')}`)
65
+ return { name, requires: { db, auth }, pages, warp, dependencies, files }
66
+ }
67
+
68
+ /** The choices with what the template implies filled in. A choice it can't work with is refused, not ignored. */
69
+ export function withTemplate(options) {
70
+ if (!options.template) return options
71
+ const {
72
+ requires: { db, auth },
73
+ files,
74
+ } = loadTemplate(options.template)
75
+ const refuse = (flag, wants) => {
76
+ throw new Error(`the ${options.template} template needs ${flag} ${wants}`)
77
+ }
78
+ if (![undefined, 'warp'].includes(options.ui)) refuse('--ui', 'warp')
79
+ if (![undefined, 'drizzle'].includes(options.layer)) refuse('--layer', 'drizzle')
80
+ if (![undefined, db].includes(options.db)) refuse('--db', db)
81
+ if (options.auth !== undefined && Boolean(options.auth) !== auth)
82
+ refuse('sign-in', auth ? 'on' : 'off')
83
+ // Its fonts and media are plugin options in code, which a json or yaml config has no place for.
84
+ const config = 'weft.config.ts' in files ? 'ts' : options.config
85
+ if (![undefined, config].includes(options.config)) refuse('--config', config)
86
+ return {
87
+ ...options,
88
+ ui: 'warp',
89
+ layer: 'drizzle',
90
+ db,
91
+ auth: options.auth ?? auth,
92
+ ...(config && { config }),
93
+ }
94
+ }
95
+
96
+ /**
97
+ * drizzle-kit's Postgres migration, made safe to run over tables that are already there: `create` only what's missing, and
98
+ * a foreign key only where the column has none, so an existing schema's differently named constraint isn't doubled.
99
+ */
100
+ export function idempotent(sql) {
101
+ return sql
102
+ .replace(/^CREATE TABLE "/gm, 'CREATE TABLE IF NOT EXISTS "')
103
+ .replace(/^CREATE (UNIQUE )?INDEX "/gm, 'CREATE $1INDEX IF NOT EXISTS "')
104
+ .replace(
105
+ /^ALTER TABLE ("\w+") ADD CONSTRAINT "\w+" FOREIGN KEY \("(\w+)"\).*?;/gm,
106
+ (statement, table, column) =>
107
+ `DO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint c JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = c.conkey[1] WHERE c.conrelid = '${table}'::regclass AND c.contype = 'f' AND a.attname = '${column}') THEN\n ${statement}\n END IF;\nEND $$;`,
108
+ )
109
+ }
110
+
111
+ const DATA = {
112
+ drizzle: 'Tables are in `src/db/schema.ts`; after changing it run `npx weft db generate`.',
113
+ prisma: 'Tables are in `prisma/schema.prisma`; after changing it run `npx weft db generate`.',
114
+ native: 'Tables are SQL in `migrations/`; `npx weft db generate <name>` starts a new one.',
115
+ }
116
+ const LOOK = {
117
+ warp: "The look is `warp({ seed, dials, radius, fonts, tokens })` in the weft config; `weft check` fails with D010 if a pair can't pass.",
118
+ headless: "The look is `src/app.css`, over warp's `--warp-*` tokens.",
119
+ tailwind: 'The look is Tailwind utilities; `src/app.css` is the entry.',
120
+ none: 'The look is `src/app.css`.',
121
+ }
122
+
123
+ /** The scaffold's AGENTS.md: enough to start. `weft agents-md` writes the full reference over it. */
124
+ export function agentsFile({ ui, db, layer, shipped, auth }) {
125
+ return [
126
+ '# AGENTS.md',
127
+ '',
128
+ "This is a **weft** app (fine-grained reactive TSX). It is not React: components run once, state is a box, and React hooks don't exist.",
129
+ '',
130
+ '## Run',
131
+ '',
132
+ '- `npx weft dev`: the dev server.',
133
+ '- `npx weft build`, then `npx weft start`: production.',
134
+ '- `npx weft check --json` after every change: each diagnostic has a code, a location, why, and a fix.',
135
+ '- `npx weft docs <topic>`: rules, props, lists, async, testing, config, diagnostics.',
136
+ ...(db !== 'none' && !shipped
137
+ ? [
138
+ '- `npx weft db generate` before the first run: the first migration. Migrations apply on the next request.',
139
+ ]
140
+ : []),
141
+ ...(shipped
142
+ ? [
143
+ '- Set `DATABASE_URL` in `.env` to an empty database. `drizzle/` applies on the first request; there is nothing to run.',
144
+ ]
145
+ : []),
146
+ '',
147
+ '## Where things go',
148
+ '',
149
+ "- `src/routes/`: pages, by file path. `_layout.tsx` wraps what's beside it.",
150
+ ...(shipped?.length
151
+ ? [`- This template's pages: ${shipped.map((p) => `\`${p}\``).join(', ')}.`]
152
+ : []),
153
+ ...(db === 'none'
154
+ ? []
155
+ : [
156
+ '- `src/server/`: `query` and `mutation` functions; call them from any component.',
157
+ `- ${DATA[layer]}`,
158
+ '- `.env`: secrets and `DATABASE_URL`, checked by `src/env.server.ts`.',
159
+ ]),
160
+ ...(auth === true
161
+ ? [
162
+ '- `src/auth.server.ts`: Better Auth. Login is an opaque session in the database, in an HTTP-only cookie, never a JWT. Pages may answer from its signed cookie cache for 5 minutes (`session.cookieCache`); mutations always read the database. JWTs are for API access only (`api` in `createAuth`, `npx weft docs start`).',
163
+ ]
164
+ : []),
165
+ '- `npx weft map` writes `.weft/map.md`: what this app has and what to call for each part.',
166
+ '',
167
+ '## Theming',
168
+ '',
169
+ shipped
170
+ ? "`src/theme.css` is this app's CSS: override `--warp-*` tokens and style `[data-scope]` parts there. The brand, dials and radius are `warp({ seed, dials, radius })` in the weft config."
171
+ : LOOK[ui],
172
+ '',
173
+ ].join('\n')
174
+ }