@zenithbuild/language-server 0.2.7 → 0.5.0-beta.2.1
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/CHANGELOG.md +6 -0
- package/README.md +14 -0
- package/RELEASE_NOTES.md +33 -0
- package/package.json +5 -2
- package/scripts/release.ts +24 -7
- package/src/code-actions.ts +60 -0
- package/src/contracts.ts +100 -0
- package/src/diagnostics.ts +391 -74
- package/src/project.ts +141 -25
- package/src/server.ts +68 -46
- package/src/settings.ts +15 -0
- package/src/types/zenith-compiler.d.ts +3 -0
- package/test/contracts.spec.ts +37 -0
- package/test/diagnostics.spec.ts +101 -0
- package/test/project-root.spec.ts +44 -0
- package/tsconfig.test.json +25 -0
- package/bun.lock +0 -83
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.8] - 2026-01-26
|
|
9
|
+
|
|
10
|
+
### 📝 Other Changes
|
|
11
|
+
|
|
12
|
+
- **** ()
|
|
13
|
+
|
|
8
14
|
## [0.2.1] - 2026-01-16
|
|
9
15
|
|
|
10
16
|
### 🐛 Bug Fixes
|
package/README.md
CHANGED
|
@@ -12,6 +12,20 @@ This package provides the "brains" for Zenith editor support. It implements the
|
|
|
12
12
|
- **Completion**: Context-aware suggestions for Zenith-specific syntax and standard HTML.
|
|
13
13
|
- **Hover Information**: Detailed documentation on hover for core components and hooks.
|
|
14
14
|
- **Document Symbols**: Outline and navigation support for complex components.
|
|
15
|
+
- **Contract Enforcement**:
|
|
16
|
+
- `on:click={handler}` event syntax diagnostics + quick fixes for `onclick` / `@click`.
|
|
17
|
+
- Component script policy (`zenith.componentScripts`: `forbid` | `allow`).
|
|
18
|
+
- CSS import contract diagnostics for local precompiled CSS only.
|
|
19
|
+
- **Project Root Resolution**:
|
|
20
|
+
- nearest `zenith.config.*`
|
|
21
|
+
- nearest `package.json` with `@zenithbuild/cli`
|
|
22
|
+
- workspace-aware fallback heuristics
|
|
23
|
+
|
|
24
|
+
## Settings
|
|
25
|
+
|
|
26
|
+
- `zenith.componentScripts`
|
|
27
|
+
- `forbid` (default): components may not contain `<script>`.
|
|
28
|
+
- `allow`: disables the component-script contract diagnostic.
|
|
15
29
|
|
|
16
30
|
## Architecture
|
|
17
31
|
|
package/RELEASE_NOTES.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# 🚀 @zenithbuild/language-server v0.2.9
|
|
2
|
+
|
|
3
|
+
## [0.2.9] - 2026-02-15
|
|
4
|
+
|
|
5
|
+
### ✨ Contract Updates
|
|
6
|
+
|
|
7
|
+
- Added component script policy diagnostics with configurable mode:
|
|
8
|
+
- `zenith.componentScripts = "forbid" | "allow"`.
|
|
9
|
+
- Added event binding syntax diagnostics and quick fixes:
|
|
10
|
+
- invalid: `onclick="..."`, `@click={...}`
|
|
11
|
+
- valid: `on:click={handler}`.
|
|
12
|
+
- Added Tailwind v4-compatible CSS import contract diagnostics:
|
|
13
|
+
- allow local CSS imports (including `?query`/`#hash` suffixes)
|
|
14
|
+
- fail bare CSS package imports and project-root traversal escapes.
|
|
15
|
+
- Added workspace-aware project root resolution heuristics.
|
|
16
|
+
- Added unit tests for root resolution, CSS suffix/path contracts, component script diagnostics, and event-binding code actions.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## 📦 Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bun add @zenithbuild/language-server@0.2.9
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
*or with npm:*
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install @zenithbuild/language-server@0.2.9
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
*Generated by Zenith Automated Release*
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenithbuild/language-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.5.0-beta.2.1",
|
|
4
4
|
"description": "Language Server for Zenith Framework",
|
|
5
5
|
"main": "./dist/server.js",
|
|
6
6
|
"types": "./dist/server.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"build": "npx esbuild src/server.ts --bundle --outdir=dist --platform=node --format=cjs",
|
|
8
|
+
"build": "npx esbuild src/server.ts --bundle --outdir=dist --platform=node --format=cjs --external:@zenithbuild/compiler --external:vscode-languageserver/node --external:vscode-languageserver-textdocument",
|
|
9
|
+
"test": "npm run test:unit",
|
|
10
|
+
"test:unit": "bun test test/*.spec.ts",
|
|
9
11
|
"dev": "npm run build -- --watch",
|
|
10
12
|
"release": "bun run scripts/release.ts",
|
|
11
13
|
"release:dry": "bun run scripts/release.ts --dry-run",
|
|
@@ -14,6 +16,7 @@
|
|
|
14
16
|
"release:major": "bun run scripts/release.ts --bump=major"
|
|
15
17
|
},
|
|
16
18
|
"dependencies": {
|
|
19
|
+
"@zenithbuild/compiler": "^1.3.0",
|
|
17
20
|
"vscode-languageserver": "^9.0.1",
|
|
18
21
|
"vscode-languageserver-textdocument": "^1.0.11"
|
|
19
22
|
},
|
package/scripts/release.ts
CHANGED
|
@@ -279,9 +279,13 @@ function generateChangelog(
|
|
|
279
279
|
changelog += `### ⚠️ BREAKING CHANGES\n\n`;
|
|
280
280
|
for (const commit of breakingChanges) {
|
|
281
281
|
const scope = commit.scope ? `**${commit.scope}**: ` : "";
|
|
282
|
-
changelog +=
|
|
282
|
+
changelog += `#### ${scope}${commit.subject} (${commit.hash.slice(0, 7)})\n`;
|
|
283
|
+
if (commit.body) {
|
|
284
|
+
changelog += `\n${commit.body.split('\n').map(line => `> ${line}`).join('\n')}\n`;
|
|
285
|
+
}
|
|
286
|
+
changelog += "\n";
|
|
283
287
|
}
|
|
284
|
-
changelog += "\n";
|
|
288
|
+
changelog += "---\n\n";
|
|
285
289
|
}
|
|
286
290
|
|
|
287
291
|
// Regular changes by type
|
|
@@ -294,7 +298,14 @@ function generateChangelog(
|
|
|
294
298
|
changelog += `### ${typeConfig.title}\n\n`;
|
|
295
299
|
for (const commit of typeCommits) {
|
|
296
300
|
const scope = commit.scope ? `**${commit.scope}**: ` : "";
|
|
297
|
-
changelog += `-
|
|
301
|
+
changelog += `- **${scope}${commit.subject}** (${commit.hash.slice(0, 7)})\n`;
|
|
302
|
+
|
|
303
|
+
// Include body if present
|
|
304
|
+
if (commit.body && commit.body.trim()) {
|
|
305
|
+
const cleanedBody = commit.body.trim();
|
|
306
|
+
// Indent body for better hierarchy
|
|
307
|
+
changelog += `${cleanedBody.split('\n').map(line => ` > ${line}`).join('\n')}\n`;
|
|
308
|
+
}
|
|
298
309
|
}
|
|
299
310
|
changelog += "\n";
|
|
300
311
|
}
|
|
@@ -303,7 +314,10 @@ function generateChangelog(
|
|
|
303
314
|
if (groupedCommits.other && groupedCommits.other.length > 0) {
|
|
304
315
|
changelog += `### 📝 Other Changes\n\n`;
|
|
305
316
|
for (const commit of groupedCommits.other) {
|
|
306
|
-
changelog += `-
|
|
317
|
+
changelog += `- **${commit.subject}** (${commit.hash.slice(0, 7)})\n`;
|
|
318
|
+
if (commit.body && commit.body.trim()) {
|
|
319
|
+
changelog += `${commit.body.trim().split('\n').map(line => ` > ${line}`).join('\n')}\n`;
|
|
320
|
+
}
|
|
307
321
|
}
|
|
308
322
|
changelog += "\n";
|
|
309
323
|
}
|
|
@@ -319,21 +333,24 @@ function generateReleaseNotes(
|
|
|
319
333
|
): string {
|
|
320
334
|
const changelog = generateChangelog(commits, newVersion, config);
|
|
321
335
|
|
|
322
|
-
return `# ${packageName} v${newVersion}
|
|
336
|
+
return `# 🚀 ${packageName} v${newVersion}
|
|
323
337
|
|
|
324
338
|
${changelog}
|
|
325
339
|
|
|
326
|
-
## Installation
|
|
340
|
+
## 📦 Installation
|
|
327
341
|
|
|
328
342
|
\`\`\`bash
|
|
329
343
|
bun add ${packageName}@${newVersion}
|
|
330
344
|
\`\`\`
|
|
331
345
|
|
|
332
|
-
or with npm
|
|
346
|
+
*or with npm:*
|
|
333
347
|
|
|
334
348
|
\`\`\`bash
|
|
335
349
|
npm install ${packageName}@${newVersion}
|
|
336
350
|
\`\`\`
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
*Generated by Zenith Automated Release*
|
|
337
354
|
`;
|
|
338
355
|
}
|
|
339
356
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ZenithDiagnostic,
|
|
3
|
+
ZenithRange
|
|
4
|
+
} from './diagnostics';
|
|
5
|
+
|
|
6
|
+
export const EVENT_BINDING_DIAGNOSTIC_CODE = 'zenith.event.binding.syntax';
|
|
7
|
+
|
|
8
|
+
export interface EventBindingCodeActionData {
|
|
9
|
+
replacement: string;
|
|
10
|
+
title: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ZenithCodeAction {
|
|
14
|
+
title: string;
|
|
15
|
+
kind: string;
|
|
16
|
+
diagnostics: ZenithDiagnostic[];
|
|
17
|
+
edit: {
|
|
18
|
+
changes: Record<string, Array<{ range: ZenithRange; newText: string }>>;
|
|
19
|
+
};
|
|
20
|
+
isPreferred?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ZenithTextDocumentLike {
|
|
24
|
+
uri: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildEventBindingCodeActions(
|
|
28
|
+
document: ZenithTextDocumentLike,
|
|
29
|
+
diagnostics: ZenithDiagnostic[]
|
|
30
|
+
): ZenithCodeAction[] {
|
|
31
|
+
const actions: ZenithCodeAction[] = [];
|
|
32
|
+
|
|
33
|
+
for (const diagnostic of diagnostics) {
|
|
34
|
+
if (diagnostic.code !== EVENT_BINDING_DIAGNOSTIC_CODE) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const data = diagnostic.data as EventBindingCodeActionData | undefined;
|
|
39
|
+
if (!data || typeof data.replacement !== 'string' || typeof data.title !== 'string') {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
actions.push({
|
|
44
|
+
title: data.title,
|
|
45
|
+
kind: 'quickfix',
|
|
46
|
+
diagnostics: [diagnostic],
|
|
47
|
+
edit: {
|
|
48
|
+
changes: {
|
|
49
|
+
[document.uri]: [{
|
|
50
|
+
range: diagnostic.range,
|
|
51
|
+
newText: data.replacement
|
|
52
|
+
}]
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
isPreferred: true
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return actions;
|
|
60
|
+
}
|
package/src/contracts.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
export type ZenithFileKind = 'page' | 'layout' | 'component' | 'unknown';
|
|
5
|
+
|
|
6
|
+
export function stripImportSuffix(specifier: string): string {
|
|
7
|
+
const hashIndex = specifier.indexOf('#');
|
|
8
|
+
const queryIndex = specifier.indexOf('?');
|
|
9
|
+
|
|
10
|
+
let cutAt = -1;
|
|
11
|
+
if (hashIndex >= 0 && queryIndex >= 0) {
|
|
12
|
+
cutAt = Math.min(hashIndex, queryIndex);
|
|
13
|
+
} else if (hashIndex >= 0) {
|
|
14
|
+
cutAt = hashIndex;
|
|
15
|
+
} else if (queryIndex >= 0) {
|
|
16
|
+
cutAt = queryIndex;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return cutAt >= 0 ? specifier.slice(0, cutAt) : specifier;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isLocalCssSpecifier(specifier: string): boolean {
|
|
23
|
+
return (
|
|
24
|
+
specifier.startsWith('./') ||
|
|
25
|
+
specifier.startsWith('../') ||
|
|
26
|
+
specifier.startsWith('/')
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isCssContractImportSpecifier(specifier: string): boolean {
|
|
31
|
+
const normalized = stripImportSuffix(specifier).trim();
|
|
32
|
+
if (!normalized) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (normalized.endsWith('.css')) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (normalized === 'tailwindcss') {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (/^@[^/]+\/css(?:$|\/)/.test(normalized)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function canonicalizePath(candidate: string): string {
|
|
52
|
+
try {
|
|
53
|
+
return fs.realpathSync.native(candidate);
|
|
54
|
+
} catch {
|
|
55
|
+
return path.resolve(candidate);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveCssImportPath(
|
|
60
|
+
importingFilePath: string,
|
|
61
|
+
specifier: string,
|
|
62
|
+
projectRoot: string
|
|
63
|
+
): { resolvedPath: string; escapesProjectRoot: boolean } {
|
|
64
|
+
const normalizedSpecifier = stripImportSuffix(specifier);
|
|
65
|
+
const importingDir = path.dirname(importingFilePath);
|
|
66
|
+
const rootCanonical = canonicalizePath(projectRoot);
|
|
67
|
+
|
|
68
|
+
const unresolvedTarget = normalizedSpecifier.startsWith('/')
|
|
69
|
+
? path.join(rootCanonical, normalizedSpecifier.slice(1))
|
|
70
|
+
: path.resolve(importingDir, normalizedSpecifier);
|
|
71
|
+
|
|
72
|
+
const targetCanonical = canonicalizePath(unresolvedTarget);
|
|
73
|
+
const relativeToRoot = path.relative(rootCanonical, targetCanonical);
|
|
74
|
+
const escapesProjectRoot =
|
|
75
|
+
relativeToRoot.startsWith('..') ||
|
|
76
|
+
path.isAbsolute(relativeToRoot);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
resolvedPath: targetCanonical,
|
|
80
|
+
escapesProjectRoot
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function classifyZenithFile(filePath: string): ZenithFileKind {
|
|
85
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
86
|
+
|
|
87
|
+
if (!normalized.endsWith('.zen')) {
|
|
88
|
+
return 'unknown';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (normalized.includes('/src/pages/') || normalized.includes('/app/pages/')) {
|
|
92
|
+
return 'page';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (normalized.includes('/src/layouts/') || normalized.includes('/app/layouts/')) {
|
|
96
|
+
return 'layout';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return 'component';
|
|
100
|
+
}
|