@weft-js/create 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -10
- 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/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; }
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Tailwind: the same files `weft add tailwind` writes (packages/cli/src/tailwind.mjs, held equal by a test).
|
|
2
|
+
/** What `--ui tailwind` adds, pinned. */
|
|
3
|
+
export const tailwindPackages = { tailwindcss: '4.3.3', '@tailwindcss/vite': '4.3.3' }
|
|
4
|
+
/** `vite.config.ts` for `--ui tailwind`. */
|
|
5
|
+
export const TAILWIND_VITE = `import tailwindcss from '@tailwindcss/vite'
|
|
6
|
+
|
|
7
|
+
export default { plugins: [tailwindcss()] }
|
|
8
|
+
`
|
|
9
|
+
/** The stylesheet for `--ui tailwind`. */
|
|
10
|
+
export const TAILWIND_CSS = `@import "tailwindcss";
|
|
11
|
+
`
|
|
12
|
+
const tw = {
|
|
13
|
+
card: 'mx-auto max-w-2xl space-y-4 rounded-lg border border-current/15 p-6',
|
|
14
|
+
title: 'text-2xl font-semibold',
|
|
15
|
+
button: 'rounded-md border border-current/30 px-3 py-1.5 hover:bg-current/5',
|
|
16
|
+
input: 'rounded-md border border-current/30 px-3 py-1.5',
|
|
17
|
+
}
|
|
18
|
+
/** The class of a scaffold element: Tailwind's utilities, or the plain CSS in src/app.css. */
|
|
19
|
+
export const classes = (ui, part) => (ui === 'tailwind' ? tw[part] : part === 'card' ? 'card' : '')
|
|
20
|
+
const attr = (ui, part) => (ui === 'tailwind' ? ` class="${tw[part]}"` : '')
|
|
21
|
+
|
|
22
|
+
/** The page's controls in each UI kit: warp's components, or plain elements with the same roles and names. */
|
|
23
|
+
export const controls = (ui) =>
|
|
24
|
+
ui === 'warp'
|
|
25
|
+
? {
|
|
26
|
+
imports: `import { Alert, Button, Card, Checkbox, Input } from '@weft-js/warp'\n`,
|
|
27
|
+
card: (title, body) => `<Card title=${JSON.stringify(title)}>${body}</Card>`,
|
|
28
|
+
input: (label, box, type = 'text') =>
|
|
29
|
+
`<Input label=${JSON.stringify(label)} type="${type}" value={${box}.value} onInput={e => (${box}.value = e.currentTarget.value)} />`,
|
|
30
|
+
submit: (label, pending) => `<Button type="submit" loading={${pending}}>${label}</Button>`,
|
|
31
|
+
button: (label, onClick, extra = '') =>
|
|
32
|
+
`<Button emphasis="ghost" ${extra}onClick={${onClick}}>${label}</Button>`,
|
|
33
|
+
error: (box) => `{${box}.value && <Alert tone="critical" live>{${box}.value}</Alert>}`,
|
|
34
|
+
row: `<Checkbox checked={t.done} onChange={() => toggleTodo({ id: t.id })}>{t.title}</Checkbox>
|
|
35
|
+
<Button emphasis="ghost" tone="neutral" size="sm" icon="x" aria-label={\`Delete \${t.title}\`} onClick={() => deleteTodo({ id: t.id })} />`,
|
|
36
|
+
}
|
|
37
|
+
: {
|
|
38
|
+
imports: '',
|
|
39
|
+
card: (title, body) =>
|
|
40
|
+
`<section class="${classes(ui, 'card')}"><h1${attr(ui, 'title')}>${title}</h1>${body}</section>`,
|
|
41
|
+
input: (label, box, type = 'text') =>
|
|
42
|
+
`<label>${label} <input${attr(ui, 'input')} type="${type}" value={${box}.value} onInput={e => (${box}.value = e.currentTarget.value)} /></label>`,
|
|
43
|
+
submit: (label, pending) =>
|
|
44
|
+
`<button${attr(ui, 'button')} type="submit" disabled={${pending}}>${label}</button>`,
|
|
45
|
+
button: (label, onClick, extra = '') =>
|
|
46
|
+
`<button${attr(ui, 'button')} type="button" ${extra}onClick={${onClick}}>${label}</button>`,
|
|
47
|
+
error: (box) => `{${box}.value && <p role="alert">{${box}.value}</p>}`,
|
|
48
|
+
row: `<label>
|
|
49
|
+
<input type="checkbox" checked={t.done} onChange={() => toggleTodo({ id: t.id })} /> {t.title}
|
|
50
|
+
</label>
|
|
51
|
+
<button type="button" aria-label={\`Delete \${t.title}\`} onClick={() => deleteTodo({ id: t.id })}>×</button>`,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `/todos`: the list on the server functions, written for this `--ui`. */
|
|
55
|
+
export function todosPage(ui) {
|
|
56
|
+
const c = controls(ui)
|
|
57
|
+
return `import type { Meta } from '@weft-js/start'
|
|
58
|
+
import { Boundary, state } from '@weft-js/core'
|
|
59
|
+
${c.imports}import { addTodo, deleteTodo, listTodos, toggleTodo } from '#/server/todos'
|
|
60
|
+
|
|
61
|
+
export const meta: Meta = { title: 'Todos', description: 'Your todo list: add, tick off and delete.' }
|
|
62
|
+
|
|
63
|
+
// The query is made in here, inside the Boundary, so the Boundary sees it: it shows \`pending\` until the list arrives.
|
|
64
|
+
function TodoList() {
|
|
65
|
+
const todos = listTodos()
|
|
66
|
+
return (
|
|
67
|
+
<ul class="todos">
|
|
68
|
+
{(todos.value ?? []).map(t => (
|
|
69
|
+
<li key={t.id} class={t.done ? 'done' : 'open'}>
|
|
70
|
+
${c.row}
|
|
71
|
+
</li>
|
|
72
|
+
))}
|
|
73
|
+
</ul>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default function Todos() {
|
|
78
|
+
const title = state('')
|
|
79
|
+
const problem = state('')
|
|
80
|
+
const add = async (e: SubmitEvent) => {
|
|
81
|
+
e.preventDefault()
|
|
82
|
+
const text = title.value.trim()
|
|
83
|
+
if (!text) return
|
|
84
|
+
title.value = ''
|
|
85
|
+
problem.value = ''
|
|
86
|
+
await addTodo({ title: text }).catch((err: Error) => (problem.value = err.message))
|
|
87
|
+
}
|
|
88
|
+
return (
|
|
89
|
+
${c.card(
|
|
90
|
+
'Todos',
|
|
91
|
+
`
|
|
92
|
+
<form onSubmit={add}>
|
|
93
|
+
${c.input('New todo', 'title')}
|
|
94
|
+
${c.submit('Add', 'addTodo.pending.value')}
|
|
95
|
+
</form>
|
|
96
|
+
${c.error('problem')}
|
|
97
|
+
<Boundary pending={<p>Loading…</p>}>
|
|
98
|
+
<TodoList />
|
|
99
|
+
</Boundary>
|
|
100
|
+
`,
|
|
101
|
+
)}
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** `/login`: sign in and sign up, written for this `--ui`. */
|
|
108
|
+
export function loginPage(ui) {
|
|
109
|
+
const c = controls(ui)
|
|
110
|
+
return `import { redirect, type LoadArgs, type Meta } from '@weft-js/start'
|
|
111
|
+
import { state } from '@weft-js/core'
|
|
112
|
+
${c.imports}import { signIn, signUp } from '#/server/session'
|
|
113
|
+
|
|
114
|
+
export const load = ({ ctx }: LoadArgs) => {
|
|
115
|
+
if (ctx.user) redirect('/todos')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const meta: Meta = { title: 'Sign in', description: 'Sign in, or make an account.' }
|
|
119
|
+
|
|
120
|
+
export default function Login() {
|
|
121
|
+
const creating = state(false)
|
|
122
|
+
const name = state('')
|
|
123
|
+
const email = state('')
|
|
124
|
+
const password = state('')
|
|
125
|
+
const problem = state('')
|
|
126
|
+
const submit = async (e: SubmitEvent) => {
|
|
127
|
+
e.preventDefault()
|
|
128
|
+
problem.value = ''
|
|
129
|
+
const done = creating.value
|
|
130
|
+
? signUp({ name: name.value, email: email.value, password: password.value })
|
|
131
|
+
: signIn({ email: email.value, password: password.value })
|
|
132
|
+
await done.catch((err: Error) => (problem.value = err.message))
|
|
133
|
+
}
|
|
134
|
+
return (
|
|
135
|
+
${c.card(
|
|
136
|
+
'Sign in',
|
|
137
|
+
`
|
|
138
|
+
<form onSubmit={submit}>
|
|
139
|
+
{creating.value && ${c.input('Name', 'name')}}
|
|
140
|
+
${c.input('Email', 'email', 'email')}
|
|
141
|
+
${c.input('Password', 'password', 'password')}
|
|
142
|
+
${c.submit(`{creating.value ? 'Create account' : 'Sign in'}`, 'signIn.pending.value || signUp.pending.value')}
|
|
143
|
+
</form>
|
|
144
|
+
${c.error('problem')}
|
|
145
|
+
${c.button(`{creating.value ? 'I have an account' : 'Create an account'}`, '() => (creating.value = !creating.value)')}
|
|
146
|
+
`,
|
|
147
|
+
)}
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
`
|
|
151
|
+
}
|