@weft-js/cli 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -14
- package/bin/weft.mjs +8 -1
- package/docs/async.md +16 -6
- package/docs/browser.md +22 -8
- package/docs/config.md +39 -9
- package/docs/core.md +30 -0
- package/docs/deploy.md +108 -0
- package/docs/design.md +12 -3
- package/docs/entrypoints.md +79 -0
- package/docs/fonts.md +12 -2
- package/docs/icons.md +13 -7
- package/docs/lists.md +3 -1
- package/docs/meta.md +13 -3
- package/docs/plugins.md +32 -9
- package/docs/props.md +13 -2
- package/docs/reactivity.md +26 -0
- package/docs/rules.md +19 -12
- package/docs/rust.md +47 -17
- package/docs/start-short.md +3 -2
- package/docs/start.md +226 -52
- package/docs/state.md +33 -7
- package/docs/style.md +31 -8
- package/docs/surface.md +61 -15
- package/docs/testing.md +15 -5
- package/docs/theme.md +182 -0
- package/docs/ui-state.md +19 -6
- package/docs/verify.md +13 -0
- package/package.json +13 -4
- package/src/add/config.mjs +93 -0
- package/src/add/errors.mjs +2 -0
- package/src/add/setup.mjs +88 -0
- package/src/add-rust.mjs +82 -27
- package/src/add.mjs +209 -181
- package/src/agents-md.mjs +144 -43
- package/src/alias.mjs +16 -5
- package/src/audit/page.mjs +313 -0
- package/src/audit/report.mjs +69 -0
- package/src/audit/utils.mjs +5 -0
- package/src/audit.mjs +174 -188
- package/src/browser/cli.mjs +24 -5
- package/src/browser/client.mjs +189 -25
- package/src/browser/commands.mjs +166 -51
- package/src/catalog/audit.mjs +129 -0
- package/src/catalog/auth.mjs +40 -0
- package/src/catalog/compiler.mjs +337 -0
- package/src/catalog/contracts.mjs +18 -0
- package/src/catalog/database.mjs +52 -0
- package/src/catalog/deploy.mjs +117 -0
- package/src/catalog/design.mjs +159 -0
- package/src/catalog/forms.mjs +25 -0
- package/src/catalog/hydration.mjs +29 -0
- package/src/catalog/icons.mjs +24 -0
- package/src/catalog/media.mjs +163 -0
- package/src/catalog/metadata.mjs +116 -0
- package/src/catalog/plugins.mjs +228 -0
- package/src/catalog/realtime.mjs +195 -0
- package/src/catalog/rust.mjs +75 -0
- package/src/catalog/server-functions.mjs +92 -0
- package/src/catalog/state.mjs +39 -0
- package/src/catalog/style-groups.mjs +54 -0
- package/src/catalog/tests.mjs +127 -0
- package/src/catalog/text.mjs +200 -0
- package/src/catalog/ui-state.mjs +42 -0
- package/src/catalog.mjs +68 -444
- package/src/check/runtime.mjs +280 -0
- package/src/check.mjs +199 -207
- package/src/cli/authoring.mjs +213 -0
- package/src/cli/project.mjs +302 -0
- package/src/cli.mjs +228 -224
- package/src/config.mjs +1 -1
- package/src/contributions.mjs +75 -25
- package/src/db-layer.mjs +13 -5
- package/src/deploy/cdn.mjs +142 -0
- package/src/deploy/files.mjs +266 -0
- package/src/deploy/fit.mjs +166 -0
- package/src/deploy/hosts.mjs +126 -0
- package/src/deploy/index/cli.mjs +20 -0
- package/src/deploy/index/environment.mjs +69 -0
- package/src/deploy/index/steps.mjs +87 -0
- package/src/deploy/index.mjs +387 -0
- package/src/deploy/needs.mjs +149 -0
- package/src/design-checks/accessible-names.mjs +62 -0
- package/src/design-checks/contrast.mjs +43 -0
- package/src/design-checks/csp.mjs +60 -0
- package/src/design-checks/files.mjs +59 -0
- package/src/design-checks/heading-links.mjs +61 -0
- package/src/design-checks/interaction.mjs +65 -0
- package/src/design-checks/jsx.mjs +67 -0
- package/src/design-checks/layout-values.mjs +68 -0
- package/src/design-checks/native-controls.mjs +48 -0
- package/src/design-checks/outline.mjs +39 -0
- package/src/design-checks/reporter.mjs +37 -0
- package/src/design-checks/rules.mjs +274 -0
- package/src/design-checks/values.mjs +262 -0
- package/src/design-checks.mjs +10 -400
- package/src/diag.mjs +32 -8
- package/src/docs.mjs +138 -42
- package/src/doctor.mjs +20 -8
- package/src/files.mjs +18 -5
- package/src/fix.mjs +10 -2
- package/src/gate/after.mjs +40 -0
- package/src/gate/before.mjs +54 -0
- package/src/gate/classify.mjs +72 -0
- package/src/gate/mutation.mjs +209 -0
- package/src/gate/resolve-cases.mjs +22 -0
- package/src/gate/result.mjs +63 -0
- package/src/gate/run.mjs +75 -0
- package/src/gate.mjs +104 -202
- package/src/infer/box-props.cjs +140 -0
- package/src/infer/core-boxes.cjs +46 -0
- package/src/infer/derived-writes.cjs +95 -0
- package/src/infer/engine/checks.cjs +193 -0
- package/src/infer/engine/flow.cjs +293 -0
- package/src/infer/engine/report.cjs +158 -0
- package/src/infer/engine/utils.cjs +15 -0
- package/src/infer/engine.cjs +108 -368
- package/src/infer/facts/sites.cjs +114 -0
- package/src/infer/facts.cjs +210 -117
- package/src/infer/hosts.cjs +69 -22
- package/src/infer/lsp.mjs +149 -40
- package/src/infer/session.cjs +73 -17
- package/src/infer/shadow.cjs +13 -5
- package/src/infer/worker.cjs +57 -9
- package/src/jsx-scan.mjs +63 -21
- package/src/load-config/schema.mjs +281 -0
- package/src/load-config.mjs +144 -116
- package/src/map.mjs +93 -26
- package/src/mcp/browser-tools.mjs +155 -0
- package/src/mcp.mjs +147 -69
- package/src/meta-table.mjs +43 -8
- package/src/mutate.mjs +120 -36
- package/src/plugin-checks.mjs +52 -23
- package/src/plugins.mjs +125 -39
- package/src/project.mjs +170 -79
- package/src/props-checks.mjs +24 -6
- package/src/props-listener.mjs +27 -9
- package/src/react-checks.mjs +35 -4
- package/src/server-state-checks.mjs +34 -0
- package/src/share-checks.mjs +121 -37
- package/src/site.mjs +33 -6
- package/src/slots.mjs +104 -19
- package/src/start/build.mjs +116 -0
- package/src/start/routes.mjs +38 -0
- package/src/start.mjs +194 -92
- package/src/state-checks/core-state.mjs +223 -0
- package/src/state-checks/modules.mjs +17 -0
- package/src/state-checks/scan.mjs +232 -0
- package/src/state-checks/server-graph.mjs +65 -0
- package/src/state-checks.mjs +109 -133
- package/src/style/find.mjs +121 -0
- package/src/style.mjs +133 -127
- package/src/surface-checks.mjs +212 -46
- package/src/tailwind.mjs +5 -4
- package/src/tasks.mjs +189 -27
- package/src/test-rules/context.mjs +109 -0
- package/src/test-rules/file-rules.mjs +279 -0
- package/src/test-rules/rule-helpers.mjs +244 -0
- package/src/test-rules/syntax.mjs +223 -0
- package/src/test-rules/test-rules.mjs +127 -0
- package/src/testdiff.mjs +94 -32
- package/src/tests.mjs +31 -483
- package/src/text-checks.mjs +229 -69
- package/src/tokens.mjs +12 -4
- package/src/trust/scan.mjs +95 -0
- package/src/trust.mjs +214 -102
- package/src/weft.mjs +41 -9
- package/src/workspace.mjs +96 -19
- package/ts-plugin/context.js +57 -0
- package/ts-plugin/index.js +4 -124
- package/ts-plugin/mapping.js +65 -0
- package/ts-plugin/proxy.js +144 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const isCap = (name) => /^[A-Z]/.test(name)
|
|
2
|
+
|
|
3
|
+
function jsxSite(ts, sf, node, enclosing) {
|
|
4
|
+
const tag = node.tagName
|
|
5
|
+
if (!ts.isIdentifier(tag) && !ts.isPropertyAccessExpression(tag)) return null
|
|
6
|
+
if (ts.isIdentifier(tag) && !isCap(tag.text)) return null
|
|
7
|
+
const span = (n) => [n.getStart(sf), n.end]
|
|
8
|
+
const site = {
|
|
9
|
+
tag: tag.getText(sf),
|
|
10
|
+
tagSpan: span(tag),
|
|
11
|
+
span: span(node),
|
|
12
|
+
attrs: [],
|
|
13
|
+
spreads: [],
|
|
14
|
+
children: null,
|
|
15
|
+
in: enclosing ?? null,
|
|
16
|
+
}
|
|
17
|
+
for (const a of node.attributes.properties) {
|
|
18
|
+
if (ts.isJsxSpreadAttribute(a)) {
|
|
19
|
+
site.spreads.push({ span: span(a.expression), text: a.expression.getText(sf) })
|
|
20
|
+
continue
|
|
21
|
+
}
|
|
22
|
+
const raw = a.name.getText(sf)
|
|
23
|
+
const bind = raw.startsWith('bind:')
|
|
24
|
+
const name = bind ? raw.slice(5) : raw
|
|
25
|
+
if (name === 'key') continue
|
|
26
|
+
const value = a.initializer
|
|
27
|
+
if (!value) {
|
|
28
|
+
site.attrs.push({ name, literal: 'true', bind })
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
if (ts.isStringLiteral(value)) {
|
|
32
|
+
site.attrs.push({
|
|
33
|
+
name,
|
|
34
|
+
literal: 'string',
|
|
35
|
+
exact: JSON.stringify(value.text),
|
|
36
|
+
whole: span(a),
|
|
37
|
+
bind,
|
|
38
|
+
})
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
const expr = value.expression
|
|
42
|
+
if (!expr) continue
|
|
43
|
+
const lambda = ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)
|
|
44
|
+
site.attrs.push({
|
|
45
|
+
name,
|
|
46
|
+
span: span(expr),
|
|
47
|
+
whole: span(a),
|
|
48
|
+
bind,
|
|
49
|
+
raw: bind ? span(a.name) : undefined,
|
|
50
|
+
untypedFn: (lambda && expr.parameters.some((p) => !p.type)) || undefined,
|
|
51
|
+
value: ts.isIdentifier(expr) && isCap(expr.text) ? expr.text : undefined,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
if (ts.isJsxOpeningElement(node)) {
|
|
55
|
+
const kids = node.parent.children.filter((c) => !(ts.isJsxText(c) && !c.text.trim()))
|
|
56
|
+
if (kids.length) {
|
|
57
|
+
const only = kids.length === 1 ? kids[0] : null
|
|
58
|
+
const expr = only && ts.isJsxExpression(only) ? only.expression : null
|
|
59
|
+
const lambda = expr && (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr))
|
|
60
|
+
site.children = lambda
|
|
61
|
+
? {
|
|
62
|
+
fn: true,
|
|
63
|
+
span: span(expr),
|
|
64
|
+
untypedFn: expr.parameters.some((p) => !p.type) || undefined,
|
|
65
|
+
}
|
|
66
|
+
: { fn: false }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return site
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** `const views = { a: A }`, `pick(A, B)`, `<X icon={A} />`: an identifier that leaves JSX tag position. */
|
|
73
|
+
function escapes(ts, node) {
|
|
74
|
+
const up = node.parent
|
|
75
|
+
if (ts.isJsxOpeningLikeElement?.(up) && up.tagName === node) return false
|
|
76
|
+
if (ts.isJsxClosingElement(up)) return false
|
|
77
|
+
if (ts.isFunctionDeclaration(up) || (ts.isVariableDeclaration(up) && up.name === node))
|
|
78
|
+
return false
|
|
79
|
+
if (ts.isPropertyAccessExpression(up) && up.name === node) return false
|
|
80
|
+
if (
|
|
81
|
+
ts.isExportSpecifier(up) ||
|
|
82
|
+
ts.isExportAssignment(up) ||
|
|
83
|
+
ts.isImportSpecifier(up) ||
|
|
84
|
+
ts.isImportClause(up)
|
|
85
|
+
)
|
|
86
|
+
return false
|
|
87
|
+
if (ts.isTypeQueryNode(up)) return false
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The names an initializer can hand back: `pick(A, B)`, `{ a: A, b: B }`, `views[k]`, `views.a`, `hoc(A)`, `x ? A : B`. */
|
|
92
|
+
function holds(ts, init) {
|
|
93
|
+
const name = (e) => (ts.isIdentifier(e) ? [e.text] : [])
|
|
94
|
+
if (ts.isCallExpression(init)) return init.arguments.flatMap((a) => name(a)).filter(isCap)
|
|
95
|
+
if (ts.isConditionalExpression(init))
|
|
96
|
+
return [...name(init.whenTrue), ...name(init.whenFalse)].filter(isCap)
|
|
97
|
+
if (ts.isObjectLiteralExpression(init))
|
|
98
|
+
return init.properties
|
|
99
|
+
.flatMap((p) =>
|
|
100
|
+
ts.isPropertyAssignment(p)
|
|
101
|
+
? name(p.initializer)
|
|
102
|
+
: ts.isShorthandPropertyAssignment(p)
|
|
103
|
+
? [p.name.text]
|
|
104
|
+
: [],
|
|
105
|
+
)
|
|
106
|
+
.filter(isCap)
|
|
107
|
+
if (ts.isElementAccessExpression(init) || ts.isPropertyAccessExpression(init))
|
|
108
|
+
return name(init.expression)
|
|
109
|
+
return []
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const isOpening = (ts, n) => ts.isJsxOpeningElement(n) || ts.isJsxSelfClosingElement(n)
|
|
113
|
+
|
|
114
|
+
module.exports = { isCap, jsxSite, escapes, holds, isOpening }
|
package/src/infer/facts.cjs
CHANGED
|
@@ -4,33 +4,46 @@
|
|
|
4
4
|
|
|
5
5
|
const { createHash } = require('node:crypto')
|
|
6
6
|
|
|
7
|
-
const hashOf = text => createHash('sha1').update(text).digest('base64url').slice(0, 16)
|
|
8
|
-
const isCap
|
|
7
|
+
const hashOf = (text) => createHash('sha1').update(text).digest('base64url').slice(0, 16)
|
|
8
|
+
const { isCap, jsxSite, escapes, holds, isOpening } = require('./facts/sites.cjs')
|
|
9
9
|
/** The comment a generated annotation starts with. It keeps the type regenerable; deleting it makes the type yours. */
|
|
10
10
|
const MARK = '/*inferred*/'
|
|
11
11
|
|
|
12
12
|
function isExported(ts, node) {
|
|
13
|
-
return
|
|
13
|
+
return (
|
|
14
|
+
!!ts.canHaveModifiers(node) &&
|
|
15
|
+
(ts.getModifiers(node) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
|
|
16
|
+
)
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
const walk = (ts, node, visit) => {
|
|
17
|
-
const go = n => {
|
|
20
|
+
const go = (n) => {
|
|
21
|
+
if (visit(n) !== false) ts.forEachChild(n, go)
|
|
22
|
+
}
|
|
18
23
|
ts.forEachChild(node, go)
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
/** A function a component could be: the node, its name, and the parameter it would type. */
|
|
22
27
|
function componentAt(ts, node) {
|
|
23
|
-
if (ts.isFunctionDeclaration(node) && node.name && isCap(node.name.text))
|
|
24
|
-
|
|
28
|
+
if (ts.isFunctionDeclaration(node) && node.name && isCap(node.name.text))
|
|
29
|
+
return { fn: node, name: node.name.text, nameNode: node.name, holder: node }
|
|
30
|
+
if (
|
|
31
|
+
ts.isVariableDeclaration(node) &&
|
|
32
|
+
ts.isIdentifier(node.name) &&
|
|
33
|
+
isCap(node.name.text) &&
|
|
34
|
+
!node.type &&
|
|
35
|
+
node.initializer
|
|
36
|
+
) {
|
|
25
37
|
const init = node.initializer
|
|
26
|
-
if (ts.isArrowFunction(init) || ts.isFunctionExpression(init))
|
|
38
|
+
if (ts.isArrowFunction(init) || ts.isFunctionExpression(init))
|
|
39
|
+
return { fn: init, name: node.name.text, nameNode: node.name, holder: node.parent.parent }
|
|
27
40
|
}
|
|
28
41
|
return null
|
|
29
42
|
}
|
|
30
43
|
|
|
31
44
|
const hasJsx = (ts, node) => {
|
|
32
45
|
let found = false
|
|
33
|
-
walk(ts, node, n => {
|
|
46
|
+
walk(ts, node, (n) => {
|
|
34
47
|
if (found) return false
|
|
35
48
|
if (ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n) || ts.isJsxFragment(n)) found = true
|
|
36
49
|
return !found
|
|
@@ -41,9 +54,20 @@ const hasJsx = (ts, node) => {
|
|
|
41
54
|
/** A parameter pattern's keys: `{ a, b = 1, c: d, ...rest }`. */
|
|
42
55
|
function patternKeys(ts, sf, pattern, out) {
|
|
43
56
|
for (const el of pattern.elements) {
|
|
44
|
-
if (el.dotDotDotToken) {
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
if (el.dotDotDotToken) {
|
|
58
|
+
if (ts.isIdentifier(el.name)) out.rest = el.name.text
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
const key =
|
|
62
|
+
el.propertyName && ts.isIdentifier(el.propertyName)
|
|
63
|
+
? el.propertyName.text
|
|
64
|
+
: ts.isIdentifier(el.name)
|
|
65
|
+
? el.name.text
|
|
66
|
+
: null
|
|
67
|
+
if (!key) {
|
|
68
|
+
out.opaque = true
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
47
71
|
if (ts.isIdentifier(el.name)) out.aliases.set(el.name.text, key)
|
|
48
72
|
const entry = out.keys.get(key) ?? { uses: [] }
|
|
49
73
|
if (el.initializer) entry.fallback = [el.initializer.getStart(sf), el.initializer.end]
|
|
@@ -54,19 +78,27 @@ function patternKeys(ts, sf, pattern, out) {
|
|
|
54
78
|
/** Reads and writes of the prop `key` at `node`, the expression that names it. */
|
|
55
79
|
function classify(ts, sf, node) {
|
|
56
80
|
const up = node.parent
|
|
57
|
-
const span = n => [n.getStart(sf), n.end]
|
|
58
|
-
if (
|
|
81
|
+
const span = (n) => [n.getStart(sf), n.end]
|
|
82
|
+
if (
|
|
83
|
+
(ts.isPostfixUnaryExpression(up) || ts.isPrefixUnaryExpression(up)) &&
|
|
84
|
+
(up.operator === ts.SyntaxKind.PlusPlusToken || up.operator === ts.SyntaxKind.MinusMinusToken)
|
|
85
|
+
)
|
|
86
|
+
return { as: 'number', span: span(node) }
|
|
59
87
|
if (ts.isBinaryExpression(up) && up.left === node) {
|
|
60
88
|
const op = up.operatorToken.kind
|
|
61
|
-
if (op === ts.SyntaxKind.EqualsToken || op === ts.SyntaxKind.PlusEqualsToken)
|
|
62
|
-
|
|
89
|
+
if (op === ts.SyntaxKind.EqualsToken || op === ts.SyntaxKind.PlusEqualsToken)
|
|
90
|
+
return { as: 'write', span: span(node), value: span(up.right) }
|
|
91
|
+
if (op >= ts.SyntaxKind.FirstCompoundAssignment && op <= ts.SyntaxKind.LastCompoundAssignment)
|
|
92
|
+
return { as: 'number', span: span(node) }
|
|
63
93
|
}
|
|
64
|
-
if (ts.isCallExpression(up) && up.expression === node)
|
|
65
|
-
|
|
94
|
+
if (ts.isCallExpression(up) && up.expression === node)
|
|
95
|
+
return { as: 'call', span: span(node), args: up.arguments.map(span) }
|
|
96
|
+
if ((ts.isPropertyAccessExpression(up) && up.expression === node) || ts.isNonNullExpression(up)) {
|
|
66
97
|
const member = ts.isPropertyAccessExpression(up) ? up.name.text : null
|
|
67
98
|
if (member) return { as: 'member', span: span(up), member }
|
|
68
99
|
}
|
|
69
|
-
if (ts.isJsxExpression(up) && ts.isJsxAttribute(up.parent))
|
|
100
|
+
if (ts.isJsxExpression(up) && ts.isJsxAttribute(up.parent))
|
|
101
|
+
return { as: 'context', span: span(node) }
|
|
70
102
|
return { as: 'read', span: span(node) }
|
|
71
103
|
}
|
|
72
104
|
|
|
@@ -79,109 +111,70 @@ function bodyUses(ts, sf, comp) {
|
|
|
79
111
|
entry.uses.push(classify(ts, sf, node))
|
|
80
112
|
info.keys.set(key, entry)
|
|
81
113
|
}
|
|
82
|
-
const shadows = (node, name) =>
|
|
114
|
+
const shadows = (node, name) =>
|
|
115
|
+
node.parameters?.some((p) => ts.isIdentifier(p.name) && p.name.text === name)
|
|
83
116
|
const visit = (node, scope) => {
|
|
84
117
|
// `const { a } = props`, `let n = props.n`: another name for the prop (research 43). Only the first level.
|
|
85
118
|
if (ts.isVariableDeclaration(node) && node.initializer) {
|
|
86
119
|
const init = node.initializer
|
|
87
|
-
if (ts.isIdentifier(init) && scope.has(init.text) && ts.isObjectBindingPattern(node.name))
|
|
88
|
-
|
|
120
|
+
if (ts.isIdentifier(init) && scope.has(init.text) && ts.isObjectBindingPattern(node.name))
|
|
121
|
+
patternKeys(ts, sf, node.name, info)
|
|
122
|
+
else if (
|
|
123
|
+
ts.isPropertyAccessExpression(init) &&
|
|
124
|
+
ts.isIdentifier(init.expression) &&
|
|
125
|
+
scope.has(init.expression.text) &&
|
|
126
|
+
ts.isIdentifier(node.name)
|
|
127
|
+
) {
|
|
89
128
|
info.aliases.set(node.name.text, init.name.text)
|
|
90
129
|
if (!info.keys.has(init.name.text)) info.keys.set(init.name.text, { uses: [] })
|
|
91
130
|
return
|
|
92
131
|
}
|
|
93
132
|
}
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
133
|
+
if (
|
|
134
|
+
ts.isPropertyAccessExpression(node) &&
|
|
135
|
+
ts.isIdentifier(node.expression) &&
|
|
136
|
+
scope.has(node.expression.text)
|
|
137
|
+
)
|
|
138
|
+
return use(node.name.text, node)
|
|
139
|
+
if (
|
|
140
|
+
ts.isElementAccessExpression(node) &&
|
|
141
|
+
ts.isIdentifier(node.expression) &&
|
|
142
|
+
scope.has(node.expression.text)
|
|
143
|
+
) {
|
|
144
|
+
if (ts.isStringLiteralLike(node.argumentExpression))
|
|
145
|
+
return use(node.argumentExpression.text, node)
|
|
97
146
|
info.opaque = true
|
|
98
147
|
}
|
|
99
|
-
if (
|
|
148
|
+
if (
|
|
149
|
+
ts.isIdentifier(node) &&
|
|
150
|
+
info.aliases.has(node.text) &&
|
|
151
|
+
ts.isExpressionNode?.(node) !== false
|
|
152
|
+
) {
|
|
100
153
|
const up = node.parent
|
|
101
|
-
const named =
|
|
154
|
+
const named =
|
|
155
|
+
(ts.isPropertyAccessExpression(up) && up.name === node) ||
|
|
156
|
+
(ts.isPropertyAssignment(up) && up.name === node) ||
|
|
157
|
+
ts.isBindingElement(up) ||
|
|
158
|
+
(ts.isVariableDeclaration(up) && up.name === node) ||
|
|
159
|
+
ts.isJsxAttribute(up)
|
|
102
160
|
if (!named) use(info.aliases.get(node.text), node)
|
|
103
161
|
}
|
|
104
162
|
if (ts.isFunctionLike(node)) {
|
|
105
163
|
let inner = scope
|
|
106
|
-
for (const n of scope)
|
|
107
|
-
|
|
164
|
+
for (const n of scope)
|
|
165
|
+
if (shadows(node, n)) {
|
|
166
|
+
inner = new Set(scope)
|
|
167
|
+
inner.delete(n)
|
|
168
|
+
}
|
|
169
|
+
ts.forEachChild(node, (c) => visit(c, inner))
|
|
108
170
|
return
|
|
109
171
|
}
|
|
110
|
-
ts.forEachChild(node, c => visit(c, scope))
|
|
172
|
+
ts.forEachChild(node, (c) => visit(c, scope))
|
|
111
173
|
}
|
|
112
174
|
if (fn.body) visit(fn.body, names)
|
|
113
175
|
}
|
|
114
176
|
|
|
115
|
-
function
|
|
116
|
-
const tag = node.tagName
|
|
117
|
-
if (!ts.isIdentifier(tag) && !ts.isPropertyAccessExpression(tag)) return null
|
|
118
|
-
if (ts.isIdentifier(tag) && !isCap(tag.text)) return null
|
|
119
|
-
const span = n => [n.getStart(sf), n.end]
|
|
120
|
-
const site = { tag: tag.getText(sf), tagSpan: span(tag), span: span(node), attrs: [], spreads: [], children: null, in: enclosing ?? null }
|
|
121
|
-
for (const a of node.attributes.properties) {
|
|
122
|
-
if (ts.isJsxSpreadAttribute(a)) { site.spreads.push({ span: span(a.expression), text: a.expression.getText(sf) }); continue }
|
|
123
|
-
const raw = a.name.getText(sf)
|
|
124
|
-
const bind = raw.startsWith('bind:')
|
|
125
|
-
const name = bind ? raw.slice(5) : raw
|
|
126
|
-
if (name === 'key') continue
|
|
127
|
-
const value = a.initializer
|
|
128
|
-
if (!value) { site.attrs.push({ name, literal: 'true', bind }); continue }
|
|
129
|
-
if (ts.isStringLiteral(value)) { site.attrs.push({ name, literal: 'string', exact: JSON.stringify(value.text), whole: span(a), bind }); continue }
|
|
130
|
-
const expr = value.expression
|
|
131
|
-
if (!expr) continue
|
|
132
|
-
const lambda = ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)
|
|
133
|
-
site.attrs.push({
|
|
134
|
-
name, span: span(expr), whole: span(a), bind, raw: bind ? span(a.name) : undefined,
|
|
135
|
-
untypedFn: lambda && expr.parameters.some(p => !p.type) || undefined,
|
|
136
|
-
value: ts.isIdentifier(expr) && isCap(expr.text) ? expr.text : undefined,
|
|
137
|
-
})
|
|
138
|
-
}
|
|
139
|
-
if (ts.isJsxOpeningElement(node)) {
|
|
140
|
-
const kids = node.parent.children.filter(c => !(ts.isJsxText(c) && !c.text.trim()))
|
|
141
|
-
if (kids.length) {
|
|
142
|
-
const only = kids.length === 1 ? kids[0] : null
|
|
143
|
-
const expr = only && ts.isJsxExpression(only) ? only.expression : null
|
|
144
|
-
const lambda = expr && (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr))
|
|
145
|
-
site.children = lambda ? { fn: true, span: span(expr), untypedFn: expr.parameters.some(p => !p.type) || undefined } : { fn: false }
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return site
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** `const views = { a: A }`, `pick(A, B)`, `<X icon={A} />`: an identifier that leaves JSX tag position. */
|
|
152
|
-
function escapes(ts, node) {
|
|
153
|
-
const up = node.parent
|
|
154
|
-
if (ts.isJsxOpeningLikeElement?.(up) && up.tagName === node) return false
|
|
155
|
-
if (ts.isJsxClosingElement(up)) return false
|
|
156
|
-
if (ts.isFunctionDeclaration(up) || ts.isVariableDeclaration(up) && up.name === node) return false
|
|
157
|
-
if (ts.isPropertyAccessExpression(up) && up.name === node) return false
|
|
158
|
-
if (ts.isExportSpecifier(up) || ts.isExportAssignment(up) || ts.isImportSpecifier(up) || ts.isImportClause(up)) return false
|
|
159
|
-
if (ts.isTypeQueryNode(up)) return false
|
|
160
|
-
return true
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** The names an initializer can hand back: `pick(A, B)`, `{ a: A, b: B }`, `views[k]`, `views.a`, `hoc(A)`, `x ? A : B`. */
|
|
164
|
-
function holds(ts, init) {
|
|
165
|
-
const name = e => (ts.isIdentifier(e) ? [e.text] : [])
|
|
166
|
-
if (ts.isCallExpression(init)) return init.arguments.flatMap(a => name(a)).filter(isCap)
|
|
167
|
-
if (ts.isConditionalExpression(init)) return [...name(init.whenTrue), ...name(init.whenFalse)].filter(isCap)
|
|
168
|
-
if (ts.isObjectLiteralExpression(init)) return init.properties.flatMap(p => (ts.isPropertyAssignment(p) ? name(p.initializer) : ts.isShorthandPropertyAssignment(p) ? [p.name.text] : [])).filter(isCap)
|
|
169
|
-
if (ts.isElementAccessExpression(init) || ts.isPropertyAccessExpression(init)) return name(init.expression)
|
|
170
|
-
return []
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const isOpening = (ts, n) => ts.isJsxOpeningElement(n) || ts.isJsxSelfClosingElement(n)
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* @returns the file's facts: `comps` (untyped or generated), `sites` (capitalised JSX tags), `imports`, `wraps`
|
|
177
|
-
* (`const W = hoc(Inner)`), `escaped` (component names used as values), `exports` (local names).
|
|
178
|
-
*/
|
|
179
|
-
function fileFacts(ts, file, text, options = {}) {
|
|
180
|
-
const hash = hashOf(text)
|
|
181
|
-
if (!file.endsWith('.tsx')) return { file, hash, comps: [], sites: [], imports: new Map(), wraps: new Map(), escaped: new Set(), exported: new Set(), parse: null }
|
|
182
|
-
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
|
|
183
|
-
const comps = [], sites = []
|
|
184
|
-
const imports = new Map(), wraps = new Map(), escaped = new Set(), exported = new Set()
|
|
177
|
+
function scanImports(ts, sf, imports, exported) {
|
|
185
178
|
let exportsLoad = false
|
|
186
179
|
|
|
187
180
|
for (const st of sf.statements) {
|
|
@@ -189,32 +182,72 @@ function fileFacts(ts, file, text, options = {}) {
|
|
|
189
182
|
const from = st.moduleSpecifier.text
|
|
190
183
|
const { name, namedBindings } = st.importClause
|
|
191
184
|
if (name) imports.set(name.text, { from, name: 'default' })
|
|
192
|
-
if (namedBindings && ts.isNamedImports(namedBindings))
|
|
185
|
+
if (namedBindings && ts.isNamedImports(namedBindings))
|
|
186
|
+
for (const el of namedBindings.elements)
|
|
187
|
+
imports.set(el.name.text, { from, name: (el.propertyName ?? el.name).text })
|
|
193
188
|
}
|
|
194
|
-
if (ts.isExportAssignment(st) && ts.isIdentifier(st.expression))
|
|
195
|
-
|
|
196
|
-
if (
|
|
197
|
-
|
|
189
|
+
if (ts.isExportAssignment(st) && ts.isIdentifier(st.expression))
|
|
190
|
+
exported.add(st.expression.text)
|
|
191
|
+
if (
|
|
192
|
+
ts.isExportDeclaration(st) &&
|
|
193
|
+
st.exportClause &&
|
|
194
|
+
ts.isNamedExports(st.exportClause) &&
|
|
195
|
+
!st.moduleSpecifier
|
|
196
|
+
)
|
|
197
|
+
for (const el of st.exportClause.elements) exported.add((el.propertyName ?? el.name).text)
|
|
198
|
+
if (isExported(ts, st) && ts.isVariableStatement(st))
|
|
199
|
+
for (const d of st.declarationList.declarations)
|
|
200
|
+
if (ts.isIdentifier(d.name) && d.name.text === 'load') exportsLoad = true
|
|
201
|
+
if (isExported(ts, st) && ts.isFunctionDeclaration(st) && st.name?.text === 'load')
|
|
202
|
+
exportsLoad = true
|
|
198
203
|
}
|
|
199
204
|
|
|
205
|
+
return exportsLoad
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function visitNodes({ ts, sf, file, text, comps, sites, wraps, escaped }) {
|
|
200
209
|
const enclosing = []
|
|
201
|
-
const visit = node => {
|
|
210
|
+
const visit = (node) => {
|
|
202
211
|
const found = componentAt(ts, node)
|
|
203
212
|
if (found) {
|
|
204
213
|
const { fn, name } = found
|
|
205
214
|
const param = fn.parameters?.[0]
|
|
206
215
|
const typed = param?.type
|
|
207
216
|
const generated = typed && text.slice(param.name.end, typed.pos + 64).includes(MARK)
|
|
208
|
-
if (
|
|
217
|
+
if (
|
|
218
|
+
param &&
|
|
219
|
+
fn.parameters.length === 1 &&
|
|
220
|
+
(!typed || generated) &&
|
|
221
|
+
!param.questionToken &&
|
|
222
|
+
!param.dotDotDotToken &&
|
|
223
|
+
!fn.typeParameters &&
|
|
224
|
+
hasJsx(ts, fn)
|
|
225
|
+
) {
|
|
209
226
|
const info = { keys: new Map(), aliases: new Map(), rest: null, opaque: false }
|
|
210
227
|
if (ts.isObjectBindingPattern(param.name)) patternKeys(ts, sf, param.name, info)
|
|
211
228
|
else if (!ts.isIdentifier(param.name)) info.opaque = true
|
|
212
229
|
const comp = {
|
|
213
|
-
id: `${file}#${name}`,
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
230
|
+
id: `${file}#${name}`,
|
|
231
|
+
file,
|
|
232
|
+
name,
|
|
233
|
+
param,
|
|
234
|
+
fn,
|
|
235
|
+
info,
|
|
236
|
+
nameSpan: [found.nameNode.getStart(sf), found.nameNode.end],
|
|
237
|
+
bodySpan: [fn.getStart(sf), fn.end],
|
|
238
|
+
exported: isExported(ts, found.holder),
|
|
239
|
+
isDefault: (ts.canHaveModifiers(found.holder)
|
|
240
|
+
? (ts.getModifiers(found.holder) ?? [])
|
|
241
|
+
: []
|
|
242
|
+
).some((m) => m.kind === ts.SyntaxKind.DefaultKeyword),
|
|
243
|
+
paramSpan: [param.getStart(sf), param.end],
|
|
244
|
+
nameEnd: param.name.end,
|
|
245
|
+
parens: !!fn.getChildren?.(sf).some((c) => c.kind === ts.SyntaxKind.OpenParenToken),
|
|
246
|
+
written: generated ? [param.name.end, typed.end] : null,
|
|
247
|
+
writtenType: generated ? text.slice(typed.getStart(sf), typed.end).trim() : null,
|
|
248
|
+
forwards: [],
|
|
249
|
+
local: enclosing.length > 0,
|
|
250
|
+
async: !!(ts.getModifiers?.(fn) ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword),
|
|
218
251
|
}
|
|
219
252
|
bodyUses(ts, sf, comp)
|
|
220
253
|
comps.push(comp)
|
|
@@ -228,25 +261,85 @@ function fileFacts(ts, file, text, options = {}) {
|
|
|
228
261
|
const site = jsxSite(ts, sf, node, enclosing[enclosing.length - 1])
|
|
229
262
|
if (site) sites.push(site)
|
|
230
263
|
}
|
|
231
|
-
if (
|
|
264
|
+
if (
|
|
265
|
+
ts.isIdentifier(node) &&
|
|
266
|
+
isCap(node.text) &&
|
|
267
|
+
node.parent &&
|
|
268
|
+
!ts.isTypeReferenceNode(node.parent) &&
|
|
269
|
+
escapes(ts, node)
|
|
270
|
+
)
|
|
271
|
+
escaped.add(node.text)
|
|
232
272
|
const push = found && found.fn
|
|
233
273
|
if (push) enclosing.push(found.name)
|
|
234
274
|
ts.forEachChild(node, visit)
|
|
235
275
|
if (push) enclosing.pop()
|
|
236
276
|
}
|
|
237
277
|
ts.forEachChild(sf, visit)
|
|
278
|
+
}
|
|
238
279
|
|
|
239
|
-
|
|
280
|
+
/**
|
|
281
|
+
* @returns the file's facts: `comps` (untyped or generated), `sites` (capitalised JSX tags), `imports`, `wraps`
|
|
282
|
+
* (`const W = hoc(Inner)`), `escaped` (component names used as values), `exports` (local names).
|
|
283
|
+
*/
|
|
284
|
+
function fileFacts(ts, file, text, options = {}) {
|
|
285
|
+
const hash = hashOf(text)
|
|
286
|
+
if (!file.endsWith('.tsx'))
|
|
287
|
+
return {
|
|
288
|
+
file,
|
|
289
|
+
hash,
|
|
290
|
+
comps: [],
|
|
291
|
+
sites: [],
|
|
292
|
+
imports: new Map(),
|
|
293
|
+
wraps: new Map(),
|
|
294
|
+
escaped: new Set(),
|
|
295
|
+
exported: new Set(),
|
|
296
|
+
parse: null,
|
|
297
|
+
}
|
|
298
|
+
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
|
|
299
|
+
const comps = [],
|
|
300
|
+
sites = []
|
|
301
|
+
const imports = new Map(),
|
|
302
|
+
wraps = new Map(),
|
|
303
|
+
escaped = new Set(),
|
|
304
|
+
exported = new Set()
|
|
305
|
+
const exportsLoad = scanImports(ts, sf, imports, exported)
|
|
306
|
+
|
|
307
|
+
visitNodes({ ts, sf, file, text, comps, sites, wraps, escaped })
|
|
308
|
+
|
|
309
|
+
for (const c of comps)
|
|
310
|
+
if (exported.has(c.name) || c.isDefault) {
|
|
311
|
+
c.exported = true
|
|
312
|
+
c.isDefault ||= false
|
|
313
|
+
}
|
|
240
314
|
// Spreads and attributes forward from a component to whatever it renders.
|
|
241
315
|
for (const c of comps) {
|
|
242
|
-
const own = new Set([
|
|
316
|
+
const own = new Set([
|
|
317
|
+
...(ts.isIdentifier(c.param.name) ? [c.param.name.text] : []),
|
|
318
|
+
...(c.info.rest ? [c.info.rest] : []),
|
|
319
|
+
])
|
|
243
320
|
for (const s of sites) {
|
|
244
321
|
if (s.span[0] < c.fn.getStart(sf) || s.span[1] > c.fn.end) continue
|
|
245
|
-
for (const sp of s.spreads)
|
|
322
|
+
for (const sp of s.spreads)
|
|
323
|
+
if (own.has(sp.text))
|
|
324
|
+
c.forwards.push({ tag: s.tag, tagSpan: s.tagSpan, given: s.attrs.map((a) => a.name) })
|
|
246
325
|
}
|
|
247
326
|
}
|
|
248
|
-
for (const c of comps) {
|
|
249
|
-
|
|
327
|
+
for (const c of comps) {
|
|
328
|
+
delete c.fn
|
|
329
|
+
delete c.param
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
file,
|
|
333
|
+
hash,
|
|
334
|
+
comps,
|
|
335
|
+
sites,
|
|
336
|
+
imports,
|
|
337
|
+
wraps,
|
|
338
|
+
escaped,
|
|
339
|
+
exported,
|
|
340
|
+
exportsLoad,
|
|
341
|
+
parse: options.keep ? sf : null,
|
|
342
|
+
}
|
|
250
343
|
}
|
|
251
344
|
|
|
252
345
|
module.exports = { fileFacts, hashOf, MARK }
|