@zenithbuild/language-server 0.6.0 → 0.6.17

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/src/settings.ts DELETED
@@ -1,18 +0,0 @@
1
- export type ComponentScriptsMode = 'forbid' | 'allow';
2
-
3
- export interface ZenithServerSettings {
4
- componentScripts: ComponentScriptsMode;
5
- strictDomLints: boolean;
6
- }
7
-
8
- export const DEFAULT_SETTINGS: ZenithServerSettings = Object.freeze({
9
- componentScripts: 'forbid',
10
- strictDomLints: false
11
- });
12
-
13
- export function normalizeSettings(input: unknown): ZenithServerSettings {
14
- const maybe = (input || {}) as { componentScripts?: unknown; strictDomLints?: unknown };
15
- const mode = maybe.componentScripts === 'allow' ? 'allow' : 'forbid';
16
- const strictDomLints = maybe.strictDomLints === true;
17
- return { componentScripts: mode, strictDomLints };
18
- }
@@ -1,3 +0,0 @@
1
- declare module '@zenithbuild/compiler' {
2
- export function compile(source: string, filePath: string): Promise<unknown>;
3
- }
@@ -1,37 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import path from 'node:path';
4
-
5
- import {
6
- stripImportSuffix,
7
- isCssContractImportSpecifier,
8
- isLocalCssSpecifier,
9
- resolveCssImportPath
10
- } from '../src/contracts';
11
-
12
- test('stripImportSuffix removes query/hash suffixes deterministically', () => {
13
- assert.equal(stripImportSuffix('./styles/output.css?v=1#hash'), './styles/output.css');
14
- assert.equal(stripImportSuffix('./styles/output.css#hash?v=1'), './styles/output.css');
15
- assert.equal(stripImportSuffix('./styles/output.css'), './styles/output.css');
16
- });
17
-
18
- test('css contract identifies local and bare css import shapes', () => {
19
- assert.equal(isCssContractImportSpecifier('./styles/output.css?v=1'), true);
20
- assert.equal(isCssContractImportSpecifier('tailwindcss'), true);
21
- assert.equal(isCssContractImportSpecifier('@scope/css'), true);
22
- assert.equal(isLocalCssSpecifier('./styles/output.css'), true);
23
- assert.equal(isLocalCssSpecifier('../styles/output.css#hash'), true);
24
- assert.equal(isLocalCssSpecifier('/src/styles/output.css'), true);
25
- assert.equal(isLocalCssSpecifier('tailwindcss'), false);
26
- });
27
-
28
- test('resolveCssImportPath flags project-root traversal escape', () => {
29
- const projectRoot = path.join('/tmp', 'zenith-site');
30
- const importer = path.join(projectRoot, 'src', 'pages', 'index.zen');
31
-
32
- const ok = resolveCssImportPath(importer, '../styles/output.css?v=1#hash', projectRoot);
33
- assert.equal(ok.escapesProjectRoot, false);
34
-
35
- const escaped = resolveCssImportPath(importer, '../../../../outside.css', projectRoot);
36
- assert.equal(escaped.escapesProjectRoot, true);
37
- });
@@ -1,120 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
-
4
- import { collectDiagnostics, collectContractDiagnostics, CONTRACT_MESSAGES } from '../src/diagnostics';
5
- import { buildEventBindingCodeActions } from '../src/code-actions';
6
- import { DEFAULT_SETTINGS, normalizeSettings } from '../src/settings';
7
-
8
- const PROJECT_ROOT = '/tmp/zenith-site';
9
-
10
- function doc(uri: string, content: string) {
11
- return {
12
- uri,
13
- getText() {
14
- return content;
15
- },
16
- positionAt(offset: number) {
17
- const bounded = Math.max(0, Math.min(offset, content.length));
18
- const before = content.slice(0, bounded);
19
- const lines = before.split('\n');
20
- return {
21
- line: lines.length - 1,
22
- character: lines[lines.length - 1]?.length || 0
23
- };
24
- }
25
- };
26
- }
27
-
28
- test('component script contract is enforced for components when mode=forbid', () => {
29
- const document = doc(
30
- 'file:///tmp/zenith-site/src/components/Hero.zen',
31
- '<section><script>const x = 1;</script><h1>Hero</h1></section>'
32
- );
33
-
34
- const diagnostics = collectContractDiagnostics(document, null, DEFAULT_SETTINGS, PROJECT_ROOT);
35
- const messageSet = diagnostics.map((item) => item.message);
36
- assert.ok(messageSet.includes(CONTRACT_MESSAGES.componentScript));
37
- });
38
-
39
- test('component script contract allows scripts when mode=allow', () => {
40
- const document = doc(
41
- 'file:///tmp/zenith-site/src/components/Hero.zen',
42
- '<section><script>const x = 1;</script><h1>Hero</h1></section>'
43
- );
44
-
45
- const diagnostics = collectContractDiagnostics(document, null, { componentScripts: 'allow' }, PROJECT_ROOT);
46
- const messageSet = diagnostics.map((item) => item.message);
47
- assert.ok(!messageSet.includes(CONTRACT_MESSAGES.componentScript));
48
- });
49
-
50
- test('route scripts are allowed by component script contract', () => {
51
- const document = doc(
52
- 'file:///tmp/zenith-site/src/pages/index.zen',
53
- '<RootLayout><script>const x = 1;</script><h1>Home</h1></RootLayout>'
54
- );
55
-
56
- const diagnostics = collectContractDiagnostics(document, null, DEFAULT_SETTINGS, PROJECT_ROOT);
57
- const messageSet = diagnostics.map((item) => item.message);
58
- assert.ok(!messageSet.includes(CONTRACT_MESSAGES.componentScript));
59
- });
60
-
61
- test('event binding diagnostics flag onclick and @click and provide quick fixes', () => {
62
- const document = doc(
63
- 'file:///tmp/zenith-site/src/pages/index.zen',
64
- '<button onclick="submitForm">Save</button><button @click={submitForm}>Save</button>'
65
- );
66
-
67
- const diagnostics = collectContractDiagnostics(document, null, DEFAULT_SETTINGS, PROJECT_ROOT)
68
- .filter((item) => String(item.code || '') === 'zenith.event.binding.syntax');
69
-
70
- assert.equal(diagnostics.length, 2);
71
- assert.equal(diagnostics[0]?.data?.replacement, 'on:click={submitForm}');
72
- assert.equal(diagnostics[1]?.data?.replacement, 'on:click={submitForm}');
73
-
74
- const actions = buildEventBindingCodeActions(document, diagnostics);
75
- assert.equal(actions.length, 2);
76
- assert.equal(actions[0]?.title, 'Convert to on:click={submitForm}');
77
- });
78
-
79
- test('css import contract flags bare imports and path escapes', () => {
80
- const document = doc(
81
- 'file:///tmp/zenith-site/src/pages/index.zen',
82
- '<RootLayout><script>import \"tailwindcss\"; import \"../../../../outside.css\";</script></RootLayout>'
83
- );
84
-
85
- const diagnostics = collectContractDiagnostics(document, null, DEFAULT_SETTINGS, PROJECT_ROOT);
86
- const messages = diagnostics.map((item) => item.message);
87
- assert.ok(messages.includes(CONTRACT_MESSAGES.cssBareImport));
88
- assert.ok(messages.includes(CONTRACT_MESSAGES.cssEscape));
89
- });
90
-
91
- test('css import contract allows local precompiled css with suffixes', () => {
92
- const document = doc(
93
- 'file:///tmp/zenith-site/src/pages/index.zen',
94
- '<RootLayout><script>import \"../styles/output.css?v=1#hash\";</script></RootLayout>'
95
- );
96
-
97
- const diagnostics = collectContractDiagnostics(document, null, DEFAULT_SETTINGS, PROJECT_ROOT);
98
- const messages = diagnostics.map((item) => item.message);
99
- assert.ok(!messages.includes(CONTRACT_MESSAGES.cssBareImport));
100
- assert.ok(!messages.includes(CONTRACT_MESSAGES.cssEscape));
101
- });
102
-
103
- test('ZEN-DOM-QUERY diagnostic appears for querySelector and severity maps with strictDomLints', async () => {
104
- const document = doc(
105
- 'file:///tmp/zenith-site/src/pages/index.zen',
106
- '<script lang="ts">\nconst el = document.querySelector(".foo");\n</script>\n<div class="foo">hi</div>'
107
- );
108
-
109
- const settingsDefault = normalizeSettings({ strictDomLints: false });
110
- const diagnosticsDefault = await collectDiagnostics(document, null, settingsDefault, PROJECT_ROOT);
111
- const queryDefault = diagnosticsDefault.filter((d) => d.code === 'ZEN-DOM-QUERY');
112
- assert.ok(queryDefault.length >= 1, `expected ZEN-DOM-QUERY diagnostic, got: ${JSON.stringify(diagnosticsDefault.map((d) => d.code))}`);
113
- assert.equal(queryDefault[0]?.severity, 2, 'ZEN-DOM-QUERY should be Warning (2) when strictDomLints=false');
114
-
115
- const settingsStrict = normalizeSettings({ strictDomLints: true });
116
- const diagnosticsStrict = await collectDiagnostics(document, null, settingsStrict, PROJECT_ROOT);
117
- const queryStrict = diagnosticsStrict.filter((d) => d.code === 'ZEN-DOM-QUERY');
118
- assert.ok(queryStrict.length >= 1, `expected ZEN-DOM-QUERY diagnostic in strict mode, got: ${JSON.stringify(diagnosticsStrict.map((d) => d.code))}`);
119
- assert.equal(queryStrict[0]?.severity, 1, 'ZEN-DOM-QUERY should be Error (1) when strictDomLints=true');
120
- });
@@ -1,77 +0,0 @@
1
- <!--
2
- Test Fixture: Content Plugin
3
-
4
- This file tests Zenith features WITH content plugin imported.
5
- The LSP should provide content-aware completions and soft warnings.
6
- -->
7
- <script>
8
- import { zenEffect, zenOnMount } from 'zenith'
9
- import { zenCollection, getCollection, getEntry } from 'zenith:content'
10
-
11
- state posts = []
12
- state currentPost = null
13
-
14
- // Define a content collection
15
- const blogCollection = zenCollection({
16
- name: 'blog',
17
- schema: {
18
- title: 'string',
19
- date: 'date',
20
- author: 'string',
21
- tags: 'array'
22
- }
23
- })
24
-
25
- zenOnMount(async () => {
26
- // Get all posts from the collection
27
- posts = await getCollection('blog')
28
- })
29
-
30
- async function loadPost(slug) {
31
- currentPost = await getEntry('blog', slug)
32
- }
33
- </script>
34
-
35
- <div class="blog-list">
36
- <h1>Blog Posts</h1>
37
-
38
- <!-- List all posts -->
39
- <ul zen:if="posts.length > 0">
40
- <li zen:for="post in posts">
41
- <h2>{post.data.title}</h2>
42
- <p>By {post.data.author} on {post.data.date}</p>
43
- <button onclick={() => loadPost(post.slug)}>Read More</button>
44
- </li>
45
- </ul>
46
-
47
- <p zen:if="posts.length === 0">Loading posts...</p>
48
-
49
- <!-- Current post display -->
50
- <article zen:if="currentPost">
51
- <h1>{currentPost.data.title}</h1>
52
- <div class="meta">
53
- <span>By {currentPost.data.author}</span>
54
- <span>{currentPost.data.date}</span>
55
- </div>
56
- <div class="content">
57
- {currentPost.content}
58
- </div>
59
- </article>
60
- </div>
61
-
62
- <style>
63
- .blog-list {
64
- max-width: 800px;
65
- margin: 0 auto;
66
- padding: 2rem;
67
- }
68
- article {
69
- margin-top: 2rem;
70
- padding-top: 2rem;
71
- border-top: 1px solid #eee;
72
- }
73
- .meta {
74
- color: #666;
75
- margin-bottom: 1rem;
76
- }
77
- </style>
@@ -1,59 +0,0 @@
1
- <!--
2
- Test Fixture: Core Only
3
-
4
- This file tests core Zenith features WITHOUT router or plugins.
5
- The LSP should work correctly with just core functionality.
6
- -->
7
- <script>
8
- import { zenEffect, zenOnMount } from 'zenith'
9
-
10
- state count = 0
11
- state message = "Hello, Zenith!"
12
-
13
- function increment() {
14
- count++
15
- }
16
-
17
- zenOnMount(() => {
18
- console.log('Component mounted')
19
- })
20
-
21
- zenEffect(() => {
22
- console.log('Count changed:', count)
23
- })
24
- </script>
25
-
26
- <div class="container">
27
- <h1>{message}</h1>
28
-
29
- <!-- zen:if directive -->
30
- <p zen:if="count > 0">Count is positive: {count}</p>
31
-
32
- <!-- zen:for directive -->
33
- <ul>
34
- <li zen:for="item in ['a', 'b', 'c']">{item}</li>
35
- </ul>
36
-
37
- <!-- zen:show directive -->
38
- <span zen:show="count !== 0">Not zero!</span>
39
-
40
- <!-- Event handlers -->
41
- <button onclick={increment}>Increment</button>
42
- <button @click={increment}>Also Increment</button>
43
-
44
- <!-- Reactive bindings -->
45
- <input :value="message" />
46
- <div :class="count > 0 ? 'positive' : 'zero'">{count}</div>
47
- </div>
48
-
49
- <style>
50
- .container {
51
- padding: 1rem;
52
- }
53
- .positive {
54
- color: green;
55
- }
56
- .zero {
57
- color: gray;
58
- }
59
- </style>
@@ -1,115 +0,0 @@
1
- <!--
2
- Test Fixture: No Plugins
3
-
4
- This file tests behavior when NO external imports are used.
5
- The LSP should work with just vanilla Zenith features.
6
-
7
- Validation rules:
8
- - Removing router should not crash the IDE
9
- - Missing plugin imports should show soft warnings
10
- - Invalid syntax should be surfaced immediately
11
- - IDE should never suggest unavailable APIs
12
- -->
13
- <script>
14
- // No imports - just vanilla state and lifecycle
15
- state items = ['Apple', 'Banana', 'Cherry']
16
- state selectedIndex = 0
17
- state filter = ''
18
-
19
- function selectItem(index) {
20
- selectedIndex = index
21
- }
22
-
23
- function addItem() {
24
- items = [...items, 'New Item']
25
- }
26
-
27
- function removeItem(index) {
28
- items = items.filter((_, i) => i !== index)
29
- }
30
-
31
- // Computed-like pattern
32
- function getFilteredItems() {
33
- if (!filter) return items
34
- return items.filter(item =>
35
- item.toLowerCase().includes(filter.toLowerCase())
36
- )
37
- }
38
- </script>
39
-
40
- <div class="app">
41
- <header>
42
- <h1>Simple List App</h1>
43
- <p>Selected: {items[selectedIndex] || 'None'}</p>
44
- </header>
45
-
46
- <div class="controls">
47
- <input
48
- type="text"
49
- placeholder="Filter items..."
50
- :value="filter"
51
- oninput={(e) => filter = e.target.value}
52
- />
53
- <button onclick={addItem}>Add Item</button>
54
- </div>
55
-
56
- <ul class="item-list">
57
- <li
58
- zen:for="item, index in getFilteredItems()"
59
- :class="index === selectedIndex ? 'selected' : ''"
60
- >
61
- <span onclick={() => selectItem(index)}>{item}</span>
62
- <button onclick={() => removeItem(index)}>Remove</button>
63
- </li>
64
- </ul>
65
-
66
- <p zen:if="getFilteredItems().length === 0">
67
- No items match the filter.
68
- </p>
69
-
70
- <footer>
71
- <p>Total items: {items.length}</p>
72
- <p zen:show="filter">Filtered: {getFilteredItems().length}</p>
73
- </footer>
74
- </div>
75
-
76
- <style>
77
- .app {
78
- max-width: 600px;
79
- margin: 0 auto;
80
- padding: 1rem;
81
- }
82
- header {
83
- margin-bottom: 1rem;
84
- }
85
- .controls {
86
- display: flex;
87
- gap: 0.5rem;
88
- margin-bottom: 1rem;
89
- }
90
- .controls input {
91
- flex: 1;
92
- padding: 0.5rem;
93
- }
94
- .item-list {
95
- list-style: none;
96
- padding: 0;
97
- }
98
- .item-list li {
99
- display: flex;
100
- justify-content: space-between;
101
- padding: 0.5rem;
102
- border-bottom: 1px solid #eee;
103
- cursor: pointer;
104
- }
105
- .item-list li.selected {
106
- background: #e0f0ff;
107
- }
108
- .item-list li:hover {
109
- background: #f5f5f5;
110
- }
111
- footer {
112
- margin-top: 1rem;
113
- color: #666;
114
- }
115
- </style>
@@ -1,76 +0,0 @@
1
- <!--
2
- Test Fixture: Router Enabled
3
-
4
- This file tests Zenith features WITH router imported.
5
- The LSP should provide router-aware completions and hovers.
6
- -->
7
- <script>
8
- import { zenEffect, zenOnMount } from 'zenith'
9
- import { ZenLink, useRoute, useRouter, navigate, prefetch } from 'zenith/router'
10
-
11
- state activeSection = 'home'
12
-
13
- // Router hooks - should have hover info
14
- const route = useRoute()
15
- const router = useRouter()
16
-
17
- zenOnMount(() => {
18
- // Prefetch common routes
19
- prefetch('/about')
20
- prefetch('/blog')
21
- })
22
-
23
- zenEffect(() => {
24
- // React to route changes
25
- activeSection = route.path.split('/')[1] || 'home'
26
- })
27
-
28
- function goToAbout() {
29
- navigate('/about')
30
- }
31
-
32
- function goBack() {
33
- router.back()
34
- }
35
- </script>
36
-
37
- <nav>
38
- <!-- ZenLink components - should have special highlighting and props -->
39
- <ZenLink to="/">Home</ZenLink>
40
- <ZenLink to="/about" preload>About</ZenLink>
41
- <ZenLink to="/blog" preload replace>Blog</ZenLink>
42
-
43
- <!-- Dynamic route params -->
44
- <ZenLink to="/blog/{route.params.slug}">Current Post</ZenLink>
45
- </nav>
46
-
47
- <main>
48
- <h1>Current path: {route.path}</h1>
49
-
50
- <!-- Route params and query -->
51
- <p zen:if="route.params.id">Viewing item: {route.params.id}</p>
52
- <p zen:if="route.query.search">Searching: {route.query.search}</p>
53
-
54
- <div class="actions">
55
- <button onclick={goToAbout}>Go to About</button>
56
- <button onclick={goBack}>Go Back</button>
57
- </div>
58
-
59
- <!-- Slot for page content -->
60
- <slot />
61
- </main>
62
-
63
- <style>
64
- nav {
65
- display: flex;
66
- gap: 1rem;
67
- padding: 1rem;
68
- background: #f0f0f0;
69
- }
70
- main {
71
- padding: 2rem;
72
- }
73
- .actions {
74
- margin-top: 1rem;
75
- }
76
- </style>
@@ -1,44 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import fs from 'node:fs';
4
- import os from 'node:os';
5
- import path from 'node:path';
6
-
7
- import { detectProjectRoot } from '../src/project';
8
-
9
- function createTempDir(prefix: string): string {
10
- return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
11
- }
12
-
13
- test('detectProjectRoot prefers nearest zenith.config.*', () => {
14
- const root = createTempDir('zenith-lsp-root-');
15
- const nested = path.join(root, 'apps', 'site', 'src', 'pages');
16
- fs.mkdirSync(nested, { recursive: true });
17
- fs.writeFileSync(path.join(root, 'zenith.config.ts'), 'export default {}\n');
18
-
19
- const detected = detectProjectRoot(path.join(nested, 'index.zen'));
20
- assert.equal(detected, root);
21
- });
22
-
23
- test('detectProjectRoot prefers nearest package.json with @zenithbuild/cli', () => {
24
- const root = createTempDir('zenith-lsp-pkg-');
25
- const nested = path.join(root, 'src', 'components');
26
- fs.mkdirSync(nested, { recursive: true });
27
- fs.writeFileSync(
28
- path.join(root, 'package.json'),
29
- JSON.stringify({ dependencies: { '@zenithbuild/cli': '^1.0.0' } }, null, 2)
30
- );
31
-
32
- const detected = detectProjectRoot(path.join(nested, 'Hero.zen'));
33
- assert.equal(detected, root);
34
- });
35
-
36
- test('detectProjectRoot falls back to matching workspace folder structure', () => {
37
- const workspace = createTempDir('zenith-lsp-workspace-');
38
- const siteRoot = path.join(workspace, 'site-a');
39
- const nested = path.join(siteRoot, 'src', 'pages', 'blog');
40
- fs.mkdirSync(nested, { recursive: true });
41
-
42
- const detected = detectProjectRoot(path.join(nested, 'first-post.zen'), [workspace, siteRoot]);
43
- assert.equal(detected, siteRoot);
44
- });
package/tsconfig.json DELETED
@@ -1,25 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "commonjs",
5
- "lib": [
6
- "ES2022"
7
- ],
8
- "outDir": "./dist",
9
- "rootDir": "./src",
10
- "strict": true,
11
- "esModuleInterop": true,
12
- "skipLibCheck": true,
13
- "forceConsistentCasingInFileNames": true,
14
- "declaration": true,
15
- "resolveJsonModule": true,
16
- "moduleResolution": "node"
17
- },
18
- "include": [
19
- "src/**/*"
20
- ],
21
- "exclude": [
22
- "node_modules",
23
- "dist"
24
- ]
25
- }
@@ -1,25 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "./.test-dist",
5
- "rootDir": ".",
6
- "declaration": false,
7
- "sourceMap": false
8
- },
9
- "include": [
10
- "src/contracts.ts",
11
- "src/project.ts",
12
- "src/settings.ts",
13
- "src/diagnostics.ts",
14
- "src/code-actions.ts",
15
- "src/imports.ts",
16
- "src/metadata/**/*.ts",
17
- "src/**/*.d.ts",
18
- "test/**/*.ts"
19
- ],
20
- "exclude": [
21
- "node_modules",
22
- "dist",
23
- ".test-dist"
24
- ]
25
- }