@weft-js/cli 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -14
- package/bin/weft.mjs +8 -1
- package/docs/async.md +16 -6
- package/docs/browser.md +22 -8
- package/docs/config.md +39 -9
- package/docs/core.md +30 -0
- package/docs/deploy.md +108 -0
- package/docs/design.md +12 -3
- package/docs/entrypoints.md +79 -0
- package/docs/fonts.md +12 -2
- package/docs/icons.md +13 -7
- package/docs/lists.md +3 -1
- package/docs/meta.md +13 -3
- package/docs/plugins.md +32 -9
- package/docs/props.md +13 -2
- package/docs/reactivity.md +26 -0
- package/docs/rules.md +19 -12
- package/docs/rust.md +47 -17
- package/docs/start-short.md +3 -2
- package/docs/start.md +226 -52
- package/docs/state.md +33 -7
- package/docs/style.md +31 -8
- package/docs/surface.md +61 -15
- package/docs/testing.md +15 -5
- package/docs/theme.md +182 -0
- package/docs/ui-state.md +19 -6
- package/docs/verify.md +13 -0
- package/package.json +13 -4
- package/src/add/config.mjs +93 -0
- package/src/add/errors.mjs +2 -0
- package/src/add/setup.mjs +88 -0
- package/src/add-rust.mjs +82 -27
- package/src/add.mjs +209 -181
- package/src/agents-md.mjs +144 -43
- package/src/alias.mjs +16 -5
- package/src/audit/page.mjs +313 -0
- package/src/audit/report.mjs +69 -0
- package/src/audit/utils.mjs +5 -0
- package/src/audit.mjs +174 -188
- package/src/browser/cli.mjs +24 -5
- package/src/browser/client.mjs +189 -25
- package/src/browser/commands.mjs +166 -51
- package/src/catalog/audit.mjs +129 -0
- package/src/catalog/auth.mjs +40 -0
- package/src/catalog/compiler.mjs +337 -0
- package/src/catalog/contracts.mjs +18 -0
- package/src/catalog/database.mjs +52 -0
- package/src/catalog/deploy.mjs +117 -0
- package/src/catalog/design.mjs +159 -0
- package/src/catalog/forms.mjs +25 -0
- package/src/catalog/hydration.mjs +29 -0
- package/src/catalog/icons.mjs +24 -0
- package/src/catalog/media.mjs +163 -0
- package/src/catalog/metadata.mjs +116 -0
- package/src/catalog/plugins.mjs +228 -0
- package/src/catalog/realtime.mjs +195 -0
- package/src/catalog/rust.mjs +75 -0
- package/src/catalog/server-functions.mjs +92 -0
- package/src/catalog/state.mjs +39 -0
- package/src/catalog/style-groups.mjs +54 -0
- package/src/catalog/tests.mjs +127 -0
- package/src/catalog/text.mjs +200 -0
- package/src/catalog/ui-state.mjs +42 -0
- package/src/catalog.mjs +68 -444
- package/src/check/runtime.mjs +280 -0
- package/src/check.mjs +199 -207
- package/src/cli/authoring.mjs +213 -0
- package/src/cli/project.mjs +302 -0
- package/src/cli.mjs +228 -224
- package/src/config.mjs +1 -1
- package/src/contributions.mjs +75 -25
- package/src/db-layer.mjs +13 -5
- package/src/deploy/cdn.mjs +142 -0
- package/src/deploy/files.mjs +266 -0
- package/src/deploy/fit.mjs +166 -0
- package/src/deploy/hosts.mjs +126 -0
- package/src/deploy/index/cli.mjs +20 -0
- package/src/deploy/index/environment.mjs +69 -0
- package/src/deploy/index/steps.mjs +87 -0
- package/src/deploy/index.mjs +387 -0
- package/src/deploy/needs.mjs +149 -0
- package/src/design-checks/accessible-names.mjs +62 -0
- package/src/design-checks/contrast.mjs +43 -0
- package/src/design-checks/csp.mjs +60 -0
- package/src/design-checks/files.mjs +59 -0
- package/src/design-checks/heading-links.mjs +61 -0
- package/src/design-checks/interaction.mjs +65 -0
- package/src/design-checks/jsx.mjs +67 -0
- package/src/design-checks/layout-values.mjs +68 -0
- package/src/design-checks/native-controls.mjs +48 -0
- package/src/design-checks/outline.mjs +39 -0
- package/src/design-checks/reporter.mjs +37 -0
- package/src/design-checks/rules.mjs +274 -0
- package/src/design-checks/values.mjs +262 -0
- package/src/design-checks.mjs +10 -400
- package/src/diag.mjs +32 -8
- package/src/docs.mjs +138 -42
- package/src/doctor.mjs +20 -8
- package/src/files.mjs +18 -5
- package/src/fix.mjs +10 -2
- package/src/gate/after.mjs +40 -0
- package/src/gate/before.mjs +54 -0
- package/src/gate/classify.mjs +72 -0
- package/src/gate/mutation.mjs +209 -0
- package/src/gate/resolve-cases.mjs +22 -0
- package/src/gate/result.mjs +63 -0
- package/src/gate/run.mjs +75 -0
- package/src/gate.mjs +104 -202
- package/src/infer/box-props.cjs +140 -0
- package/src/infer/core-boxes.cjs +46 -0
- package/src/infer/derived-writes.cjs +95 -0
- package/src/infer/engine/checks.cjs +193 -0
- package/src/infer/engine/flow.cjs +293 -0
- package/src/infer/engine/report.cjs +158 -0
- package/src/infer/engine/utils.cjs +15 -0
- package/src/infer/engine.cjs +108 -368
- package/src/infer/facts/sites.cjs +114 -0
- package/src/infer/facts.cjs +210 -117
- package/src/infer/hosts.cjs +69 -22
- package/src/infer/lsp.mjs +149 -40
- package/src/infer/session.cjs +73 -17
- package/src/infer/shadow.cjs +13 -5
- package/src/infer/worker.cjs +57 -9
- package/src/jsx-scan.mjs +63 -21
- package/src/load-config/schema.mjs +281 -0
- package/src/load-config.mjs +144 -116
- package/src/map.mjs +93 -26
- package/src/mcp/browser-tools.mjs +155 -0
- package/src/mcp.mjs +147 -69
- package/src/meta-table.mjs +43 -8
- package/src/mutate.mjs +120 -36
- package/src/plugin-checks.mjs +52 -23
- package/src/plugins.mjs +125 -39
- package/src/project.mjs +170 -79
- package/src/props-checks.mjs +24 -6
- package/src/props-listener.mjs +27 -9
- package/src/react-checks.mjs +35 -4
- package/src/server-state-checks.mjs +34 -0
- package/src/share-checks.mjs +121 -37
- package/src/site.mjs +33 -6
- package/src/slots.mjs +104 -19
- package/src/start/build.mjs +116 -0
- package/src/start/routes.mjs +38 -0
- package/src/start.mjs +194 -92
- package/src/state-checks/core-state.mjs +223 -0
- package/src/state-checks/modules.mjs +17 -0
- package/src/state-checks/scan.mjs +232 -0
- package/src/state-checks/server-graph.mjs +65 -0
- package/src/state-checks.mjs +109 -133
- package/src/style/find.mjs +121 -0
- package/src/style.mjs +133 -127
- package/src/surface-checks.mjs +212 -46
- package/src/tailwind.mjs +5 -4
- package/src/tasks.mjs +189 -27
- package/src/test-rules/context.mjs +109 -0
- package/src/test-rules/file-rules.mjs +279 -0
- package/src/test-rules/rule-helpers.mjs +244 -0
- package/src/test-rules/syntax.mjs +223 -0
- package/src/test-rules/test-rules.mjs +127 -0
- package/src/testdiff.mjs +94 -32
- package/src/tests.mjs +31 -483
- package/src/text-checks.mjs +229 -69
- package/src/tokens.mjs +12 -4
- package/src/trust/scan.mjs +95 -0
- package/src/trust.mjs +214 -102
- package/src/weft.mjs +41 -9
- package/src/workspace.mjs +96 -19
- package/ts-plugin/context.js +57 -0
- package/ts-plugin/index.js +4 -124
- package/ts-plugin/mapping.js +65 -0
- package/ts-plugin/proxy.js +144 -0
package/src/add.mjs
CHANGED
|
@@ -3,33 +3,68 @@
|
|
|
3
3
|
|
|
4
4
|
import { execFileSync } from 'node:child_process'
|
|
5
5
|
import { randomBytes } from 'node:crypto'
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
7
|
-
import { createRequire } from 'node:module'
|
|
8
|
-
import { pathToFileURL } from 'node:url'
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
9
7
|
import { dirname, join, relative, resolve } from 'node:path'
|
|
10
|
-
import ts from 'typescript'
|
|
11
8
|
import { addRust } from './add-rust.mjs'
|
|
12
9
|
import { writeAgentsMd } from './agents-md.mjs'
|
|
13
|
-
import { findConfig, loadConfig
|
|
10
|
+
import { findConfig, loadConfig } from './load-config.mjs'
|
|
14
11
|
import { sync } from './plugins.mjs'
|
|
15
12
|
import { cssEntry, packages, viteConfig } from './tailwind.mjs'
|
|
16
13
|
import { database } from './map.mjs'
|
|
17
14
|
import { dbLayer } from './db-layer.mjs'
|
|
18
15
|
import { packageOf, trustStatus } from './trust.mjs'
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
import { usageError, refused } from './add/errors.mjs'
|
|
18
|
+
import { addToTs, addToData, listed, names } from './add/config.mjs'
|
|
19
|
+
import { setupOf, planSetup, applySetup, setupActions, authEdits } from './add/setup.mjs'
|
|
22
20
|
|
|
23
|
-
const ALIASES = {
|
|
21
|
+
const ALIASES = {
|
|
22
|
+
warp: '@weft-js/warp',
|
|
23
|
+
icons: '@weft-js/warp-icons',
|
|
24
|
+
text: '@weft-js/text',
|
|
25
|
+
media: '@weft-js/plugin-media',
|
|
26
|
+
'media-ffmpeg': '@weft-js/plugin-media-ffmpeg',
|
|
27
|
+
'surface-keel': '@weft-js/plugin-surface-keel',
|
|
28
|
+
redis: '@weft-js/realtime-redis',
|
|
29
|
+
nats: '@weft-js/realtime-nats',
|
|
30
|
+
pay: '@weft-js/pay',
|
|
31
|
+
'pay-stripe': '@weft-js/pay/stripe',
|
|
32
|
+
}
|
|
24
33
|
|
|
25
34
|
/** Add-ons `weft add` knows by the name after the scope, as `weft add realtime-redis`. */
|
|
26
|
-
const ADDONS = new Set([
|
|
35
|
+
const ADDONS = new Set([
|
|
36
|
+
'@weft-js/text',
|
|
37
|
+
'@weft-js/pay',
|
|
38
|
+
'@weft-js/plugin-media',
|
|
39
|
+
'@weft-js/plugin-media-ffmpeg',
|
|
40
|
+
'@weft-js/plugin-surface-keel',
|
|
41
|
+
'@weft-js/multiplayer',
|
|
42
|
+
'@weft-js/realtime-durable-object',
|
|
43
|
+
'@weft-js/realtime-livekit',
|
|
44
|
+
'@weft-js/realtime-nats',
|
|
45
|
+
'@weft-js/realtime-redis',
|
|
46
|
+
'@weft-js/realtime-socketio',
|
|
47
|
+
'@weft-js/realtime-webrtc',
|
|
48
|
+
])
|
|
27
49
|
|
|
28
|
-
const INSTALL = {
|
|
50
|
+
const INSTALL = {
|
|
51
|
+
'pnpm-lock.yaml': ['pnpm', 'add'],
|
|
52
|
+
'bun.lock': ['bun', 'add'],
|
|
53
|
+
'yarn.lock': ['yarn', 'add'],
|
|
54
|
+
'deno.lock': ['deno', 'add'],
|
|
55
|
+
'package-lock.json': ['npm', 'install'],
|
|
56
|
+
}
|
|
29
57
|
|
|
30
58
|
function installer(root) {
|
|
31
|
-
const lock = Object.keys(INSTALL).find(f => existsSync(join(root, f)))
|
|
32
|
-
return
|
|
59
|
+
const lock = Object.keys(INSTALL).find((f) => existsSync(join(root, f)))
|
|
60
|
+
return (
|
|
61
|
+
INSTALL[lock] ??
|
|
62
|
+
(process.versions.deno
|
|
63
|
+
? INSTALL['deno.lock']
|
|
64
|
+
: process.versions.bun
|
|
65
|
+
? INSTALL['bun.lock']
|
|
66
|
+
: INSTALL['package-lock.json'])
|
|
67
|
+
)
|
|
33
68
|
}
|
|
34
69
|
|
|
35
70
|
/** What to install: for an app on local tarballs (`local.mjs`) those go straight in `package.json`, since npm refuses any
|
|
@@ -38,111 +73,58 @@ export function pinned(root, specs) {
|
|
|
38
73
|
const file = join(root, 'package.json')
|
|
39
74
|
const pkg = JSON.parse(readFileSync(file, 'utf8'))
|
|
40
75
|
const local = {}
|
|
41
|
-
const rest = specs.filter(spec => {
|
|
76
|
+
const rest = specs.filter((spec) => {
|
|
42
77
|
const name = spec.replace(/(?<=.)@.*$/, '')
|
|
43
78
|
const override = pkg.overrides?.[name]
|
|
44
79
|
if (typeof override !== 'string' || !override.startsWith('file:')) return true
|
|
45
80
|
local[name] = override
|
|
46
81
|
return false
|
|
47
82
|
})
|
|
48
|
-
if (Object.keys(local).length)
|
|
83
|
+
if (Object.keys(local).length)
|
|
84
|
+
writeFileSync(
|
|
85
|
+
file,
|
|
86
|
+
JSON.stringify({ ...pkg, dependencies: { ...pkg.dependencies, ...local } }, null, 2),
|
|
87
|
+
)
|
|
49
88
|
return rest
|
|
50
89
|
}
|
|
51
90
|
|
|
52
91
|
function installSpecs(root, manager, verb, specs) {
|
|
53
92
|
const rest = pinned(root, specs)
|
|
54
|
-
execFileSync(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const require = createRequire(join(root, 'package.json'))
|
|
60
|
-
const name = pluginName(pkg)
|
|
61
|
-
for (const spec of [`${pkg}/plugin`, pkg]) {
|
|
62
|
-
let path
|
|
63
|
-
try {
|
|
64
|
-
path = require.resolve(spec)
|
|
65
|
-
} catch {
|
|
66
|
-
continue
|
|
67
|
-
}
|
|
68
|
-
const factory = pluginFactory(createRequire(import.meta.url)(path), pkg)
|
|
69
|
-
if (!factory) throw refused(`${spec} exports no plugin factory to call, so nothing was changed.`)
|
|
70
|
-
return { name: factory.name, line: `import ${factory.named ? `{ ${factory.name} }` : factory.name} from '${spec}'` }
|
|
71
|
-
}
|
|
72
|
-
return { name, line: `import ${name} from '${pkg}'` } // not installed yet: only a dry run gets this far
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** The object literal `export default` gives, directly or through `defineConfig(…)`. */
|
|
76
|
-
function configObject(sf) {
|
|
77
|
-
for (const st of sf.statements) {
|
|
78
|
-
if (!ts.isExportAssignment(st)) continue
|
|
79
|
-
const value = ts.isCallExpression(st.expression) ? st.expression.arguments[0] : st.expression
|
|
80
|
-
if (value && ts.isObjectLiteralExpression(value)) return value
|
|
81
|
-
}
|
|
82
|
-
return null
|
|
93
|
+
execFileSync(
|
|
94
|
+
manager,
|
|
95
|
+
[verb, ...(manager === 'deno' ? rest.map((spec) => `npm:${spec}`) : rest)],
|
|
96
|
+
{ cwd: root, stdio: ['ignore', 'ignore', 'inherit'], shell: process.platform === 'win32' },
|
|
97
|
+
) // npm is npm.cmd there
|
|
83
98
|
}
|
|
84
99
|
|
|
85
|
-
function addToTs(root, file, text, pkg) {
|
|
86
|
-
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true)
|
|
87
|
-
const object = configObject(sf)
|
|
88
|
-
if (!object) throw refused(`${file} doesn't export a plain object, so \`weft add\` can't edit it. Add \`${pkg}\` to \`plugins\` by hand.`)
|
|
89
|
-
const { name, line } = pluginImport(root, pkg)
|
|
90
|
-
const prop = object.properties.find(p => p.name?.getText(sf) === 'plugins')
|
|
91
|
-
if (prop && !(ts.isPropertyAssignment(prop) && ts.isArrayLiteralExpression(prop.initializer))) throw refused(`\`plugins\` in ${file} isn't a plain array, so \`weft add\` can't edit it. Add \`${pkg}\` by hand.`)
|
|
92
|
-
const edits = []
|
|
93
|
-
const imports = sf.statements.filter(ts.isImportDeclaration)
|
|
94
|
-
const after = imports.at(-1)?.end ?? 0
|
|
95
|
-
edits.push([after, `${imports.length ? '\n' : ''}${line}${imports.length ? '' : '\n'}`])
|
|
96
|
-
if (prop) {
|
|
97
|
-
const list = prop.initializer
|
|
98
|
-
const last = list.elements.at(-1)
|
|
99
|
-
edits.push(last ? [last.end, `, ${name}()`] : [list.getStart(sf) + 1, `${name}()`])
|
|
100
|
-
} else {
|
|
101
|
-
const last = object.properties.at(-1)
|
|
102
|
-
edits.push(last ? [object.properties.end, `${object.properties.hasTrailingComma ? '' : ','}\n plugins: [${name}()],`] : [object.getStart(sf) + 1, `\n plugins: [${name}()],\n`])
|
|
103
|
-
}
|
|
104
|
-
return edits.toSorted((a, b) => b[0] - a[0]).reduce((t, [at, insert]) => t.slice(0, at) + insert + t.slice(at), text)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function addToData(file, text, pkg) {
|
|
108
|
-
if (file.endsWith('.json')) {
|
|
109
|
-
const data = JSON.parse(text)
|
|
110
|
-
data.plugins = [...(data.plugins ?? []), pkg]
|
|
111
|
-
return `${JSON.stringify(data, null, 2)}\n`
|
|
112
|
-
}
|
|
113
|
-
const { parseDocument } = createRequire(import.meta.url)('yaml')
|
|
114
|
-
const doc = parseDocument(text)
|
|
115
|
-
if (doc.has('plugins')) doc.addIn(['plugins'], pkg)
|
|
116
|
-
else doc.set('plugins', [pkg])
|
|
117
|
-
return doc.toString()
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const listed = (config, pkg) => (config.plugins ?? []).some(p => p.name === pkg)
|
|
121
|
-
|
|
122
|
-
/** The config names the package already (imported or listed), which a read can tell without loading it. */
|
|
123
|
-
const names = (text, pkg) => new RegExp(`['"]${pkg.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')}(/[\\w-]+)?['"]`).test(text)
|
|
124
|
-
|
|
125
100
|
const VITE = ['vite.config.ts', 'vite.config.mts', 'vite.config.js', 'vite.config.mjs']
|
|
126
101
|
|
|
127
102
|
/** The app's Vite config with Tailwind in it. Stops rather than guess at one it can't place the plugin in. */
|
|
128
103
|
function viteWithTailwind(file, text) {
|
|
129
104
|
if (text.includes('@tailwindcss/vite')) return text
|
|
130
|
-
if (!/plugins:\s*\[/.test(text))
|
|
105
|
+
if (!/plugins:\s*\[/.test(text))
|
|
106
|
+
throw refused(
|
|
107
|
+
`${file} has no \`plugins: [\` to add Tailwind to. Add \`tailwindcss()\` from @tailwindcss/vite there, then run this again. Nothing was changed.`,
|
|
108
|
+
)
|
|
131
109
|
return `import tailwindcss from '@tailwindcss/vite'\n${text.replace(/plugins:\s*\[/, '$&tailwindcss(), ')}`
|
|
132
110
|
}
|
|
133
111
|
|
|
134
112
|
/** Tailwind's packages, the Vite plugin, and the top of src/app.css, imported first by the root layout. */
|
|
135
113
|
function addTailwind(root, { dryRun, install }) {
|
|
136
|
-
if (!existsSync(join(root, 'package.json')))
|
|
114
|
+
if (!existsSync(join(root, 'package.json')))
|
|
115
|
+
throw refused('`weft add tailwind` needs a package.json. Nothing was changed.')
|
|
137
116
|
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
138
117
|
const cssFile = join(root, 'src/app.css')
|
|
139
118
|
const css = existsSync(cssFile) ? readFileSync(cssFile, 'utf8') : ''
|
|
140
|
-
const viteName = VITE.find(f => existsSync(join(root, f)))
|
|
141
|
-
const vite = viteName
|
|
119
|
+
const viteName = VITE.find((f) => existsSync(join(root, f)))
|
|
120
|
+
const vite = viteName
|
|
121
|
+
? viteWithTailwind(viteName, readFileSync(join(root, viteName), 'utf8'))
|
|
122
|
+
: viteConfig
|
|
142
123
|
const layoutFile = join(root, 'src/routes/_layout.tsx')
|
|
143
124
|
const layout = existsSync(layoutFile) ? readFileSync(layoutFile, 'utf8') : null
|
|
144
125
|
const has = /['"]tailwindcss['"]|tailwindcss\/utilities/.test(css)
|
|
145
|
-
if (has && pkg.dependencies?.tailwindcss)
|
|
126
|
+
if (has && pkg.dependencies?.tailwindcss)
|
|
127
|
+
return { ok: true, already: true, package: 'tailwindcss', actions: [] }
|
|
146
128
|
|
|
147
129
|
const actions = []
|
|
148
130
|
const [manager, verb] = installer(root)
|
|
@@ -152,9 +134,20 @@ function addTailwind(root, { dryRun, install }) {
|
|
|
152
134
|
if (!dryRun) installSpecs(root, manager, verb, specs)
|
|
153
135
|
}
|
|
154
136
|
const warp = '@weft-js/warp' in { ...pkg.dependencies, ...pkg.devDependencies }
|
|
155
|
-
const edits = [
|
|
156
|
-
|
|
157
|
-
|
|
137
|
+
const edits = [
|
|
138
|
+
[join(root, viteName ?? 'vite.config.ts'), vite],
|
|
139
|
+
...(has ? [] : [[cssFile, `${cssEntry(warp)}${css && '\n'}${css}`]]),
|
|
140
|
+
]
|
|
141
|
+
actions.push(
|
|
142
|
+
`add Tailwind to ${viteName ?? 'vite.config.ts'}`,
|
|
143
|
+
...(has
|
|
144
|
+
? []
|
|
145
|
+
: [
|
|
146
|
+
`add Tailwind to src/app.css${warp ? ' (without its reset, which would override warp)' : ''}`,
|
|
147
|
+
]),
|
|
148
|
+
)
|
|
149
|
+
if (layout === null)
|
|
150
|
+
actions.push('import src/app.css from your root layout: there is no src/routes/_layout.tsx')
|
|
158
151
|
else if (!/['"][^'"]*app\.css['"]/.test(layout)) {
|
|
159
152
|
edits.push([layoutFile, `import '../app.css'\n${layout}`])
|
|
160
153
|
actions.push('import src/app.css first in src/routes/_layout.tsx')
|
|
@@ -170,17 +163,27 @@ const LAYER_PLUGIN = { prisma: '@weft-js/db/plugin-prisma', native: '@weft-js/db
|
|
|
170
163
|
|
|
171
164
|
/** A database on an app that has none: the wiring a scaffold writes, with no todo app. Never touches what's there. */
|
|
172
165
|
async function addDb(root, engine = 'sqlite', { layer = 'drizzle', dryRun, install }) {
|
|
173
|
-
if (!ENGINES.includes(engine))
|
|
174
|
-
|
|
175
|
-
if (
|
|
166
|
+
if (!ENGINES.includes(engine))
|
|
167
|
+
throw usageError(`weft add db takes ${ENGINES.join(', ')}, not \`${engine}\``)
|
|
168
|
+
if (!['drizzle', ...Object.keys(LAYER_PLUGIN)].includes(layer))
|
|
169
|
+
throw usageError(`--layer takes drizzle, prisma or native, not \`${layer}\``)
|
|
170
|
+
if (await dbLayer(root))
|
|
171
|
+
throw refused(
|
|
172
|
+
"This app already has a database. Changing engine or layer changes your queries, so it's yours to do. Nothing was changed.",
|
|
173
|
+
)
|
|
176
174
|
const { stackDeps, stackFiles } = await import('@weft-js/create/stack')
|
|
177
175
|
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
178
176
|
const name = String(pkg.name ?? 'app').replace(/^@.*\//, '')
|
|
179
177
|
const files = stackFiles({ db: engine, layer, auth: false, name, secret: '' })
|
|
180
178
|
delete files['src/routes/todos/index.tsx']
|
|
181
179
|
delete files['src/server/todos.ts']
|
|
182
|
-
const taken = Object.keys(files).filter(
|
|
183
|
-
|
|
180
|
+
const taken = Object.keys(files).filter(
|
|
181
|
+
(f) => f !== '.env' && f !== '.env.example' && existsSync(join(root, f)),
|
|
182
|
+
)
|
|
183
|
+
if (taken.length)
|
|
184
|
+
throw refused(
|
|
185
|
+
`${taken.join(', ')} already exist, so this can't add a database without overwriting them. Nothing was changed.`,
|
|
186
|
+
)
|
|
184
187
|
|
|
185
188
|
const { deps, dev } = stackDeps({ db: engine, layer, auth: false })
|
|
186
189
|
const actions = []
|
|
@@ -190,15 +193,17 @@ async function addDb(root, engine = 'sqlite', { layer = 'drizzle', dryRun, insta
|
|
|
190
193
|
actions.push(`install ${specs.join(' ')} with ${manager}`)
|
|
191
194
|
if (!dryRun) installSpecs(root, manager, verb, specs)
|
|
192
195
|
}
|
|
193
|
-
actions.push(...Object.keys(files).map(f => `write ${f}`))
|
|
196
|
+
actions.push(...Object.keys(files).map((f) => `write ${f}`))
|
|
194
197
|
if (dryRun) return { ok: true, package: `${layer} on ${engine}`, actions, dryRun: true }
|
|
195
198
|
for (const [file, text] of Object.entries(files)) {
|
|
196
199
|
const path = join(root, file)
|
|
197
200
|
mkdirSync(dirname(path), { recursive: true })
|
|
198
|
-
if (file.startsWith('.env') && existsSync(path))
|
|
201
|
+
if (file.startsWith('.env') && existsSync(path))
|
|
202
|
+
writeFileSync(path, `${readFileSync(path, 'utf8').trimEnd()}\n${text}`)
|
|
199
203
|
else writeFileSync(path, text)
|
|
200
204
|
}
|
|
201
|
-
if (LAYER_PLUGIN[layer])
|
|
205
|
+
if (LAYER_PLUGIN[layer])
|
|
206
|
+
actions.push(...(await addCommand(root, LAYER_PLUGIN[layer], { install: false })).actions)
|
|
202
207
|
actions.push('next: `weft db generate`')
|
|
203
208
|
return { ok: true, package: `${layer} on ${engine}`, actions }
|
|
204
209
|
}
|
|
@@ -206,117 +211,109 @@ async function addDb(root, engine = 'sqlite', { layer = 'drizzle', dryRun, insta
|
|
|
206
211
|
/** Better Auth on an app with a database. Its tables go beside yours, and your own routes stay as they are. */
|
|
207
212
|
async function addAuth(root, { dryRun, install }) {
|
|
208
213
|
const layer = await dbLayer(root)
|
|
209
|
-
if (!layer)
|
|
210
|
-
|
|
214
|
+
if (!layer)
|
|
215
|
+
throw refused(
|
|
216
|
+
'Better Auth keeps users in a database: run `weft add db` first. Nothing was changed.',
|
|
217
|
+
)
|
|
218
|
+
if (existsSync(join(root, 'src/auth.server.ts')))
|
|
219
|
+
throw refused(
|
|
220
|
+
'src/auth.server.ts already exists, so this app has an auth provider. Nothing was changed.',
|
|
221
|
+
)
|
|
211
222
|
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
212
223
|
const db = database(root, layer).engine
|
|
213
224
|
const ui = '@weft-js/warp' in { ...pkg.dependencies, ...pkg.devDependencies } ? 'warp' : 'none'
|
|
214
225
|
const { authFiles, stackDeps } = await import('@weft-js/create/stack')
|
|
215
226
|
const { files, context, tables } = authFiles({ db, layer: layer.layer, ui })
|
|
216
227
|
const contextFile = join(root, 'src/context.server.ts')
|
|
217
|
-
if (
|
|
218
|
-
|
|
228
|
+
if (
|
|
229
|
+
existsSync(contextFile) &&
|
|
230
|
+
!/export default \(\) => \(\{ db \}\)/.test(readFileSync(contextFile, 'utf8'))
|
|
231
|
+
)
|
|
232
|
+
throw refused(
|
|
233
|
+
'src/context.server.ts has been changed. Spread `session(auth, request)` from @weft-js/start/auth into it, then run this again. Nothing was changed.',
|
|
234
|
+
)
|
|
235
|
+
const taken = Object.keys(files).filter((f) => existsSync(join(root, f)))
|
|
219
236
|
if (taken.length) throw refused(`${taken.join(', ')} already exist. Nothing was changed.`)
|
|
220
237
|
const envModule = join(root, 'src/env.server.ts')
|
|
221
238
|
const envText = existsSync(envModule) ? readFileSync(envModule, 'utf8') : ''
|
|
222
|
-
if (!envText.includes('BETTER_AUTH_SECRET') && !/server: \{\n/.test(envText))
|
|
239
|
+
if (!envText.includes('BETTER_AUTH_SECRET') && !/server: \{\n/.test(envText))
|
|
240
|
+
throw refused(
|
|
241
|
+
'src/env.server.ts has no `server: {` block to add BETTER_AUTH_SECRET to. Nothing was changed.',
|
|
242
|
+
)
|
|
223
243
|
|
|
224
244
|
const actions = []
|
|
225
245
|
const [manager, verb] = installer(root)
|
|
226
246
|
const wanted = stackDeps({ db, layer: layer.layer, auth: true }).deps
|
|
227
|
-
const specs = Object.entries(wanted)
|
|
247
|
+
const specs = Object.entries(wanted)
|
|
248
|
+
.filter(([name]) => !(name in (pkg.dependencies ?? {})))
|
|
249
|
+
.map(([n, v]) => `${n}@${v}`)
|
|
228
250
|
if (install && specs.length) {
|
|
229
251
|
actions.push(`install ${specs.join(' ')} with ${manager}`)
|
|
230
252
|
if (!dryRun) installSpecs(root, manager, verb, specs)
|
|
231
253
|
}
|
|
232
|
-
const edits = {
|
|
233
|
-
|
|
234
|
-
if (layer.layer === 'drizzle') {
|
|
235
|
-
edits['src/db/auth.schema.ts'] = tables
|
|
236
|
-
edits['src/db/schema.ts'] = `${readFileSync(join(root, 'src/db/schema.ts'), 'utf8').trimEnd()}\n\nexport * from './auth.schema'\n`
|
|
237
|
-
} else if (layer.layer === 'prisma') {
|
|
238
|
-
edits['prisma/schema.prisma'] = `${readFileSync(join(root, 'prisma/schema.prisma'), 'utf8').trimEnd()}\n${tables}`
|
|
239
|
-
} else {
|
|
240
|
-
const next = existsSync(join(root, 'migrations')) ? readdirSync(join(root, 'migrations')).filter(f => f.endsWith('.sql')).length : 0
|
|
241
|
-
edits[`migrations/${String(next).padStart(4, '0')}_auth.sql`] = tables
|
|
242
|
-
}
|
|
243
|
-
actions.push(...Object.keys(edits).map(f => `write ${f}`))
|
|
254
|
+
const edits = authEdits({ root, files, context, tables, envText, layer })
|
|
255
|
+
actions.push(...Object.keys(edits).map((f) => `write ${f}`))
|
|
244
256
|
if (!dryRun) {
|
|
245
257
|
for (const [file, text] of Object.entries(edits)) {
|
|
246
258
|
mkdirSync(dirname(join(root, file)), { recursive: true })
|
|
247
259
|
writeFileSync(join(root, file), text)
|
|
248
260
|
}
|
|
249
261
|
const env = join(root, '.env')
|
|
250
|
-
if (!(existsSync(env) && readFileSync(env, 'utf8').includes('BETTER_AUTH_SECRET')))
|
|
262
|
+
if (!(existsSync(env) && readFileSync(env, 'utf8').includes('BETTER_AUTH_SECRET')))
|
|
263
|
+
writeFileSync(
|
|
264
|
+
env,
|
|
265
|
+
`${existsSync(env) ? `${readFileSync(env, 'utf8').trimEnd()}\n` : ''}BETTER_AUTH_SECRET=${randomBytes(32).toString('base64')}\nBETTER_AUTH_URL=http://localhost:5173\n`,
|
|
266
|
+
)
|
|
251
267
|
}
|
|
252
|
-
actions.push(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
268
|
+
actions.push(
|
|
269
|
+
'add BETTER_AUTH_SECRET to .env',
|
|
270
|
+
...(layer.schema ? ['next: `weft db generate`'] : []),
|
|
271
|
+
'next: reload the app: the migration applies on the next request',
|
|
272
|
+
"next: add `auth: 'user'` to what needs a user, and link to /login",
|
|
273
|
+
)
|
|
274
|
+
return {
|
|
275
|
+
ok: true,
|
|
276
|
+
package: `Better Auth on ${layer.layer}`,
|
|
277
|
+
actions,
|
|
278
|
+
...(dryRun && { dryRun: true }),
|
|
263
279
|
}
|
|
264
|
-
const mod = await import(pathToFileURL(path).href)
|
|
265
|
-
const make = mod.default ?? Object.values(mod).find(v => typeof v === 'function')
|
|
266
|
-
return make?.().contribute({ command: 'check', mode: 'development', root }).setup
|
|
267
280
|
}
|
|
268
281
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const read = rel => (existsSync(join(root, rel)) ? readFileSync(join(root, rel), 'utf8') : undefined)
|
|
275
|
-
let plan
|
|
276
|
-
try {
|
|
277
|
-
plan = setup.run({ engine: layer ? database(root, layer).engine : null, layer: layer?.layer ?? null, read })
|
|
278
|
-
} catch (error) {
|
|
279
|
-
throw refused(`${error.message}. Nothing was changed.`)
|
|
280
|
-
}
|
|
281
|
-
const taken = Object.keys(plan.files ?? {}).filter(f => existsSync(join(root, f)))
|
|
282
|
-
if (taken.length) throw refused(`${taken.join(', ')} already exist. Nothing was changed.`)
|
|
283
|
-
return plan
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function applySetup(root, plan) {
|
|
287
|
-
for (const [file, text] of Object.entries({ ...plan.files, ...plan.edits })) {
|
|
288
|
-
mkdirSync(dirname(join(root, file)), { recursive: true })
|
|
289
|
-
writeFileSync(join(root, file), text)
|
|
290
|
-
}
|
|
291
|
-
for (const name of ['.env', '.env.example']) {
|
|
292
|
-
const file = join(root, name)
|
|
293
|
-
const had = existsSync(file) ? readFileSync(file, 'utf8') : ''
|
|
294
|
-
const lines = Object.entries(plan.env ?? {}).filter(([k]) => !had.includes(`${k}=`)).map(([k, v]) => `${k}=${v}\n`)
|
|
295
|
-
if (lines.length) writeFileSync(file, `${had && `${had.trimEnd()}\n`}${lines.join('')}`)
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const setupActions = plan => [...Object.keys({ ...plan.files, ...plan.edits }).map(f => `write ${f}`), ...(plan.env ? [`add ${Object.keys(plan.env).join(', ')} to .env and .env.example`] : []), ...(plan.next ?? []).map(n => `next: ${n}`)]
|
|
300
|
-
|
|
301
|
-
export async function addCommand(root, feature, { dryRun = false, install = true, engine, layer, remote = false } = {}) {
|
|
282
|
+
export async function addCommand(
|
|
283
|
+
root,
|
|
284
|
+
feature,
|
|
285
|
+
{ dryRun = false, install = true, engine, layer, remote = false } = {},
|
|
286
|
+
) {
|
|
302
287
|
root = resolve(root)
|
|
303
|
-
if (!feature)
|
|
288
|
+
if (!feature)
|
|
289
|
+
throw usageError(
|
|
290
|
+
`weft add needs a feature: db, auth, tailwind, rust, ${Object.keys(ALIASES).join(', ')}, or a package name`,
|
|
291
|
+
)
|
|
304
292
|
if (feature === 'rust') {
|
|
305
293
|
const [manager, verb] = installer(root)
|
|
306
|
-
return addRust(root, engine, {
|
|
294
|
+
return addRust(root, engine, {
|
|
295
|
+
remote,
|
|
296
|
+
dryRun,
|
|
297
|
+
install,
|
|
298
|
+
installSpecs: (specs) => installSpecs(root, manager, verb, specs),
|
|
299
|
+
})
|
|
307
300
|
}
|
|
308
301
|
if (feature === 'tailwind') return addTailwind(root, { dryRun, install })
|
|
309
302
|
if (feature === 'db') return addDb(root, engine, { layer, dryRun, install })
|
|
310
303
|
if (feature === 'auth') return addAuth(root, { dryRun, install })
|
|
311
|
-
const pkg =
|
|
304
|
+
const pkg =
|
|
305
|
+
ALIASES[feature] ?? (ADDONS.has(`@weft-js/${feature}`) ? `@weft-js/${feature}` : feature)
|
|
312
306
|
const file = findConfig(root) ?? join(root, 'weft.config.ts')
|
|
313
307
|
const config = relative(root, file)
|
|
314
|
-
const text = existsSync(file)
|
|
308
|
+
const text = existsSync(file)
|
|
309
|
+
? readFileSync(file, 'utf8')
|
|
310
|
+
: "import { defineConfig } from '@weft-js/core/config'\n\nexport default defineConfig({})\n"
|
|
315
311
|
const named = names(text, pkg)
|
|
316
312
|
// A config can name a package that isn't installed yet, and importing it then would leave this process's module
|
|
317
313
|
// resolver sure the package has no `exports`, so the load after the install would fail too. Load it once it's there.
|
|
318
314
|
const present = existsSync(join(root, 'node_modules', packageOf(pkg), 'package.json'))
|
|
319
|
-
if (present && (named || listed((await loadConfig(root)).config, pkg)))
|
|
315
|
+
if (present && (named || listed((await loadConfig(root)).config, pkg)))
|
|
316
|
+
return { ok: true, already: true, package: pkg, config, actions: [] }
|
|
320
317
|
|
|
321
318
|
const home = packageOf(pkg)
|
|
322
319
|
|
|
@@ -327,32 +324,63 @@ export async function addCommand(root, feature, { dryRun = false, install = true
|
|
|
327
324
|
if (!dryRun) installSpecs(root, manager, verb, [home])
|
|
328
325
|
}
|
|
329
326
|
const setup = await setupOf(root, pkg)
|
|
330
|
-
const plan = setup && await planSetup(root, setup)
|
|
331
|
-
const next = named
|
|
327
|
+
const plan = setup && (await planSetup(root, setup))
|
|
328
|
+
const next = named
|
|
329
|
+
? text
|
|
330
|
+
: /\.(json|ya?ml)$/.test(file)
|
|
331
|
+
? addToData(file, text, pkg)
|
|
332
|
+
: addToTs(root, file, text, pkg)
|
|
332
333
|
if (!named) actions.push(`add ${pkg} to ${config}`)
|
|
333
|
-
if (dryRun)
|
|
334
|
+
if (dryRun)
|
|
335
|
+
return {
|
|
336
|
+
ok: true,
|
|
337
|
+
package: pkg,
|
|
338
|
+
config,
|
|
339
|
+
actions: [...actions, ...(plan ? setupActions(plan) : [])],
|
|
340
|
+
dryRun: true,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return finishAdd({ root, file, next, text, config, pkg, install, actions, plan, setup })
|
|
344
|
+
}
|
|
334
345
|
|
|
346
|
+
async function finishAdd({ root, file, next, text, config, pkg, install, actions, plan, setup }) {
|
|
335
347
|
writeFileSync(file, next)
|
|
336
348
|
const after = await loadConfig(root)
|
|
337
|
-
const failed = after.diagnostics.filter(d => d.severity === 'error')
|
|
349
|
+
const failed = after.diagnostics.filter((d) => d.severity === 'error')
|
|
338
350
|
if (failed.length) {
|
|
339
351
|
writeFileSync(file, text)
|
|
340
|
-
throw refused(
|
|
352
|
+
throw refused(
|
|
353
|
+
`${config} doesn't load with ${pkg} in it${install ? ', though the package installed' : ''}, so it's back as it was: ${failed[0].code} ${failed[0].why}`,
|
|
354
|
+
)
|
|
341
355
|
}
|
|
342
356
|
sync(root, after)
|
|
343
357
|
actions.push('regenerate .weft/slots')
|
|
344
358
|
if (plan) {
|
|
345
|
-
for (const other of setup.with ?? [])
|
|
359
|
+
for (const other of setup.with ?? [])
|
|
360
|
+
actions.push(...(await addCommand(root, other, { install })).actions)
|
|
346
361
|
applySetup(root, plan)
|
|
347
362
|
actions.push(...setupActions(plan))
|
|
348
363
|
}
|
|
349
364
|
// Never approved here: installing a plugin isn't having read what it asks for.
|
|
350
|
-
const trust = trustStatus(root, after).plugins.find(p => p.name === pkg)
|
|
351
|
-
|
|
365
|
+
const trust = trustStatus(root, after).plugins.find((p) => p.name === pkg)
|
|
366
|
+
// Only the generated file: a scaffold's short one is the app's own until `weft agents-md` is asked for.
|
|
367
|
+
if (
|
|
368
|
+
existsSync(join(root, 'AGENTS.md')) &&
|
|
369
|
+
readFileSync(join(root, 'AGENTS.md'), 'utf8').includes('Generated by `weft agents-md`')
|
|
370
|
+
) {
|
|
352
371
|
await writeAgentsMd(root)
|
|
353
372
|
actions.push('regenerate AGENTS.md')
|
|
354
373
|
}
|
|
355
|
-
actions.push(...(after.contributions?.find(c => c.name === pkg)?.manifest?.notes ?? []))
|
|
356
|
-
if (trust?.blocked)
|
|
357
|
-
|
|
374
|
+
actions.push(...(after.contributions?.find((c) => c.name === pkg)?.manifest?.notes ?? []))
|
|
375
|
+
if (trust?.blocked)
|
|
376
|
+
actions.push(
|
|
377
|
+
`${pkg} asks for ${[...trust.added, ...trust.hidden.map((n) => `${n} (undeclared)`)].join(', ') || 'a code change'}; \`weft check\` stops until you run \`weft plugins approve ${pkg}\``,
|
|
378
|
+
)
|
|
379
|
+
return {
|
|
380
|
+
ok: true,
|
|
381
|
+
package: pkg,
|
|
382
|
+
config,
|
|
383
|
+
actions,
|
|
384
|
+
...(trust?.blocked && { approve: { added: trust.added, undeclared: trust.hidden } }),
|
|
385
|
+
}
|
|
358
386
|
}
|