@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/share-checks.mjs
CHANGED
|
@@ -11,21 +11,35 @@ const USER_TYPED = /\b(?:location\.|URLSearchParams|useSearchParams|searchParams
|
|
|
11
11
|
/** warp components that take `ui`. Anything else would write `ui="[object Object]"` on its element. */
|
|
12
12
|
const TAKES_UI = new Set(['Button', 'Checkbox', 'Switch', 'Radio', 'Toggle', 'Chip', 'Card'])
|
|
13
13
|
|
|
14
|
-
const rowsOf = sf => {
|
|
14
|
+
const rowsOf = (sf) => {
|
|
15
15
|
const rows = new Set()
|
|
16
|
-
const visit = n => {
|
|
17
|
-
if (
|
|
16
|
+
const visit = (n) => {
|
|
17
|
+
if (
|
|
18
|
+
ts.isVariableDeclaration(n) &&
|
|
19
|
+
ts.isIdentifier(n.name) &&
|
|
20
|
+
n.initializer &&
|
|
21
|
+
ts.isCallExpression(n.initializer) &&
|
|
22
|
+
n.initializer.expression.getText?.(sf) === 'interaction'
|
|
23
|
+
)
|
|
24
|
+
rows.add(n.name.text)
|
|
18
25
|
ts.forEachChild(n, visit)
|
|
19
26
|
}
|
|
20
27
|
visit(sf)
|
|
21
28
|
return rows
|
|
22
29
|
}
|
|
23
30
|
|
|
24
|
-
const warpNames = sf => {
|
|
31
|
+
const warpNames = (sf) => {
|
|
25
32
|
const names = new Map()
|
|
26
33
|
for (const n of sf.statements) {
|
|
27
|
-
if (
|
|
28
|
-
|
|
34
|
+
if (
|
|
35
|
+
!ts.isImportDeclaration(n) ||
|
|
36
|
+
!n.moduleSpecifier.text.startsWith('@weft-js/warp') ||
|
|
37
|
+
!n.importClause?.namedBindings ||
|
|
38
|
+
!ts.isNamedImports(n.importClause.namedBindings)
|
|
39
|
+
)
|
|
40
|
+
continue
|
|
41
|
+
for (const s of n.importClause.namedBindings.elements)
|
|
42
|
+
names.set(s.name.text, (s.propertyName ?? s.name).text)
|
|
29
43
|
}
|
|
30
44
|
return names
|
|
31
45
|
}
|
|
@@ -34,41 +48,111 @@ export function shareChecks(root, { files }) {
|
|
|
34
48
|
const out = []
|
|
35
49
|
let sharesIn
|
|
36
50
|
for (const { rel, text } of files) {
|
|
37
|
-
|
|
38
|
-
if (!usesRow && !/\bui=\{/.test(text)) continue
|
|
39
|
-
const sf = ts.createSourceFile(rel, text, ts.ScriptTarget.Latest, true, /\.[jt]sx$/.test(rel) ? ts.ScriptKind.TSX : ts.ScriptKind.TS)
|
|
40
|
-
const pos = positions(text)
|
|
41
|
-
const report = (code, n, why) => out.push(diagnostic({ code, file: rel, span: pos.span(n.getStart(sf), n.end), offset: [n.getStart(sf), n.end], why }))
|
|
42
|
-
const rows = rowsOf(sf), warp = warpNames(sf)
|
|
43
|
-
const visit = n => {
|
|
44
|
-
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === 'interaction' && usesRow) {
|
|
45
|
-
const share = n.arguments[0] && ts.isObjectLiteralExpression(n.arguments[0]) ? n.arguments[0].properties.find(p => p.name?.getText(sf) === 'share') : undefined
|
|
46
|
-
if (share) {
|
|
47
|
-
sharesIn ??= rel
|
|
48
|
-
const value = share.getText(sf)
|
|
49
|
-
if (UNSTABLE.test(value)) report('U001', share, 'A `share` key that changes on every load never meets another tab or client.')
|
|
50
|
-
else if (USER_TYPED.test(value)) report('U002', share, 'This `share` key comes from what the user typed or the URL, so `realtime({ share })` is authorising a string they chose.')
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
54
|
-
const l = n.left
|
|
55
|
-
const target = ts.isPropertyAccessExpression(l) && l.name.text === 'value' ? l.expression : l
|
|
56
|
-
if (ts.isPropertyAccessExpression(target) && target.name.text === 'remote' && ts.isIdentifier(target.expression) && rows.has(target.expression.text))
|
|
57
|
-
report('U004', n, '`remote` is what others are doing: only the network writes it.')
|
|
58
|
-
}
|
|
59
|
-
if (ts.isJsxSelfClosingElement(n) || ts.isJsxOpeningElement(n)) {
|
|
60
|
-
const tag = n.tagName.getText(sf), ui = n.attributes.properties.find(a => ts.isJsxAttribute(a) && a.name.getText(sf) === 'ui')
|
|
61
|
-
if (ui && warp.has(tag) && !TAKES_UI.has(warp.get(tag))) report('U005', ui, `\`${warp.get(tag)}\` doesn't take \`ui\` yet, so this writes an attribute and draws nothing.`)
|
|
62
|
-
}
|
|
63
|
-
ts.forEachChild(n, visit)
|
|
64
|
-
}
|
|
65
|
-
visit(sf)
|
|
51
|
+
sharesIn = checkFile(rel, text, out, sharesIn)
|
|
66
52
|
}
|
|
67
53
|
if (sharesIn && existsSync(join(root, 'src/routes'))) {
|
|
68
|
-
const file = ['src/realtime.server.ts', 'src/realtime.server.js']
|
|
54
|
+
const file = ['src/realtime.server.ts', 'src/realtime.server.js']
|
|
55
|
+
.map((f) => join(root, f))
|
|
56
|
+
.find(existsSync)
|
|
69
57
|
if (!file || !/\bshare\s*:/.test(readFileSync(file, 'utf8'))) {
|
|
70
|
-
out.push(
|
|
58
|
+
out.push(
|
|
59
|
+
diagnostic({
|
|
60
|
+
code: 'U003',
|
|
61
|
+
file: sharesIn,
|
|
62
|
+
span: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
|
|
63
|
+
offset: [0, 0],
|
|
64
|
+
why: 'This app shares UI state and no `realtime({ share })` says who may: tabs work, and the socket refuses every key.',
|
|
65
|
+
}),
|
|
66
|
+
)
|
|
71
67
|
}
|
|
72
68
|
}
|
|
73
69
|
return out
|
|
74
70
|
}
|
|
71
|
+
|
|
72
|
+
function checkFile(rel, text, out, sharesIn) {
|
|
73
|
+
const usesRow = text.includes('@weft-js/core/interaction')
|
|
74
|
+
if (!usesRow && !/\bui=\{/.test(text)) return sharesIn
|
|
75
|
+
const sf = ts.createSourceFile(
|
|
76
|
+
rel,
|
|
77
|
+
text,
|
|
78
|
+
ts.ScriptTarget.Latest,
|
|
79
|
+
true,
|
|
80
|
+
/\.[jt]sx$/.test(rel) ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
81
|
+
)
|
|
82
|
+
const pos = positions(text)
|
|
83
|
+
const report = (code, n, why) =>
|
|
84
|
+
out.push(
|
|
85
|
+
diagnostic({
|
|
86
|
+
code,
|
|
87
|
+
file: rel,
|
|
88
|
+
span: pos.span(n.getStart(sf), n.end),
|
|
89
|
+
offset: [n.getStart(sf), n.end],
|
|
90
|
+
why,
|
|
91
|
+
}),
|
|
92
|
+
)
|
|
93
|
+
const rows = rowsOf(sf),
|
|
94
|
+
warp = warpNames(sf)
|
|
95
|
+
const markShare = () => {
|
|
96
|
+
sharesIn ??= rel
|
|
97
|
+
}
|
|
98
|
+
const visit = makeVisitor({ sf, usesRow, rows, warp, report, markShare })
|
|
99
|
+
visit(sf)
|
|
100
|
+
return sharesIn
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function makeVisitor({ sf, usesRow, rows, warp, report, markShare }) {
|
|
104
|
+
const visit = (n) => {
|
|
105
|
+
if (
|
|
106
|
+
ts.isCallExpression(n) &&
|
|
107
|
+
ts.isIdentifier(n.expression) &&
|
|
108
|
+
n.expression.text === 'interaction' &&
|
|
109
|
+
usesRow
|
|
110
|
+
) {
|
|
111
|
+
const share =
|
|
112
|
+
n.arguments[0] && ts.isObjectLiteralExpression(n.arguments[0])
|
|
113
|
+
? n.arguments[0].properties.find((p) => p.name?.getText(sf) === 'share')
|
|
114
|
+
: undefined
|
|
115
|
+
if (share) {
|
|
116
|
+
markShare()
|
|
117
|
+
const value = share.getText(sf)
|
|
118
|
+
if (UNSTABLE.test(value))
|
|
119
|
+
report(
|
|
120
|
+
'U001',
|
|
121
|
+
share,
|
|
122
|
+
'A `share` key that changes on every load never meets another tab or client.',
|
|
123
|
+
)
|
|
124
|
+
else if (USER_TYPED.test(value))
|
|
125
|
+
report(
|
|
126
|
+
'U002',
|
|
127
|
+
share,
|
|
128
|
+
'This `share` key comes from what the user typed or the URL, so `realtime({ share })` is authorising a string they chose.',
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
133
|
+
const l = n.left
|
|
134
|
+
const target = ts.isPropertyAccessExpression(l) && l.name.text === 'value' ? l.expression : l
|
|
135
|
+
if (
|
|
136
|
+
ts.isPropertyAccessExpression(target) &&
|
|
137
|
+
target.name.text === 'remote' &&
|
|
138
|
+
ts.isIdentifier(target.expression) &&
|
|
139
|
+
rows.has(target.expression.text)
|
|
140
|
+
)
|
|
141
|
+
report('U004', n, '`remote` is what others are doing: only the network writes it.')
|
|
142
|
+
}
|
|
143
|
+
if (ts.isJsxSelfClosingElement(n) || ts.isJsxOpeningElement(n)) {
|
|
144
|
+
const tag = n.tagName.getText(sf),
|
|
145
|
+
ui = n.attributes.properties.find(
|
|
146
|
+
(a) => ts.isJsxAttribute(a) && a.name.getText(sf) === 'ui',
|
|
147
|
+
)
|
|
148
|
+
if (ui && warp.has(tag) && !TAKES_UI.has(warp.get(tag)))
|
|
149
|
+
report(
|
|
150
|
+
'U005',
|
|
151
|
+
ui,
|
|
152
|
+
`\`${warp.get(tag)}\` doesn't take \`ui\` yet, so this writes an attribute and draws nothing.`,
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
ts.forEachChild(n, visit)
|
|
156
|
+
}
|
|
157
|
+
return visit
|
|
158
|
+
}
|
package/src/site.mjs
CHANGED
|
@@ -7,13 +7,40 @@ const absolute = /^(https?:)?\/\//
|
|
|
7
7
|
export function checkSite(site, report) {
|
|
8
8
|
const image = typeof site.image === 'object' ? site.image.url : site.image
|
|
9
9
|
if (image && !site.url && !absolute.test(image)) {
|
|
10
|
-
const what =
|
|
11
|
-
|
|
10
|
+
const what =
|
|
11
|
+
image === 'auto'
|
|
12
|
+
? "The generated card (`image: 'auto'`) is served from a relative URL"
|
|
13
|
+
: `\`site.image\` (\`${image}\`) is relative`
|
|
14
|
+
report(
|
|
15
|
+
'H001',
|
|
16
|
+
`${what} and \`site.url\` is not set, so og:image (and canonical, og:url) can't be absolute. Crawlers don't resolve relative URLs.`,
|
|
17
|
+
'image',
|
|
18
|
+
)
|
|
12
19
|
}
|
|
13
|
-
if (!site.description?.trim())
|
|
20
|
+
if (!site.description?.trim())
|
|
21
|
+
report(
|
|
22
|
+
'H003',
|
|
23
|
+
'Pages have no description: `site.description` is not set and no route overrides it yet.',
|
|
24
|
+
'site',
|
|
25
|
+
)
|
|
14
26
|
// The default page title is `name`; a template's fixed part is in every other title.
|
|
15
27
|
const fixed = site.title ? site.title.replace('%s', '').length : 0
|
|
16
|
-
if (site.name && site.name.length > 60)
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
if (site.name && site.name.length > 60)
|
|
29
|
+
report(
|
|
30
|
+
'H004',
|
|
31
|
+
`The default title \`${site.name}\` is ${site.name.length} characters (over 60).`,
|
|
32
|
+
'name',
|
|
33
|
+
)
|
|
34
|
+
if (fixed > 40)
|
|
35
|
+
report(
|
|
36
|
+
'H004',
|
|
37
|
+
`The title template's fixed part is ${fixed} characters, leaving under 20 of 60 for the page title.`,
|
|
38
|
+
'title',
|
|
39
|
+
)
|
|
40
|
+
if (site.description && site.description.length > 160)
|
|
41
|
+
report(
|
|
42
|
+
'H004',
|
|
43
|
+
`\`site.description\` is ${site.description.length} characters (over 160).`,
|
|
44
|
+
'description',
|
|
45
|
+
)
|
|
19
46
|
}
|
package/src/slots.mjs
CHANGED
|
@@ -6,21 +6,92 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export const slotTable = [
|
|
8
8
|
{ id: 'font.build', version: 1, cardinality: 'one', sides: ['build'], default: null, phase: '2' },
|
|
9
|
-
{
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
{
|
|
9
|
+
{
|
|
10
|
+
id: 'media.build',
|
|
11
|
+
version: 1,
|
|
12
|
+
cardinality: 'one',
|
|
13
|
+
sides: ['build'],
|
|
14
|
+
default: null,
|
|
15
|
+
phase: '2',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'media.encode',
|
|
19
|
+
version: 1,
|
|
20
|
+
cardinality: 'keyed',
|
|
21
|
+
sides: ['build', 'server'],
|
|
22
|
+
default: null,
|
|
23
|
+
phase: '3c',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'media.server',
|
|
27
|
+
version: 1,
|
|
28
|
+
cardinality: 'one',
|
|
29
|
+
sides: ['server'],
|
|
30
|
+
default: null,
|
|
31
|
+
phase: '3',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'text.measure',
|
|
35
|
+
version: 1,
|
|
36
|
+
cardinality: 'one',
|
|
37
|
+
sides: ['client', 'worker'],
|
|
38
|
+
default: null,
|
|
39
|
+
phase: '2 (Surfaces)',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'surface.renderer',
|
|
43
|
+
version: 1,
|
|
44
|
+
cardinality: 'keyed',
|
|
45
|
+
sides: ['client'],
|
|
46
|
+
default: ['canvas2d', 'webgl2'],
|
|
47
|
+
phase: '2 (Surfaces)',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'realtime.bus',
|
|
51
|
+
version: 1,
|
|
52
|
+
cardinality: 'one',
|
|
53
|
+
sides: ['server'],
|
|
54
|
+
default: 'memory',
|
|
55
|
+
phase: '3',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'db.adapter',
|
|
59
|
+
version: 1,
|
|
60
|
+
cardinality: 'keyed',
|
|
61
|
+
sides: ['server'],
|
|
62
|
+
default: ['drizzle'],
|
|
63
|
+
phase: '3c',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'server.extension',
|
|
67
|
+
version: 1,
|
|
68
|
+
cardinality: 'keyed',
|
|
69
|
+
sides: ['server'],
|
|
70
|
+
default: null,
|
|
71
|
+
phase: '6c',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'pay.rail',
|
|
75
|
+
version: 1,
|
|
76
|
+
cardinality: 'keyed',
|
|
77
|
+
sides: ['server', 'client', 'native'],
|
|
78
|
+
default: null,
|
|
79
|
+
phase: '6c',
|
|
80
|
+
},
|
|
18
81
|
]
|
|
19
82
|
|
|
20
83
|
export function slotDef(id) {
|
|
21
|
-
const def = slotTable.find(s => s.id === id)
|
|
84
|
+
const def = slotTable.find((s) => s.id === id)
|
|
22
85
|
if (def) return def
|
|
23
|
-
if (/^device\.[a-z][\w-]*$/.test(id))
|
|
86
|
+
if (/^device\.[a-z][\w-]*$/.test(id))
|
|
87
|
+
return {
|
|
88
|
+
id,
|
|
89
|
+
version: 1,
|
|
90
|
+
cardinality: 'keyed',
|
|
91
|
+
sides: ['native', 'client'],
|
|
92
|
+
default: null,
|
|
93
|
+
phase: '6b',
|
|
94
|
+
}
|
|
24
95
|
return undefined
|
|
25
96
|
}
|
|
26
97
|
|
|
@@ -32,7 +103,7 @@ export function slotDef(id) {
|
|
|
32
103
|
export function bindSlots(config, report) {
|
|
33
104
|
const plugins = config.plugins ?? []
|
|
34
105
|
const chosen = config.slots ?? {}
|
|
35
|
-
const names = new Set(plugins.map(p => p.name))
|
|
106
|
+
const names = new Set(plugins.map((p) => p.name))
|
|
36
107
|
|
|
37
108
|
/** slot id → key ('' for `one`) → provider names */
|
|
38
109
|
const offers = new Map()
|
|
@@ -40,7 +111,11 @@ export function bindSlots(config, report) {
|
|
|
40
111
|
for (const item of p.provides ?? []) {
|
|
41
112
|
const { slot, key = '' } = typeof item === 'string' ? { slot: item } : item
|
|
42
113
|
if (!slotDef(slot) && !slot.startsWith(`${p.name}.`)) {
|
|
43
|
-
report(
|
|
114
|
+
report(
|
|
115
|
+
'P008',
|
|
116
|
+
`\`${p.name}\` provides \`${slot}\`, which is not a slot weft defines.`,
|
|
117
|
+
p.name,
|
|
118
|
+
)
|
|
44
119
|
continue
|
|
45
120
|
}
|
|
46
121
|
const byKey = offers.get(slot) ?? new Map()
|
|
@@ -57,13 +132,15 @@ export function bindSlots(config, report) {
|
|
|
57
132
|
const picks = typeof pick === 'string' ? { '': pick } : pick
|
|
58
133
|
for (const [key, name] of Object.entries(picks)) {
|
|
59
134
|
if (name.startsWith('weft:default')) continue
|
|
60
|
-
if (!names.has(name))
|
|
61
|
-
|
|
135
|
+
if (!names.has(name))
|
|
136
|
+
report('P009', `\`slots['${id}']\` picks \`${name}\`, which is not in \`plugins\`.`, id)
|
|
137
|
+
else if (!(offers.get(id)?.get(key) ?? []).includes(name))
|
|
138
|
+
report('P009', `\`slots['${id}']\` picks \`${name}\`, which does not provide it.`, id)
|
|
62
139
|
}
|
|
63
140
|
}
|
|
64
141
|
|
|
65
142
|
/** @type {Set<string>} */
|
|
66
|
-
const known = new Set([...slotTable.map(s => s.id), ...offers.keys()])
|
|
143
|
+
const known = new Set([...slotTable.map((s) => s.id), ...offers.keys()])
|
|
67
144
|
const ids = [...known].sort()
|
|
68
145
|
const out = {}
|
|
69
146
|
for (const id of ids) {
|
|
@@ -77,7 +154,11 @@ export function bindSlots(config, report) {
|
|
|
77
154
|
if (providers.length === 1) return providers[0]
|
|
78
155
|
if (providers.length > 1) {
|
|
79
156
|
const where = key ? `\`${id}\` key \`${key}\`` : `\`${id}\``
|
|
80
|
-
report(
|
|
157
|
+
report(
|
|
158
|
+
'P100',
|
|
159
|
+
`${where} has ${providers.length} providers: ${providers.join(', ')}. Array order never decides.`,
|
|
160
|
+
providers[0],
|
|
161
|
+
)
|
|
81
162
|
return null
|
|
82
163
|
}
|
|
83
164
|
return fallback
|
|
@@ -86,9 +167,13 @@ export function bindSlots(config, report) {
|
|
|
86
167
|
out[id] = resolve('', typeof def.default === 'string' ? `weft:default:${def.default}` : null)
|
|
87
168
|
} else {
|
|
88
169
|
/** @type {Set<string>} */
|
|
89
|
-
const keys = new Set([
|
|
170
|
+
const keys = new Set([
|
|
171
|
+
...(Array.isArray(def.default) ? def.default : []),
|
|
172
|
+
...[...byKey.keys()].filter(Boolean),
|
|
173
|
+
])
|
|
90
174
|
const block = {}
|
|
91
|
-
for (const key of [...keys].sort())
|
|
175
|
+
for (const key of [...keys].sort())
|
|
176
|
+
block[key] = resolve(key, def.default?.includes(key) ? 'weft:default' : null)
|
|
92
177
|
out[id] = block
|
|
93
178
|
}
|
|
94
179
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { audit, builtPublic } from '../audit.mjs'
|
|
2
|
+
import { writeMap } from '../map.mjs'
|
|
3
|
+
import { diagnostic } from '../diag.mjs'
|
|
4
|
+
import { loadConfig } from '../load-config.mjs'
|
|
5
|
+
import { host, presetFor } from '../deploy/hosts.mjs'
|
|
6
|
+
|
|
7
|
+
export async function finishBuild({
|
|
8
|
+
root,
|
|
9
|
+
config,
|
|
10
|
+
preset,
|
|
11
|
+
rust,
|
|
12
|
+
refuse,
|
|
13
|
+
running,
|
|
14
|
+
shipSidecars,
|
|
15
|
+
output,
|
|
16
|
+
warned,
|
|
17
|
+
unsafe,
|
|
18
|
+
forced,
|
|
19
|
+
loaded,
|
|
20
|
+
t,
|
|
21
|
+
writeContract,
|
|
22
|
+
}) {
|
|
23
|
+
const contract = await writeContract({
|
|
24
|
+
root,
|
|
25
|
+
site: config.site,
|
|
26
|
+
preset,
|
|
27
|
+
rust,
|
|
28
|
+
live: (await loadConfig(root)).config?.live,
|
|
29
|
+
verify: true,
|
|
30
|
+
})
|
|
31
|
+
for (const refused of contract.diagnostics.filter((d) => d.severity === 'error')) refuse(refused)
|
|
32
|
+
if (running) shipSidecars(root)
|
|
33
|
+
await writeMap(root)
|
|
34
|
+
// Last, the pages as built, in a clean Chrome at a phone's width and a desktop's (audit.mjs): an error fails the build.
|
|
35
|
+
const settings = (await loadConfig(root)).config?.build?.audit ?? true
|
|
36
|
+
const audited =
|
|
37
|
+
settings === false
|
|
38
|
+
? {
|
|
39
|
+
diagnostics: [
|
|
40
|
+
diagnostic({
|
|
41
|
+
code: 'L000',
|
|
42
|
+
severity: 'warning',
|
|
43
|
+
file: 'weft.config.ts',
|
|
44
|
+
why: '`build.audit` is false: the built pages were not audited.',
|
|
45
|
+
}),
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
: await audit(root, {
|
|
49
|
+
dir: builtPublic(root),
|
|
50
|
+
...(typeof settings === 'object' && settings),
|
|
51
|
+
})
|
|
52
|
+
const pages = audited.diagnostics.map((d) =>
|
|
53
|
+
d.severity === 'error' && unsafe.allows(d) ? unsafe.let(d) : d,
|
|
54
|
+
)
|
|
55
|
+
const kept = [
|
|
56
|
+
...contract.diagnostics.filter((d) => !(d.severity === 'error' && unsafe.allows(d))),
|
|
57
|
+
...warned,
|
|
58
|
+
].map((d) => diagnostic({ file: '.weft/contract.json', ...d }))
|
|
59
|
+
const diagnostics = [
|
|
60
|
+
...kept,
|
|
61
|
+
...pages,
|
|
62
|
+
...forced.map((d) => diagnostic({ file: '.weft/contract.json', ...d })),
|
|
63
|
+
]
|
|
64
|
+
return {
|
|
65
|
+
ok: !diagnostics.some((d) => d.severity === 'error'),
|
|
66
|
+
preset,
|
|
67
|
+
output,
|
|
68
|
+
contract: contract.path,
|
|
69
|
+
functions: contract.functions,
|
|
70
|
+
notes: loaded.contributions.flatMap((c) => c.manifest.notes ?? []),
|
|
71
|
+
audited: audited.pages?.length ?? 0,
|
|
72
|
+
diagnostics,
|
|
73
|
+
ms: Math.round(performance.now() - t),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function startBuildSidecars({
|
|
78
|
+
start,
|
|
79
|
+
root,
|
|
80
|
+
config,
|
|
81
|
+
preset,
|
|
82
|
+
rust,
|
|
83
|
+
refuse,
|
|
84
|
+
writeContract,
|
|
85
|
+
}) {
|
|
86
|
+
const { buildSidecars, shipSidecars, startSidecars, usesRemote } = await start(root, 'sidecar')
|
|
87
|
+
const contracted = () => writeContract({ root, site: config.site, preset, rust })
|
|
88
|
+
let warned = []
|
|
89
|
+
// Sidecars first: prerendering a share card runs its route's `load`, which may read one.
|
|
90
|
+
const running = usesRemote(root)
|
|
91
|
+
? await contracted().then(async (c) => {
|
|
92
|
+
const stop = c.diagnostics.find((d) => d.severity === 'error')
|
|
93
|
+
if (stop) refuse(stop)
|
|
94
|
+
const built = await buildSidecars({ root, contract: c.contract, rust, preset })
|
|
95
|
+
const failed = built.diagnostics.find((d) => d.severity === 'error')
|
|
96
|
+
if (failed) refuse(failed)
|
|
97
|
+
warned = built.diagnostics
|
|
98
|
+
return startSidecars({ root, built: true, log: () => {} })
|
|
99
|
+
})
|
|
100
|
+
: undefined
|
|
101
|
+
return { running, warned, shipSidecars }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function buildPreset(root, { preset, runtime, hostRuntime, PRESETS }) {
|
|
105
|
+
const chosen = runtime ?? (await loadConfig(root)).config?.runtime ?? hostRuntime()
|
|
106
|
+
// `deploy.target` picks the preset when nothing else does: a build for Fly is the runtime's server, for Netlify `netlify`
|
|
107
|
+
const target = !preset && !runtime && (await loadConfig(root)).config?.deploy?.target
|
|
108
|
+
if (target) preset = presetFor(host(target), chosen)
|
|
109
|
+
if (!preset && !PRESETS[chosen])
|
|
110
|
+
throw Object.assign(
|
|
111
|
+
new Error(`unknown runtime "${chosen}": use ${Object.keys(PRESETS).join(', ')}`),
|
|
112
|
+
{ code: 'E_USAGE' },
|
|
113
|
+
)
|
|
114
|
+
preset ??= PRESETS[chosen]
|
|
115
|
+
return preset
|
|
116
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { diagnostic, locate, positions } from '../diag.mjs'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Route findings as diagnostics, pointing at the `meta` export, or at where one goes. H011's fix is that export, as an
|
|
7
|
+
* edit an agent can apply: not safe, since the words are a start and not the page's best.
|
|
8
|
+
*/
|
|
9
|
+
export function routeDiagnostics(root, found) {
|
|
10
|
+
const text = (f) => {
|
|
11
|
+
try {
|
|
12
|
+
return readFileSync(join(root, f), 'utf8')
|
|
13
|
+
} catch {
|
|
14
|
+
return ''
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return found.map(({ code, severity, file, route, why, help }) => {
|
|
18
|
+
const source = text(file)
|
|
19
|
+
const at =
|
|
20
|
+
code === 'H011' &&
|
|
21
|
+
help?.startsWith('export const') &&
|
|
22
|
+
!/\bexport\s+(?:const|function|async function)\s+meta\b/.test(source)
|
|
23
|
+
? source.indexOf('export default')
|
|
24
|
+
: -1
|
|
25
|
+
const place = positions(source)
|
|
26
|
+
const fix = at >= 0 && {
|
|
27
|
+
fix: {
|
|
28
|
+
safe: false,
|
|
29
|
+
description: 'Add `meta`',
|
|
30
|
+
edits: [{ file, span: place.span(at, at), offset: [at, at], text: `${help}\n\n` }],
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
...diagnostic({ code, severity, file, why, help, ...locate(source, 'meta'), ...fix }),
|
|
35
|
+
route,
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
}
|