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