@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.
Files changed (171) hide show
  1. package/README.md +38 -14
  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 +108 -0
  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 +174 -188
  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 +68 -444
  65. package/src/check/runtime.mjs +280 -0
  66. package/src/check.mjs +199 -207
  67. package/src/cli/authoring.mjs +213 -0
  68. package/src/cli/project.mjs +302 -0
  69. package/src/cli.mjs +228 -224
  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 +142 -0
  74. package/src/deploy/files.mjs +266 -0
  75. package/src/deploy/fit.mjs +166 -0
  76. package/src/deploy/hosts.mjs +126 -0
  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 +387 -0
  81. package/src/deploy/needs.mjs +149 -0
  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 -42
  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 +144 -116
  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 +194 -92
  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
@@ -3,12 +3,15 @@
3
3
  // file overlay: `tsc --noEmit` of the source tree, with no files on disk and no plugin. `languageHost` reuses a language
4
4
  // service, which is what a tsserver plugin has, and what a project on TypeScript 6 gets.
5
5
 
6
+ const { boxMismatch } = require('./box-props.cjs')
7
+ const { derivedWrite } = require('./derived-writes.cjs')
8
+
6
9
  const NO_TRUNCATE = 1
7
10
 
8
11
  /** The node exactly on [start, end]: the deepest one, because `x` and `x;` can share an end but not a start. */
9
12
  function nodeAt(each, root, start, end) {
10
13
  let best = null
11
- const go = n => {
14
+ const go = (n) => {
12
15
  if (n.pos <= start && n.end >= end) {
13
16
  if (n.end === end) best = n
14
17
  each(n, go)
@@ -21,7 +24,9 @@ function nodeAt(each, root, start, end) {
21
24
  /** The flags the engine reads. `lit`: the widened text differs from the exact one. */
22
25
  function describe(checker, type, flagsAny, node) {
23
26
  if (!type) return { text: 'any', any: true }
24
- const widened = checker.getWidenedType?.(checker.getBaseTypeOfLiteralType(type)) ?? checker.getBaseTypeOfLiteralType(type)
27
+ const widened =
28
+ checker.getWidenedType?.(checker.getBaseTypeOfLiteralType(type)) ??
29
+ checker.getBaseTypeOfLiteralType(type)
25
30
  const text = checker.typeToString(widened, node, NO_TRUNCATE)
26
31
  const exact = checker.typeToString(type, node, NO_TRUNCATE)
27
32
  return { text, exact: exact === text ? undefined : exact, any: !!(type.flags & flagsAny) }
@@ -30,34 +35,52 @@ function describe(checker, type, flagsAny, node) {
30
35
  function answer(checker, node, req, env) {
31
36
  if (!node) return { text: 'any', any: true, missing: true }
32
37
  switch (req.kind) {
33
- case 'type': return describe(checker, checker.getTypeAtLocation(node), env.Any, node)
34
- case 'context': return describe(checker, checker.getContextualType(node), env.Any, node)
38
+ case 'type':
39
+ return describe(checker, checker.getTypeAtLocation(node), env.Any, node)
40
+ case 'context':
41
+ return describe(checker, checker.getContextualType(node), env.Any, node)
35
42
  case 'spread': {
36
43
  const type = checker.getTypeAtLocation(node)
37
44
  if (!type || type.flags & env.Any) return { any: true, props: [] }
38
- return { props: checker.getPropertiesOfType(type).map(s => ({ name: s.name, optional: !!(s.flags & env.Optional), ...describe(checker, checker.getTypeOfSymbol(s), env.Any, node) })) }
45
+ return {
46
+ props: checker.getPropertiesOfType(type).map((s) => ({
47
+ name: s.name,
48
+ optional: !!(s.flags & env.Optional),
49
+ ...describe(checker, checker.getTypeOfSymbol(s), env.Any, node),
50
+ })),
51
+ }
39
52
  }
40
53
  case 'decls': {
41
54
  const type = checker.getTypeAtLocation(node)
42
55
  if (!type) return { decls: [] }
43
56
  const members = env.union(type) ?? [type]
44
- return { decls: members.flatMap(t => checker.getSignaturesOfType(t, env.Call).map(s => env.place(s))).filter(Boolean) }
57
+ return {
58
+ decls: members
59
+ .flatMap((t) => checker.getSignaturesOfType(t, env.Call).map((s) => env.place(s)))
60
+ .filter(Boolean),
61
+ }
45
62
  }
46
63
  }
47
64
  }
48
65
 
49
66
  /** Over TypeScript 7's API. `typescript/unstable/*` is a moving name, so this is the one file that touches it. */
50
67
  async function tsgoHost({ root, config, typescriptDir, overlay }) {
51
- const load = name => import(require('node:url').pathToFileURL(require('node:path').join(typescriptDir, name)).href)
68
+ const load = (name) =>
69
+ import(require('node:url').pathToFileURL(require('node:path').join(typescriptDir, name)).href)
52
70
  const { API, SignatureKind, SymbolFlags, TypeFlags } = await load('dist/api/sync/api.js')
53
71
  const each = (n, f) => n.forEachChild(f)
54
- const api = new API({ cwd: root, fs: { readFile: f => overlay.get(f) } })
72
+ const api = new API({ cwd: root, fs: { readFile: (f) => overlay.get(f) } })
55
73
  let snap = api.updateSnapshot({ openProject: config })
56
74
  let project = snap.getProject(config) ?? snap.getProjects()[0]
57
75
  const env = {
58
- Any: TypeFlags.Any, Optional: SymbolFlags.Optional, Call: SignatureKind.Call,
59
- union: t => (t.isUnionType() ? t.getTypes() : null),
60
- place(s) { const n = s.declaration?.resolve(project); return n && { file: s.declaration.path, pos: n.pos, end: n.end } },
76
+ Any: TypeFlags.Any,
77
+ Optional: SymbolFlags.Optional,
78
+ Call: SignatureKind.Call,
79
+ union: (t) => (t.isUnionType() ? t.getTypes() : null),
80
+ place(s) {
81
+ const n = s.declaration?.resolve(project)
82
+ return n && { file: s.declaration.path, pos: n.pos, end: n.end }
83
+ },
61
84
  }
62
85
  return {
63
86
  kind: 'tsgo',
@@ -71,41 +94,65 @@ async function tsgoHost({ root, config, typescriptDir, overlay }) {
71
94
  query(file, reqs) {
72
95
  const sf = project.program.getSourceFile(file)
73
96
  if (!sf) return reqs.map(() => ({ text: 'any', any: true, missing: true }))
74
- return reqs.map(r => answer(project.checker, nodeAt(each, sf, r.start, r.end), r, env))
97
+ return reqs.map((r) => answer(project.checker, nodeAt(each, sf, r.start, r.end), r, env))
75
98
  },
76
99
  diagnostics(file) {
77
- return [...project.program.getSyntacticDiagnostics(file), ...project.program.getSemanticDiagnostics(file)].map(d => ({ start: d.pos, length: d.end - d.pos, code: d.code, text: flatten(d) }))
100
+ return [
101
+ ...project.program.getSyntacticDiagnostics(file),
102
+ ...project.program.getSemanticDiagnostics(file),
103
+ ].map((d) => ({ start: d.pos, length: d.end - d.pos, code: d.code, text: flatten(d) }))
78
104
  },
79
105
  allDiagnostics() {
80
- return project.program.getSourceFileNames().filter(f => !f.includes('/node_modules/')).flatMap(f => this.diagnostics(f).map(d => ({ ...d, file: f })))
106
+ return project.program
107
+ .getSourceFileNames()
108
+ .filter((f) => !f.includes('/node_modules/'))
109
+ .flatMap((f) => this.diagnostics(f).map((d) => ({ ...d, file: f })))
110
+ },
111
+ close() {
112
+ snap.dispose?.()
113
+ api.close()
81
114
  },
82
- close() { snap.dispose?.(); api.close() },
83
115
  }
84
116
  }
85
117
 
86
- const flatten = d => (typeof d.text === 'string' ? d.text : d.message ?? d.messageText ?? String(d.text)) + (d.messageChain?.length ? `\n ${d.messageChain.map(flatten).join('\n ')}` : '')
118
+ const flatten = (d) =>
119
+ (typeof d.text === 'string' ? d.text : (d.message ?? d.messageText ?? String(d.text))) +
120
+ (d.messageChain?.length ? `\n ${d.messageChain.map(flatten).join('\n ')}` : '')
87
121
 
88
122
  /** Over a TypeScript language service: `service()` returns it, `sync()` makes it see what `update` was told. */
89
123
  function languageHost(ts, service, sync = () => {}) {
90
124
  const env = {
91
- Any: ts.TypeFlags.Any, Optional: ts.SymbolFlags.Optional, Call: ts.SignatureKind.Call,
92
- union: t => (t.isUnion() ? t.types : null),
93
- place(s) { const n = s.declaration; return n && { file: n.getSourceFile().fileName, pos: n.pos, end: n.end } },
125
+ Any: ts.TypeFlags.Any,
126
+ Optional: ts.SymbolFlags.Optional,
127
+ Call: ts.SignatureKind.Call,
128
+ union: (t) => (t.isUnion() ? t.types : null),
129
+ place(s) {
130
+ const n = s.declaration
131
+ return n && { file: n.getSourceFile().fileName, pos: n.pos, end: n.end }
132
+ },
94
133
  }
95
134
  const each = (n, f) => ts.forEachChild(n, f)
96
135
  return {
97
136
  kind: 'language-service',
98
- update(files, touched = []) { sync(files, touched) },
137
+ update(files, touched = []) {
138
+ sync(files, touched)
139
+ },
99
140
  query(file, reqs) {
100
141
  const program = service().getProgram()
101
142
  const sf = program.getSourceFile(file)
102
143
  if (!sf) return reqs.map(() => ({ text: 'any', any: true, missing: true }))
103
144
  const checker = program.getTypeChecker()
104
- return reqs.map(r => answer(checker, nodeAt(each, sf, r.start, r.end), r, env))
145
+ return reqs.map((r) => answer(checker, nodeAt(each, sf, r.start, r.end), r, env))
105
146
  },
106
147
  diagnostics(file) {
107
148
  const s = service()
108
- return [...s.getSyntacticDiagnostics(file), ...s.getSemanticDiagnostics(file)].map(d => ({ start: d.start ?? 0, length: d.length ?? 0, code: d.code, text: ts.flattenDiagnosticMessageText(d.messageText, '\n') }))
149
+ return [...s.getSyntacticDiagnostics(file), ...s.getSemanticDiagnostics(file)].map((d) => ({
150
+ start: d.start ?? 0,
151
+ length: d.length ?? 0,
152
+ code: d.code,
153
+ text: ts.flattenDiagnosticMessageText(d.messageText, '\n'),
154
+ weft: derivedWrite(ts, s.getProgram(), d) ?? boxMismatch(ts, s.getProgram(), d),
155
+ }))
109
156
  },
110
157
  close() {},
111
158
  }
package/src/infer/lsp.mjs CHANGED
@@ -12,11 +12,19 @@ const require = createRequire(import.meta.url)
12
12
  const { Session, projectTypescript } = require('./session.cjs')
13
13
  const { inGenerated, toShadow, toSource } = require('./shadow.cjs')
14
14
 
15
- const RANGES = new Set(['range', 'selectionRange', 'targetRange', 'targetSelectionRange', 'originSelectionRange', 'insert', 'replace'])
15
+ const RANGES = new Set([
16
+ 'range',
17
+ 'selectionRange',
18
+ 'targetRange',
19
+ 'targetSelectionRange',
20
+ 'originSelectionRange',
21
+ 'insert',
22
+ 'replace',
23
+ ])
16
24
 
17
25
  function framed(stream, on) {
18
26
  let buffer = Buffer.alloc(0)
19
- stream.on('data', d => {
27
+ stream.on('data', (d) => {
20
28
  buffer = Buffer.concat([buffer, d])
21
29
  for (;;) {
22
30
  const head = buffer.indexOf('\r\n\r\n')
@@ -29,11 +37,18 @@ function framed(stream, on) {
29
37
  }
30
38
  })
31
39
  }
32
- const frame = msg => { const body = Buffer.from(JSON.stringify(msg)); return Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r\n\r\n`), body]) }
40
+ const frame = (msg) => {
41
+ const body = Buffer.from(JSON.stringify(msg))
42
+ return Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r\n\r\n`), body])
43
+ }
33
44
 
34
45
  const offsetAt = (text, { line, character }) => {
35
46
  let at = 0
36
- for (let i = 0; i < line; i++) { const n = text.indexOf('\n', at); if (n < 0) return text.length; at = n + 1 }
47
+ for (let i = 0; i < line; i++) {
48
+ const n = text.indexOf('\n', at)
49
+ if (n < 0) return text.length
50
+ at = n + 1
51
+ }
37
52
  return at + character
38
53
  }
39
54
  const positionAt = (text, offset) => {
@@ -44,59 +59,159 @@ const positionAt = (text, offset) => {
44
59
 
45
60
  export function lsp(root, { input = process.stdin, output = process.stdout } = {}) {
46
61
  const dir = projectTypescript(root)
47
- if (!dir) throw Object.assign(new Error('`weft lsp` needs TypeScript 7 in the project; older versions use the tsserver plugin'), { code: 'E_USAGE' })
48
- const exe = join(dirname(createRequire(join(realpathSync(dir), 'package.json')).resolve(`@typescript/typescript-${process.platform}-${process.arch}/package.json`)), 'lib', process.platform === 'win32' ? 'tsc.exe' : 'tsc')
62
+ if (!dir)
63
+ throw Object.assign(
64
+ new Error(
65
+ '`weft lsp` needs TypeScript 7 in the project; older versions use the tsserver plugin',
66
+ ),
67
+ { code: 'E_USAGE' },
68
+ )
69
+ const exe = join(
70
+ dirname(
71
+ createRequire(join(realpathSync(dir), 'package.json')).resolve(
72
+ `@typescript/typescript-${process.platform}-${process.arch}/package.json`,
73
+ ),
74
+ ),
75
+ 'lib',
76
+ process.platform === 'win32' ? 'tsc.exe' : 'tsc',
77
+ )
49
78
  const server = spawn(exe, ['--lsp', '--stdio'], { cwd: root, stdio: ['pipe', 'pipe', 'inherit'] })
50
79
  const session = new Session(ts, root)
51
80
  const docs = new Map() // uri → { file, text, sent, version }
52
81
  const pending = new Map() // request id → uri
53
- const toServer = m => server.stdin.write(frame(m))
54
- const toClient = m => output.write(frame(m))
55
- const fileOf = uri => fileURLToPath(uri)
56
- const mapOf = doc => doc && session.engine.maps.get(doc.file)
82
+ const toServer = (m) => server.stdin.write(frame(m))
83
+ const toClient = (m) => output.write(frame(m))
84
+ const fileOf = (uri) => fileURLToPath(uri)
85
+ const { pos, back, items } = makeMapping(session, docs)
86
+
87
+ const client = makeClient({ docs, session, toServer, pos, fileOf, pending })
88
+
89
+ // One at a time: a request has to see the shadow its position is mapped for.
90
+ let line = Promise.resolve()
91
+ framed(input, (msg) => {
92
+ line = line.then(() => client(msg)).catch((e) => process.stderr.write(`weft lsp: ${e.stack}\n`))
93
+ })
94
+
95
+ framed(server.stdout, (msg) => {
96
+ if (msg.method === 'textDocument/publishDiagnostics') {
97
+ const doc = docs.get(msg.params.uri)
98
+ return toClient(
99
+ doc
100
+ ? { ...msg, params: { ...msg.params, diagnostics: items(doc, msg.params.diagnostics) } }
101
+ : msg,
102
+ )
103
+ }
104
+ const hold = msg.id !== undefined && !msg.method ? pending.get(msg.id) : null
105
+ if (!hold) return toClient(msg)
106
+ pending.delete(msg.id)
107
+ if (hold.method === 'textDocument/diagnostic' && msg.result?.items)
108
+ return toClient({
109
+ ...msg,
110
+ result: { ...msg.result, items: items(hold.doc, msg.result.items) },
111
+ })
112
+ toClient({ ...msg, result: back(msg.result, hold.doc) })
113
+ })
114
+ server.on('exit', (code) => {
115
+ session.close()
116
+ process.exitCode = code ?? 0
117
+ })
118
+ return {
119
+ server,
120
+ close: () => {
121
+ server.kill()
122
+ session.close()
123
+ },
124
+ }
125
+ }
126
+
127
+ function makeMapping(session, docs) {
128
+ const mapOf = (doc) => doc && session.engine.maps.get(doc.file)
57
129
 
58
130
  const pos = (doc, p, to) => {
59
131
  const map = mapOf(doc)
60
132
  if (!map) return p
61
- return to ? positionAt(map.text, toShadow(map, offsetAt(doc.text, p))) : positionAt(doc.text, toSource(map, offsetAt(map.text, p)))
133
+ return to
134
+ ? positionAt(map.text, toShadow(map, offsetAt(doc.text, p)))
135
+ : positionAt(doc.text, toSource(map, offsetAt(map.text, p)))
62
136
  }
63
137
  const back = (value, doc) => {
64
- if (Array.isArray(value)) return value.map(v => back(v, doc))
138
+ if (Array.isArray(value)) return value.map((v) => back(v, doc))
65
139
  if (!value || typeof value !== 'object') return value
66
140
  const uri = value.uri ?? value.targetUri
67
- const here = uri ? docs.get(uri) ?? doc : doc
141
+ const here = uri ? (docs.get(uri) ?? doc) : doc
68
142
  const out = {}
69
143
  for (const [k, v] of Object.entries(value)) {
70
- if (RANGES.has(k) && v?.start && mapOf(here)) out[k] = { start: pos(here, v.start), end: pos(here, v.end) }
144
+ if (RANGES.has(k) && v?.start && mapOf(here))
145
+ out[k] = { start: pos(here, v.start), end: pos(here, v.end) }
71
146
  else out[k] = back(v, here)
72
147
  }
73
148
  return out
74
149
  }
75
- const generated = (doc, d) => { const map = mapOf(doc); if (!map) return false; const a = offsetAt(map.text, d.range.start); return inGenerated(map, a, offsetAt(map.text, d.range.end) - a) }
76
- const ours = doc => (session.engine.findings.get(doc.file) ?? []).map(f => ({
77
- range: { start: positionAt(doc.text, f.start), end: positionAt(doc.text, f.end) }, severity: f.severity === 'warning' ? 2 : 1, code: f.code, source: 'weft', message: `${f.why} Fix: ${f.help}`,
78
- }))
79
- const items = (doc, list) => [...back(list.filter(d => !generated(doc, d)), doc).filter(d => !(session.engine.findings.get(doc.file) ?? []).some(f => f.code === 'W084' && offsetAt(doc.text, d.range.start) < f.end && f.start <= offsetAt(doc.text, d.range.start))), ...ours(doc)]
150
+ const generated = (doc, d) => {
151
+ const map = mapOf(doc)
152
+ if (!map) return false
153
+ const a = offsetAt(map.text, d.range.start)
154
+ return inGenerated(map, a, offsetAt(map.text, d.range.end) - a)
155
+ }
156
+ const ours = (doc) =>
157
+ (session.engine.findings.get(doc.file) ?? []).map((f) => ({
158
+ range: { start: positionAt(doc.text, f.start), end: positionAt(doc.text, f.end) },
159
+ severity: f.severity === 'warning' ? 2 : 1,
160
+ code: f.code,
161
+ source: 'weft',
162
+ message: `${f.why} Fix: ${f.help}`,
163
+ }))
164
+ const items = (doc, list) => [
165
+ ...back(
166
+ list.filter((d) => !generated(doc, d)),
167
+ doc,
168
+ ).filter(
169
+ (d) =>
170
+ !(session.engine.findings.get(doc.file) ?? []).some(
171
+ (f) =>
172
+ f.code === 'W084' &&
173
+ offsetAt(doc.text, d.range.start) < f.end &&
174
+ f.start <= offsetAt(doc.text, d.range.start),
175
+ ),
176
+ ),
177
+ ...ours(doc),
178
+ ]
80
179
 
180
+ return { pos, back, items }
181
+ }
182
+
183
+ function makeClient({ docs, session, toServer, pos, fileOf, pending }) {
81
184
  /** Inference over the buffers, then every open file whose shadow moved. */
82
185
  async function refresh(changed) {
83
- if (session.host) session.host.update(new Map(changed.flatMap(f => (session.engine.maps.has(f) ? [] : [[f, session.engine.texts.get(f)]]))))
186
+ if (session.host)
187
+ session.host.update(
188
+ new Map(
189
+ changed.flatMap((f) =>
190
+ session.engine.maps.has(f) ? [] : [[f, session.engine.texts.get(f)]],
191
+ ),
192
+ ),
193
+ )
84
194
  await session.analyze(session.host ? changed : undefined)
85
195
  for (const [uri, doc] of docs) {
86
196
  const text = session.engine.maps.get(doc.file)?.text ?? doc.text
87
197
  if (text === doc.sent) continue
88
198
  doc.sent = text
89
- toServer({ jsonrpc: '2.0', method: 'textDocument/didChange', params: { textDocument: { uri, version: ++doc.version }, contentChanges: [{ text }] } })
199
+ toServer({
200
+ jsonrpc: '2.0',
201
+ method: 'textDocument/didChange',
202
+ params: { textDocument: { uri, version: ++doc.version }, contentChanges: [{ text }] },
203
+ })
90
204
  }
91
205
  }
92
206
 
93
- // One at a time: a request has to see the shadow its position is mapped for.
94
- let line = Promise.resolve()
95
- framed(input, msg => { line = line.then(() => client(msg)).catch(e => process.stderr.write(`weft lsp: ${e.stack}\n`)) })
96
207
  async function client(msg) {
97
208
  const p = msg.params
98
209
  if (msg.method === 'textDocument/didOpen') {
99
- const doc = { file: fileOf(p.textDocument.uri), text: p.textDocument.text, version: p.textDocument.version }
210
+ const doc = {
211
+ file: fileOf(p.textDocument.uri),
212
+ text: p.textDocument.text,
213
+ version: p.textDocument.version,
214
+ }
100
215
  docs.set(p.textDocument.uri, doc)
101
216
  session.engine.set(doc.file, doc.text)
102
217
  await refresh([doc.file])
@@ -105,27 +220,21 @@ export function lsp(root, { input = process.stdin, output = process.stdout } = {
105
220
  }
106
221
  if (msg.method === 'textDocument/didChange') {
107
222
  const doc = docs.get(p.textDocument.uri)
108
- for (const c of p.contentChanges) doc.text = c.range ? doc.text.slice(0, offsetAt(doc.text, c.range.start)) + c.text + doc.text.slice(offsetAt(doc.text, c.range.end)) : c.text
223
+ for (const c of p.contentChanges)
224
+ doc.text = c.range
225
+ ? doc.text.slice(0, offsetAt(doc.text, c.range.start)) +
226
+ c.text +
227
+ doc.text.slice(offsetAt(doc.text, c.range.end))
228
+ : c.text
109
229
  session.engine.set(doc.file, doc.text)
110
230
  return refresh([doc.file])
111
231
  }
112
232
  const doc = p?.textDocument && docs.get(p.textDocument.uri)
113
233
  if (msg.id !== undefined && doc) pending.set(msg.id, { doc, method: msg.method })
114
- if (doc && p.position) return toServer({ ...msg, params: { ...p, position: pos(doc, p.position, true) } })
234
+ if (doc && p.position)
235
+ return toServer({ ...msg, params: { ...p, position: pos(doc, p.position, true) } })
115
236
  toServer(msg)
116
237
  }
117
238
 
118
- framed(server.stdout, msg => {
119
- if (msg.method === 'textDocument/publishDiagnostics') {
120
- const doc = docs.get(msg.params.uri)
121
- return toClient(doc ? { ...msg, params: { ...msg.params, diagnostics: items(doc, msg.params.diagnostics) } } : msg)
122
- }
123
- const hold = msg.id !== undefined && !msg.method ? pending.get(msg.id) : null
124
- if (!hold) return toClient(msg)
125
- pending.delete(msg.id)
126
- if (hold.method === 'textDocument/diagnostic' && msg.result?.items) return toClient({ ...msg, result: { ...msg.result, items: items(hold.doc, msg.result.items) } })
127
- toClient({ ...msg, result: back(msg.result, hold.doc) })
128
- })
129
- server.on('exit', code => { session.close(); process.exitCode = code ?? 0 })
130
- return { server, close: () => { server.kill(); session.close() } }
239
+ return client
131
240
  }
@@ -6,6 +6,7 @@ const { createRequire } = require('node:module')
6
6
  const { dirname, join } = require('node:path')
7
7
  const { Engine } = require('./engine.cjs')
8
8
  const { languageHost, tsgoHost } = require('./hosts.cjs')
9
+ const { withBoxProps } = require('./box-props.cjs')
9
10
 
10
11
  /** The project's TypeScript when it can be driven without a plugin (7's API), else ours. */
11
12
  function projectTypescript(root) {
@@ -23,16 +24,34 @@ function memoryHost(ts, root, config, parsed, overlay) {
23
24
  const host = {
24
25
  getCompilationSettings: () => parsed.options,
25
26
  getScriptFileNames: () => parsed.fileNames,
26
- getScriptVersion: f => String(versions.get(f) ?? 0),
27
- getScriptSnapshot(f) { const t = overlay.get(f) ?? (existsSync(f) ? readFileSync(f, 'utf8') : undefined); return t === undefined ? undefined : ts.ScriptSnapshot.fromString(t) },
27
+ getScriptVersion: (f) => String(versions.get(f) ?? 0),
28
+ getScriptSnapshot(f) {
29
+ const t = overlay.get(f) ?? (existsSync(f) ? readFileSync(f, 'utf8') : undefined)
30
+ return t === undefined ? undefined : ts.ScriptSnapshot.fromString(t)
31
+ },
28
32
  getCurrentDirectory: () => root,
29
- getDefaultLibFileName: o => ts.getDefaultLibFilePath(o),
30
- fileExists: ts.sys.fileExists, readFile: ts.sys.readFile, readDirectory: ts.sys.readDirectory, directoryExists: ts.sys.directoryExists, getDirectories: ts.sys.getDirectories,
33
+ getDefaultLibFileName: (o) => ts.getDefaultLibFilePath(o),
34
+ fileExists: ts.sys.fileExists,
35
+ readFile: ts.sys.readFile,
36
+ readDirectory: ts.sys.readDirectory,
37
+ directoryExists: ts.sys.directoryExists,
38
+ getDirectories: ts.sys.getDirectories,
31
39
  }
32
40
  const service = ts.createLanguageService(host)
33
- const bump = f => versions.set(f, (versions.get(f) ?? 0) + 1)
34
- const out = languageHost(ts, () => service, (files, touched) => { for (const [f, t] of files) { overlay.set(f, t); bump(f) } touched.forEach(bump) })
35
- out.allDiagnostics = () => parsed.fileNames.flatMap(f => out.diagnostics(f).map(d => ({ ...d, file: f })))
41
+ const bump = (f) => versions.set(f, (versions.get(f) ?? 0) + 1)
42
+ const out = languageHost(
43
+ ts,
44
+ () => service,
45
+ (files, touched) => {
46
+ for (const [f, t] of files) {
47
+ overlay.set(f, t)
48
+ bump(f)
49
+ }
50
+ touched.forEach(bump)
51
+ },
52
+ )
53
+ out.allDiagnostics = () =>
54
+ parsed.fileNames.flatMap((f) => out.diagnostics(f).map((d) => ({ ...d, file: f })))
36
55
  return out
37
56
  }
38
57
 
@@ -43,23 +62,38 @@ class Session {
43
62
  this.config = options.config ?? join(root, 'tsconfig.json')
44
63
  const raw = ts.readConfigFile(this.config, ts.sys.readFile)
45
64
  this.parsed = ts.parseJsonConfigFileContent(raw.config ?? {}, ts.sys, root)
46
- this.engine = new Engine({ ts, read: f => (existsSync(f) ? readFileSync(f, 'utf8') : undefined), routes: options.routes })
65
+ this.engine = new Engine({
66
+ ts,
67
+ read: (f) => (existsSync(f) ? readFileSync(f, 'utf8') : undefined),
68
+ routes: options.routes,
69
+ })
47
70
  this.host = null
48
71
  this.overlay = new Map()
49
72
  this.errors = new Map()
50
- for (const f of this.parsed.fileNames) if (f.endsWith('.tsx')) this.engine.set(f)
73
+ for (const f of this.parsed.fileNames) if (/\.(?:[cm]?ts|tsx)$/.test(f)) this.engine.set(f)
51
74
  }
52
75
 
53
76
  async start() {
54
77
  if (this.host) return this.host
55
78
  const dir = projectTypescript(this.root)
56
- this.host = dir ? await tsgoHost({ root: this.root, config: this.config, typescriptDir: dir, overlay: this.overlay }) : memoryHost(this.ts, this.root, this.config, this.parsed, this.overlay)
79
+ this.host = dir
80
+ ? await tsgoHost({
81
+ root: this.root,
82
+ config: this.config,
83
+ typescriptDir: dir,
84
+ overlay: this.overlay,
85
+ })
86
+ : memoryHost(this.ts, this.root, this.config, this.parsed, this.overlay)
87
+ if (dir)
88
+ this.host = withBoxProps(this.host, () =>
89
+ memoryHost(this.ts, this.root, this.config, this.parsed, this.overlay),
90
+ )
57
91
  return this.host
58
92
  }
59
93
 
60
94
  /** Saved files: the checker reads them again, except where a shadow still stands in for them. */
61
95
  touch(files) {
62
- const plain = files.filter(f => !this.engine.maps.has(f))
96
+ const plain = files.filter((f) => !this.engine.maps.has(f))
63
97
  for (const f of plain) this.overlay.delete(f)
64
98
  this.host.update(new Map(), plain)
65
99
  }
@@ -70,9 +104,15 @@ class Session {
70
104
  const fresh = !this.host
71
105
  const host = await this.start()
72
106
  const findings = this.engine.run(host, fresh ? undefined : changed)
73
- const redo = fresh || !changed ? this.engine.affected() : this.engine.touched
107
+ const redo =
108
+ fresh || !changed || this.engine.typedChecksNeeded()
109
+ ? this.engine.affected()
110
+ : this.engine.touched
74
111
  for (const file of redo) {
75
- if (!this.engine.facts.has(file)) { this.errors.delete(file); continue }
112
+ if (!this.engine.facts.has(file)) {
113
+ this.errors.delete(file)
114
+ continue
115
+ }
76
116
  const list = this.engine.errors(host, file)
77
117
  if (list.length) this.errors.set(file, list)
78
118
  else this.errors.delete(file)
@@ -86,11 +126,24 @@ class Session {
86
126
  const findings = this.engine.needed() ? this.engine.run(host) : new Map()
87
127
  const diags = host.allDiagnostics()
88
128
  const errors = new Map()
89
- const files = new Set(diags.map(d => d.file))
129
+ const files = new Set(diags.map((d) => d.file))
90
130
  for (const file of files) {
91
- const real = [...this.engine.facts.keys()].find(f => f.toLowerCase() === file.toLowerCase()) ?? file
131
+ const real =
132
+ [...this.engine.facts.keys()].find((f) => f.toLowerCase() === file.toLowerCase()) ?? file
92
133
  if (!this.engine.facts.has(real)) {
93
- errors.set(real, diags.filter(d => d.file === file).map(d => ({ code: `TS${d.code}`, start: d.start, end: d.start + d.length, why: d.text, help: 'Fix the type error' })))
134
+ errors.set(
135
+ real,
136
+ diags
137
+ .filter((d) => d.file === file)
138
+ .map((d) => ({
139
+ code: `TS${d.code}`,
140
+ start: d.start,
141
+ end: d.start + d.length,
142
+ why: d.text,
143
+ help: 'Fix the type error',
144
+ ...d.weft,
145
+ })),
146
+ )
94
147
  continue
95
148
  }
96
149
  const list = this.engine.errors(host, real)
@@ -99,7 +152,10 @@ class Session {
99
152
  return { findings, errors }
100
153
  }
101
154
 
102
- close() { this.host?.close(); this.host = null }
155
+ close() {
156
+ this.host?.close()
157
+ this.host = null
158
+ }
103
159
  }
104
160
 
105
161
  module.exports = { Session, projectTypescript }
@@ -6,7 +6,8 @@ function shadow(text, edits) {
6
6
  const sorted = [...edits].sort((a, b) => a.start - b.start)
7
7
  const parts = []
8
8
  const segments = [] // { src, out, len }: runs that are unchanged
9
- let at = 0, out = 0
9
+ let at = 0,
10
+ out = 0
10
11
  for (const e of sorted) {
11
12
  const len = e.start - at
12
13
  segments.push({ src: at, out, len })
@@ -20,7 +21,8 @@ function shadow(text, edits) {
20
21
  }
21
22
 
22
23
  function find(segments, pos, key) {
23
- let lo = 0, hi = segments.length - 1
24
+ let lo = 0,
25
+ hi = segments.length - 1
24
26
  while (lo < hi) {
25
27
  const mid = (lo + hi + 1) >> 1
26
28
  if (segments[mid][key] <= pos) lo = mid
@@ -30,10 +32,16 @@ function find(segments, pos, key) {
30
32
  }
31
33
 
32
34
  /** Source position to shadow position. */
33
- const toShadow = (map, pos) => { const s = find(map.segments, pos, 'src'); return s.out + Math.min(pos - s.src, s.len) }
35
+ const toShadow = (map, pos) => {
36
+ const s = find(map.segments, pos, 'src')
37
+ return s.out + Math.min(pos - s.src, s.len)
38
+ }
34
39
 
35
40
  /** Shadow position to source position. A position inside generated text lands where that text was inserted. */
36
- const toSource = (map, pos) => { const s = find(map.segments, pos, 'out'); return s.src + Math.min(pos - s.out, s.len) }
41
+ const toSource = (map, pos) => {
42
+ const s = find(map.segments, pos, 'out')
43
+ return s.src + Math.min(pos - s.out, s.len)
44
+ }
37
45
 
38
46
  /** Whether a shadow span is inside text the source doesn't have. */
39
47
  const inGenerated = (map, start, length) => {
@@ -41,6 +49,6 @@ const inGenerated = (map, start, length) => {
41
49
  return start >= s.out + s.len && length >= 0
42
50
  }
43
51
 
44
- const identity = text => ({ text, segments: [{ src: 0, out: 0, len: text.length }], edits: [] })
52
+ const identity = (text) => ({ text, segments: [{ src: 0, out: 0, len: text.length }], edits: [] })
45
53
 
46
54
  module.exports = { shadow, toShadow, toSource, inGenerated, identity }