@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.
Files changed (171) hide show
  1. package/README.md +534 -54
  2. package/bin/weft.mjs +8 -1
  3. package/docs/async.md +16 -6
  4. package/docs/browser.md +22 -8
  5. package/docs/config.md +39 -9
  6. package/docs/core.md +30 -0
  7. package/docs/deploy.md +33 -8
  8. package/docs/design.md +12 -3
  9. package/docs/entrypoints.md +79 -0
  10. package/docs/fonts.md +12 -2
  11. package/docs/icons.md +13 -7
  12. package/docs/lists.md +3 -1
  13. package/docs/meta.md +13 -3
  14. package/docs/plugins.md +32 -9
  15. package/docs/props.md +13 -2
  16. package/docs/reactivity.md +26 -0
  17. package/docs/rules.md +19 -12
  18. package/docs/rust.md +47 -17
  19. package/docs/start-short.md +3 -2
  20. package/docs/start.md +226 -52
  21. package/docs/state.md +33 -7
  22. package/docs/style.md +31 -8
  23. package/docs/surface.md +61 -15
  24. package/docs/testing.md +15 -5
  25. package/docs/theme.md +182 -0
  26. package/docs/ui-state.md +19 -6
  27. package/docs/verify.md +13 -0
  28. package/package.json +13 -4
  29. package/src/add/config.mjs +93 -0
  30. package/src/add/errors.mjs +2 -0
  31. package/src/add/setup.mjs +88 -0
  32. package/src/add-rust.mjs +82 -27
  33. package/src/add.mjs +209 -181
  34. package/src/agents-md.mjs +144 -43
  35. package/src/alias.mjs +16 -5
  36. package/src/audit/page.mjs +313 -0
  37. package/src/audit/report.mjs +69 -0
  38. package/src/audit/utils.mjs +5 -0
  39. package/src/audit.mjs +159 -206
  40. package/src/browser/cli.mjs +24 -5
  41. package/src/browser/client.mjs +189 -25
  42. package/src/browser/commands.mjs +166 -51
  43. package/src/catalog/audit.mjs +129 -0
  44. package/src/catalog/auth.mjs +40 -0
  45. package/src/catalog/compiler.mjs +337 -0
  46. package/src/catalog/contracts.mjs +18 -0
  47. package/src/catalog/database.mjs +52 -0
  48. package/src/catalog/deploy.mjs +117 -0
  49. package/src/catalog/design.mjs +159 -0
  50. package/src/catalog/forms.mjs +25 -0
  51. package/src/catalog/hydration.mjs +29 -0
  52. package/src/catalog/icons.mjs +24 -0
  53. package/src/catalog/media.mjs +163 -0
  54. package/src/catalog/metadata.mjs +116 -0
  55. package/src/catalog/plugins.mjs +228 -0
  56. package/src/catalog/realtime.mjs +195 -0
  57. package/src/catalog/rust.mjs +75 -0
  58. package/src/catalog/server-functions.mjs +92 -0
  59. package/src/catalog/state.mjs +39 -0
  60. package/src/catalog/style-groups.mjs +54 -0
  61. package/src/catalog/tests.mjs +127 -0
  62. package/src/catalog/text.mjs +200 -0
  63. package/src/catalog/ui-state.mjs +42 -0
  64. package/src/catalog.mjs +67 -460
  65. package/src/check/runtime.mjs +280 -0
  66. package/src/check.mjs +198 -208
  67. package/src/cli/authoring.mjs +213 -0
  68. package/src/cli/project.mjs +302 -0
  69. package/src/cli.mjs +222 -250
  70. package/src/config.mjs +1 -1
  71. package/src/contributions.mjs +75 -25
  72. package/src/db-layer.mjs +13 -5
  73. package/src/deploy/cdn.mjs +94 -16
  74. package/src/deploy/files.mjs +100 -21
  75. package/src/deploy/fit.mjs +127 -23
  76. package/src/deploy/hosts.mjs +90 -20
  77. package/src/deploy/index/cli.mjs +20 -0
  78. package/src/deploy/index/environment.mjs +69 -0
  79. package/src/deploy/index/steps.mjs +87 -0
  80. package/src/deploy/index.mjs +228 -114
  81. package/src/deploy/needs.mjs +88 -31
  82. package/src/design-checks/accessible-names.mjs +62 -0
  83. package/src/design-checks/contrast.mjs +43 -0
  84. package/src/design-checks/csp.mjs +60 -0
  85. package/src/design-checks/files.mjs +59 -0
  86. package/src/design-checks/heading-links.mjs +61 -0
  87. package/src/design-checks/interaction.mjs +65 -0
  88. package/src/design-checks/jsx.mjs +67 -0
  89. package/src/design-checks/layout-values.mjs +68 -0
  90. package/src/design-checks/native-controls.mjs +48 -0
  91. package/src/design-checks/outline.mjs +39 -0
  92. package/src/design-checks/reporter.mjs +37 -0
  93. package/src/design-checks/rules.mjs +274 -0
  94. package/src/design-checks/values.mjs +262 -0
  95. package/src/design-checks.mjs +10 -400
  96. package/src/diag.mjs +32 -8
  97. package/src/docs.mjs +138 -43
  98. package/src/doctor.mjs +20 -8
  99. package/src/files.mjs +18 -5
  100. package/src/fix.mjs +10 -2
  101. package/src/gate/after.mjs +40 -0
  102. package/src/gate/before.mjs +54 -0
  103. package/src/gate/classify.mjs +72 -0
  104. package/src/gate/mutation.mjs +209 -0
  105. package/src/gate/resolve-cases.mjs +22 -0
  106. package/src/gate/result.mjs +63 -0
  107. package/src/gate/run.mjs +75 -0
  108. package/src/gate.mjs +104 -202
  109. package/src/infer/box-props.cjs +140 -0
  110. package/src/infer/core-boxes.cjs +46 -0
  111. package/src/infer/derived-writes.cjs +95 -0
  112. package/src/infer/engine/checks.cjs +193 -0
  113. package/src/infer/engine/flow.cjs +293 -0
  114. package/src/infer/engine/report.cjs +158 -0
  115. package/src/infer/engine/utils.cjs +15 -0
  116. package/src/infer/engine.cjs +108 -368
  117. package/src/infer/facts/sites.cjs +114 -0
  118. package/src/infer/facts.cjs +210 -117
  119. package/src/infer/hosts.cjs +69 -22
  120. package/src/infer/lsp.mjs +149 -40
  121. package/src/infer/session.cjs +73 -17
  122. package/src/infer/shadow.cjs +13 -5
  123. package/src/infer/worker.cjs +57 -9
  124. package/src/jsx-scan.mjs +63 -21
  125. package/src/load-config/schema.mjs +281 -0
  126. package/src/load-config.mjs +137 -138
  127. package/src/map.mjs +93 -26
  128. package/src/mcp/browser-tools.mjs +155 -0
  129. package/src/mcp.mjs +147 -69
  130. package/src/meta-table.mjs +43 -8
  131. package/src/mutate.mjs +120 -36
  132. package/src/plugin-checks.mjs +52 -23
  133. package/src/plugins.mjs +125 -39
  134. package/src/project.mjs +170 -79
  135. package/src/props-checks.mjs +24 -6
  136. package/src/props-listener.mjs +27 -9
  137. package/src/react-checks.mjs +35 -4
  138. package/src/server-state-checks.mjs +34 -0
  139. package/src/share-checks.mjs +121 -37
  140. package/src/site.mjs +33 -6
  141. package/src/slots.mjs +104 -19
  142. package/src/start/build.mjs +116 -0
  143. package/src/start/routes.mjs +38 -0
  144. package/src/start.mjs +193 -95
  145. package/src/state-checks/core-state.mjs +223 -0
  146. package/src/state-checks/modules.mjs +17 -0
  147. package/src/state-checks/scan.mjs +232 -0
  148. package/src/state-checks/server-graph.mjs +65 -0
  149. package/src/state-checks.mjs +109 -133
  150. package/src/style/find.mjs +121 -0
  151. package/src/style.mjs +133 -127
  152. package/src/surface-checks.mjs +212 -46
  153. package/src/tailwind.mjs +5 -4
  154. package/src/tasks.mjs +189 -27
  155. package/src/test-rules/context.mjs +109 -0
  156. package/src/test-rules/file-rules.mjs +279 -0
  157. package/src/test-rules/rule-helpers.mjs +244 -0
  158. package/src/test-rules/syntax.mjs +223 -0
  159. package/src/test-rules/test-rules.mjs +127 -0
  160. package/src/testdiff.mjs +94 -32
  161. package/src/tests.mjs +31 -483
  162. package/src/text-checks.mjs +229 -69
  163. package/src/tokens.mjs +12 -4
  164. package/src/trust/scan.mjs +95 -0
  165. package/src/trust.mjs +214 -102
  166. package/src/weft.mjs +41 -9
  167. package/src/workspace.mjs +96 -19
  168. package/ts-plugin/context.js +57 -0
  169. package/ts-plugin/index.js +4 -124
  170. package/ts-plugin/mapping.js +65 -0
  171. 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 }
@@ -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 = name => /^[A-Z]/.test(name)
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 !!ts.canHaveModifiers(node) && (ts.getModifiers(node) ?? []).some(m => m.kind === ts.SyntaxKind.ExportKeyword)
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 => { if (visit(n) !== false) ts.forEachChild(n, go) }
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)) return { fn: node, name: node.name.text, nameNode: node.name, holder: node }
24
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && isCap(node.name.text) && !node.type && node.initializer) {
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)) return { fn: init, name: node.name.text, nameNode: node.name, holder: node.parent.parent }
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) { if (ts.isIdentifier(el.name)) out.rest = el.name.text; continue }
45
- const key = el.propertyName && ts.isIdentifier(el.propertyName) ? el.propertyName.text : ts.isIdentifier(el.name) ? el.name.text : null
46
- if (!key) { out.opaque = true; continue }
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 ((ts.isPostfixUnaryExpression(up) || ts.isPrefixUnaryExpression(up)) && (up.operator === ts.SyntaxKind.PlusPlusToken || up.operator === ts.SyntaxKind.MinusMinusToken)) return { as: 'number', span: span(node) }
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) return { as: 'write', span: span(node), value: span(up.right) }
62
- if (op >= ts.SyntaxKind.FirstCompoundAssignment && op <= ts.SyntaxKind.LastCompoundAssignment) return { as: 'number', span: span(node) }
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) return { as: 'call', span: span(node), args: up.arguments.map(span) }
65
- if ((ts.isPropertyAccessExpression(up) && up.expression === node) || (ts.isNonNullExpression(up))) {
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)) return { as: 'context', span: span(node) }
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) => node.parameters?.some(p => ts.isIdentifier(p.name) && p.name.text === 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)) patternKeys(ts, sf, node.name, info)
88
- else if (ts.isPropertyAccessExpression(init) && ts.isIdentifier(init.expression) && scope.has(init.expression.text) && ts.isIdentifier(node.name)) {
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 (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && scope.has(node.expression.text)) return use(node.name.text, node)
95
- if (ts.isElementAccessExpression(node) && ts.isIdentifier(node.expression) && scope.has(node.expression.text)) {
96
- if (ts.isStringLiteralLike(node.argumentExpression)) return use(node.argumentExpression.text, node)
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 (ts.isIdentifier(node) && info.aliases.has(node.text) && ts.isExpressionNode?.(node) !== false) {
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 = (ts.isPropertyAccessExpression(up) && up.name === node) || (ts.isPropertyAssignment(up) && up.name === node) || ts.isBindingElement(up) || (ts.isVariableDeclaration(up) && up.name === node) || ts.isJsxAttribute(up)
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) if (shadows(node, n)) { inner = new Set(scope); inner.delete(n) }
107
- ts.forEachChild(node, c => visit(c, inner))
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 jsxSite(ts, sf, node, enclosing) {
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)) for (const el of namedBindings.elements) imports.set(el.name.text, { from, name: (el.propertyName ?? el.name).text })
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)) exported.add(st.expression.text)
195
- if (ts.isExportDeclaration(st) && st.exportClause && ts.isNamedExports(st.exportClause) && !st.moduleSpecifier) for (const el of st.exportClause.elements) exported.add((el.propertyName ?? el.name).text)
196
- if (isExported(ts, st) && ts.isVariableStatement(st)) for (const d of st.declarationList.declarations) if (ts.isIdentifier(d.name) && d.name.text === 'load') exportsLoad = true
197
- if (isExported(ts, st) && ts.isFunctionDeclaration(st) && st.name?.text === 'load') exportsLoad = true
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 (param && fn.parameters.length === 1 && (!typed || generated) && !param.questionToken && !param.dotDotDotToken && !fn.typeParameters && hasJsx(ts, fn)) {
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}`, file, name, param, fn, info, nameSpan: [found.nameNode.getStart(sf), found.nameNode.end],
214
- bodySpan: [fn.getStart(sf), fn.end], exported: isExported(ts, found.holder), isDefault: (ts.canHaveModifiers(found.holder) ? ts.getModifiers(found.holder) ?? [] : []).some(m => m.kind === ts.SyntaxKind.DefaultKeyword),
215
- paramSpan: [param.getStart(sf), param.end], nameEnd: param.name.end, parens: !!fn.getChildren?.(sf).some(c => c.kind === ts.SyntaxKind.OpenParenToken),
216
- written: generated ? [param.name.end, typed.end] : null, writtenType: generated ? text.slice(typed.getStart(sf), typed.end).trim() : null,
217
- forwards: [], local: enclosing.length > 0, async: !!(ts.getModifiers?.(fn) ?? []).some(m => m.kind === ts.SyntaxKind.AsyncKeyword),
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 (ts.isIdentifier(node) && isCap(node.text) && node.parent && !ts.isTypeReferenceNode(node.parent) && escapes(ts, node)) escaped.add(node.text)
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
- for (const c of comps) if (exported.has(c.name) || c.isDefault) { c.exported = true; c.isDefault ||= false }
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([...(ts.isIdentifier(c.param.name) ? [c.param.name.text] : []), ...(c.info.rest ? [c.info.rest] : [])])
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) if (own.has(sp.text)) c.forwards.push({ tag: s.tag, tagSpan: s.tagSpan, given: s.attrs.map(a => a.name) })
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) { delete c.fn; delete c.param }
249
- return { file, hash, comps, sites, imports, wraps, escaped, exported, exportsLoad, parse: options.keep ? sf : null }
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 }