@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/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
+ }