@weft-js/cli 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 +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 +33 -8
- 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 +159 -206
- 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 +67 -460
- package/src/check/runtime.mjs +280 -0
- package/src/check.mjs +198 -208
- package/src/cli/authoring.mjs +213 -0
- package/src/cli/project.mjs +302 -0
- package/src/cli.mjs +222 -250
- package/src/config.mjs +1 -1
- package/src/contributions.mjs +75 -25
- package/src/db-layer.mjs +13 -5
- package/src/deploy/cdn.mjs +94 -16
- package/src/deploy/files.mjs +100 -21
- package/src/deploy/fit.mjs +127 -23
- package/src/deploy/hosts.mjs +90 -20
- 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 +228 -114
- package/src/deploy/needs.mjs +88 -31
- 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 -43
- 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 +137 -138
- 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 +193 -95
- 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/load-config.mjs
CHANGED
|
@@ -7,23 +7,56 @@ import { join, relative } from 'node:path'
|
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
8
8
|
import ts from 'typescript'
|
|
9
9
|
import { z } from 'zod'
|
|
10
|
+
import { createSchemas } from './load-config/schema.mjs'
|
|
10
11
|
import { diagnostic, locate } from './diag.mjs'
|
|
11
12
|
import { checkSite } from './site.mjs'
|
|
12
13
|
import { contributions } from './contributions.mjs'
|
|
13
14
|
import { bindSlots } from './slots.mjs'
|
|
14
15
|
|
|
15
|
-
export const configNames = [
|
|
16
|
-
|
|
16
|
+
export const configNames = [
|
|
17
|
+
'weft.config.ts',
|
|
18
|
+
'weft.config.mts',
|
|
19
|
+
'weft.config.js',
|
|
20
|
+
'weft.config.mjs',
|
|
21
|
+
'weft.config.json',
|
|
22
|
+
'weft.config.yaml',
|
|
23
|
+
'weft.config.yml',
|
|
24
|
+
]
|
|
25
|
+
export const builtinNames = [
|
|
26
|
+
'add',
|
|
27
|
+
'map',
|
|
28
|
+
'dev',
|
|
29
|
+
'build',
|
|
30
|
+
'start',
|
|
31
|
+
'db',
|
|
32
|
+
'test',
|
|
33
|
+
'check',
|
|
34
|
+
'doctor',
|
|
35
|
+
'fix',
|
|
36
|
+
'tokens',
|
|
37
|
+
'browser',
|
|
38
|
+
'mcp',
|
|
39
|
+
'tasks',
|
|
40
|
+
'run',
|
|
41
|
+
'create',
|
|
42
|
+
'docs',
|
|
43
|
+
'agents-md',
|
|
44
|
+
'plugins',
|
|
45
|
+
'deploy',
|
|
46
|
+
]
|
|
17
47
|
|
|
18
|
-
const configModule = pathToFileURL(
|
|
48
|
+
const configModule = pathToFileURL(
|
|
49
|
+
join(fileURLToPath(new URL('.', import.meta.url)), 'config.mjs'),
|
|
50
|
+
).href
|
|
19
51
|
const nativeRequire = createRequire(import.meta.url)
|
|
20
52
|
/** Node's sync module hooks. Bun has none, and Deno's break `.node` requires, like Rolldown's; both read TypeScript without help (`importConfig`). */
|
|
21
|
-
const registerHooks =
|
|
53
|
+
const registerHooks =
|
|
54
|
+
process.versions.bun || process.versions.deno ? null : nodeModule.registerHooks
|
|
22
55
|
/** `weft.config.ts.weft-load-<n>`: a fresh module id per load, so a long-lived `weft mcp` sees edits. */
|
|
23
56
|
const tag = /\.weft-load-\d+$/
|
|
24
57
|
let hooked = false
|
|
25
58
|
|
|
26
|
-
const transpile = path =>
|
|
59
|
+
const transpile = (path) =>
|
|
27
60
|
ts.transpileModule(readFileSync(path, 'utf8'), {
|
|
28
61
|
fileName: path,
|
|
29
62
|
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
|
|
@@ -38,7 +71,8 @@ function hook() {
|
|
|
38
71
|
hooked = true
|
|
39
72
|
registerHooks({
|
|
40
73
|
resolve(specifier, context, next) {
|
|
41
|
-
if (tag.test(specifier))
|
|
74
|
+
if (tag.test(specifier))
|
|
75
|
+
return { url: pathToFileURL(specifier).href, format: 'module', shortCircuit: true }
|
|
42
76
|
if (specifier === '@weft-js/core/config' || specifier === '@weft-js/cli/config') {
|
|
43
77
|
try {
|
|
44
78
|
return next(specifier, context)
|
|
@@ -48,7 +82,16 @@ function hook() {
|
|
|
48
82
|
}
|
|
49
83
|
// the app's own imports are bundler-style (`./src/expression`): try what Vite would before failing
|
|
50
84
|
if (/^\.{1,2}\//.test(specifier) && !/\.[cm]?[jt]sx?$|\.json$/.test(specifier)) {
|
|
51
|
-
for (const end of [
|
|
85
|
+
for (const end of [
|
|
86
|
+
'',
|
|
87
|
+
'.ts',
|
|
88
|
+
'.tsx',
|
|
89
|
+
'.mts',
|
|
90
|
+
'.js',
|
|
91
|
+
'/index.ts',
|
|
92
|
+
'/index.tsx',
|
|
93
|
+
'/index.js',
|
|
94
|
+
]) {
|
|
52
95
|
try {
|
|
53
96
|
return next(specifier + end, context)
|
|
54
97
|
} catch {}
|
|
@@ -61,9 +104,14 @@ function hook() {
|
|
|
61
104
|
const path = fileURLToPath(url)
|
|
62
105
|
if (tag.test(path)) {
|
|
63
106
|
const real = path.replace(tag, '')
|
|
64
|
-
return {
|
|
107
|
+
return {
|
|
108
|
+
format: 'module',
|
|
109
|
+
source: /\.[mc]?ts$/.test(real) ? transpile(real) : readFileSync(real, 'utf8'),
|
|
110
|
+
shortCircuit: true,
|
|
111
|
+
}
|
|
65
112
|
}
|
|
66
|
-
if (/\.[mc]?ts$/.test(path) && !path.includes('/node_modules/'))
|
|
113
|
+
if (/\.[mc]?ts$/.test(path) && !path.includes('/node_modules/'))
|
|
114
|
+
return { format: 'module', source: transpile(path), shortCircuit: true }
|
|
67
115
|
return next(url, context)
|
|
68
116
|
},
|
|
69
117
|
})
|
|
@@ -80,7 +128,13 @@ async function importConfig(file) {
|
|
|
80
128
|
if (!/weft\/config|@weft-js\/cli\/config/.test(error.message)) throw error
|
|
81
129
|
}
|
|
82
130
|
const copy = `${file}.weft-load-${generation++}.mjs`
|
|
83
|
-
writeFileSync(
|
|
131
|
+
writeFileSync(
|
|
132
|
+
copy,
|
|
133
|
+
transpile(file).replace(
|
|
134
|
+
/(['"])(?:weft\/config|@weft-js\/cli\/config)\1/g,
|
|
135
|
+
JSON.stringify(configModule),
|
|
136
|
+
),
|
|
137
|
+
)
|
|
84
138
|
try {
|
|
85
139
|
return (await import(pathToFileURL(copy).href)).default ?? {}
|
|
86
140
|
} finally {
|
|
@@ -88,110 +142,12 @@ async function importConfig(file) {
|
|
|
88
142
|
}
|
|
89
143
|
}
|
|
90
144
|
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const task = z.strictObject({
|
|
95
|
-
run: z.string().min(1),
|
|
96
|
-
description: z.string().optional(),
|
|
97
|
-
sideEffects: z.array(z.string()).optional(),
|
|
98
|
-
env: z.array(z.string()).optional(),
|
|
99
|
-
args: z.union([zodSchema, z.record(z.string(), z.unknown())]).optional(),
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
const pluginEntry = z.looseObject({
|
|
103
|
-
name: z.string().min(1),
|
|
104
|
-
version: z.string().optional(),
|
|
105
|
-
provides: z.array(z.union([z.string(), z.strictObject({ slot: z.string(), key: z.string() })])).optional(),
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
const image = z.strictObject({ url: z.string().min(1), width: z.int().positive().optional(), height: z.int().positive().optional(), alt: z.string().optional() })
|
|
109
|
-
|
|
110
|
-
const hex = z.string().regex(/^#[0-9a-fA-F]{6}$/, { error: 'must be #rrggbb' })
|
|
111
|
-
const colors = { background: hex.optional(), text: hex.optional(), subtle: hex.optional(), accent: hex.optional() }
|
|
112
|
-
const cardFont = z.strictObject({ family: z.string().regex(/^[\w -]+$/, { error: 'must be a font family name' }), src: z.string().min(1), weight: z.number().optional() })
|
|
113
|
-
const card = z.strictObject({ ...colors, scheme: z.enum(['dark', 'light']).optional(), dark: z.strictObject(colors).optional(), light: z.strictObject(colors).optional(), fonts: z.strictObject({ display: cardFont.optional(), body: cardFont.optional() }).optional() })
|
|
114
|
-
|
|
115
|
-
const site = z.strictObject({
|
|
116
|
-
headers: z.record(z.string(), z.record(z.string(), z.string())).optional(),
|
|
117
|
-
name: z.string().min(1).optional(),
|
|
118
|
-
url: z.url({ protocol: /^https?$/, error: 'must be an absolute http(s) URL' }).optional(),
|
|
119
|
-
title: z.string().includes('%s', { error: 'must contain `%s` (the page title)' }).optional(),
|
|
120
|
-
description: z.string().optional(),
|
|
121
|
-
locale: z.string().regex(/^[a-z]{2,3}(_[A-Z]{2})?$/, { error: 'must look like `en_US`' }).optional(),
|
|
122
|
-
inlineCss: z.boolean().optional(),
|
|
123
|
-
criticalCss: z.boolean().optional(),
|
|
124
|
-
criticalKeep: z.array(z.string()).optional(),
|
|
125
|
-
image: z.union([z.literal('auto'), z.literal(false), z.string().min(1), image]).optional(),
|
|
126
|
-
card: card.optional(),
|
|
127
|
-
head: z.string().optional(),
|
|
128
|
-
twitter: z
|
|
129
|
-
.strictObject({ card: z.enum(['summary', 'summary_large_image', 'app', 'player']).optional(), site: z.string().optional(), creator: z.string().optional() })
|
|
130
|
-
.optional(),
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
export const configSchema = z.strictObject({
|
|
134
|
-
scope: z.strictObject({ write: globs.optional(), read: globs.optional(), forbid: globs.optional() }).optional(),
|
|
135
|
-
tasks: z.record(z.string(), task).optional(),
|
|
136
|
-
environments: z.record(z.string(), z.looseObject({ url: z.string().optional() })).optional(),
|
|
137
|
-
test: z
|
|
138
|
-
.strictObject({
|
|
139
|
-
include: globs.optional(),
|
|
140
|
-
lanes: z.array(z.enum(['fast', 'browser', 'e2e'])).optional(),
|
|
141
|
-
gate: z.strictObject({ mutationBudgetSec: z.number().positive().optional(), maxMutants: z.number().int().positive().optional() }).optional(),
|
|
142
|
-
})
|
|
143
|
-
.optional(),
|
|
144
|
-
build: z.strictObject({
|
|
145
|
-
audit: z.union([z.boolean(), z.strictObject({ routes: z.array(z.string()).optional(), all: z.boolean().optional(), max: z.number().int().positive().optional() })]).optional(),
|
|
146
|
-
unsafeAllow: z.record(z.string().regex(/^(all|[A-Z]|[A-Z]+\d{3})$/, 'a code (R002), a family (L) or all'), z.string().min(1, 'say why')).optional(),
|
|
147
|
-
}).optional(),
|
|
148
|
-
check: z.strictObject({ include: globs.optional(), exclude: globs.optional(), design: z.boolean().optional(), routes: z.boolean().optional(), props: z.union([z.boolean(), z.literal('manual')]).optional() }).optional(),
|
|
149
|
-
plugins: z.array(pluginEntry).optional(),
|
|
150
|
-
slots: z.record(z.string(), z.union([z.string(), z.record(z.string(), z.string())])).optional(),
|
|
151
|
-
site: site.optional(),
|
|
152
|
-
runtime: z.enum(['node', 'bun', 'deno']).optional(),
|
|
153
|
-
hydration: z.enum(['lazy', 'partial', 'full']).optional(),
|
|
154
|
-
rust: z.strictObject({
|
|
155
|
-
unions: z.literal('allow').optional(),
|
|
156
|
-
services: z.record(z.string(), z.strictObject({ crate: z.string().optional(), external: z.boolean().optional(), wasm: z.literal(false).optional() })).optional(),
|
|
157
|
-
}).optional(),
|
|
158
|
-
/** `weft deploy` (research 46): names, regions and sizes. Credentials come from the environment, never here (E008). */
|
|
159
|
-
deploy: z.strictObject({
|
|
160
|
-
target: z.enum(['netlify', 'vercel', 'cloudflare', 'fly', 'render']).optional(),
|
|
161
|
-
netlify: z.strictObject({ site: z.string().min(1).optional() }).optional(),
|
|
162
|
-
vercel: z.strictObject({ project: z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/i, 'a name: letters, digits and dashes').optional() }).optional(),
|
|
163
|
-
cloudflare: z.strictObject({ name: z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/i, 'a name: letters, digits and dashes').optional() }).optional(),
|
|
164
|
-
fly: z.strictObject({
|
|
165
|
-
app: z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/i, 'a name: letters, digits and dashes').optional(), region: z.string().regex(/^[a-z]{3}$/, 'a Fly region code, like `iad`').optional(), memory: z.string().regex(/^\d+(mb|gb)$/, 'like `512mb`').optional(),
|
|
166
|
-
volume: z.strictObject({ name: z.string().regex(/^[a-z0-9_]+$/, 'lowercase letters, digits and underscores').optional(), path: z.string().startsWith('/').optional(), sizeGb: z.number().int().positive().optional() }).optional(),
|
|
167
|
-
}).optional(),
|
|
168
|
-
render: z.strictObject({
|
|
169
|
-
service: z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/i, 'a name: letters, digits and dashes').optional(), plan: z.string().optional(), region: z.string().optional(),
|
|
170
|
-
disk: z.strictObject({ path: z.string().startsWith('/').optional(), sizeGb: z.number().int().positive().optional() }).optional(),
|
|
171
|
-
}).optional(),
|
|
172
|
-
ci: z.union([z.literal('github'), z.literal(false)]).optional(),
|
|
173
|
-
branch: z.string().min(1).optional(),
|
|
174
|
-
cdn: z.strictObject({
|
|
175
|
-
proxy: z.literal('cloudflare').optional(),
|
|
176
|
-
images: z.strictObject({ provider: z.literal('cloudinary'), cloud: z.string().regex(/^[\w-]+$/, "the cloud's name, not its URL").optional() }).optional(),
|
|
177
|
-
}).optional(),
|
|
178
|
-
}).optional(),
|
|
179
|
-
/** Named targets for a query's `live: { verify: 'staging' }`. `db` is an env var's name. */
|
|
180
|
-
live: z.strictObject({
|
|
181
|
-
targets: z.record(z.string(), z.strictObject({ url: z.string().optional(), db: z.string().optional(), production: z.boolean().optional() })).optional(),
|
|
182
|
-
}).optional(),
|
|
183
|
-
trust: z.enum(['enforce', 'warn']).optional(),
|
|
184
|
-
})
|
|
145
|
+
const { configSchema, dataSchema } = createSchemas(z)
|
|
146
|
+
export { configSchema }
|
|
185
147
|
|
|
186
|
-
|
|
187
|
-
const dataSchema = configSchema.extend({
|
|
188
|
-
$schema: z.string().optional(),
|
|
189
|
-
plugins: z.array(z.union([z.string(), z.tuple([z.string(), z.unknown()]), pluginEntry])).optional(),
|
|
190
|
-
})
|
|
148
|
+
export const isDataConfig = (file) => /\.(json|ya?ml)$/.test(file)
|
|
191
149
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const configFiles = root => configNames.map(name => join(root, name)).filter(existsSync)
|
|
150
|
+
const configFiles = (root) => configNames.map((name) => join(root, name)).filter(existsSync)
|
|
195
151
|
|
|
196
152
|
export function findConfig(root) {
|
|
197
153
|
return configFiles(root)[0] ?? null
|
|
@@ -200,16 +156,24 @@ export function findConfig(root) {
|
|
|
200
156
|
/** The schema `$schema` points at, for completion in `weft.config.json` and `.yaml`. */
|
|
201
157
|
export function writeConfigSchema(root) {
|
|
202
158
|
mkdirSync(join(root, '.weft'), { recursive: true })
|
|
203
|
-
writeFileSync(
|
|
159
|
+
writeFileSync(
|
|
160
|
+
join(root, '.weft', 'config.schema.json'),
|
|
161
|
+
`${JSON.stringify(z.toJSONSchema(dataSchema, { unrepresentable: 'any' }), null, 2)}\n`,
|
|
162
|
+
)
|
|
204
163
|
}
|
|
205
164
|
|
|
206
165
|
/** `@weft-js/text` → `text`, `@weft-js/db/plugin-native` → `native`: the name a plugin's named export goes by. */
|
|
207
|
-
export const pluginName = pkg
|
|
166
|
+
export const pluginName = (pkg) =>
|
|
167
|
+
pkg
|
|
168
|
+
.replace(/^@[^/]+\/(?:[^/]+\/)?/, '')
|
|
169
|
+
.replace(/^plugin-/, '')
|
|
170
|
+
.replace(/[^a-zA-Z0-9]+(.)?/g, (_, c = '') => c.toUpperCase())
|
|
208
171
|
|
|
209
172
|
/** A plugin module's factory, and its import name: the default, or a named export (`surface-keel` may export `keel`). */
|
|
210
173
|
export function pluginFactory(mod, pkg) {
|
|
211
|
-
if (typeof mod.default === 'function')
|
|
212
|
-
|
|
174
|
+
if (typeof mod.default === 'function')
|
|
175
|
+
return { value: mod.default, name: pluginName(pkg), named: false }
|
|
176
|
+
const name = [pluginName(pkg), pkg.split('-').at(-1)].find((n) => typeof mod[n] === 'function')
|
|
213
177
|
return name ? { value: mod[name], name, named: true } : null
|
|
214
178
|
}
|
|
215
179
|
|
|
@@ -236,12 +200,38 @@ function loadPlugin(root, entry) {
|
|
|
236
200
|
function readDataConfig(root, file, text) {
|
|
237
201
|
const data = file.endsWith('.json') ? JSON.parse(text) : nativeRequire('yaml').parse(text)
|
|
238
202
|
const { $schema: _schema, ...config } = data ?? {}
|
|
239
|
-
if (Array.isArray(config.plugins)) config.plugins = config.plugins.map(p => loadPlugin(root, p))
|
|
203
|
+
if (Array.isArray(config.plugins)) config.plugins = config.plugins.map((p) => loadPlugin(root, p))
|
|
240
204
|
return config
|
|
241
205
|
}
|
|
242
206
|
|
|
243
207
|
let generation = 0
|
|
244
208
|
|
|
209
|
+
function validateTasks(config, report) {
|
|
210
|
+
const tasks = config.tasks ?? {}
|
|
211
|
+
const commands = new Map()
|
|
212
|
+
for (const [name, t] of Object.entries(tasks)) {
|
|
213
|
+
if (builtinNames.includes(name))
|
|
214
|
+
report('P003', `Task \`${name}\` has the same name as the built-in \`weft ${name}\`.`, name)
|
|
215
|
+
const twin = commands.get(t.run.trim())
|
|
216
|
+
if (twin)
|
|
217
|
+
report('P003', `Task \`${name}\` runs the same command as \`${twin}\` (\`${t.run}\`).`, name)
|
|
218
|
+
else commands.set(t.run.trim(), name)
|
|
219
|
+
const missing = [
|
|
220
|
+
!t.description?.trim() && '`description`',
|
|
221
|
+
!t.sideEffects && '`sideEffects`',
|
|
222
|
+
].filter(Boolean)
|
|
223
|
+
if (missing.length) report('P004', `Task \`${name}\` has no ${missing.join(' or ')}.`, name)
|
|
224
|
+
for (const env of t.env ?? []) {
|
|
225
|
+
if (config.environments && !(env in config.environments))
|
|
226
|
+
report(
|
|
227
|
+
'P007',
|
|
228
|
+
`Task \`${name}\` allows environment \`${env}\`, which \`environments\` does not define.`,
|
|
229
|
+
name,
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
245
235
|
/**
|
|
246
236
|
* @param {string} root project root
|
|
247
237
|
* @returns {Promise<{ file: string | null, config: import('./config').WeftConfig, diagnostics: object[], slots: object, contributions: ReturnType<typeof contributions> }>}
|
|
@@ -251,18 +241,31 @@ export async function loadConfig(root) {
|
|
|
251
241
|
const diagnostics = []
|
|
252
242
|
const rel = file ? relative(root, file).split('\\').join('/') : 'weft.config.ts'
|
|
253
243
|
const text = file ? readFileSync(file, 'utf8') : ''
|
|
254
|
-
const report = (code, why, near, extra) =>
|
|
255
|
-
|
|
256
|
-
|
|
244
|
+
const report = (code, why, near, extra) =>
|
|
245
|
+
diagnostics.push(diagnostic({ code, file: rel, why, ...locate(text, near), ...extra }))
|
|
246
|
+
const others = configFiles(root)
|
|
247
|
+
.slice(1)
|
|
248
|
+
.map((f) => relative(root, f))
|
|
249
|
+
if (others.length)
|
|
250
|
+
report(
|
|
251
|
+
'P010',
|
|
252
|
+
`\`${rel}\` is the config in use, and \`${others.join('`, `')}\` would be ignored.`,
|
|
253
|
+
null,
|
|
254
|
+
)
|
|
257
255
|
|
|
258
256
|
let config = {}
|
|
259
257
|
if (file) {
|
|
260
258
|
if (registerHooks) hook()
|
|
261
259
|
try {
|
|
262
|
-
config = isDataConfig(file)
|
|
260
|
+
config = isDataConfig(file)
|
|
261
|
+
? readDataConfig(root, file, text)
|
|
262
|
+
: registerHooks
|
|
263
|
+
? (nativeRequire(`${file}.weft-load-${generation++}`).default ?? {})
|
|
264
|
+
: await importConfig(file)
|
|
263
265
|
} catch (error) {
|
|
264
266
|
// A plugin factory rejecting its options throws while the config module evaluates (@weft-js/core/plugin).
|
|
265
|
-
if (error?.code === 'P115')
|
|
267
|
+
if (error?.code === 'P115')
|
|
268
|
+
report('P115', error.message.split('\n')[0], error.message.match(/^`([^`]+)`/)?.[1] ?? null)
|
|
266
269
|
else report('P007', `weft.config could not be loaded: ${error.message.split('\n')[0]}`, null)
|
|
267
270
|
return { file, config: {}, diagnostics, slots: bindSlots({}, () => {}), contributions: [] }
|
|
268
271
|
}
|
|
@@ -270,32 +273,28 @@ export async function loadConfig(root) {
|
|
|
270
273
|
if (!parsed.success) {
|
|
271
274
|
for (const issue of parsed.error.issues) {
|
|
272
275
|
const path = issue.path.join('.')
|
|
273
|
-
report(
|
|
276
|
+
report(
|
|
277
|
+
'P007',
|
|
278
|
+
`\`${path || '(root)'}\`: ${issue.message}`,
|
|
279
|
+
String(issue.path.at(-1) ?? ''),
|
|
280
|
+
{ help: 'Fix the key at the path shown (see `weft docs config`).' },
|
|
281
|
+
)
|
|
274
282
|
}
|
|
275
283
|
return { file, config, diagnostics, slots: bindSlots({}, () => {}), contributions: [] }
|
|
276
284
|
}
|
|
277
285
|
}
|
|
278
286
|
|
|
279
|
-
|
|
280
|
-
const commands = new Map()
|
|
281
|
-
for (const [name, t] of Object.entries(tasks)) {
|
|
282
|
-
if (builtinNames.includes(name)) report('P003', `Task \`${name}\` has the same name as the built-in \`weft ${name}\`.`, name)
|
|
283
|
-
const twin = commands.get(t.run.trim())
|
|
284
|
-
if (twin) report('P003', `Task \`${name}\` runs the same command as \`${twin}\` (\`${t.run}\`).`, name)
|
|
285
|
-
else commands.set(t.run.trim(), name)
|
|
286
|
-
const missing = [!t.description?.trim() && '`description`', !t.sideEffects && '`sideEffects`'].filter(Boolean)
|
|
287
|
-
if (missing.length) report('P004', `Task \`${name}\` has no ${missing.join(' or ')}.`, name)
|
|
288
|
-
for (const env of t.env ?? []) {
|
|
289
|
-
if (config.environments && !(env in config.environments)) report('P007', `Task \`${name}\` allows environment \`${env}\`, which \`environments\` does not define.`, name)
|
|
290
|
-
}
|
|
291
|
-
}
|
|
287
|
+
validateTasks(config, report)
|
|
292
288
|
|
|
293
289
|
const seen = new Set()
|
|
294
290
|
for (const p of config.plugins ?? []) {
|
|
295
|
-
if (seen.has(p.name))
|
|
291
|
+
if (seen.has(p.name))
|
|
292
|
+
report('P116', `Plugin \`${p.name}\` is listed twice in \`plugins\`.`, p.name)
|
|
296
293
|
seen.add(p.name)
|
|
297
294
|
}
|
|
298
|
-
const unique = (config.plugins ?? []).filter(
|
|
295
|
+
const unique = (config.plugins ?? []).filter(
|
|
296
|
+
(p, i, all) => all.findIndex((q) => q.name === p.name) === i,
|
|
297
|
+
)
|
|
299
298
|
const contributed = contributions(unique, report, { root })
|
|
300
299
|
const slots = bindSlots({ ...config, plugins: unique }, report)
|
|
301
300
|
if (config.site) checkSite(config.site, report)
|
package/src/map.mjs
CHANGED
|
@@ -9,39 +9,68 @@ import { loadConfig } from './load-config.mjs'
|
|
|
9
9
|
import { dbLayer } from './db-layer.mjs'
|
|
10
10
|
import { listSurfaces } from './surface-checks.mjs'
|
|
11
11
|
|
|
12
|
-
const read = (root, rel) =>
|
|
13
|
-
|
|
12
|
+
const read = (root, rel) =>
|
|
13
|
+
existsSync(join(root, rel)) ? readFileSync(join(root, rel), 'utf8') : ''
|
|
14
|
+
const names = (text, re) => [...text.matchAll(re)].map((m) => m[1])
|
|
14
15
|
/** Up to `max` names: enough to recognise the app, while the row's call gives the whole list. */
|
|
15
|
-
const sample = (list, max = 6) =>
|
|
16
|
+
const sample = (list, max = 6) =>
|
|
17
|
+
list.length > max ? `${list.slice(0, max).join(', ')}, +${list.length - max}` : list.join(', ')
|
|
16
18
|
|
|
17
19
|
function serverFunctions(root) {
|
|
18
|
-
const files = discover(root, {
|
|
20
|
+
const files = discover(root, {
|
|
21
|
+
include: ['src/server/**/*.{ts,mts}', 'src/**/*.server.{ts,mts}'],
|
|
22
|
+
exclude: [],
|
|
23
|
+
})
|
|
19
24
|
const out = { functions: [], channels: [] }
|
|
20
25
|
for (const rel of files) {
|
|
21
|
-
const found = [
|
|
22
|
-
|
|
26
|
+
const found = [
|
|
27
|
+
...read(root, rel).matchAll(
|
|
28
|
+
/export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(query|mutation|stream|channel)\s*\(/g,
|
|
29
|
+
),
|
|
30
|
+
]
|
|
31
|
+
const fns = found.filter((m) => m[2] !== 'channel').map((m) => m[1])
|
|
23
32
|
if (fns.length) out.functions.push(`\`${rel}\`: ${fns.join(', ')}`)
|
|
24
|
-
out.channels.push(...found.filter(m => m[2] === 'channel').map(m => m[1]))
|
|
33
|
+
out.channels.push(...found.filter((m) => m[2] === 'channel').map((m) => m[1]))
|
|
25
34
|
}
|
|
26
35
|
return out
|
|
27
36
|
}
|
|
28
37
|
|
|
29
|
-
const sqlText = (root, dir) =>
|
|
38
|
+
const sqlText = (root, dir) =>
|
|
39
|
+
existsSync(join(root, dir))
|
|
40
|
+
? readdirSync(join(root, dir))
|
|
41
|
+
.filter((f) => f.endsWith('.sql'))
|
|
42
|
+
.map((f) => read(root, join(dir, f)))
|
|
43
|
+
.join('\n')
|
|
44
|
+
: ''
|
|
30
45
|
const ENGINE = { PrismaLibSql: 'sqlite', PrismaPg: 'postgres', PrismaMariaDb: 'mysql' }
|
|
31
46
|
|
|
32
47
|
export function database(root, layer) {
|
|
33
48
|
if (!layer) return null
|
|
34
49
|
const entry = read(root, 'src/db/index.server.ts')
|
|
35
|
-
const engine =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
const engine =
|
|
51
|
+
entry.match(/@weft-js\/(?:start\/db|db\/plugin-native)\/(\w+)/)?.[1] ??
|
|
52
|
+
ENGINE[entry.match(/Prisma(?:LibSql|Pg|MariaDb)/)?.[0]] ??
|
|
53
|
+
'unknown'
|
|
54
|
+
const tables =
|
|
55
|
+
layer.layer === 'prisma'
|
|
56
|
+
? names(read(root, layer.schema), /^model\s+(\w+)/gm)
|
|
57
|
+
: layer.layer === 'native'
|
|
58
|
+
? names(
|
|
59
|
+
sqlText(root, layer.migrations),
|
|
60
|
+
/create\s+table\s+(?:if\s+not\s+exists\s+)?["`]?(\w+)/gi,
|
|
61
|
+
)
|
|
62
|
+
: names(
|
|
63
|
+
read(root, 'src/db/schema.ts'),
|
|
64
|
+
/export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*\w+Table\s*\(/g,
|
|
65
|
+
)
|
|
39
66
|
return { engine, layer: layer.layer, tables }
|
|
40
67
|
}
|
|
41
68
|
|
|
42
69
|
function routes(root) {
|
|
43
70
|
const files = discover(root, { include: ['src/routes/**/*.{ts,tsx}'], exclude: [] })
|
|
44
|
-
return files
|
|
71
|
+
return files
|
|
72
|
+
.map((f) => f.replace(/^src\/routes\//, '').replace(/\.[jt]sx?$/, ''))
|
|
73
|
+
.filter((f) => !/(^|\/)_/.test(f))
|
|
45
74
|
}
|
|
46
75
|
|
|
47
76
|
/** The map as data. */
|
|
@@ -51,44 +80,82 @@ export async function agentMap(root) {
|
|
|
51
80
|
const { config } = loaded
|
|
52
81
|
const pages = routes(root)
|
|
53
82
|
const fns = serverFunctions(root)
|
|
54
|
-
const env = ['src/env.ts', 'src/env.server.ts'].find(f => existsSync(join(root, f)))
|
|
83
|
+
const env = ['src/env.ts', 'src/env.server.ts'].find((f) => existsSync(join(root, f)))
|
|
55
84
|
const layer = await dbLayer(root, loaded)
|
|
56
85
|
const db = database(root, layer)
|
|
57
86
|
const authFile = read(root, 'src/auth.server.ts')
|
|
58
87
|
const auth = authFile && (authFile.includes('better-auth') ? 'better-auth' : 'custom')
|
|
59
|
-
const surfaces = listSurfaces(root).map(s => s.name)
|
|
60
|
-
const styled = existsSync(join(root, '.weft/styles.json'))
|
|
61
|
-
|
|
88
|
+
const surfaces = listSurfaces(root).map((s) => s.name)
|
|
89
|
+
const styled = existsSync(join(root, '.weft/styles.json'))
|
|
90
|
+
? Object.keys(JSON.parse(read(root, '.weft/styles.json')).groups ?? {})
|
|
91
|
+
: []
|
|
92
|
+
const bound = Object.entries(loaded.slots ?? {})
|
|
93
|
+
.filter(([, v]) => typeof v === 'string' && !v.startsWith('weft:default'))
|
|
94
|
+
.map(([slot]) => slot)
|
|
62
95
|
const tasks = Object.keys(config.tasks ?? {})
|
|
63
96
|
const row = (want, call, have) => ({ want, call, ...(have && { have }) })
|
|
64
97
|
|
|
65
98
|
const rows = [
|
|
66
99
|
pages.length && row('Pages, their data and metadata', '`weft routes --meta`', sample(pages)),
|
|
67
|
-
fns.functions.length &&
|
|
100
|
+
fns.functions.length &&
|
|
101
|
+
row(
|
|
102
|
+
'Server functions',
|
|
103
|
+
'The file named here; signatures in `.weft/contract.json` after a build',
|
|
104
|
+
fns.functions.join('; '),
|
|
105
|
+
),
|
|
68
106
|
fns.channels.length && row('Channels', '`weft docs start`', sample(fns.channels)),
|
|
69
|
-
db &&
|
|
107
|
+
db &&
|
|
108
|
+
row(
|
|
109
|
+
'Tables and migrations',
|
|
110
|
+
layer.schema
|
|
111
|
+
? `\`${layer.schema}\`, then \`weft db generate\``
|
|
112
|
+
: `\`weft db generate <name>\`, then SQL in \`${layer.migrations}/\``,
|
|
113
|
+
sample(db.tables),
|
|
114
|
+
),
|
|
70
115
|
auth && row('Who the user is', '`ctx.user` on the server; `src/auth.server.ts`', auth),
|
|
71
|
-
row(
|
|
116
|
+
row(
|
|
117
|
+
'Styles',
|
|
118
|
+
'Before writing one: `weft style find <what it is>`',
|
|
119
|
+
styled.length ? sample(styled) : undefined,
|
|
120
|
+
),
|
|
72
121
|
env && row('Environment variables', `\`${env}\`; values in \`.env\``),
|
|
73
122
|
surfaces.length && row('Canvas and WebGL', '`weft surfaces`', sample(surfaces)),
|
|
74
|
-
...loaded.contributions
|
|
75
|
-
|
|
123
|
+
...loaded.contributions
|
|
124
|
+
.flatMap((c) => c.manifest.map ?? [])
|
|
125
|
+
.map((r) => row(r.want, r.call, r.have)),
|
|
126
|
+
(config.plugins?.length || bound.length) &&
|
|
127
|
+
row('Plugins and slots', '`weft plugins`, `weft plugins explain <slot>`', sample(bound)),
|
|
76
128
|
tasks.length && row('Project tasks', '`weft tasks --json`', sample(tasks)),
|
|
77
129
|
row('A rule or a diagnostic', '`weft docs <topic or code>`'),
|
|
78
130
|
row('The running app', '`weft browser snapshot`'),
|
|
79
131
|
].filter(Boolean)
|
|
80
132
|
|
|
81
|
-
return {
|
|
133
|
+
return {
|
|
134
|
+
version: 1,
|
|
135
|
+
app: config.site?.name ?? null,
|
|
136
|
+
stack: {
|
|
137
|
+
runtime: config.runtime ?? 'node',
|
|
138
|
+
ui: config.plugins?.map((p) => p.name) ?? [],
|
|
139
|
+
db,
|
|
140
|
+
auth: auth || null,
|
|
141
|
+
},
|
|
142
|
+
rows,
|
|
143
|
+
}
|
|
82
144
|
}
|
|
83
145
|
|
|
84
146
|
export function formatMap(map) {
|
|
85
147
|
const { stack } = map
|
|
86
|
-
const parts = [
|
|
87
|
-
|
|
148
|
+
const parts = [
|
|
149
|
+
`runtime ${stack.runtime}`,
|
|
150
|
+
stack.db && `${stack.db.layer} on ${stack.db.engine}`,
|
|
151
|
+
stack.auth,
|
|
152
|
+
...stack.ui,
|
|
153
|
+
].filter(Boolean)
|
|
154
|
+
const table = map.rows.map((r) => `| ${r.want} | ${r.call} | ${r.have ?? ''} |`)
|
|
88
155
|
return [
|
|
89
156
|
`# ${map.app ?? 'This app'}: agent map`,
|
|
90
157
|
'',
|
|
91
|
-
|
|
158
|
+
"<!-- Generated by `weft map` from this app's config and code. Do not edit. -->",
|
|
92
159
|
'',
|
|
93
160
|
`Stack: ${parts.join(', ')}.`,
|
|
94
161
|
'',
|