@weft-js/create 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +393 -19
- package/bin/create-weft.mjs +62 -14
- package/package.json +26 -16
- package/scripts/tarballs.mjs +56 -6
- package/src/data.mjs +90 -18
- package/src/index.mjs +210 -51
- package/src/stack/pages.mjs +151 -0
- package/src/stack/schema.mjs +195 -0
- package/src/stack.mjs +102 -313
- package/src/templates.mjs +174 -0
- package/src/wizard.mjs +65 -16
- package/templates/admin/files/drizzle/.schema-hash +1 -0
- package/templates/admin/files/drizzle/0000_admin.sql +136 -0
- package/templates/admin/files/drizzle/meta/0000_snapshot.json +771 -0
- package/templates/admin/files/drizzle/meta/_journal.json +13 -0
- package/templates/admin/files/public/favicon.svg +1 -0
- package/templates/admin/files/src/auth.server.ts +24 -0
- package/templates/admin/files/src/components/AuthPage.tsx +33 -0
- package/templates/admin/files/src/components/Failed.tsx +19 -0
- package/templates/admin/files/src/components/Logo.tsx +16 -0
- package/templates/admin/files/src/components/OrderDetail.tsx +93 -0
- package/templates/admin/files/src/components/OrdersFilters.tsx +44 -0
- package/templates/admin/files/src/components/OrdersTable.tsx +144 -0
- package/templates/admin/files/src/components/Overview.tsx +113 -0
- package/templates/admin/files/src/components/StatusBadge.tsx +12 -0
- package/templates/admin/files/src/db/schema.ts +68 -0
- package/templates/admin/files/src/lib/orders.ts +60 -0
- package/templates/admin/files/src/routes/404.tsx +20 -0
- package/templates/admin/files/src/routes/_error.tsx +20 -0
- package/templates/admin/files/src/routes/_layout.tsx +6 -0
- package/templates/admin/files/src/routes/admin/_layout.tsx +89 -0
- package/templates/admin/files/src/routes/admin/index.tsx +45 -0
- package/templates/admin/files/src/routes/admin/nested/_layout.tsx +35 -0
- package/templates/admin/files/src/routes/admin/nested/index.tsx +77 -0
- package/templates/admin/files/src/routes/admin/orders/[id].tsx +24 -0
- package/templates/admin/files/src/routes/admin/orders/index.tsx +28 -0
- package/templates/admin/files/src/routes/admin/per-row.tsx +81 -0
- package/templates/admin/files/src/routes/admin/slow.tsx +74 -0
- package/templates/admin/files/src/routes/index.tsx +13 -0
- package/templates/admin/files/src/routes/login.tsx +53 -0
- package/templates/admin/files/src/routes/signup.tsx +60 -0
- package/templates/admin/files/src/server/orders.ts +193 -0
- package/templates/admin/files/src/server/reports.ts +92 -0
- package/templates/admin/files/src/server/session.ts +42 -0
- package/templates/admin/files/src/theme.css +43 -0
- package/templates/admin/files/weft.config.ts +31 -0
- package/templates/admin/template.json +22 -0
- package/templates/catalog/files/drizzle/.schema-hash +1 -0
- package/templates/catalog/files/drizzle/0000_catalog.sql +47 -0
- package/templates/catalog/files/drizzle/meta/0000_snapshot.json +263 -0
- package/templates/catalog/files/drizzle/meta/_journal.json +13 -0
- package/templates/catalog/files/public/favicon.svg +1 -0
- package/templates/catalog/files/seed.sql +13 -0
- package/templates/catalog/files/src/components/AddToCart.tsx +22 -0
- package/templates/catalog/files/src/components/Aisles.tsx +31 -0
- package/templates/catalog/files/src/components/CartLines.tsx +96 -0
- package/templates/catalog/files/src/components/CartLink.tsx +29 -0
- package/templates/catalog/files/src/components/Failed.tsx +19 -0
- package/templates/catalog/files/src/components/Featured.tsx +19 -0
- package/templates/catalog/files/src/components/Filters.tsx +40 -0
- package/templates/catalog/files/src/components/Logo.tsx +16 -0
- package/templates/catalog/files/src/components/ProductArt.tsx +42 -0
- package/templates/catalog/files/src/components/ProductCard.tsx +42 -0
- package/templates/catalog/files/src/components/ProductList.tsx +65 -0
- package/templates/catalog/files/src/components/Related.tsx +19 -0
- package/templates/catalog/files/src/components/SiteFooter.tsx +9 -0
- package/templates/catalog/files/src/components/SiteHeader.tsx +16 -0
- package/templates/catalog/files/src/db/schema.ts +53 -0
- package/templates/catalog/files/src/lib/catalog.ts +35 -0
- package/templates/catalog/files/src/routes/404.tsx +18 -0
- package/templates/catalog/files/src/routes/_error.tsx +18 -0
- package/templates/catalog/files/src/routes/_layout.tsx +14 -0
- package/templates/catalog/files/src/routes/c/[slug].tsx +53 -0
- package/templates/catalog/files/src/routes/cart.tsx +29 -0
- package/templates/catalog/files/src/routes/index.tsx +84 -0
- package/templates/catalog/files/src/routes/p/[slug].tsx +67 -0
- package/templates/catalog/files/src/server/cart.ts +125 -0
- package/templates/catalog/files/src/server/catalog.ts +107 -0
- package/templates/catalog/files/src/theme.css +159 -0
- package/templates/catalog/files/weft.config.ts +30 -0
- package/templates/catalog/template.json +20 -0
- package/templates/landing/files/.weft/plugins.lock.json +42 -0
- package/templates/landing/files/assets/fonts/LICENSE-figtree.txt +93 -0
- package/templates/landing/files/assets/fonts/figtree.woff2 +0 -0
- package/templates/landing/files/assets/hero.webp +0 -0
- package/templates/landing/files/drizzle/.schema-hash +1 -0
- package/templates/landing/files/drizzle/0000_landing.sql +6 -0
- package/templates/landing/files/drizzle/meta/0000_snapshot.json +60 -0
- package/templates/landing/files/drizzle/meta/_journal.json +13 -0
- package/templates/landing/files/public/favicon.svg +1 -0
- package/templates/landing/files/src/components/SiteFooter.tsx +24 -0
- package/templates/landing/files/src/components/SiteHeader.tsx +23 -0
- package/templates/landing/files/src/components/WaitlistForm.tsx +36 -0
- package/templates/landing/files/src/db/schema.ts +7 -0
- package/templates/landing/files/src/routes/404.tsx +24 -0
- package/templates/landing/files/src/routes/_error.tsx +24 -0
- package/templates/landing/files/src/routes/_layout.tsx +14 -0
- package/templates/landing/files/src/routes/index.tsx +130 -0
- package/templates/landing/files/src/routes/joined.tsx +30 -0
- package/templates/landing/files/src/routes/pricing.tsx +131 -0
- package/templates/landing/files/src/routes/privacy.tsx +56 -0
- package/templates/landing/files/src/server/waitlist.ts +13 -0
- package/templates/landing/files/src/theme.css +108 -0
- package/templates/landing/files/weft.config.ts +44 -0
- package/templates/landing/template.json +17 -0
- package/templates/todo/files/drizzle/.schema-hash +1 -0
- package/templates/todo/files/drizzle/0000_todo.sql +82 -0
- package/templates/todo/files/drizzle/meta/0000_snapshot.json +462 -0
- package/templates/todo/files/drizzle/meta/_journal.json +13 -0
- package/templates/todo/files/public/favicon.svg +1 -0
- package/templates/todo/files/src/components/AuthPage.tsx +33 -0
- package/templates/todo/files/src/components/Logo.tsx +14 -0
- package/templates/todo/files/src/components/NewTodo.tsx +27 -0
- package/templates/todo/files/src/components/TodoList.tsx +101 -0
- package/templates/todo/files/src/db/schema.ts +18 -0
- package/templates/todo/files/src/lib/views.ts +13 -0
- package/templates/todo/files/src/routes/404.tsx +20 -0
- package/templates/todo/files/src/routes/_error.tsx +20 -0
- package/templates/todo/files/src/routes/_layout.tsx +6 -0
- package/templates/todo/files/src/routes/index.tsx +13 -0
- package/templates/todo/files/src/routes/login.tsx +53 -0
- package/templates/todo/files/src/routes/signup.tsx +57 -0
- package/templates/todo/files/src/routes/todos.tsx +87 -0
- package/templates/todo/files/src/server/todos.ts +98 -0
- package/templates/todo/files/src/theme.css +71 -0
- package/templates/todo/files/weft.config.ts +30 -0
- 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
|
|
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 {
|
|
5
|
-
|
|
6
|
-
|
|
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',
|
|
9
|
-
|
|
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(
|
|
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')
|
|
24
|
-
|
|
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(
|
|
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 = {
|
|
35
|
-
|
|
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 && {
|
|
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
|
-
({
|
|
74
|
-
|
|
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 =>
|
|
238
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
${
|
|
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 }`
|
|
294
|
-
|
|
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
|
+
}
|