@weft-js/cli 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +534 -54
- 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/infer/worker.cjs
CHANGED
|
@@ -11,7 +11,11 @@ const { Session } = require('./session.cjs')
|
|
|
11
11
|
const { root, write, debounce = 25, settle = 400, rest = 120_000 } = workerData
|
|
12
12
|
const session = new Session(ts, root)
|
|
13
13
|
const pending = new Set()
|
|
14
|
-
let timer = null,
|
|
14
|
+
let timer = null,
|
|
15
|
+
running = false,
|
|
16
|
+
last = '',
|
|
17
|
+
writes = null,
|
|
18
|
+
resting = null
|
|
15
19
|
|
|
16
20
|
function publish(list) {
|
|
17
21
|
const text = JSON.stringify(list)
|
|
@@ -24,13 +28,28 @@ function publish(list) {
|
|
|
24
28
|
return true
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
const applyEdits = (text, edits) =>
|
|
31
|
+
const applyEdits = (text, edits) =>
|
|
32
|
+
[...edits]
|
|
33
|
+
.sort((a, b) => b.start - a.start)
|
|
34
|
+
.reduce((t, e) => t.slice(0, e.start) + e.text + t.slice(e.end), text)
|
|
28
35
|
|
|
29
36
|
/** Exported types go into the source once the file has been quiet, and only if it's still the text they were made from. */
|
|
30
37
|
function scheduleWrites(findings) {
|
|
31
38
|
clearTimeout(writes)
|
|
32
39
|
if (!write) return
|
|
33
|
-
const jobs = [...findings].flatMap(([file, list]) => {
|
|
40
|
+
const jobs = [...findings].flatMap(([file, list]) => {
|
|
41
|
+
const edits = list.filter((f) => f.safe && f.edits).flatMap((f) => f.edits)
|
|
42
|
+
return edits.length
|
|
43
|
+
? [
|
|
44
|
+
{
|
|
45
|
+
file,
|
|
46
|
+
edits,
|
|
47
|
+
hash: session.engine.facts.get(file)?.hash,
|
|
48
|
+
codes: list.map((f) => f.code),
|
|
49
|
+
},
|
|
50
|
+
]
|
|
51
|
+
: []
|
|
52
|
+
})
|
|
34
53
|
if (!jobs.length) return
|
|
35
54
|
writes = setTimeout(() => {
|
|
36
55
|
for (const job of jobs) {
|
|
@@ -51,18 +70,39 @@ function rested() {
|
|
|
51
70
|
|
|
52
71
|
async function first() {
|
|
53
72
|
running = true
|
|
54
|
-
try {
|
|
73
|
+
try {
|
|
74
|
+
await report(performance.now())
|
|
75
|
+
} catch (e) {
|
|
76
|
+
parentPort.postMessage({ type: 'failed', message: e.stack })
|
|
77
|
+
} finally {
|
|
78
|
+
running = false
|
|
79
|
+
}
|
|
55
80
|
await drain()
|
|
56
81
|
}
|
|
57
82
|
|
|
58
83
|
async function report(t, files) {
|
|
59
84
|
const { findings, errors } = await session.analyze(files)
|
|
60
85
|
const flat = []
|
|
61
|
-
for (const [file, list] of [...findings, ...errors])
|
|
86
|
+
for (const [file, list] of [...findings, ...errors])
|
|
87
|
+
for (const f of list)
|
|
88
|
+
flat.push({
|
|
89
|
+
file: relative(root, file),
|
|
90
|
+
line: session.engine.texts.get(file).slice(0, f.start).split('\n').length,
|
|
91
|
+
code: f.code,
|
|
92
|
+
severity: f.severity ?? 'error',
|
|
93
|
+
why: f.why,
|
|
94
|
+
help: f.help,
|
|
95
|
+
})
|
|
62
96
|
scheduleWrites(findings)
|
|
63
97
|
const changed = publish(flat)
|
|
64
98
|
rested()
|
|
65
|
-
parentPort.postMessage({
|
|
99
|
+
parentPort.postMessage({
|
|
100
|
+
type: 'done',
|
|
101
|
+
ms: performance.now() - t,
|
|
102
|
+
at: Date.now(),
|
|
103
|
+
changed,
|
|
104
|
+
diagnostics: flat,
|
|
105
|
+
})
|
|
66
106
|
}
|
|
67
107
|
|
|
68
108
|
async function drain() {
|
|
@@ -76,7 +116,10 @@ async function drain() {
|
|
|
76
116
|
let changed = false
|
|
77
117
|
for (const f of files) if (session.engine.set(f)) changed = true
|
|
78
118
|
if (session.host) session.touch(files)
|
|
79
|
-
if (!changed || !files.some(f => session.engine.relevant(f))) {
|
|
119
|
+
if (!changed || !files.some((f) => session.engine.relevant(f))) {
|
|
120
|
+
parentPort.postMessage({ type: 'idle', ms: performance.now() - t })
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
80
123
|
await report(t, files)
|
|
81
124
|
}
|
|
82
125
|
} catch (e) {
|
|
@@ -86,8 +129,13 @@ async function drain() {
|
|
|
86
129
|
}
|
|
87
130
|
}
|
|
88
131
|
|
|
89
|
-
parentPort.on('message', msg => {
|
|
90
|
-
if (msg === 'close') {
|
|
132
|
+
parentPort.on('message', (msg) => {
|
|
133
|
+
if (msg === 'close') {
|
|
134
|
+
clearTimeout(timer)
|
|
135
|
+
clearTimeout(writes)
|
|
136
|
+
session.close()
|
|
137
|
+
process.exit(0)
|
|
138
|
+
}
|
|
91
139
|
pending.add(msg)
|
|
92
140
|
clearTimeout(timer)
|
|
93
141
|
timer = setTimeout(() => void drain(), debounce)
|
package/src/jsx-scan.mjs
CHANGED
|
@@ -16,51 +16,53 @@ import ts from 'typescript'
|
|
|
16
16
|
* @returns {{ imports: Import[], elements: Element[], functions: Map<string, string> }}
|
|
17
17
|
*/
|
|
18
18
|
export function scanJsx(text, file = 'file.tsx') {
|
|
19
|
-
const sf = ts.createSourceFile(
|
|
19
|
+
const sf = ts.createSourceFile(
|
|
20
|
+
file,
|
|
21
|
+
text,
|
|
22
|
+
ts.ScriptTarget.Latest,
|
|
23
|
+
true,
|
|
24
|
+
file.endsWith('.jsx') ? ts.ScriptKind.JSX : ts.ScriptKind.TSX,
|
|
25
|
+
)
|
|
20
26
|
const imports = []
|
|
21
27
|
const elements = []
|
|
22
28
|
const functions = new Map()
|
|
23
29
|
const stack = []
|
|
24
30
|
|
|
25
|
-
const attrsOf = attributes =>
|
|
26
|
-
attributes.properties.map(p => {
|
|
27
|
-
if (ts.isJsxSpreadAttribute(p)) return { name: '...', kind: 'spread', expr: p.expression.getText(sf), start: p.getStart(sf), end: p.end }
|
|
28
|
-
const name = p.name.getText(sf)
|
|
29
|
-
const base = { name, start: p.getStart(sf), end: p.end }
|
|
30
|
-
if (!p.initializer) return { ...base, kind: 'bool' }
|
|
31
|
-
if (ts.isStringLiteral(p.initializer)) return { ...base, kind: 'string', value: p.initializer.text, valueStart: p.initializer.getStart(sf), valueEnd: p.initializer.end }
|
|
32
|
-
const e = p.initializer.expression
|
|
33
|
-
if (e && (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e))) return { ...base, kind: 'string', value: e.text, valueStart: e.getStart(sf), valueEnd: e.end }
|
|
34
|
-
return { ...base, kind: 'expr', expr: e ? e.getText(sf) : '', valueStart: e?.getStart(sf), valueEnd: e?.end }
|
|
35
|
-
})
|
|
36
|
-
|
|
37
31
|
const record = (open, node) => {
|
|
38
|
-
const attrs = attrsOf(open.attributes)
|
|
32
|
+
const attrs = attrsOf(sf, open.attributes)
|
|
39
33
|
const el = {
|
|
40
34
|
name: open.tagName.getText(sf),
|
|
41
35
|
attrs,
|
|
42
|
-
attr: n => attrs.find(a => a.name === n),
|
|
36
|
+
attr: (n) => attrs.find((a) => a.name === n),
|
|
43
37
|
ancestors: [...stack],
|
|
44
38
|
start: node.getStart(sf),
|
|
45
39
|
end: node.end,
|
|
46
40
|
tagEnd: open.tagName.end,
|
|
47
|
-
children: ts.isJsxElement(node) ? node.children.map(c => c.getText(sf)).join('') : '',
|
|
41
|
+
children: ts.isJsxElement(node) ? node.children.map((c) => c.getText(sf)).join('') : '',
|
|
48
42
|
}
|
|
49
43
|
elements.push(el)
|
|
50
44
|
return el
|
|
51
45
|
}
|
|
52
46
|
|
|
53
|
-
const visit = node => {
|
|
47
|
+
const visit = (node) => {
|
|
54
48
|
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
55
49
|
const source = node.moduleSpecifier.text
|
|
56
50
|
const c = node.importClause
|
|
57
51
|
const at = { source, start: node.getStart(sf), end: node.end }
|
|
58
52
|
if (c?.name) imports.push({ local: c.name.text, ...at })
|
|
59
|
-
if (c?.namedBindings && ts.isNamedImports(c.namedBindings))
|
|
53
|
+
if (c?.namedBindings && ts.isNamedImports(c.namedBindings))
|
|
54
|
+
for (const s of c.namedBindings.elements)
|
|
55
|
+
imports.push({ local: s.name.text, imported: s.propertyName?.text ?? s.name.text, ...at })
|
|
60
56
|
if (!c) imports.push({ local: '', ...at })
|
|
61
57
|
}
|
|
62
|
-
if (ts.isFunctionDeclaration(node) && node.name && node.body)
|
|
63
|
-
|
|
58
|
+
if (ts.isFunctionDeclaration(node) && node.name && node.body)
|
|
59
|
+
functions.set(node.name.text, node.body.getText(sf))
|
|
60
|
+
if (
|
|
61
|
+
ts.isVariableDeclaration(node) &&
|
|
62
|
+
ts.isIdentifier(node.name) &&
|
|
63
|
+
node.initializer &&
|
|
64
|
+
(ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))
|
|
65
|
+
) {
|
|
64
66
|
functions.set(node.name.text, node.initializer.body.getText(sf))
|
|
65
67
|
}
|
|
66
68
|
if (ts.isJsxSelfClosingElement(node)) {
|
|
@@ -82,5 +84,45 @@ export function scanJsx(text, file = 'file.tsx') {
|
|
|
82
84
|
/** The import an identifier in an expression (`{hero}`, `{hero.src}`) comes from, or undefined. */
|
|
83
85
|
export function importOf(scan, expr) {
|
|
84
86
|
const id = /^\s*([A-Za-z_$][\w$]*)/.exec(expr ?? '')?.[1]
|
|
85
|
-
return id ? scan.imports.find(i => i.local === id) : undefined
|
|
87
|
+
return id ? scan.imports.find((i) => i.local === id) : undefined
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function attrsOf(sf, attributes) {
|
|
91
|
+
return attributes.properties.map((p) => {
|
|
92
|
+
if (ts.isJsxSpreadAttribute(p))
|
|
93
|
+
return {
|
|
94
|
+
name: '...',
|
|
95
|
+
kind: 'spread',
|
|
96
|
+
expr: p.expression.getText(sf),
|
|
97
|
+
start: p.getStart(sf),
|
|
98
|
+
end: p.end,
|
|
99
|
+
}
|
|
100
|
+
const name = p.name.getText(sf)
|
|
101
|
+
const base = { name, start: p.getStart(sf), end: p.end }
|
|
102
|
+
if (!p.initializer) return { ...base, kind: 'bool' }
|
|
103
|
+
if (ts.isStringLiteral(p.initializer))
|
|
104
|
+
return {
|
|
105
|
+
...base,
|
|
106
|
+
kind: 'string',
|
|
107
|
+
value: p.initializer.text,
|
|
108
|
+
valueStart: p.initializer.getStart(sf),
|
|
109
|
+
valueEnd: p.initializer.end,
|
|
110
|
+
}
|
|
111
|
+
const e = p.initializer.expression
|
|
112
|
+
if (e && (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e)))
|
|
113
|
+
return {
|
|
114
|
+
...base,
|
|
115
|
+
kind: 'string',
|
|
116
|
+
value: e.text,
|
|
117
|
+
valueStart: e.getStart(sf),
|
|
118
|
+
valueEnd: e.end,
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
...base,
|
|
122
|
+
kind: 'expr',
|
|
123
|
+
expr: e ? e.getText(sf) : '',
|
|
124
|
+
valueStart: e?.getStart(sf),
|
|
125
|
+
valueEnd: e?.end,
|
|
126
|
+
}
|
|
127
|
+
})
|
|
86
128
|
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
function baseSchemas(z) {
|
|
2
|
+
const zodSchema = z.custom(
|
|
3
|
+
(v) => v != null && typeof v === 'object' && ('_zod' in v || '~standard' in v),
|
|
4
|
+
)
|
|
5
|
+
const globs = z.array(z.string())
|
|
6
|
+
|
|
7
|
+
const task = z.strictObject({
|
|
8
|
+
run: z.string().min(1),
|
|
9
|
+
description: z.string().optional(),
|
|
10
|
+
sideEffects: z.array(z.string()).optional(),
|
|
11
|
+
env: z.array(z.string()).optional(),
|
|
12
|
+
args: z.union([zodSchema, z.record(z.string(), z.unknown())]).optional(),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const pluginEntry = z.looseObject({
|
|
16
|
+
name: z.string().min(1),
|
|
17
|
+
version: z.string().optional(),
|
|
18
|
+
provides: z
|
|
19
|
+
.array(z.union([z.string(), z.strictObject({ slot: z.string(), key: z.string() })]))
|
|
20
|
+
.optional(),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const image = z.strictObject({
|
|
24
|
+
url: z.string().min(1),
|
|
25
|
+
width: z.int().positive().optional(),
|
|
26
|
+
height: z.int().positive().optional(),
|
|
27
|
+
alt: z.string().optional(),
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const hex = z.string().regex(/^#[0-9a-fA-F]{6}$/, { error: 'must be #rrggbb' })
|
|
31
|
+
const colors = {
|
|
32
|
+
background: hex.optional(),
|
|
33
|
+
text: hex.optional(),
|
|
34
|
+
subtle: hex.optional(),
|
|
35
|
+
accent: hex.optional(),
|
|
36
|
+
}
|
|
37
|
+
const cardFont = z.strictObject({
|
|
38
|
+
family: z.string().regex(/^[\w -]+$/, { error: 'must be a font family name' }),
|
|
39
|
+
src: z.string().min(1),
|
|
40
|
+
weight: z.number().optional(),
|
|
41
|
+
})
|
|
42
|
+
const card = z.strictObject({
|
|
43
|
+
...colors,
|
|
44
|
+
scheme: z.enum(['dark', 'light']).optional(),
|
|
45
|
+
dark: z.strictObject(colors).optional(),
|
|
46
|
+
light: z.strictObject(colors).optional(),
|
|
47
|
+
fonts: z.strictObject({ display: cardFont.optional(), body: cardFont.optional() }).optional(),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const site = z.strictObject({
|
|
51
|
+
headers: z.record(z.string(), z.record(z.string(), z.string())).optional(),
|
|
52
|
+
name: z.string().min(1).optional(),
|
|
53
|
+
url: z.url({ protocol: /^https?$/, error: 'must be an absolute http(s) URL' }).optional(),
|
|
54
|
+
title: z.string().includes('%s', { error: 'must contain `%s` (the page title)' }).optional(),
|
|
55
|
+
description: z.string().optional(),
|
|
56
|
+
locale: z
|
|
57
|
+
.string()
|
|
58
|
+
.regex(/^[a-z]{2,3}(_[A-Z]{2})?$/, { error: 'must look like `en_US`' })
|
|
59
|
+
.optional(),
|
|
60
|
+
inlineCss: z.boolean().optional(),
|
|
61
|
+
criticalCss: z.boolean().optional(),
|
|
62
|
+
criticalKeep: z.array(z.string()).optional(),
|
|
63
|
+
image: z.union([z.literal('auto'), z.literal(false), z.string().min(1), image]).optional(),
|
|
64
|
+
card: card.optional(),
|
|
65
|
+
head: z.string().optional(),
|
|
66
|
+
twitter: z
|
|
67
|
+
.strictObject({
|
|
68
|
+
card: z.enum(['summary', 'summary_large_image', 'app', 'player']).optional(),
|
|
69
|
+
site: z.string().optional(),
|
|
70
|
+
creator: z.string().optional(),
|
|
71
|
+
})
|
|
72
|
+
.optional(),
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
return { globs, task, pluginEntry, site }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function mainProperties(z, { globs, task, pluginEntry, site }) {
|
|
79
|
+
return {
|
|
80
|
+
scope: z
|
|
81
|
+
.strictObject({ write: globs.optional(), read: globs.optional(), forbid: globs.optional() })
|
|
82
|
+
.optional(),
|
|
83
|
+
tasks: z.record(z.string(), task).optional(),
|
|
84
|
+
environments: z.record(z.string(), z.looseObject({ url: z.string().optional() })).optional(),
|
|
85
|
+
test: z
|
|
86
|
+
.strictObject({
|
|
87
|
+
include: globs.optional(),
|
|
88
|
+
lanes: z.array(z.enum(['fast', 'browser', 'e2e'])).optional(),
|
|
89
|
+
gate: z
|
|
90
|
+
.strictObject({
|
|
91
|
+
mutationBudgetSec: z.number().positive().optional(),
|
|
92
|
+
maxMutants: z.number().int().positive().optional(),
|
|
93
|
+
})
|
|
94
|
+
.optional(),
|
|
95
|
+
})
|
|
96
|
+
.optional(),
|
|
97
|
+
build: z
|
|
98
|
+
.strictObject({
|
|
99
|
+
audit: z
|
|
100
|
+
.union([
|
|
101
|
+
z.boolean(),
|
|
102
|
+
z.strictObject({
|
|
103
|
+
routes: z.array(z.string()).optional(),
|
|
104
|
+
all: z.boolean().optional(),
|
|
105
|
+
max: z.number().int().positive().optional(),
|
|
106
|
+
indexing: z.enum(['auto', 'public', 'private']).optional(),
|
|
107
|
+
}),
|
|
108
|
+
])
|
|
109
|
+
.optional(),
|
|
110
|
+
unsafeAllow: z
|
|
111
|
+
.record(
|
|
112
|
+
z.string().regex(/^(all|[A-Z]|[A-Z]+\d{3})$/, 'a code (R002), a family (L) or all'),
|
|
113
|
+
z.string().min(1, 'say why'),
|
|
114
|
+
)
|
|
115
|
+
.optional(),
|
|
116
|
+
})
|
|
117
|
+
.optional(),
|
|
118
|
+
check: z
|
|
119
|
+
.strictObject({
|
|
120
|
+
include: globs.optional(),
|
|
121
|
+
exclude: globs.optional(),
|
|
122
|
+
design: z.boolean().optional(),
|
|
123
|
+
routes: z.boolean().optional(),
|
|
124
|
+
props: z.union([z.boolean(), z.literal('manual')]).optional(),
|
|
125
|
+
})
|
|
126
|
+
.optional(),
|
|
127
|
+
plugins: z.array(pluginEntry).optional(),
|
|
128
|
+
slots: z.record(z.string(), z.union([z.string(), z.record(z.string(), z.string())])).optional(),
|
|
129
|
+
site: site.optional(),
|
|
130
|
+
runtime: z.enum(['node', 'bun', 'deno']).optional(),
|
|
131
|
+
hydration: z.enum(['lazy', 'partial', 'full']).optional(),
|
|
132
|
+
rust: z
|
|
133
|
+
.strictObject({
|
|
134
|
+
unions: z.literal('allow').optional(),
|
|
135
|
+
services: z
|
|
136
|
+
.record(
|
|
137
|
+
z.string(),
|
|
138
|
+
z.strictObject({
|
|
139
|
+
crate: z.string().optional(),
|
|
140
|
+
external: z.boolean().optional(),
|
|
141
|
+
wasm: z.literal(false).optional(),
|
|
142
|
+
}),
|
|
143
|
+
)
|
|
144
|
+
.optional(),
|
|
145
|
+
})
|
|
146
|
+
.optional(),
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function deployFirst(z) {
|
|
151
|
+
return {
|
|
152
|
+
target: z.enum(['netlify', 'vercel', 'cloudflare', 'fly', 'render']).optional(),
|
|
153
|
+
netlify: z.strictObject({ site: z.string().min(1).optional() }).optional(),
|
|
154
|
+
vercel: z
|
|
155
|
+
.strictObject({
|
|
156
|
+
project: z
|
|
157
|
+
.string()
|
|
158
|
+
.regex(/^[a-z0-9][a-z0-9-]{0,62}$/i, 'a name: letters, digits and dashes')
|
|
159
|
+
.optional(),
|
|
160
|
+
})
|
|
161
|
+
.optional(),
|
|
162
|
+
cloudflare: z
|
|
163
|
+
.strictObject({
|
|
164
|
+
name: z
|
|
165
|
+
.string()
|
|
166
|
+
.regex(/^[a-z0-9][a-z0-9-]{0,62}$/i, 'a name: letters, digits and dashes')
|
|
167
|
+
.optional(),
|
|
168
|
+
})
|
|
169
|
+
.optional(),
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function deployFly(z) {
|
|
174
|
+
return {
|
|
175
|
+
fly: z
|
|
176
|
+
.strictObject({
|
|
177
|
+
app: z
|
|
178
|
+
.string()
|
|
179
|
+
.regex(/^[a-z0-9][a-z0-9-]{0,62}$/i, 'a name: letters, digits and dashes')
|
|
180
|
+
.optional(),
|
|
181
|
+
region: z
|
|
182
|
+
.string()
|
|
183
|
+
.regex(/^[a-z]{3}$/, 'a Fly region code, like `iad`')
|
|
184
|
+
.optional(),
|
|
185
|
+
memory: z
|
|
186
|
+
.string()
|
|
187
|
+
.regex(/^\d+(mb|gb)$/, 'like `512mb`')
|
|
188
|
+
.optional(),
|
|
189
|
+
volume: z
|
|
190
|
+
.strictObject({
|
|
191
|
+
name: z
|
|
192
|
+
.string()
|
|
193
|
+
.regex(/^[a-z0-9_]+$/, 'lowercase letters, digits and underscores')
|
|
194
|
+
.optional(),
|
|
195
|
+
path: z.string().startsWith('/').optional(),
|
|
196
|
+
sizeGb: z.number().int().positive().optional(),
|
|
197
|
+
})
|
|
198
|
+
.optional(),
|
|
199
|
+
})
|
|
200
|
+
.optional(),
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function deployRest(z) {
|
|
205
|
+
return {
|
|
206
|
+
render: z
|
|
207
|
+
.strictObject({
|
|
208
|
+
service: z
|
|
209
|
+
.string()
|
|
210
|
+
.regex(/^[a-z0-9][a-z0-9-]{0,62}$/i, 'a name: letters, digits and dashes')
|
|
211
|
+
.optional(),
|
|
212
|
+
plan: z.string().optional(),
|
|
213
|
+
region: z.string().optional(),
|
|
214
|
+
disk: z
|
|
215
|
+
.strictObject({
|
|
216
|
+
path: z.string().startsWith('/').optional(),
|
|
217
|
+
sizeGb: z.number().int().positive().optional(),
|
|
218
|
+
})
|
|
219
|
+
.optional(),
|
|
220
|
+
})
|
|
221
|
+
.optional(),
|
|
222
|
+
ci: z.union([z.literal('github'), z.literal(false)]).optional(),
|
|
223
|
+
branch: z.string().min(1).optional(),
|
|
224
|
+
cdn: z
|
|
225
|
+
.strictObject({
|
|
226
|
+
proxy: z.literal('cloudflare').optional(),
|
|
227
|
+
images: z
|
|
228
|
+
.strictObject({
|
|
229
|
+
provider: z.literal('cloudinary'),
|
|
230
|
+
cloud: z
|
|
231
|
+
.string()
|
|
232
|
+
.regex(/^[\w-]+$/, "the cloud's name, not its URL")
|
|
233
|
+
.optional(),
|
|
234
|
+
})
|
|
235
|
+
.optional(),
|
|
236
|
+
})
|
|
237
|
+
.optional(),
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function deploySchema(z) {
|
|
242
|
+
return z.strictObject({ ...deployFirst(z), ...deployFly(z), ...deployRest(z) }).optional()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function remainingProperties(z) {
|
|
246
|
+
return {
|
|
247
|
+
/** Named targets for a query's `live: { verify: 'staging' }`. `db` is an env var's name. */
|
|
248
|
+
live: z
|
|
249
|
+
.strictObject({
|
|
250
|
+
targets: z
|
|
251
|
+
.record(
|
|
252
|
+
z.string(),
|
|
253
|
+
z.strictObject({
|
|
254
|
+
url: z.string().optional(),
|
|
255
|
+
db: z.string().optional(),
|
|
256
|
+
production: z.boolean().optional(),
|
|
257
|
+
}),
|
|
258
|
+
)
|
|
259
|
+
.optional(),
|
|
260
|
+
})
|
|
261
|
+
.optional(),
|
|
262
|
+
trust: z.enum(['enforce', 'warn']).optional(),
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function createSchemas(z) {
|
|
267
|
+
const base = baseSchemas(z)
|
|
268
|
+
const configSchema = z.strictObject({
|
|
269
|
+
...mainProperties(z, base),
|
|
270
|
+
deploy: deploySchema(z),
|
|
271
|
+
...remainingProperties(z),
|
|
272
|
+
})
|
|
273
|
+
const { pluginEntry } = base
|
|
274
|
+
const dataSchema = configSchema.extend({
|
|
275
|
+
$schema: z.string().optional(),
|
|
276
|
+
plugins: z
|
|
277
|
+
.array(z.union([z.string(), z.tuple([z.string(), z.unknown()]), pluginEntry]))
|
|
278
|
+
.optional(),
|
|
279
|
+
})
|
|
280
|
+
return { configSchema, dataSchema }
|
|
281
|
+
}
|