@statorjs/language-server 0.1.0
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/LICENSE +21 -0
- package/bin/stator-language-server.js +3 -0
- package/dist/server.cjs +45939 -0
- package/package.json +52 -0
- package/src/diagnostics.ts +75 -0
- package/src/index.ts +47 -0
- package/src/language-plugin.ts +136 -0
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@statorjs/language-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Volar-based language server for .stator files.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/statorjs/stator.git",
|
|
10
|
+
"directory": "packages/language-server"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/statorjs/stator#readme",
|
|
13
|
+
"bin": {
|
|
14
|
+
"stator-language-server": "./bin/stator-language-server.js"
|
|
15
|
+
},
|
|
16
|
+
"main": "./src/index.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./src/index.ts",
|
|
19
|
+
"./plugin": "./src/language-plugin.ts",
|
|
20
|
+
"./server": "./dist/server.cjs",
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"bin",
|
|
25
|
+
"dist",
|
|
26
|
+
"src"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@volar/language-core": "~2.4.0",
|
|
30
|
+
"@volar/language-server": "~2.4.0",
|
|
31
|
+
"@volar/typescript": "~2.4.0",
|
|
32
|
+
"volar-service-css": "0.0.71",
|
|
33
|
+
"volar-service-typescript": "0.0.71",
|
|
34
|
+
"vscode-css-languageservice": "^6.3.0",
|
|
35
|
+
"vscode-uri": "^3.0.8",
|
|
36
|
+
"@statorjs/stator": "1.0.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"typescript": "^5.6.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.7.0",
|
|
43
|
+
"esbuild": "^0.24.0",
|
|
44
|
+
"typescript": "^5.6.0",
|
|
45
|
+
"vitest": "^2.1.0"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"build": "esbuild src/index.ts --bundle --platform=node --format=cjs --outfile=dist/server.cjs --external:typescript --external:vscode-css-languageservice"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stator semantic diagnostics: run the REAL compiler over the `.stator`
|
|
3
|
+
* source and surface its `CompileError`s as editor squiggles — so the editor
|
|
4
|
+
* and the Vite build can never disagree about what's valid. (The TS and CSS
|
|
5
|
+
* services cover their regions; this covers Stator's own rules: directive
|
|
6
|
+
* misuse, client-component name matching, frontmatter capabilities, spread,
|
|
7
|
+
* malformed templates.)
|
|
8
|
+
*
|
|
9
|
+
* The service runs against the ROOT virtual code (languageId `stator`, full
|
|
10
|
+
* source text, identity-mapped back to the file — see language-plugin.ts),
|
|
11
|
+
* so positions pass through 1:1.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { CompileError, compile } from '@statorjs/stator/compiler'
|
|
15
|
+
import type { Diagnostic, LanguageServicePlugin } from '@volar/language-server'
|
|
16
|
+
import { URI } from 'vscode-uri'
|
|
17
|
+
|
|
18
|
+
const ROUTE_PATH_RE = /[\\/]routes[\\/].*\.stator$/
|
|
19
|
+
|
|
20
|
+
/** Pure core, unit-tested directly: compile and translate the failure. */
|
|
21
|
+
export function statorDiagnostics(text: string, filePath: string): Diagnostic[] {
|
|
22
|
+
const kind = ROUTE_PATH_RE.test(filePath) ? 'route' : 'component'
|
|
23
|
+
try {
|
|
24
|
+
compile(text, { id: filePath, kind })
|
|
25
|
+
return []
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (err instanceof CompileError) {
|
|
28
|
+
// CompileError carries a 1-based line/column into the original source;
|
|
29
|
+
// LSP positions are 0-based. Errors without a location anchor at 0:0.
|
|
30
|
+
const line = (err.loc?.line ?? 1) - 1
|
|
31
|
+
const character = (err.loc?.column ?? 1) - 1
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
range: { start: { line, character }, end: { line, character: character + 1 } },
|
|
35
|
+
message: err.message,
|
|
36
|
+
severity: 1, // Error
|
|
37
|
+
source: 'stator',
|
|
38
|
+
},
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
// Non-CompileError throws are compiler bugs — still show them rather
|
|
42
|
+
// than swallowing, anchored at the top of the file.
|
|
43
|
+
return [
|
|
44
|
+
{
|
|
45
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
|
|
46
|
+
message: `stator: internal compiler error — ${String(err)}`,
|
|
47
|
+
severity: 1,
|
|
48
|
+
source: 'stator',
|
|
49
|
+
},
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createStatorDiagnosticsService(): LanguageServicePlugin {
|
|
55
|
+
return {
|
|
56
|
+
name: 'stator-diagnostics',
|
|
57
|
+
capabilities: {
|
|
58
|
+
diagnosticProvider: {
|
|
59
|
+
interFileDependencies: false,
|
|
60
|
+
workspaceDiagnostics: false,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
create(context) {
|
|
64
|
+
return {
|
|
65
|
+
provideDiagnostics(document) {
|
|
66
|
+
if (document.languageId !== 'stator') return
|
|
67
|
+
const uri = URI.parse(document.uri)
|
|
68
|
+
const decoded = context.decodeEmbeddedDocumentUri(uri)
|
|
69
|
+
const path = decoded ? decoded[0].fsPath : uri.fsPath
|
|
70
|
+
return statorDiagnostics(document.getText(), path)
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `.stator` language server — a standard Volar node server.
|
|
3
|
+
*
|
|
4
|
+
* Editor-agnostic: any LSP client launches this over stdio (the VSCode extension
|
|
5
|
+
* is one client; nvim/emacs/helix/zed configure the same binary). It federates
|
|
6
|
+
* the TypeScript and CSS language services over the virtual code produced by the
|
|
7
|
+
* Stator language plugin.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
createConnection,
|
|
12
|
+
createServer,
|
|
13
|
+
createTypeScriptProject,
|
|
14
|
+
loadTsdkByPath,
|
|
15
|
+
} from '@volar/language-server/node'
|
|
16
|
+
import { create as createCssService } from 'volar-service-css'
|
|
17
|
+
import { create as createTypeScriptServices } from 'volar-service-typescript'
|
|
18
|
+
import { createStatorDiagnosticsService } from './diagnostics.ts'
|
|
19
|
+
import { statorLanguagePlugin } from './language-plugin.ts'
|
|
20
|
+
|
|
21
|
+
const connection = createConnection()
|
|
22
|
+
const server = createServer(connection)
|
|
23
|
+
|
|
24
|
+
connection.onInitialize((params) => {
|
|
25
|
+
const options = params.initializationOptions as { typescript?: { tsdk?: string } } | undefined
|
|
26
|
+
const tsdkPath = options?.typescript?.tsdk
|
|
27
|
+
if (!tsdkPath) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
'stator language server: initializationOptions.typescript.tsdk (path to the TS lib dir) is required.',
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
const tsdk = loadTsdkByPath(tsdkPath, params.locale)
|
|
33
|
+
return server.initialize(
|
|
34
|
+
params,
|
|
35
|
+
createTypeScriptProject(tsdk.typescript, tsdk.diagnosticMessages, () => ({
|
|
36
|
+
languagePlugins: [statorLanguagePlugin],
|
|
37
|
+
})),
|
|
38
|
+
[
|
|
39
|
+
...createTypeScriptServices(tsdk.typescript),
|
|
40
|
+
createCssService(),
|
|
41
|
+
createStatorDiagnosticsService(),
|
|
42
|
+
],
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
connection.onInitialized(() => server.initialized())
|
|
47
|
+
connection.listen()
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Volar language plugin for `.stator` files. It adapts the compiler's
|
|
3
|
+
* framework-neutral virtual-code emit (`toVirtualCode`) into Volar's virtual
|
|
4
|
+
* codes + `CodeMapping`s, so Volar can federate the TypeScript and CSS language
|
|
5
|
+
* services over the right regions with correct source mapping.
|
|
6
|
+
*
|
|
7
|
+
* All `.stator` syntax knowledge lives in the compiler; this file is purely the
|
|
8
|
+
* Volar adapter (mapping-shape translation + which embedded code is the TS
|
|
9
|
+
* script). Keeping the split means the language server and the runtime compiler
|
|
10
|
+
* never disagree about the file.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { toVirtualCode, type VirtualMapping } from '@statorjs/stator/compiler'
|
|
14
|
+
import type { CodeMapping, LanguagePlugin, VirtualCode } from '@volar/language-core'
|
|
15
|
+
import { forEachEmbeddedCode } from '@volar/language-core'
|
|
16
|
+
// Activates the `typescript` field on LanguagePlugin (module augmentation).
|
|
17
|
+
import type {} from '@volar/typescript'
|
|
18
|
+
import type { IScriptSnapshot } from 'typescript'
|
|
19
|
+
import type { URI } from 'vscode-uri'
|
|
20
|
+
|
|
21
|
+
const TSX_SCRIPT_KIND = 4 // ts.ScriptKind.TSX
|
|
22
|
+
const DEFERRED_SCRIPT_KIND = 7 // ts.ScriptKind.Deferred
|
|
23
|
+
|
|
24
|
+
/** Feature flags for a fully-serviced region. */
|
|
25
|
+
const FULL: CodeMapping['data'] = {
|
|
26
|
+
verification: true,
|
|
27
|
+
completion: true,
|
|
28
|
+
semantic: true,
|
|
29
|
+
navigation: true,
|
|
30
|
+
structure: true,
|
|
31
|
+
format: false,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const statorLanguagePlugin: LanguagePlugin<URI, StatorVirtualCode> = {
|
|
35
|
+
getLanguageId(uri) {
|
|
36
|
+
if (uri.path.endsWith('.stator')) return 'stator'
|
|
37
|
+
return undefined
|
|
38
|
+
},
|
|
39
|
+
createVirtualCode(_uri, languageId, snapshot) {
|
|
40
|
+
if (languageId !== 'stator') return undefined
|
|
41
|
+
return new StatorVirtualCode(snapshot)
|
|
42
|
+
},
|
|
43
|
+
updateVirtualCode(_uri, code, snapshot) {
|
|
44
|
+
code.update(snapshot)
|
|
45
|
+
return code
|
|
46
|
+
},
|
|
47
|
+
typescript: {
|
|
48
|
+
extraFileExtensions: [
|
|
49
|
+
{
|
|
50
|
+
extension: 'stator',
|
|
51
|
+
isMixedContent: true,
|
|
52
|
+
scriptKind: DEFERRED_SCRIPT_KIND,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
getServiceScript(root) {
|
|
56
|
+
for (const code of forEachEmbeddedCode(root)) {
|
|
57
|
+
if (code.id === 'tsx') {
|
|
58
|
+
return { code, extension: '.tsx', scriptKind: TSX_SCRIPT_KIND }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return undefined
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class StatorVirtualCode implements VirtualCode {
|
|
67
|
+
id = 'root'
|
|
68
|
+
languageId = 'stator'
|
|
69
|
+
snapshot: IScriptSnapshot
|
|
70
|
+
// TS/CSS features are served by the embedded codes' mappings. The root
|
|
71
|
+
// itself carries one identity mapping with `verification` only, which makes
|
|
72
|
+
// it visible to the stator diagnostics service (full source, positions
|
|
73
|
+
// passing through 1:1) without inviting TS/CSS features onto it.
|
|
74
|
+
mappings: CodeMapping[] = []
|
|
75
|
+
embeddedCodes: VirtualCode[] = []
|
|
76
|
+
|
|
77
|
+
constructor(snapshot: IScriptSnapshot) {
|
|
78
|
+
this.snapshot = snapshot
|
|
79
|
+
this.update(snapshot)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
update(snapshot: IScriptSnapshot): void {
|
|
83
|
+
this.snapshot = snapshot
|
|
84
|
+
const text = snapshot.getText(0, snapshot.getLength())
|
|
85
|
+
this.mappings = [
|
|
86
|
+
{
|
|
87
|
+
sourceOffsets: [0],
|
|
88
|
+
generatedOffsets: [0],
|
|
89
|
+
lengths: [text.length],
|
|
90
|
+
data: {
|
|
91
|
+
verification: true,
|
|
92
|
+
completion: false,
|
|
93
|
+
semantic: false,
|
|
94
|
+
navigation: false,
|
|
95
|
+
structure: false,
|
|
96
|
+
format: false,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
const vc = toVirtualCode(text)
|
|
101
|
+
this.embeddedCodes = [
|
|
102
|
+
embed('tsx', 'typescriptreact', vc.tsx.code, vc.tsx.mappings),
|
|
103
|
+
...vc.styles.map((s, i) => embed(`css_${i}`, 'css', s.code, s.mappings)),
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function embed(
|
|
109
|
+
id: string,
|
|
110
|
+
languageId: string,
|
|
111
|
+
code: string,
|
|
112
|
+
mappings: VirtualMapping[],
|
|
113
|
+
): VirtualCode {
|
|
114
|
+
return {
|
|
115
|
+
id,
|
|
116
|
+
languageId,
|
|
117
|
+
snapshot: stringSnapshot(code),
|
|
118
|
+
mappings: [
|
|
119
|
+
{
|
|
120
|
+
sourceOffsets: mappings.map((m) => m.sourceOffset),
|
|
121
|
+
generatedOffsets: mappings.map((m) => m.generatedOffset),
|
|
122
|
+
lengths: mappings.map((m) => m.length),
|
|
123
|
+
data: FULL,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
embeddedCodes: [],
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function stringSnapshot(text: string): IScriptSnapshot {
|
|
131
|
+
return {
|
|
132
|
+
getText: (start, end) => text.slice(start, end),
|
|
133
|
+
getLength: () => text.length,
|
|
134
|
+
getChangeRange: () => undefined,
|
|
135
|
+
}
|
|
136
|
+
}
|