@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/check.mjs
CHANGED
|
@@ -2,18 +2,25 @@
|
|
|
2
2
|
// `check` hooks, the test T-codes, the config P-codes, the style D-codes (style.mjs) and the design D-codes (design-checks.mjs), and keep `.weft/styles.json`
|
|
3
3
|
// current when the project has style groups.
|
|
4
4
|
|
|
5
|
-
import { existsSync,
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
6
6
|
import { createRequire } from 'node:module'
|
|
7
7
|
import { isAbsolute, join, relative, resolve } from 'node:path'
|
|
8
8
|
import { pathToFileURL } from 'node:url'
|
|
9
|
-
import { diagnostic,
|
|
9
|
+
import { diagnostic, positions, sortDiagnostics, summarize } from './diag.mjs'
|
|
10
10
|
import { discover } from './files.mjs'
|
|
11
11
|
import { isDataConfig, loadConfig, writeConfigSchema } from './load-config.mjs'
|
|
12
12
|
import { sync } from './plugins.mjs'
|
|
13
13
|
import { writeMap } from './map.mjs'
|
|
14
14
|
import { projectView } from './project.mjs'
|
|
15
15
|
import { designChecks, hasTokens } from './design-checks.mjs'
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
classSoup,
|
|
18
|
+
matrixDiagnostics,
|
|
19
|
+
styleFiles,
|
|
20
|
+
styleModule,
|
|
21
|
+
usesTailwind,
|
|
22
|
+
writeMatrixFile,
|
|
23
|
+
} from './style.mjs'
|
|
17
24
|
import { checkTests, isTestFile, testGlobs } from './tests.mjs'
|
|
18
25
|
import { pluginChecks } from './plugin-checks.mjs'
|
|
19
26
|
import { propChecks } from './props-checks.mjs'
|
|
@@ -21,35 +28,60 @@ import { reactChecks } from './react-checks.mjs'
|
|
|
21
28
|
import { trustDiagnostics } from './trust.mjs'
|
|
22
29
|
import { shareChecks } from './share-checks.mjs'
|
|
23
30
|
import { stateChecks } from './state-checks.mjs'
|
|
31
|
+
import { serverStateChecks } from './server-state-checks.mjs'
|
|
24
32
|
import { surfaceChecks } from './surface-checks.mjs'
|
|
25
33
|
import { textChecks } from './text-checks.mjs'
|
|
26
34
|
import { loadCompiler, resolveWeft } from './weft.mjs'
|
|
27
35
|
import { dbLayer } from './db-layer.mjs'
|
|
28
|
-
import { routeDiagnostics, schemaHash, siteOf, start } from './start.mjs'
|
|
36
|
+
import { loadEnv, routeDiagnostics, schemaHash, siteOf, start } from './start.mjs'
|
|
29
37
|
import { deployChecks } from './deploy/index.mjs'
|
|
38
|
+
import { rustChecks, liveChecks, socketChecks, hygiene } from './check/runtime.mjs'
|
|
30
39
|
|
|
31
|
-
const posix = p => p.split('\\').join('/')
|
|
40
|
+
const posix = (p) => p.split('\\').join('/')
|
|
32
41
|
|
|
33
42
|
/** Q015: the schema's hash against the one `weft db generate` recorded (research 28 §2). */
|
|
34
43
|
function schemaDrift(root, layer) {
|
|
35
44
|
const schema = layer?.schema && join(root, layer.schema)
|
|
36
45
|
if (!schema || !existsSync(schema)) return []
|
|
37
46
|
const file = join(root, layer.migrations, '.schema-hash')
|
|
38
|
-
if (existsSync(file) && readFileSync(file, 'utf8').trim() === schemaHash(readFileSync(schema)))
|
|
39
|
-
|
|
47
|
+
if (existsSync(file) && readFileSync(file, 'utf8').trim() === schemaHash(readFileSync(schema)))
|
|
48
|
+
return []
|
|
49
|
+
return [
|
|
50
|
+
diagnostic({
|
|
51
|
+
code: 'Q015',
|
|
52
|
+
file: layer.schema,
|
|
53
|
+
why: existsSync(file)
|
|
54
|
+
? 'The schema changed since the last `weft db generate`: the database will not match it.'
|
|
55
|
+
: 'No migrations yet: the first request that reads a table fails with Q014.',
|
|
56
|
+
}),
|
|
57
|
+
]
|
|
40
58
|
}
|
|
41
59
|
|
|
42
60
|
/** I001–I003: the walk a build does (@weft-js/warp-icons/compile), when the project has the package. */
|
|
43
61
|
async function iconChecks(root) {
|
|
44
62
|
const require = createRequire(join(root, 'package.json'))
|
|
45
63
|
let compile
|
|
46
|
-
for (const from of [
|
|
47
|
-
.resolve('@weft-js/warp-icons/compile')
|
|
48
|
-
|
|
64
|
+
for (const from of [
|
|
65
|
+
() => require.resolve('@weft-js/warp-icons/compile'),
|
|
66
|
+
() => createRequire(require.resolve('@weft-js/warp')).resolve('@weft-js/warp-icons/compile'),
|
|
67
|
+
]) {
|
|
68
|
+
try {
|
|
69
|
+
compile = from()
|
|
70
|
+
break
|
|
71
|
+
} catch {
|
|
72
|
+
/* not this way */
|
|
73
|
+
}
|
|
49
74
|
}
|
|
50
75
|
if (!compile) return []
|
|
51
76
|
const { findings } = await import(pathToFileURL(compile).href)
|
|
52
|
-
return findings(root).map(f =>
|
|
77
|
+
return findings(root).map((f) =>
|
|
78
|
+
diagnostic({
|
|
79
|
+
code: f.code,
|
|
80
|
+
file: f.file,
|
|
81
|
+
span: [f.line, f.column, f.line, f.column],
|
|
82
|
+
why: f.why,
|
|
83
|
+
}),
|
|
84
|
+
)
|
|
53
85
|
}
|
|
54
86
|
|
|
55
87
|
/** H010: what the last build or `weft dev` found keeps an island from waiting (`.weft/hydration.json`, @weft-js/core/hydration). */
|
|
@@ -57,7 +89,17 @@ function hydrationChecks(root) {
|
|
|
57
89
|
const file = join(root, '.weft', 'hydration.json')
|
|
58
90
|
if (!existsSync(file)) return []
|
|
59
91
|
try {
|
|
60
|
-
return JSON.parse(readFileSync(file, 'utf8'))
|
|
92
|
+
return JSON.parse(readFileSync(file, 'utf8'))
|
|
93
|
+
.diagnostics.filter((d) => existsSync(join(root, d.file)))
|
|
94
|
+
.map((d) =>
|
|
95
|
+
diagnostic({
|
|
96
|
+
code: d.code,
|
|
97
|
+
file: d.file,
|
|
98
|
+
span: [d.line, 1, d.line, 1],
|
|
99
|
+
why: d.why,
|
|
100
|
+
help: d.fix,
|
|
101
|
+
}),
|
|
102
|
+
)
|
|
61
103
|
} catch {
|
|
62
104
|
return []
|
|
63
105
|
}
|
|
@@ -68,46 +110,40 @@ function hydrationChecks(root) {
|
|
|
68
110
|
* when a route is among them. An app that can't run here is still read: a check never fails for what it couldn't start.
|
|
69
111
|
*/
|
|
70
112
|
async function routeChecks(root, loaded, given, reported) {
|
|
71
|
-
if (
|
|
72
|
-
|
|
113
|
+
if (
|
|
114
|
+
!existsSync(join(root, 'src', 'routes')) ||
|
|
115
|
+
loaded.config?.check?.routes === false ||
|
|
116
|
+
loaded.diagnostics.some((d) => d.code === 'P007')
|
|
117
|
+
)
|
|
118
|
+
return []
|
|
119
|
+
if (given?.length && !given.some((f) => f.startsWith('src/routes/'))) return []
|
|
73
120
|
let inspect
|
|
74
|
-
|
|
75
|
-
|
|
121
|
+
// The routes run as they do under `weft dev`, so they get the same `.env` (an env schema throws without it).
|
|
122
|
+
loadEnv(root)
|
|
123
|
+
try {
|
|
124
|
+
inspect = await start(root, 'inspect')
|
|
125
|
+
} catch {
|
|
126
|
+
return []
|
|
127
|
+
}
|
|
128
|
+
const keep = (list) =>
|
|
129
|
+
routeDiagnostics(root, list).filter((d) => !(d.code === 'H001' && reported.has('H001')))
|
|
76
130
|
try {
|
|
77
131
|
return keep((await inspect.routesMeta({ root, site: siteOf(loaded) })).diagnostics)
|
|
78
132
|
} catch (error) {
|
|
79
|
-
return keep([
|
|
133
|
+
return keep([
|
|
134
|
+
...inspect.staticRouteChecks(root),
|
|
135
|
+
{
|
|
136
|
+
code: 'H016',
|
|
137
|
+
severity: 'info',
|
|
138
|
+
file: 'src/routes',
|
|
139
|
+
route: '*',
|
|
140
|
+
why: `The routes didn't load here, so only their source was checked: ${String(error?.message ?? error).split('\n')[0]}`,
|
|
141
|
+
},
|
|
142
|
+
])
|
|
80
143
|
}
|
|
81
144
|
}
|
|
82
145
|
|
|
83
|
-
|
|
84
|
-
* @param {string} root
|
|
85
|
-
* @param {{ files?: string[] }} [options] files relative to `root` (or absolute); default: discovered
|
|
86
|
-
*/
|
|
87
|
-
export async function check(root, options = {}) {
|
|
88
|
-
const started = performance.now()
|
|
89
|
-
root = resolve(root)
|
|
90
|
-
const weft = resolveWeft(root)
|
|
91
|
-
const compiler = loadCompiler(weft)
|
|
92
|
-
const loaded = await loadConfig(root)
|
|
93
|
-
const { config } = loaded
|
|
94
|
-
// Slot modules and plugin token files stay current with the config (research 13 §2.6).
|
|
95
|
-
if (!loaded.diagnostics.some(d => d.code === 'P007') && (config.plugins?.length || existsSync(join(root, '.weft', 'slots')))) sync(root, loaded)
|
|
96
|
-
|
|
97
|
-
if (loaded.file && isDataConfig(loaded.file)) writeConfigSchema(root)
|
|
98
|
-
if (existsSync(join(root, '.weft'))) await writeMap(root)
|
|
99
|
-
|
|
100
|
-
const exclude = [...(config.check?.exclude ?? []), ...(config.scope?.forbid ?? [])]
|
|
101
|
-
const given = options.files?.map(f => posix(relative(root, isAbsolute(f) ? f : join(root, f))))
|
|
102
|
-
const include = config.check?.include ?? ['**/*.{tsx,jsx}']
|
|
103
|
-
const rels = given?.length ? given.filter(f => /\.[jt]sx$/.test(f) || !isTestFile(f)) : discover(root, { include, exclude })
|
|
104
|
-
const testRels = given?.length ? given.filter(isTestFile) : discover(root, { include: config.test?.include ?? testGlobs, exclude })
|
|
105
|
-
|
|
106
|
-
// Style groups (research 23): the project's definitions first, so each file compiles with the groups it imports.
|
|
107
|
-
const style = await styleModule(weft)
|
|
108
|
-
const sfiles = style ? styleFiles(root, config) : []
|
|
109
|
-
const scan = sfiles.some(f => readFileSync(join(root, f), 'utf8').includes('@weft-js/core/style')) ? style.scanProject(root, sfiles, (...a) => compiler.transform(...a)) : null
|
|
110
|
-
|
|
146
|
+
function compileFiles(root, rels, compiler, scan, style) {
|
|
111
147
|
const files = []
|
|
112
148
|
const posByAbs = new Map()
|
|
113
149
|
for (const rel of rels) {
|
|
@@ -115,40 +151,69 @@ export async function check(root, options = {}) {
|
|
|
115
151
|
const text = readFileSync(abs, 'utf8')
|
|
116
152
|
const pos = positions(text)
|
|
117
153
|
posByAbs.set(abs, pos)
|
|
118
|
-
const groups = scan
|
|
119
|
-
|
|
120
|
-
|
|
154
|
+
const groups = scan
|
|
155
|
+
? style.importedGroups(abs, text, (f) => scan.groups.filter((g) => join(root, g.file) === f))
|
|
156
|
+
: {}
|
|
157
|
+
const out = compiler.transform(
|
|
158
|
+
text,
|
|
159
|
+
Object.keys(groups).length
|
|
160
|
+
? { filename: abs, checkOnly: true, groups }
|
|
161
|
+
: { filename: rel, checkOnly: true },
|
|
162
|
+
)
|
|
163
|
+
const diagnostics = out.diagnostics.map((d) => {
|
|
121
164
|
const from = pos.fromByte(d.start)
|
|
122
165
|
const end = pos.fromByte(d.end)
|
|
123
166
|
const fix = d.fix && {
|
|
124
167
|
description: d.fix.description,
|
|
125
168
|
safe: d.fix.safe,
|
|
126
|
-
edits: d.fix.edits.map(e => {
|
|
169
|
+
edits: d.fix.edits.map((e) => {
|
|
127
170
|
const a = pos.fromByte(e.start)
|
|
128
171
|
const b = pos.fromByte(e.end)
|
|
129
172
|
return { file: rel, span: pos.span(a, b), offset: [a, b], text: e.text }
|
|
130
173
|
}),
|
|
131
174
|
}
|
|
132
|
-
return diagnostic({
|
|
175
|
+
return diagnostic({
|
|
176
|
+
code: d.code,
|
|
177
|
+
severity: d.severity,
|
|
178
|
+
file: rel,
|
|
179
|
+
span: pos.span(from, end),
|
|
180
|
+
offset: [from, end],
|
|
181
|
+
why: d.message,
|
|
182
|
+
help: d.help,
|
|
183
|
+
fix,
|
|
184
|
+
})
|
|
133
185
|
})
|
|
134
186
|
files.push({ abs, rel, text, diagnostics })
|
|
135
187
|
}
|
|
136
188
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const matrix = scan && style.buildMatrix(scan.groups, scan.uses)
|
|
140
|
-
if (matrix && scan.groups.length) writeMatrixFile(root, matrix)
|
|
141
|
-
const styles = [...(usesTailwind(root) ? [] : classSoup(files.filter(f => /\.[jt]sx$/.test(f.rel)))), ...(matrix ? matrixDiagnostics(root, matrix) : [])]
|
|
189
|
+
return { files, posByAbs }
|
|
190
|
+
}
|
|
142
191
|
|
|
143
|
-
|
|
192
|
+
async function collectDiagnostics({
|
|
193
|
+
root,
|
|
194
|
+
loaded,
|
|
195
|
+
config,
|
|
196
|
+
files,
|
|
197
|
+
exclude,
|
|
198
|
+
styles,
|
|
199
|
+
testRels,
|
|
200
|
+
options,
|
|
201
|
+
given,
|
|
202
|
+
}) {
|
|
203
|
+
const fontBuild =
|
|
204
|
+
typeof loaded.slots['font.build'] === 'string' &&
|
|
205
|
+
!loaded.slots['font.build'].startsWith('weft:default')
|
|
144
206
|
const diagnostics = [
|
|
145
207
|
...loaded.diagnostics,
|
|
146
|
-
...(loaded.diagnostics.some(d => d.code === 'P007') ? [] : trustDiagnostics(root, loaded)),
|
|
147
|
-
...files.flatMap(f => f.diagnostics),
|
|
208
|
+
...(loaded.diagnostics.some((d) => d.code === 'P007') ? [] : trustDiagnostics(root, loaded)),
|
|
209
|
+
...files.flatMap((f) => f.diagnostics),
|
|
148
210
|
...styles,
|
|
149
|
-
...(config.check?.design === false
|
|
211
|
+
...(config.check?.design === false
|
|
212
|
+
? []
|
|
213
|
+
: designChecks(root, { files, exclude, tokens: hasTokens(root) })),
|
|
150
214
|
...textChecks(root, { files, exclude, fontBuild }),
|
|
151
215
|
...stateChecks(root, { files, exclude }),
|
|
216
|
+
...serverStateChecks(root, { files, exclude }),
|
|
152
217
|
...shareChecks(root, { files }),
|
|
153
218
|
...reactChecks({ files }),
|
|
154
219
|
...surfaceChecks(root, { files, exclude, slots: loaded.slots }),
|
|
@@ -162,10 +227,79 @@ export async function check(root, options = {}) {
|
|
|
162
227
|
...schemaDrift(root, await dbLayer(root, loaded)),
|
|
163
228
|
...(await rustChecks(root, loaded)),
|
|
164
229
|
...(await liveChecks(root, loaded, options.preset)),
|
|
165
|
-
...(loaded.diagnostics.some(d => d.code === 'P007')
|
|
230
|
+
...(loaded.diagnostics.some((d) => d.code === 'P007')
|
|
231
|
+
? []
|
|
232
|
+
: deployChecks(root, config, { preset: options.preset })),
|
|
166
233
|
]
|
|
167
|
-
diagnostics.push(
|
|
234
|
+
diagnostics.push(
|
|
235
|
+
...(await routeChecks(root, loaded, given, new Set(loaded.diagnostics.map((d) => d.code)))),
|
|
236
|
+
)
|
|
168
237
|
sortDiagnostics(diagnostics)
|
|
238
|
+
return diagnostics
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @param {string} root
|
|
243
|
+
* @param {{ files?: string[] }} [options] files relative to `root` (or absolute); default: discovered
|
|
244
|
+
*/
|
|
245
|
+
export async function check(root, options = {}) {
|
|
246
|
+
const started = performance.now()
|
|
247
|
+
root = resolve(root)
|
|
248
|
+
const weft = resolveWeft(root)
|
|
249
|
+
const compiler = loadCompiler(weft)
|
|
250
|
+
const loaded = await loadConfig(root)
|
|
251
|
+
const { config } = loaded
|
|
252
|
+
// Slot modules and plugin token files stay current with the config (research 13 §2.6).
|
|
253
|
+
if (
|
|
254
|
+
!loaded.diagnostics.some((d) => d.code === 'P007') &&
|
|
255
|
+
(config.plugins?.length || existsSync(join(root, '.weft', 'slots')))
|
|
256
|
+
)
|
|
257
|
+
sync(root, loaded)
|
|
258
|
+
|
|
259
|
+
if (loaded.file && isDataConfig(loaded.file)) writeConfigSchema(root)
|
|
260
|
+
if (existsSync(join(root, '.weft'))) await writeMap(root)
|
|
261
|
+
|
|
262
|
+
const exclude = [...(config.check?.exclude ?? []), ...(config.scope?.forbid ?? [])]
|
|
263
|
+
const given = options.files?.map((f) => posix(relative(root, isAbsolute(f) ? f : join(root, f))))
|
|
264
|
+
const include = config.check?.include ?? ['**/*.{tsx,jsx}']
|
|
265
|
+
const rels = given?.length
|
|
266
|
+
? given.filter((f) => /\.[jt]sx$/.test(f) || !isTestFile(f))
|
|
267
|
+
: discover(root, { include, exclude })
|
|
268
|
+
const testRels = given?.length
|
|
269
|
+
? given.filter(isTestFile)
|
|
270
|
+
: discover(root, { include: config.test?.include ?? testGlobs, exclude })
|
|
271
|
+
|
|
272
|
+
// Style groups (research 23): the project's definitions first, so each file compiles with the groups it imports.
|
|
273
|
+
const style = await styleModule(weft)
|
|
274
|
+
const sfiles = style ? styleFiles(root, config) : []
|
|
275
|
+
const scan = sfiles.some((f) =>
|
|
276
|
+
readFileSync(join(root, f), 'utf8').includes('@weft-js/core/style'),
|
|
277
|
+
)
|
|
278
|
+
? style.scanProject(root, sfiles, (...a) => compiler.transform(...a))
|
|
279
|
+
: null
|
|
280
|
+
|
|
281
|
+
const { files, posByAbs } = compileFiles(root, rels, compiler, scan, style)
|
|
282
|
+
|
|
283
|
+
projectView(files, (file) => posByAbs.get(file.abs))
|
|
284
|
+
|
|
285
|
+
const matrix = scan && style.buildMatrix(scan.groups, scan.uses)
|
|
286
|
+
if (matrix && scan.groups.length) writeMatrixFile(root, matrix)
|
|
287
|
+
const styles = [
|
|
288
|
+
...(usesTailwind(root) ? [] : classSoup(files.filter((f) => /\.[jt]sx$/.test(f.rel)))),
|
|
289
|
+
...(matrix ? matrixDiagnostics(root, matrix) : []),
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
const diagnostics = await collectDiagnostics({
|
|
293
|
+
root,
|
|
294
|
+
loaded,
|
|
295
|
+
config,
|
|
296
|
+
files,
|
|
297
|
+
exclude,
|
|
298
|
+
styles,
|
|
299
|
+
testRels,
|
|
300
|
+
options,
|
|
301
|
+
given,
|
|
302
|
+
})
|
|
169
303
|
const summary = { files: files.length, tests: testRels.length, ...summarize(diagnostics) }
|
|
170
304
|
return {
|
|
171
305
|
version: 1,
|
|
@@ -176,155 +310,11 @@ export async function check(root, options = {}) {
|
|
|
176
310
|
summary,
|
|
177
311
|
diagnostics,
|
|
178
312
|
slots: loaded.slots,
|
|
179
|
-
styles: matrix
|
|
313
|
+
styles: matrix
|
|
314
|
+
? { groups: Object.keys(matrix.groups).length, matrix: '.weft/styles.json' }
|
|
315
|
+
: null,
|
|
180
316
|
files: rels,
|
|
181
317
|
tests: testRels,
|
|
182
318
|
ms: Math.round(performance.now() - started),
|
|
183
319
|
}
|
|
184
320
|
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* R101, R102 from the contract, and R103, R109 from `cargo check`, for an app with a sidecar: an agent gets the same
|
|
188
|
-
* answer here as from `weft build`, with the function's id on it.
|
|
189
|
-
*/
|
|
190
|
-
async function rustChecks(root, loaded) {
|
|
191
|
-
let sidecar
|
|
192
|
-
try { sidecar = await start(root, 'sidecar') } catch { return [] }
|
|
193
|
-
if (!sidecar.usesRemote(root)) return []
|
|
194
|
-
const rust = loaded.config?.rust
|
|
195
|
-
const held = process.env.WEFT_BUILD
|
|
196
|
-
process.env.WEFT_BUILD = '1'
|
|
197
|
-
try {
|
|
198
|
-
const { writeContract } = await start(root, 'contract')
|
|
199
|
-
const preset = { node: 'node', bun: 'bun', deno: 'deno_server' }[loaded.config?.runtime ?? 'node']
|
|
200
|
-
const { contract, diagnostics } = await writeContract({ root, site: loaded.config?.site, rust, preset })
|
|
201
|
-
const out = diagnostics.filter(d => d.code.startsWith('R1')).map(d => diagnostic({ ...d, file: '.weft/contract.json' }))
|
|
202
|
-
if (out.some(d => d.severity === 'error')) return out
|
|
203
|
-
for (const service of Object.keys(contract.services ?? {})) {
|
|
204
|
-
if (rust?.services?.[service]?.external) continue
|
|
205
|
-
const dir = sidecar.crateDir(root, rust, service)
|
|
206
|
-
if (!existsSync(dir)) {
|
|
207
|
-
out.push(diagnostic({ code: 'R109', file: '.weft/contract.json', why: `\`rust://${service}\` has no crate at \`${posix(relative(root, dir))}\`.` }))
|
|
208
|
-
continue
|
|
209
|
-
}
|
|
210
|
-
const checked = await sidecar.cargo('check', { root, dir, service, contract })
|
|
211
|
-
out.push(...checked.diagnostics.map(({ rendered, ...d }) => diagnostic({ ...d, help: rendered })))
|
|
212
|
-
out.push(...untouchedQueries(root, dir, contract, service))
|
|
213
|
-
if (contract.services[service].kind !== 'native' || !checked.ok) continue
|
|
214
|
-
const wasm = await sidecar.checkWasm({ root, dir, service, contract, preset, rust })
|
|
215
|
-
out.push(...wasm.diagnostics.map(d => diagnostic(d)))
|
|
216
|
-
}
|
|
217
|
-
return out
|
|
218
|
-
} catch (e) {
|
|
219
|
-
return [diagnostic({ code: 'R109', severity: 'warning', file: '.weft/contract.json', why: `the contract couldn't be written, so the sidecars weren't checked: ${e.message.split('\n')[0]}` })]
|
|
220
|
-
} finally {
|
|
221
|
-
if (held === undefined) delete process.env.WEFT_BUILD
|
|
222
|
-
else process.env.WEFT_BUILD = held
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/** R105 before anything runs: a cached query with tags whose Rust never mentions `touch`, or hands `ctx` to anything. */
|
|
227
|
-
function untouchedQueries(root, dir, contract, service) {
|
|
228
|
-
const sources = readdirSync(join(dir, 'src'), { recursive: true, withFileTypes: true }).filter(f => f.isFile() && f.name.endsWith('.rs')).map(f => join(f.parentPath, f.name))
|
|
229
|
-
const out = []
|
|
230
|
-
for (const f of contract.functions) {
|
|
231
|
-
if (f.impl?.service !== service || !f.cache || !(f.tags === 'dynamic' || f.tags?.length)) continue
|
|
232
|
-
const method = f.name.replace(/[A-Z]/g, (c, i) => `${i ? '_' : ''}${c.toLowerCase()}`)
|
|
233
|
-
for (const file of sources) {
|
|
234
|
-
const text = readFileSync(file, 'utf8')
|
|
235
|
-
const at = text.search(new RegExp(`\\bfn\\s+${method}\\s*\\(`))
|
|
236
|
-
if (at < 0) continue
|
|
237
|
-
const open = text.indexOf('{', at)
|
|
238
|
-
let depth = 0, end = open
|
|
239
|
-
for (; end < text.length; end++) {
|
|
240
|
-
if (text[end] === '{') depth++
|
|
241
|
-
if (text[end] === '}' && --depth === 0) break
|
|
242
|
-
}
|
|
243
|
-
const body = text.slice(open, end)
|
|
244
|
-
const line = text.slice(0, at).split('\n').length
|
|
245
|
-
if (!/\.touch\(|\(\s*&?ctx\b|,\s*&?ctx\b/.test(body)) out.push(diagnostic({ code: 'R105', severity: 'warning', file: posix(relative(root, file)), span: [line, 1, line, 1], why: `\`${f.id}\` is cached under tags and reports no table: nothing can tell whether they cover what it reads. Call \`ctx.touch("table")\`, or drop \`tags\` if it reads none.` }))
|
|
246
|
-
break
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return out
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/** R013, R014 and R018: what the deploy target does to live data. Only an app with `changes` or `required` loads anything. */
|
|
253
|
-
async function liveChecks(root, loaded, preset) {
|
|
254
|
-
const file = join(root, 'src/realtime.server.ts')
|
|
255
|
-
let target
|
|
256
|
-
try { target = await start(root, 'live-target') } catch { return [] }
|
|
257
|
-
const required = target.scanRequired(root)
|
|
258
|
-
if (!required.length && !target.literalSecrets(root).length && !(existsSync(file) && /\bchanges\s*[:,}]/.test(readFileSync(file, 'utf8')))) return []
|
|
259
|
-
const held = process.env.WEFT_BUILD
|
|
260
|
-
process.env.WEFT_BUILD = '1'
|
|
261
|
-
try {
|
|
262
|
-
const { readLivePlan } = await start(root, 'contract')
|
|
263
|
-
const plan = await readLivePlan({ root, site: loaded.config?.site, live: loaded.config?.live, preset: preset ?? { node: 'node', bun: 'bun', deno: 'deno_server' }[loaded.config?.runtime ?? 'node'] })
|
|
264
|
-
return plan.diagnostics.map(d => diagnostic({ ...d, file: d.file ?? required[0]?.file ?? 'src/realtime.server.ts' }))
|
|
265
|
-
} catch (e) {
|
|
266
|
-
return [diagnostic({ code: 'R014', severity: 'warning', file: 'src/realtime.server.ts', why: `the deploy target couldn't be checked: ${e.message.split('\n')[0]}` })]
|
|
267
|
-
} finally {
|
|
268
|
-
if (held === undefined) delete process.env.WEFT_BUILD
|
|
269
|
-
else process.env.WEFT_BUILD = held
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/** R001 for a server module's `channel`, and R004 for a socket handler of the app's own (research 32 §1). */
|
|
274
|
-
async function socketChecks(root) {
|
|
275
|
-
const src = join(root, 'src')
|
|
276
|
-
if (!existsSync(src)) return []
|
|
277
|
-
const out = []
|
|
278
|
-
let withIds
|
|
279
|
-
for (const f of readdirSync(src, { recursive: true, withFileTypes: true })) {
|
|
280
|
-
if (!f.isFile() || !/\.[cm]?[jt]sx?$/.test(f.name) || f.name.endsWith('.d.ts')) continue
|
|
281
|
-
const abs = join(f.parentPath, f.name), rel = relative(root, abs).replaceAll('\\', '/'), text = readFileSync(abs, 'utf8')
|
|
282
|
-
const hand = /from\s+['"]crossws|\bdefineWebSocketHandler\b/.exec(text)
|
|
283
|
-
if (hand) out.push(diagnostic({ code: 'R004', file: rel, ...locate(text, hand[0]), why: 'A WebSocket handler of its own skips the Origin check, `authorize`, the limits and resume. Use `channel`.' }))
|
|
284
|
-
if (!/\b(channel|sharedState)\s*[(<]/.test(text) || !/^src\/server\/|\.server\.[cm]?[jt]sx?$/.test(rel)) continue
|
|
285
|
-
if (withIds === undefined) {
|
|
286
|
-
try { withIds = (await import(pathToFileURL(createRequire(join(root, 'package.json')).resolve('@weft-js/start/vite')).href)).withIds } catch { withIds = null }
|
|
287
|
-
}
|
|
288
|
-
try { withIds?.(text, abs, rel) } catch (e) {
|
|
289
|
-
if (e.code !== 'R001') continue
|
|
290
|
-
const line = +/:(\d+) /.exec(e.message)?.[1]
|
|
291
|
-
out.push(diagnostic({ code: 'R001', file: rel, span: [line, 1, line, 1], why: e.message.split('\n')[0].replace(/^R001: \S+ /, '') }))
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
return out
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/** weft owns the connection: these bypass reconnects, resume and the fake transport (research 30 §8). */
|
|
298
|
-
const SOCKET_PACKAGES = ['ws', 'socket.io-client', 'reconnecting-websocket', '@microsoft/fetch-event-source']
|
|
299
|
-
|
|
300
|
-
/** P002 (`package.json` scripts that aren't `weft …`), P120 (weft plugins wired straight into Vite), R007 (socket libraries). */
|
|
301
|
-
function hygiene(root) {
|
|
302
|
-
const out = []
|
|
303
|
-
const pkgFile = join(root, 'package.json')
|
|
304
|
-
if (existsSync(pkgFile)) {
|
|
305
|
-
const text = readFileSync(pkgFile, 'utf8')
|
|
306
|
-
let scripts = {}, deps = {}
|
|
307
|
-
try {
|
|
308
|
-
const pkg = JSON.parse(text)
|
|
309
|
-
scripts = pkg.scripts ?? {}
|
|
310
|
-
deps = pkg.dependencies ?? {}
|
|
311
|
-
} catch {}
|
|
312
|
-
for (const name of SOCKET_PACKAGES) {
|
|
313
|
-
if (deps[name]) out.push(diagnostic({ code: 'R007', file: 'package.json', ...locate(text, `"${name}"`), why: `\`${name}\` is a dependency: its connection has no resume, no cleanup and no fake in @weft-js/core/test. Use \`stream\`, \`.live()\` or \`channel\`.` }))
|
|
314
|
-
}
|
|
315
|
-
for (const [name, cmd] of Object.entries(scripts)) {
|
|
316
|
-
if (!/^weft(\s|$)/.test(cmd.trim())) {
|
|
317
|
-
out.push(diagnostic({ code: 'P002', file: 'package.json', ...locate(text, `"${name}"`), why: `Script \`${name}\` runs \`${cmd}\`, not \`weft …\`: agents can't see what it does or what it touches.` }))
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
for (const name of ['vite.config.ts', 'vite.config.mts', 'vite.config.js', 'vite.config.mjs']) {
|
|
322
|
-
const file = join(root, name)
|
|
323
|
-
if (!existsSync(file)) continue
|
|
324
|
-
const text = readFileSync(file, 'utf8')
|
|
325
|
-
for (const m of text.matchAll(/from\s+['"](@weft-js\/(?:plugin-[\w-]+|text))/g)) {
|
|
326
|
-
out.push(diagnostic({ code: 'P120', file: name, ...locate(text, m[1]), why: `\`${m[1]}\` is registered in ${name}, which bypasses weft.config.ts.` }))
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
return out
|
|
330
|
-
}
|