@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/README.md +8 -39
- package/bin/zenith-language-server.js +2 -0
- package/dist/server.mjs +597 -0
- package/package.json +37 -26
- package/.github/workflows/release.yml +0 -261
- package/.releaserc.json +0 -73
- package/CHANGELOG.md +0 -59
- package/RELEASE_NOTES.md +0 -33
- package/RELEASE_NOTES_v0.6.0.md +0 -39
- package/scripts/release.ts +0 -571
- package/src/code-actions.ts +0 -219
- package/src/contracts.ts +0 -100
- package/src/diagnostics.ts +0 -603
- package/src/imports.ts +0 -207
- package/src/metadata/core-imports.ts +0 -163
- package/src/metadata/directive-metadata.ts +0 -109
- package/src/metadata/plugin-imports.ts +0 -116
- package/src/project.ts +0 -283
- package/src/router.ts +0 -180
- package/src/server.ts +0 -937
- package/src/settings.ts +0 -18
- package/src/types/zenith-compiler.d.ts +0 -3
- package/test/contracts.spec.ts +0 -37
- package/test/diagnostics.spec.ts +0 -120
- package/test/fixtures/content-plugin.zen +0 -77
- package/test/fixtures/core-only.zen +0 -59
- package/test/fixtures/no-plugins.zen +0 -115
- package/test/fixtures/router-enabled.zen +0 -76
- package/test/project-root.spec.ts +0 -44
- package/tsconfig.json +0 -25
- package/tsconfig.test.json +0 -25
package/src/project.ts
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Zenith Project Graph
|
|
3
|
-
*
|
|
4
|
-
* Uses the compiler's discovery logic to build a project graph
|
|
5
|
-
* Ensures LSP understands the same structure as the compiler
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as fs from 'fs';
|
|
9
|
-
import * as path from 'path';
|
|
10
|
-
|
|
11
|
-
export interface ComponentInfo {
|
|
12
|
-
name: string;
|
|
13
|
-
filePath: string;
|
|
14
|
-
type: 'layout' | 'component' | 'page';
|
|
15
|
-
props: string[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface ProjectGraph {
|
|
19
|
-
root: string;
|
|
20
|
-
layouts: Map<string, ComponentInfo>;
|
|
21
|
-
components: Map<string, ComponentInfo>;
|
|
22
|
-
pages: Map<string, ComponentInfo>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const ZENITH_CONFIG_CANDIDATES = [
|
|
26
|
-
'zenith.config.ts',
|
|
27
|
-
'zenith.config.js',
|
|
28
|
-
'zenith.config.mjs',
|
|
29
|
-
'zenith.config.cjs',
|
|
30
|
-
'zenith.config.json'
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
function hasZenithConfig(dir: string): boolean {
|
|
34
|
-
return ZENITH_CONFIG_CANDIDATES.some((fileName) => fs.existsSync(path.join(dir, fileName)));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function hasZenithCliDependency(dir: string): boolean {
|
|
38
|
-
const packageJsonPath = path.join(dir, 'package.json');
|
|
39
|
-
if (!fs.existsSync(packageJsonPath)) {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const raw = fs.readFileSync(packageJsonPath, 'utf-8');
|
|
45
|
-
const pkg = JSON.parse(raw) as {
|
|
46
|
-
dependencies?: Record<string, string>;
|
|
47
|
-
devDependencies?: Record<string, string>;
|
|
48
|
-
peerDependencies?: Record<string, string>;
|
|
49
|
-
optionalDependencies?: Record<string, string>;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const deps = [
|
|
53
|
-
pkg.dependencies || {},
|
|
54
|
-
pkg.devDependencies || {},
|
|
55
|
-
pkg.peerDependencies || {},
|
|
56
|
-
pkg.optionalDependencies || {}
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
return deps.some((group) => Object.prototype.hasOwnProperty.call(group, '@zenithbuild/cli'));
|
|
60
|
-
} catch {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function hasZenithStructure(dir: string): boolean {
|
|
66
|
-
const srcDir = path.join(dir, 'src');
|
|
67
|
-
if (fs.existsSync(srcDir)) {
|
|
68
|
-
const hasPages = fs.existsSync(path.join(srcDir, 'pages'));
|
|
69
|
-
const hasLayouts = fs.existsSync(path.join(srcDir, 'layouts'));
|
|
70
|
-
if (hasPages || hasLayouts) {
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const appDir = path.join(dir, 'app');
|
|
76
|
-
if (fs.existsSync(appDir)) {
|
|
77
|
-
const hasPages = fs.existsSync(path.join(appDir, 'pages'));
|
|
78
|
-
const hasLayouts = fs.existsSync(path.join(appDir, 'layouts'));
|
|
79
|
-
if (hasPages || hasLayouts) {
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function findNearestByRule(startPath: string, predicate: (dir: string) => boolean): string | null {
|
|
88
|
-
let current = path.resolve(startPath);
|
|
89
|
-
if (!fs.existsSync(current)) {
|
|
90
|
-
current = path.dirname(current);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
while (!fs.existsSync(current) && current !== path.dirname(current)) {
|
|
94
|
-
current = path.dirname(current);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (!fs.existsSync(current)) {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (!fs.statSync(current).isDirectory()) {
|
|
102
|
-
current = path.dirname(current);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
while (current !== path.dirname(current)) {
|
|
106
|
-
if (predicate(current)) {
|
|
107
|
-
return current;
|
|
108
|
-
}
|
|
109
|
-
current = path.dirname(current);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (predicate(current)) {
|
|
113
|
-
return current;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function findFallbackRoot(startPath: string): string | null {
|
|
120
|
-
return findNearestByRule(startPath, (dir) => {
|
|
121
|
-
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
if (hasZenithStructure(dir)) {
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
return false;
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Detect Zenith project root
|
|
133
|
-
* Priority:
|
|
134
|
-
* 1) nearest zenith.config.*
|
|
135
|
-
* 2) nearest package.json with @zenithbuild/cli
|
|
136
|
-
* 3) nearest Zenith structure (src/pages|layouts or app/pages|layouts)
|
|
137
|
-
* 4) workspace folder fallbacks (if provided)
|
|
138
|
-
* 5) nearest package.json or Zenith structure
|
|
139
|
-
*/
|
|
140
|
-
export function detectProjectRoot(startPath: string, workspaceFolders: string[] = []): string | null {
|
|
141
|
-
const localConfigRoot = findNearestByRule(startPath, hasZenithConfig);
|
|
142
|
-
if (localConfigRoot) {
|
|
143
|
-
return localConfigRoot;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const localCliRoot = findNearestByRule(startPath, hasZenithCliDependency);
|
|
147
|
-
if (localCliRoot) {
|
|
148
|
-
return localCliRoot;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const localStructureRoot = findNearestByRule(startPath, hasZenithStructure);
|
|
152
|
-
if (localStructureRoot) {
|
|
153
|
-
return localStructureRoot;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const absoluteStart = path.resolve(startPath);
|
|
157
|
-
const matchingWorkspaceFolders = workspaceFolders
|
|
158
|
-
.map((workspacePath) => path.resolve(workspacePath))
|
|
159
|
-
.filter((workspacePath) => absoluteStart === workspacePath || absoluteStart.startsWith(`${workspacePath}${path.sep}`))
|
|
160
|
-
.sort((a, b) => b.length - a.length);
|
|
161
|
-
|
|
162
|
-
for (const workspaceRoot of matchingWorkspaceFolders) {
|
|
163
|
-
if (hasZenithConfig(workspaceRoot)) {
|
|
164
|
-
return workspaceRoot;
|
|
165
|
-
}
|
|
166
|
-
if (hasZenithCliDependency(workspaceRoot)) {
|
|
167
|
-
return workspaceRoot;
|
|
168
|
-
}
|
|
169
|
-
if (hasZenithStructure(workspaceRoot)) {
|
|
170
|
-
return workspaceRoot;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return findFallbackRoot(startPath);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Extract props from a .zen file
|
|
179
|
-
* Infers props from usage patterns (Astro/Vue style)
|
|
180
|
-
*/
|
|
181
|
-
function extractPropsFromFile(filePath: string): string[] {
|
|
182
|
-
try {
|
|
183
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
184
|
-
const props: string[] = [];
|
|
185
|
-
|
|
186
|
-
// Look for Props interface/type
|
|
187
|
-
const propsMatch = content.match(/(?:interface|type)\s+Props\s*[={]\s*\{([^}]+)\}/);
|
|
188
|
-
if (propsMatch && propsMatch[1]) {
|
|
189
|
-
const propNames = propsMatch[1].match(/([a-zA-Z_$][a-zA-Z0-9_$?]*)\s*[?:]?\s*:/g);
|
|
190
|
-
if (propNames) {
|
|
191
|
-
for (const p of propNames) {
|
|
192
|
-
const name = p.replace(/[?:\s]/g, '');
|
|
193
|
-
if (name && !props.includes(name)) {
|
|
194
|
-
props.push(name);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Look for common prop patterns in expressions
|
|
201
|
-
const usagePatterns = content.matchAll(/\{(title|lang|className|children|href|src|alt|id|name)\}/g);
|
|
202
|
-
for (const match of usagePatterns) {
|
|
203
|
-
if (match[1] && !props.includes(match[1])) {
|
|
204
|
-
props.push(match[1]);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return props;
|
|
209
|
-
} catch {
|
|
210
|
-
return [];
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Discover all .zen files in a directory
|
|
216
|
-
*/
|
|
217
|
-
function discoverZenFiles(dir: string, type: 'layout' | 'component' | 'page'): Map<string, ComponentInfo> {
|
|
218
|
-
const result = new Map<string, ComponentInfo>();
|
|
219
|
-
|
|
220
|
-
if (!fs.existsSync(dir)) {
|
|
221
|
-
return result;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function scanDir(currentDir: string) {
|
|
225
|
-
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
226
|
-
|
|
227
|
-
for (const entry of entries) {
|
|
228
|
-
const fullPath = path.join(currentDir, entry.name);
|
|
229
|
-
|
|
230
|
-
if (entry.isDirectory()) {
|
|
231
|
-
scanDir(fullPath);
|
|
232
|
-
} else if (entry.name.endsWith('.zen')) {
|
|
233
|
-
const name = path.basename(entry.name, '.zen');
|
|
234
|
-
const props = extractPropsFromFile(fullPath);
|
|
235
|
-
|
|
236
|
-
result.set(name, {
|
|
237
|
-
name,
|
|
238
|
-
filePath: fullPath,
|
|
239
|
-
type,
|
|
240
|
-
props
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
scanDir(dir);
|
|
247
|
-
return result;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Build project graph from root directory
|
|
252
|
-
*/
|
|
253
|
-
export function buildProjectGraph(root: string): ProjectGraph {
|
|
254
|
-
const srcDir = fs.existsSync(path.join(root, 'src')) ? path.join(root, 'src') : path.join(root, 'app');
|
|
255
|
-
|
|
256
|
-
const layouts = discoverZenFiles(path.join(srcDir, 'layouts'), 'layout');
|
|
257
|
-
const components = discoverZenFiles(path.join(srcDir, 'components'), 'component');
|
|
258
|
-
const pages = discoverZenFiles(path.join(srcDir, 'pages'), 'page');
|
|
259
|
-
|
|
260
|
-
return {
|
|
261
|
-
root,
|
|
262
|
-
layouts,
|
|
263
|
-
components,
|
|
264
|
-
pages
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Resolve a component/layout by name
|
|
270
|
-
*/
|
|
271
|
-
export function resolveComponent(graph: ProjectGraph, name: string): ComponentInfo | undefined {
|
|
272
|
-
// Check layouts first (common pattern for <DefaultLayout>)
|
|
273
|
-
if (graph.layouts.has(name)) {
|
|
274
|
-
return graph.layouts.get(name);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Then components
|
|
278
|
-
if (graph.components.has(name)) {
|
|
279
|
-
return graph.components.get(name);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return undefined;
|
|
283
|
-
}
|
package/src/router.ts
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Router Awareness
|
|
3
|
-
*
|
|
4
|
-
* Special support for Zenith Router features.
|
|
5
|
-
* The LSP provides router-aware completions and hovers when zenith/router is imported.
|
|
6
|
-
*
|
|
7
|
-
* Important: No route file system assumptions or runtime navigation simulation.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export interface RouterHookMetadata {
|
|
11
|
-
name: string;
|
|
12
|
-
owner: string;
|
|
13
|
-
description: string;
|
|
14
|
-
restrictions: string;
|
|
15
|
-
returns: string;
|
|
16
|
-
signature: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface ZenLinkPropMetadata {
|
|
20
|
-
name: string;
|
|
21
|
-
type: string;
|
|
22
|
-
required: boolean;
|
|
23
|
-
description: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface RouteFieldMetadata {
|
|
27
|
-
name: string;
|
|
28
|
-
type: string;
|
|
29
|
-
description: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Router hook definitions
|
|
34
|
-
*/
|
|
35
|
-
export const ROUTER_HOOKS: Record<string, RouterHookMetadata> = {
|
|
36
|
-
useRoute: {
|
|
37
|
-
name: 'useRoute',
|
|
38
|
-
owner: 'Router Hook (zenith/router)',
|
|
39
|
-
description: 'Provides reactive access to the current route state.',
|
|
40
|
-
restrictions: 'Must be called at top-level script scope.',
|
|
41
|
-
returns: '{ path: string; params: Record<string, string>; query: Record<string, string> }',
|
|
42
|
-
signature: 'useRoute(): RouteState'
|
|
43
|
-
},
|
|
44
|
-
useRouter: {
|
|
45
|
-
name: 'useRouter',
|
|
46
|
-
owner: 'Router Hook (zenith/router)',
|
|
47
|
-
description: 'Provides programmatic navigation methods.',
|
|
48
|
-
restrictions: 'Must be called at top-level script scope.',
|
|
49
|
-
returns: '{ navigate, back, forward, go }',
|
|
50
|
-
signature: 'useRouter(): Router'
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* ZenLink component props
|
|
56
|
-
*/
|
|
57
|
-
export const ZENLINK_PROPS: ZenLinkPropMetadata[] = [
|
|
58
|
-
{
|
|
59
|
-
name: 'to',
|
|
60
|
-
type: 'string',
|
|
61
|
-
required: true,
|
|
62
|
-
description: 'The route path to navigate to.'
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
name: 'preload',
|
|
66
|
-
type: 'boolean',
|
|
67
|
-
required: false,
|
|
68
|
-
description: 'Whether to prefetch the route on hover.'
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
name: 'replace',
|
|
72
|
-
type: 'boolean',
|
|
73
|
-
required: false,
|
|
74
|
-
description: 'Whether to replace the current history entry instead of pushing a new one.'
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
name: 'class',
|
|
78
|
-
type: 'string',
|
|
79
|
-
required: false,
|
|
80
|
-
description: 'CSS class to apply to the link.'
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
name: 'activeClass',
|
|
84
|
-
type: 'string',
|
|
85
|
-
required: false,
|
|
86
|
-
description: 'CSS class to apply when the link is active.'
|
|
87
|
-
}
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Route state fields available from useRoute()
|
|
92
|
-
*/
|
|
93
|
-
export const ROUTE_FIELDS: RouteFieldMetadata[] = [
|
|
94
|
-
{
|
|
95
|
-
name: 'path',
|
|
96
|
-
type: 'string',
|
|
97
|
-
description: 'The current route path (e.g., "/blog/my-post").'
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
name: 'params',
|
|
101
|
-
type: 'Record<string, string>',
|
|
102
|
-
description: 'Dynamic route parameters (e.g., { slug: "my-post" }).'
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
name: 'query',
|
|
106
|
-
type: 'Record<string, string>',
|
|
107
|
-
description: 'Query string parameters (e.g., { page: "1" }).'
|
|
108
|
-
}
|
|
109
|
-
];
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Router navigation functions
|
|
113
|
-
*/
|
|
114
|
-
export const ROUTER_FUNCTIONS = [
|
|
115
|
-
{
|
|
116
|
-
name: 'navigate',
|
|
117
|
-
description: 'Navigate to a route programmatically.',
|
|
118
|
-
signature: 'navigate(to: string, options?: { replace?: boolean }): void'
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
name: 'prefetch',
|
|
122
|
-
description: 'Prefetch a route for faster navigation.',
|
|
123
|
-
signature: 'prefetch(path: string): Promise<void>'
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
name: 'isActive',
|
|
127
|
-
description: 'Check if a route is currently active.',
|
|
128
|
-
signature: 'isActive(path: string, exact?: boolean): boolean'
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
name: 'back',
|
|
132
|
-
description: 'Navigate back in history.',
|
|
133
|
-
signature: 'back(): void'
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
name: 'forward',
|
|
137
|
-
description: 'Navigate forward in history.',
|
|
138
|
-
signature: 'forward(): void'
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
name: 'go',
|
|
142
|
-
description: 'Navigate to a specific history entry.',
|
|
143
|
-
signature: 'go(delta: number): void'
|
|
144
|
-
}
|
|
145
|
-
];
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Get router hook metadata
|
|
149
|
-
*/
|
|
150
|
-
export function getRouterHook(name: string): RouterHookMetadata | undefined {
|
|
151
|
-
return ROUTER_HOOKS[name];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Check if a name is a router hook
|
|
156
|
-
*/
|
|
157
|
-
export function isRouterHook(name: string): boolean {
|
|
158
|
-
return name in ROUTER_HOOKS;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Get ZenLink prop metadata
|
|
163
|
-
*/
|
|
164
|
-
export function getZenLinkProp(name: string): ZenLinkPropMetadata | undefined {
|
|
165
|
-
return ZENLINK_PROPS.find(p => p.name === name);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Get all ZenLink prop names
|
|
170
|
-
*/
|
|
171
|
-
export function getZenLinkPropNames(): string[] {
|
|
172
|
-
return ZENLINK_PROPS.map(p => p.name);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Get route field metadata
|
|
177
|
-
*/
|
|
178
|
-
export function getRouteField(name: string): RouteFieldMetadata | undefined {
|
|
179
|
-
return ROUTE_FIELDS.find(f => f.name === name);
|
|
180
|
-
}
|