@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/scripts/tarballs.mjs
CHANGED
|
@@ -1,11 +1,44 @@
|
|
|
1
1
|
// Every package as a tarball, and an app's package.json pointed at them: what the packed gates and `local.mjs` share.
|
|
2
2
|
import { execFileSync } from 'node:child_process'
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs'
|
|
4
|
-
import { join } from 'node:path'
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { join, resolve } from 'node:path'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
6
|
|
|
7
7
|
export const pkgs = fileURLToPath(new URL('../../', import.meta.url))
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Write the install policy and tarball overrides for a fresh pnpm workspace. weft's own dependency patches stay
|
|
11
|
+
* behind: their files live in this repo, not in the app.
|
|
12
|
+
*/
|
|
13
|
+
export function writePackedPolicy(dir, tarballs = {}, packages = []) {
|
|
14
|
+
const policy = readFileSync(new URL('../../../pnpm-workspace.yaml', import.meta.url), 'utf8')
|
|
15
|
+
.replace(/^packages:\n(?:[ \t].*\n)*/m, '')
|
|
16
|
+
.replace(/^(?:#.*\n)*patchedDependencies:\n(?:[ \t].*\n)*/m, '')
|
|
17
|
+
const overrides = Object.entries(tarballs)
|
|
18
|
+
.map(([name, spec]) => ` ${JSON.stringify(name)}: ${JSON.stringify(spec)}\n`)
|
|
19
|
+
.join('')
|
|
20
|
+
const merged = /^overrides:\n/m.test(policy)
|
|
21
|
+
? policy.replace(/^overrides:\n/m, `overrides:\n${overrides}`)
|
|
22
|
+
: `${policy}${overrides ? `\noverrides:\n${overrides}` : ''}`
|
|
23
|
+
writeFileSync(
|
|
24
|
+
join(dir, 'pnpm-workspace.yaml'),
|
|
25
|
+
`${packages.length ? `packages:\n${packages.map((name) => ` - ${JSON.stringify(name)}\n`).join('')}` : ''}${merged}\n`,
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Pack one package to a known path, independent of lifecycle output. */
|
|
30
|
+
export function pack(dir, dest) {
|
|
31
|
+
const { name, version } = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
|
|
32
|
+
const file = join(resolve(dest), `${name.replace(/^@/, '').replaceAll('/', '-')}-${version}.tgz`)
|
|
33
|
+
execFileSync('pnpm', ['pack', '--out', file], {
|
|
34
|
+
cwd: dir,
|
|
35
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
36
|
+
shell: process.platform === 'win32',
|
|
37
|
+
})
|
|
38
|
+
if (!existsSync(file)) throw new Error(`pnpm pack did not create ${file}`)
|
|
39
|
+
return `file:${file}`
|
|
40
|
+
}
|
|
41
|
+
|
|
9
42
|
/** Pack each package into `dest`. Returns `{ name: 'file:…tgz' }`. */
|
|
10
43
|
export function packAll(dest) {
|
|
11
44
|
mkdirSync(dest, { recursive: true })
|
|
@@ -14,14 +47,31 @@ export function packAll(dest) {
|
|
|
14
47
|
const file = join(pkgs, dir, 'package.json')
|
|
15
48
|
if (!existsSync(file)) continue
|
|
16
49
|
const { name } = JSON.parse(readFileSync(file, 'utf8'))
|
|
17
|
-
|
|
18
|
-
tarballs[name] = `file:${join(dest, packed.trim().split('\n').pop())}`
|
|
50
|
+
tarballs[name] = pack(join(pkgs, dir), dest)
|
|
19
51
|
}
|
|
20
52
|
return tarballs
|
|
21
53
|
}
|
|
22
54
|
|
|
23
55
|
/** `pkg` with its weft dependencies on the tarballs. The overrides reach the ones packages declare for each other. */
|
|
56
|
+
/**
|
|
57
|
+
* The same overrides in the app's own pnpm-workspace.yaml, when create wrote one: that file makes the app its own
|
|
58
|
+
* workspace root, and pnpm reads overrides only from there (not from package.json, nor from a workspace above it).
|
|
59
|
+
*/
|
|
60
|
+
export function pinWorkspace(dir, tarballs) {
|
|
61
|
+
const file = join(dir, 'pnpm-workspace.yaml')
|
|
62
|
+
if (!existsSync(file) || !Object.keys(tarballs).length) return
|
|
63
|
+
const lines = Object.entries(tarballs)
|
|
64
|
+
.map(([name, spec]) => ` ${JSON.stringify(name)}: ${JSON.stringify(spec)}\n`)
|
|
65
|
+
.join('')
|
|
66
|
+
writeFileSync(file, `${readFileSync(file, 'utf8')}overrides:\n${lines}`)
|
|
67
|
+
}
|
|
68
|
+
|
|
24
69
|
export function pin(pkg, tarballs) {
|
|
25
|
-
for (const deps of [pkg.dependencies, pkg.devDependencies])
|
|
26
|
-
|
|
70
|
+
for (const deps of [pkg.dependencies, pkg.devDependencies])
|
|
71
|
+
for (const name in deps) if (tarballs[name]) deps[name] = tarballs[name]
|
|
72
|
+
return {
|
|
73
|
+
...pkg,
|
|
74
|
+
overrides: { ...pkg.overrides, ...tarballs },
|
|
75
|
+
pnpm: { ...pkg.pnpm, overrides: { ...pkg.pnpm?.overrides, ...tarballs } },
|
|
76
|
+
}
|
|
27
77
|
}
|
package/src/data.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The Native and Prisma data layers of a scaffold
|
|
1
|
+
// The Native and Prisma data layers of a scaffold, beside Drizzle's in stack.mjs. Each writes the same
|
|
2
2
|
// todo app, so the same e2e holds for every layer. `weft add db` writes these files too.
|
|
3
3
|
|
|
4
4
|
const engineSql = {
|
|
@@ -9,23 +9,71 @@ const engineSql = {
|
|
|
9
9
|
|
|
10
10
|
// Better Auth's tables under its default names, which are camelCase, so they're quoted. MySQL can't key a TEXT column.
|
|
11
11
|
export function nativeAuthSql(db) {
|
|
12
|
-
const q = name => (db === 'mysql' ? `\`${name}\`` : `"${name}"`)
|
|
12
|
+
const q = (name) => (db === 'mysql' ? `\`${name}\`` : `"${name}"`)
|
|
13
13
|
const key = 'varchar(255)'
|
|
14
14
|
const text = 'text'
|
|
15
15
|
const when = { sqlite: 'date', postgres: 'timestamptz', mysql: 'datetime(3)' }[db]
|
|
16
16
|
const bool = { sqlite: 'integer', postgres: 'boolean', mysql: 'boolean' }[db]
|
|
17
|
-
const table = (name, columns) =>
|
|
18
|
-
|
|
19
|
-
const
|
|
17
|
+
const table = (name, columns) =>
|
|
18
|
+
`create table ${q(name)} (\n${columns.map(([c, type]) => ` ${q(c)} ${type}`).join(',\n')}\n);\n`
|
|
19
|
+
const ref = (to) => `references ${q(to)} (${q('id')}) on delete cascade`
|
|
20
|
+
const stamps = [
|
|
21
|
+
['createdAt', `${when} not null`],
|
|
22
|
+
['updatedAt', `${when} not null`],
|
|
23
|
+
]
|
|
20
24
|
return [
|
|
21
|
-
table('user', [
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
table('user', [
|
|
26
|
+
['id', `${key} primary key`],
|
|
27
|
+
['name', `${text} not null`],
|
|
28
|
+
['email', `${key} not null unique`],
|
|
29
|
+
['emailVerified', `${bool} not null`],
|
|
30
|
+
['image', text],
|
|
31
|
+
...stamps,
|
|
32
|
+
]),
|
|
33
|
+
table('session', [
|
|
34
|
+
['id', `${key} primary key`],
|
|
35
|
+
['expiresAt', `${when} not null`],
|
|
36
|
+
['token', `${key} not null unique`],
|
|
37
|
+
...stamps,
|
|
38
|
+
['ipAddress', text],
|
|
39
|
+
['userAgent', text],
|
|
40
|
+
['userId', `${key} not null ${ref('user')}`],
|
|
41
|
+
]),
|
|
42
|
+
table('account', [
|
|
43
|
+
['id', `${key} primary key`],
|
|
44
|
+
['accountId', `${text} not null`],
|
|
45
|
+
['providerId', `${text} not null`],
|
|
46
|
+
['userId', `${key} not null ${ref('user')}`],
|
|
47
|
+
['accessToken', text],
|
|
48
|
+
['refreshToken', text],
|
|
49
|
+
['idToken', text],
|
|
50
|
+
['accessTokenExpiresAt', when],
|
|
51
|
+
['refreshTokenExpiresAt', when],
|
|
52
|
+
['scope', text],
|
|
53
|
+
['password', text],
|
|
54
|
+
...stamps,
|
|
55
|
+
]),
|
|
56
|
+
table('verification', [
|
|
57
|
+
['id', `${key} primary key`],
|
|
58
|
+
['identifier', `${text} not null`],
|
|
59
|
+
['value', `${text} not null`],
|
|
60
|
+
['expiresAt', `${when} not null`],
|
|
61
|
+
['createdAt', when],
|
|
62
|
+
['updatedAt', when],
|
|
63
|
+
]),
|
|
64
|
+
table('jwks', [
|
|
65
|
+
['id', `${key} primary key`],
|
|
66
|
+
['publicKey', `${text} not null`],
|
|
67
|
+
['privateKey', `${text} not null`],
|
|
68
|
+
['createdAt', `${when} not null`],
|
|
69
|
+
['expiresAt', when],
|
|
70
|
+
['alg', text],
|
|
71
|
+
['crv', text],
|
|
72
|
+
]),
|
|
25
73
|
].join('\n')
|
|
26
74
|
}
|
|
27
75
|
|
|
28
|
-
const nativeTodos = auth => {
|
|
76
|
+
const nativeTodos = (auth) => {
|
|
29
77
|
const use = auth ? `\n auth: 'user',` : ''
|
|
30
78
|
const mine = auth ? ` and user_id = \${ctx.user.id}` : ''
|
|
31
79
|
return `import { mutation, query } from '@weft-js/start'
|
|
@@ -103,13 +151,21 @@ export default createAuth({ db, secret: env.BETTER_AUTH_SECRET, options: { datab
|
|
|
103
151
|
|
|
104
152
|
const PROVIDER = { sqlite: 'sqlite', postgres: 'postgresql', mysql: 'mysql' }
|
|
105
153
|
const ADAPTER = {
|
|
106
|
-
sqlite: {
|
|
107
|
-
|
|
154
|
+
sqlite: {
|
|
155
|
+
pkg: '@prisma/adapter-libsql',
|
|
156
|
+
name: 'PrismaLibSql',
|
|
157
|
+
make: '{ url: env.DATABASE_URL }',
|
|
158
|
+
},
|
|
159
|
+
postgres: {
|
|
160
|
+
pkg: '@prisma/adapter-pg',
|
|
161
|
+
name: 'PrismaPg',
|
|
162
|
+
make: '{ connectionString: env.DATABASE_URL }',
|
|
163
|
+
},
|
|
108
164
|
mysql: { pkg: '@prisma/adapter-mariadb', name: 'PrismaMariaDb', make: 'env.DATABASE_URL' },
|
|
109
165
|
}
|
|
110
|
-
export const prismaAdapterPackage = db => ADAPTER[db].pkg
|
|
166
|
+
export const prismaAdapterPackage = (db) => ADAPTER[db].pkg
|
|
111
167
|
|
|
112
|
-
const authModels = db => {
|
|
168
|
+
const authModels = (db) => {
|
|
113
169
|
const long = db === 'mysql' ? ' @db.Text' : ''
|
|
114
170
|
return `
|
|
115
171
|
// Better Auth's tables: it reads and writes them through Prisma, with these names.
|
|
@@ -171,12 +227,24 @@ model Verification {
|
|
|
171
227
|
|
|
172
228
|
@@map("verification")
|
|
173
229
|
}
|
|
230
|
+
|
|
231
|
+
model Jwks {
|
|
232
|
+
id String @id
|
|
233
|
+
publicKey String${long} @map("public_key")
|
|
234
|
+
privateKey String${long} @map("private_key")
|
|
235
|
+
createdAt DateTime @map("created_at")
|
|
236
|
+
expiresAt DateTime? @map("expires_at")
|
|
237
|
+
alg String?
|
|
238
|
+
crv String?
|
|
239
|
+
|
|
240
|
+
@@map("jwks")
|
|
241
|
+
}
|
|
174
242
|
`
|
|
175
243
|
}
|
|
176
244
|
|
|
177
|
-
export const prismaAuthModels = db => authModels(db).replace(' todos Todo[]\n', '')
|
|
245
|
+
export const prismaAuthModels = (db) => authModels(db).replace(' todos Todo[]\n', '')
|
|
178
246
|
|
|
179
|
-
const prismaTodos = auth => {
|
|
247
|
+
const prismaTodos = (auth) => {
|
|
180
248
|
const use = auth ? `\n auth: 'user',` : ''
|
|
181
249
|
const mine = auth ? `, userId: ctx.user.id` : ''
|
|
182
250
|
return `import { mutation, query } from '@weft-js/start'
|
|
@@ -238,9 +306,13 @@ model Todo {
|
|
|
238
306
|
id String @id
|
|
239
307
|
title String
|
|
240
308
|
done Boolean @default(false)
|
|
241
|
-
createdAt DateTime @default(now()) @map("created_at")${
|
|
309
|
+
createdAt DateTime @default(now()) @map("created_at")${
|
|
310
|
+
auth
|
|
311
|
+
? `
|
|
242
312
|
userId String @map("user_id")
|
|
243
|
-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)`
|
|
313
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)`
|
|
314
|
+
: ''
|
|
315
|
+
}
|
|
244
316
|
|
|
245
317
|
@@map("todos")
|
|
246
318
|
}
|
package/src/index.mjs
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
|
-
//
|
|
2
|
-
// weft
|
|
3
|
-
// `
|
|
4
|
-
//
|
|
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 {
|
|
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
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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 }) => [
|
|
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': [
|
|
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(
|
|
45
|
-
|
|
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 = {
|
|
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)
|
|
60
|
-
|
|
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
|
|
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(
|
|
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))
|
|
84
|
-
|
|
85
|
-
if (!
|
|
86
|
-
|
|
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: {
|
|
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, {
|
|
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',
|
|
107
|
-
|
|
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({
|
|
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'
|
|
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 =
|
|
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 ?
|
|
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 =
|
|
231
|
-
|
|
232
|
-
|
|
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; }
|