@weft-js/cli 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -14
- package/bin/weft.mjs +8 -1
- package/docs/async.md +16 -6
- package/docs/browser.md +22 -8
- package/docs/config.md +39 -9
- package/docs/core.md +30 -0
- package/docs/deploy.md +33 -8
- package/docs/design.md +12 -3
- package/docs/entrypoints.md +79 -0
- package/docs/fonts.md +12 -2
- package/docs/icons.md +13 -7
- package/docs/lists.md +3 -1
- package/docs/meta.md +13 -3
- package/docs/plugins.md +32 -9
- package/docs/props.md +13 -2
- package/docs/reactivity.md +26 -0
- package/docs/rules.md +19 -12
- package/docs/rust.md +47 -17
- package/docs/start-short.md +3 -2
- package/docs/start.md +226 -52
- package/docs/state.md +33 -7
- package/docs/style.md +31 -8
- package/docs/surface.md +61 -15
- package/docs/testing.md +15 -5
- package/docs/theme.md +182 -0
- package/docs/ui-state.md +19 -6
- package/docs/verify.md +13 -0
- package/package.json +13 -4
- package/src/add/config.mjs +93 -0
- package/src/add/errors.mjs +2 -0
- package/src/add/setup.mjs +88 -0
- package/src/add-rust.mjs +82 -27
- package/src/add.mjs +209 -181
- package/src/agents-md.mjs +144 -43
- package/src/alias.mjs +16 -5
- package/src/audit/page.mjs +313 -0
- package/src/audit/report.mjs +69 -0
- package/src/audit/utils.mjs +5 -0
- package/src/audit.mjs +159 -206
- package/src/browser/cli.mjs +24 -5
- package/src/browser/client.mjs +189 -25
- package/src/browser/commands.mjs +166 -51
- package/src/catalog/audit.mjs +129 -0
- package/src/catalog/auth.mjs +40 -0
- package/src/catalog/compiler.mjs +337 -0
- package/src/catalog/contracts.mjs +18 -0
- package/src/catalog/database.mjs +52 -0
- package/src/catalog/deploy.mjs +117 -0
- package/src/catalog/design.mjs +159 -0
- package/src/catalog/forms.mjs +25 -0
- package/src/catalog/hydration.mjs +29 -0
- package/src/catalog/icons.mjs +24 -0
- package/src/catalog/media.mjs +163 -0
- package/src/catalog/metadata.mjs +116 -0
- package/src/catalog/plugins.mjs +228 -0
- package/src/catalog/realtime.mjs +195 -0
- package/src/catalog/rust.mjs +75 -0
- package/src/catalog/server-functions.mjs +92 -0
- package/src/catalog/state.mjs +39 -0
- package/src/catalog/style-groups.mjs +54 -0
- package/src/catalog/tests.mjs +127 -0
- package/src/catalog/text.mjs +200 -0
- package/src/catalog/ui-state.mjs +42 -0
- package/src/catalog.mjs +67 -460
- package/src/check/runtime.mjs +280 -0
- package/src/check.mjs +198 -208
- package/src/cli/authoring.mjs +213 -0
- package/src/cli/project.mjs +302 -0
- package/src/cli.mjs +222 -250
- package/src/config.mjs +1 -1
- package/src/contributions.mjs +75 -25
- package/src/db-layer.mjs +13 -5
- package/src/deploy/cdn.mjs +94 -16
- package/src/deploy/files.mjs +100 -21
- package/src/deploy/fit.mjs +127 -23
- package/src/deploy/hosts.mjs +90 -20
- package/src/deploy/index/cli.mjs +20 -0
- package/src/deploy/index/environment.mjs +69 -0
- package/src/deploy/index/steps.mjs +87 -0
- package/src/deploy/index.mjs +228 -114
- package/src/deploy/needs.mjs +88 -31
- package/src/design-checks/accessible-names.mjs +62 -0
- package/src/design-checks/contrast.mjs +43 -0
- package/src/design-checks/csp.mjs +60 -0
- package/src/design-checks/files.mjs +59 -0
- package/src/design-checks/heading-links.mjs +61 -0
- package/src/design-checks/interaction.mjs +65 -0
- package/src/design-checks/jsx.mjs +67 -0
- package/src/design-checks/layout-values.mjs +68 -0
- package/src/design-checks/native-controls.mjs +48 -0
- package/src/design-checks/outline.mjs +39 -0
- package/src/design-checks/reporter.mjs +37 -0
- package/src/design-checks/rules.mjs +274 -0
- package/src/design-checks/values.mjs +262 -0
- package/src/design-checks.mjs +10 -400
- package/src/diag.mjs +32 -8
- package/src/docs.mjs +138 -43
- package/src/doctor.mjs +20 -8
- package/src/files.mjs +18 -5
- package/src/fix.mjs +10 -2
- package/src/gate/after.mjs +40 -0
- package/src/gate/before.mjs +54 -0
- package/src/gate/classify.mjs +72 -0
- package/src/gate/mutation.mjs +209 -0
- package/src/gate/resolve-cases.mjs +22 -0
- package/src/gate/result.mjs +63 -0
- package/src/gate/run.mjs +75 -0
- package/src/gate.mjs +104 -202
- package/src/infer/box-props.cjs +140 -0
- package/src/infer/core-boxes.cjs +46 -0
- package/src/infer/derived-writes.cjs +95 -0
- package/src/infer/engine/checks.cjs +193 -0
- package/src/infer/engine/flow.cjs +293 -0
- package/src/infer/engine/report.cjs +158 -0
- package/src/infer/engine/utils.cjs +15 -0
- package/src/infer/engine.cjs +108 -368
- package/src/infer/facts/sites.cjs +114 -0
- package/src/infer/facts.cjs +210 -117
- package/src/infer/hosts.cjs +69 -22
- package/src/infer/lsp.mjs +149 -40
- package/src/infer/session.cjs +73 -17
- package/src/infer/shadow.cjs +13 -5
- package/src/infer/worker.cjs +57 -9
- package/src/jsx-scan.mjs +63 -21
- package/src/load-config/schema.mjs +281 -0
- package/src/load-config.mjs +137 -138
- package/src/map.mjs +93 -26
- package/src/mcp/browser-tools.mjs +155 -0
- package/src/mcp.mjs +147 -69
- package/src/meta-table.mjs +43 -8
- package/src/mutate.mjs +120 -36
- package/src/plugin-checks.mjs +52 -23
- package/src/plugins.mjs +125 -39
- package/src/project.mjs +170 -79
- package/src/props-checks.mjs +24 -6
- package/src/props-listener.mjs +27 -9
- package/src/react-checks.mjs +35 -4
- package/src/server-state-checks.mjs +34 -0
- package/src/share-checks.mjs +121 -37
- package/src/site.mjs +33 -6
- package/src/slots.mjs +104 -19
- package/src/start/build.mjs +116 -0
- package/src/start/routes.mjs +38 -0
- package/src/start.mjs +193 -95
- package/src/state-checks/core-state.mjs +223 -0
- package/src/state-checks/modules.mjs +17 -0
- package/src/state-checks/scan.mjs +232 -0
- package/src/state-checks/server-graph.mjs +65 -0
- package/src/state-checks.mjs +109 -133
- package/src/style/find.mjs +121 -0
- package/src/style.mjs +133 -127
- package/src/surface-checks.mjs +212 -46
- package/src/tailwind.mjs +5 -4
- package/src/tasks.mjs +189 -27
- package/src/test-rules/context.mjs +109 -0
- package/src/test-rules/file-rules.mjs +279 -0
- package/src/test-rules/rule-helpers.mjs +244 -0
- package/src/test-rules/syntax.mjs +223 -0
- package/src/test-rules/test-rules.mjs +127 -0
- package/src/testdiff.mjs +94 -32
- package/src/tests.mjs +31 -483
- package/src/text-checks.mjs +229 -69
- package/src/tokens.mjs +12 -4
- package/src/trust/scan.mjs +95 -0
- package/src/trust.mjs +214 -102
- package/src/weft.mjs +41 -9
- package/src/workspace.mjs +96 -19
- package/ts-plugin/context.js +57 -0
- package/ts-plugin/index.js +4 -124
- package/ts-plugin/mapping.js +65 -0
- package/ts-plugin/proxy.js +144 -0
package/src/infer/hosts.cjs
CHANGED
|
@@ -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 =
|
|
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':
|
|
34
|
-
|
|
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 {
|
|
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 {
|
|
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 =>
|
|
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,
|
|
59
|
-
|
|
60
|
-
|
|
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 [
|
|
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
|
|
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 =
|
|
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,
|
|
92
|
-
|
|
93
|
-
|
|
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 = []) {
|
|
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 => ({
|
|
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([
|
|
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 =
|
|
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++) {
|
|
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)
|
|
48
|
-
|
|
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
|
|
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
|
|
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))
|
|
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) => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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)
|
|
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({
|
|
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 = {
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
}
|
package/src/infer/session.cjs
CHANGED
|
@@ -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) {
|
|
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,
|
|
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(
|
|
35
|
-
|
|
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({
|
|
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 (
|
|
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
|
|
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 =
|
|
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)) {
|
|
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 =
|
|
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(
|
|
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() {
|
|
155
|
+
close() {
|
|
156
|
+
this.host?.close()
|
|
157
|
+
this.host = null
|
|
158
|
+
}
|
|
103
159
|
}
|
|
104
160
|
|
|
105
161
|
module.exports = { Session, projectTypescript }
|
package/src/infer/shadow.cjs
CHANGED
|
@@ -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,
|
|
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,
|
|
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) => {
|
|
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) => {
|
|
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 }
|