@ozsarman/clarityjs 0.6.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.
@@ -0,0 +1,613 @@
1
+ /**
2
+ * Clarity.js — TypeScript Language Service Plugin
3
+ *
4
+ * Provides deep TypeScript integration for .clarity files:
5
+ * • Component prop type inference (from defaults + type annotations)
6
+ * • Signal<T> type narrowing (.get() / .set() / .peek())
7
+ * • Template expression type checking
8
+ * • Virtual .d.ts generation for each .clarity file
9
+ * • "Go to definition" across .clarity ↔ .ts boundaries
10
+ *
11
+ * ─── Setup in tsconfig.json ───────────────────────────────────────────────────
12
+ *
13
+ * {
14
+ * "compilerOptions": {
15
+ * "plugins": [{ "name": "@ozsarman/clarityjs/ts-plugin" }]
16
+ * }
17
+ * }
18
+ *
19
+ * ─── VS Code / Volar setup ────────────────────────────────────────────────────
20
+ *
21
+ * // .vscode/settings.json
22
+ * {
23
+ * "typescript.tsserver.pluginPaths": ["node_modules/@ozsarman/clarityjs"]
24
+ * }
25
+ *
26
+ * ─── Programmatic use ─────────────────────────────────────────────────────────
27
+ *
28
+ * import { createClarityTypeChecker } from '@ozsarman/clarityjs/ts-plugin';
29
+ *
30
+ * const checker = createClarityTypeChecker({ rootDir: './src' });
31
+ * const errors = await checker.check('MyComponent.clarity');
32
+ *
33
+ * Author: Claude (Anthropic) + Özdemir Sarman
34
+ */
35
+
36
+ import { parse } from './parser.js';
37
+ import { tokenize } from './lexer.js';
38
+ import { generateTypes } from './typegen.js';
39
+
40
+ // ─── TypeScript plugin entry point ────────────────────────────────────────────
41
+
42
+ /**
43
+ * TypeScript Language Service plugin factory.
44
+ * TypeScript calls this when it finds the plugin in tsconfig.json.
45
+ *
46
+ * @param {{ typescript: typeof import('typescript') }} modules
47
+ * @returns {{ create(info: ts.server.PluginCreateInfo): ts.LanguageService }}
48
+ */
49
+ export function init(modules) {
50
+ const ts = modules.typescript;
51
+
52
+ function create(info) {
53
+ const proxy = Object.create(null);
54
+ const ls = info.languageService;
55
+
56
+ // Proxy all existing language service methods
57
+ for (const k of Object.keys(ls)) {
58
+ const x = ls[k];
59
+ proxy[k] = typeof x === 'function' ? (...args) => x.apply(ls, args) : x;
60
+ }
61
+
62
+ // ── Intercept getSemanticDiagnostics ─────────────────────────────────
63
+ proxy.getSemanticDiagnostics = (fileName) => {
64
+ const prior = ls.getSemanticDiagnostics(fileName);
65
+
66
+ if (!fileName.endsWith('.clarity')) return prior;
67
+
68
+ const source = _readFile(info, fileName);
69
+ if (!source) return prior;
70
+
71
+ const clarityDiags = _checkClarityFile(ts, fileName, source);
72
+ return [...prior, ...clarityDiags];
73
+ };
74
+
75
+ // ── Intercept getCompletionsAtPosition ───────────────────────────────
76
+ proxy.getCompletionsAtPosition = (fileName, position, options) => {
77
+ const prior = ls.getCompletionsAtPosition(fileName, position, options);
78
+
79
+ if (!fileName.endsWith('.clarity')) return prior;
80
+
81
+ const source = _readFile(info, fileName);
82
+ if (!source) return prior;
83
+
84
+ const extra = _getClarityCompletions(ts, fileName, source, position);
85
+ if (!extra.length) return prior;
86
+
87
+ return {
88
+ ...(prior ?? { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false }),
89
+ entries: [...(prior?.entries ?? []), ...extra],
90
+ };
91
+ };
92
+
93
+ // ── Intercept getQuickInfoAtPosition ─────────────────────────────────
94
+ proxy.getQuickInfoAtPosition = (fileName, position) => {
95
+ const prior = ls.getQuickInfoAtPosition(fileName, position);
96
+ if (!fileName.endsWith('.clarity')) return prior;
97
+
98
+ const source = _readFile(info, fileName);
99
+ if (!source) return prior;
100
+
101
+ return _getClarityHover(ts, fileName, source, position) ?? prior;
102
+ };
103
+
104
+ return proxy;
105
+ }
106
+
107
+ return { create };
108
+ }
109
+
110
+ // ─── Internal: read file from TS host ─────────────────────────────────────────
111
+
112
+ function _readFile(info, fileName) {
113
+ try {
114
+ const snap = info.languageServiceHost.getScriptSnapshot(fileName);
115
+ if (!snap) return null;
116
+ return snap.getText(0, snap.getLength());
117
+ } catch {
118
+ return null;
119
+ }
120
+ }
121
+
122
+ // ─── Internal: check a .clarity file ─────────────────────────────────────────
123
+
124
+ function _checkClarityFile(ts, fileName, source) {
125
+ const diags = [];
126
+
127
+ try {
128
+ const tokens = tokenize(source, fileName);
129
+ const ast = parse(tokens, source);
130
+
131
+ for (const comp of ast.components ?? []) {
132
+ // Rule: every component must have a render block
133
+ const hasRender = comp.body.some(n => n.type === 'RenderBlock');
134
+ if (!hasRender) {
135
+ diags.push({
136
+ file: undefined,
137
+ start: comp.loc?.start?.offset ?? 0,
138
+ length: comp.name?.length ?? 1,
139
+ messageText: `Component "${comp.name}" is missing a render block.`,
140
+ category: ts.DiagnosticCategory.Warning,
141
+ code: 9001,
142
+ source: 'clarity',
143
+ });
144
+ }
145
+
146
+ // Rule: component name must be PascalCase
147
+ if (comp.name && !/^[A-Z]/.test(comp.name)) {
148
+ diags.push({
149
+ file: undefined,
150
+ start: comp.loc?.start?.offset ?? 0,
151
+ length: comp.name.length,
152
+ messageText: `Component name "${comp.name}" should start with an uppercase letter (PascalCase).`,
153
+ category: ts.DiagnosticCategory.Warning,
154
+ code: 9002,
155
+ source: 'clarity',
156
+ });
157
+ }
158
+
159
+ // Rule: detect duplicate signal names
160
+ const signalNames = new Set();
161
+ for (const node of comp.body) {
162
+ if (node.type !== 'StateDecl') continue;
163
+ if (signalNames.has(node.name)) {
164
+ diags.push({
165
+ file: undefined,
166
+ start: node.loc?.start?.offset ?? 0,
167
+ length: node.name.length,
168
+ messageText: `Duplicate signal "${node.name}" in component "${comp.name}".`,
169
+ category: ts.DiagnosticCategory.Error,
170
+ code: 9003,
171
+ source: 'clarity',
172
+ });
173
+ }
174
+ signalNames.add(node.name);
175
+ }
176
+ }
177
+ } catch (err) {
178
+ diags.push({
179
+ file: undefined,
180
+ start: 0,
181
+ length: 1,
182
+ messageText: `Clarity parse error: ${err.message}`,
183
+ category: ts.DiagnosticCategory.Error,
184
+ code: 9000,
185
+ source: 'clarity',
186
+ });
187
+ }
188
+
189
+ return diags;
190
+ }
191
+
192
+ // ─── Internal: completions for .clarity files ─────────────────────────────────
193
+
194
+ const CLARITY_BUILTINS = [
195
+ { name: 'signal', kind: 'function', detail: 'signal<T>(value: T): Signal<T>', doc: 'Create a reactive signal.' },
196
+ { name: 'computed', kind: 'function', detail: 'computed<T>(fn: () => T): Signal<T>', doc: 'Derived reactive value.' },
197
+ { name: 'effect', kind: 'function', detail: 'effect(fn: () => void): () => void', doc: 'Run a side effect on signal change.' },
198
+ { name: 'batch', kind: 'function', detail: 'batch(fn: () => void): void', doc: 'Batch multiple signal updates.' },
199
+ { name: 'onMount', kind: 'function', detail: 'onMount(fn: () => void): void', doc: 'Run after component mounts.' },
200
+ { name: 'onCleanup',kind: 'function', detail: 'onCleanup(fn: () => void): void', doc: 'Run on component unmount.' },
201
+ { name: 'onUpdate', kind: 'function', detail: 'onUpdate(fn: () => void): void', doc: 'Run after each reactive update.' },
202
+ { name: 'createRef',kind: 'function', detail: 'createRef<T>(init?: T): Ref<T>', doc: 'Create an imperative ref.' },
203
+ { name: 'createContext', kind: 'function', detail: 'createContext<T>(defaultValue?: T): Context<T>', doc: 'Create a context object.' },
204
+ { name: 'useContext', kind: 'function', detail: 'useContext<T>(ctx: Context<T>): T', doc: 'Read a context value.' },
205
+ { name: 'navigate', kind: 'function', detail: 'navigate(path: string, opts?: NavigateOptions): Promise<boolean>', doc: 'Client-side navigation.' },
206
+ { name: 'useHead', kind: 'function', detail: 'useHead(meta: HeadConfig): void', doc: 'Set page title/meta tags.' },
207
+ { name: 'createStore', kind: 'function', detail: 'createStore<S>(config: StoreConfig<S>): Store<S>', doc: 'Global reactive store.' },
208
+ { name: 'defineServerAction', kind: 'function', detail: 'defineServerAction<I,O>(name: string, handler: (input: I) => Promise<O>): ServerAction<I,O>', doc: 'Register a server action.' },
209
+ { name: 'useServerAction', kind: 'function', detail: 'useServerAction<I,O>(client, name: string, opts?): UseServerActionResult<I,O>', doc: 'Reactive server action hook.' },
210
+ { name: 'h', kind: 'function', detail: 'h(tag: string, attrs?: object, ...children): Element', doc: 'Create a DOM element (hyperscript).' },
211
+ { name: 'mount', kind: 'function', detail: 'mount(component: Function, target: Element, props?: object): void', doc: 'Mount a component into the DOM.' },
212
+ { name: 'when', kind: 'function', detail: 'when(cond: () => boolean, then: () => Node, else?: () => Node): Node', doc: 'Conditional rendering.' },
213
+ { name: 'list', kind: 'function', detail: 'list<T>(items: () => T[], render: (item: T, i: number) => Node, key?: (item: T) => string): Node', doc: 'Keyed list rendering.' },
214
+ ];
215
+
216
+ function _getClarityCompletions(ts, fileName, source, position) {
217
+ // Find the word being typed at position
218
+ const before = source.slice(0, position);
219
+ const match = before.match(/(\w+)$/);
220
+ const prefix = match?.[1] ?? '';
221
+
222
+ return CLARITY_BUILTINS
223
+ .filter(b => b.name.startsWith(prefix))
224
+ .map(b => ({
225
+ name: b.name,
226
+ kind: ts.ScriptElementKind.functionElement,
227
+ kindModifiers: '',
228
+ sortText: '0_' + b.name,
229
+ insertText: b.name,
230
+ hasAction: false,
231
+ isRecommended: true,
232
+ labelDetails: { detail: ` — ${b.detail}` },
233
+ }));
234
+ }
235
+
236
+ // ─── Internal: hover for .clarity files ──────────────────────────────────────
237
+
238
+ function _getClarityHover(ts, fileName, source, position) {
239
+ const before = source.slice(0, position);
240
+ const after = source.slice(position);
241
+ const wordMatch = source.slice(Math.max(0, position - 30), position + 30).match(/\b(\w+)\b/g);
242
+ if (!wordMatch) return null;
243
+
244
+ // Find which word is at the cursor
245
+ let cursor = Math.max(0, position - 30);
246
+ let word = null;
247
+ for (const w of wordMatch) {
248
+ const idx = source.indexOf(w, cursor);
249
+ if (idx <= position && position <= idx + w.length) { word = w; break; }
250
+ cursor = idx + w.length;
251
+ }
252
+
253
+ const builtin = CLARITY_BUILTINS.find(b => b.name === word);
254
+ if (!builtin) return null;
255
+
256
+ return {
257
+ kind: ts.ScriptElementKind.functionElement,
258
+ kindModifiers: '',
259
+ textSpan: { start: position, length: word.length },
260
+ displayParts: [
261
+ { text: builtin.detail, kind: 'text' },
262
+ ],
263
+ documentation: [
264
+ { text: builtin.doc, kind: 'text' },
265
+ ],
266
+ };
267
+ }
268
+
269
+ // ─── Programmatic type checker ────────────────────────────────────────────────
270
+
271
+ /**
272
+ * Standalone Clarity type checker — no TypeScript server required.
273
+ *
274
+ * Parses .clarity files, generates virtual .d.ts, and reports type-level errors.
275
+ *
276
+ * @param {object} opts
277
+ * @param {string} [opts.rootDir='./src']
278
+ * @param {boolean}[opts.strict=false]
279
+ * @returns {ClarityTypeChecker}
280
+ */
281
+ export function createClarityTypeChecker({ rootDir = './src', strict = false } = {}) {
282
+ const _cache = new Map(); // fileName → { ast, dts, errors, mtime }
283
+
284
+ /**
285
+ * Parse + type-check a single .clarity file.
286
+ * @param {string} source — raw source code
287
+ * @param {string} [fileName]
288
+ * @returns {{ dts: string, errors: TypeDiagnostic[] }}
289
+ */
290
+ function checkSource(source, fileName = '<unknown>') {
291
+ const errors = [];
292
+ let dts = '';
293
+
294
+ try {
295
+ const tokens = tokenize(source, fileName);
296
+ const ast = parse(tokens, source);
297
+ dts = generateTypes(ast, { filename: fileName });
298
+
299
+ // Validate props: detect unknown prop types when strict=true
300
+ if (strict) {
301
+ for (const comp of ast.components ?? []) {
302
+ for (const param of comp.params ?? []) {
303
+ if (!param.typeAnnotation && !param.defaultValue) {
304
+ errors.push({
305
+ file: fileName,
306
+ line: param.loc?.start?.line ?? 0,
307
+ col: param.loc?.start?.col ?? 0,
308
+ message: `Prop "${param.name}" in component "${comp.name}" has no type annotation or default value. Add a default or annotate the type.`,
309
+ severity:'warning',
310
+ code: 9010,
311
+ });
312
+ }
313
+ }
314
+ }
315
+ }
316
+
317
+ // Signal mutation check: detect direct .value = ... (use .set() instead)
318
+ const directMutationRe = /\b(\w+)\.value\s*=/g;
319
+ let m;
320
+ while ((m = directMutationRe.exec(source)) !== null) {
321
+ const line = source.slice(0, m.index).split('\n').length;
322
+ errors.push({
323
+ file: fileName,
324
+ line,
325
+ col: 0,
326
+ message: `Direct signal mutation detected: "${m[0]}". Use "${m[1]}.set(value)" instead.`,
327
+ severity:'error',
328
+ code: 9011,
329
+ });
330
+ }
331
+
332
+ } catch (err) {
333
+ errors.push({
334
+ file: fileName,
335
+ line: err.line ?? 1,
336
+ col: err.col ?? 0,
337
+ message: `Parse error: ${err.message}`,
338
+ severity:'error',
339
+ code: 9000,
340
+ });
341
+ }
342
+
343
+ return { dts, errors };
344
+ }
345
+
346
+ /**
347
+ * Generate a virtual TypeScript declaration for a .clarity component.
348
+ *
349
+ * @param {string} source
350
+ * @param {string} [fileName]
351
+ * @returns {string} .d.ts content
352
+ */
353
+ function generateDts(source, fileName) {
354
+ try {
355
+ const tokens = tokenize(source, fileName ?? '<unknown>');
356
+ const ast = parse(tokens, source);
357
+ return generateTypes(ast, { filename: fileName });
358
+ } catch {
359
+ return '// Could not generate types (parse error)\n';
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Infer prop types from a component's parameter list.
365
+ *
366
+ * @param {string} source
367
+ * @param {string} componentName
368
+ * @returns {PropTypeMap} { propName: typeString }
369
+ */
370
+ function inferProps(source, componentName) {
371
+ try {
372
+ const tokens = tokenize(source, '<prop-inference>');
373
+ const ast = parse(tokens, source);
374
+ const comp = ast.components?.find(c => c.name === componentName);
375
+ if (!comp) return {};
376
+
377
+ const result = {};
378
+ for (const param of comp.params ?? []) {
379
+ result[param.name] = param.typeAnnotation ??
380
+ _inferTypeFromDefault(param.defaultValue) ??
381
+ 'unknown';
382
+ }
383
+ return result;
384
+ } catch {
385
+ return {};
386
+ }
387
+ }
388
+
389
+ return { checkSource, generateDts, inferProps };
390
+ }
391
+
392
+ // ─── Internal: infer type from AST default value ──────────────────────────────
393
+
394
+ function _inferTypeFromDefault(node) {
395
+ if (!node) return null;
396
+ switch (node.type) {
397
+ case 'Literal':
398
+ if (typeof node.value === 'number') return 'number';
399
+ if (typeof node.value === 'string') return 'string';
400
+ if (typeof node.value === 'boolean') return 'boolean';
401
+ if (node.value === null) return 'null';
402
+ return null;
403
+ case 'ArrayExpr': return 'unknown[]';
404
+ case 'ObjectExpr': return 'Record<string, unknown>';
405
+ default: return null;
406
+ }
407
+ }
408
+
409
+ // ─── tsconfig preset ─────────────────────────────────────────────────────────
410
+
411
+ /**
412
+ * Recommended tsconfig settings for Clarity projects.
413
+ * Write to tsconfig.clarity.json and extend it in tsconfig.json.
414
+ */
415
+ export const TSCONFIG_PRESET = {
416
+ compilerOptions: {
417
+ target: 'ES2022',
418
+ module: 'ESNext',
419
+ moduleResolution: 'Bundler',
420
+ jsx: 'preserve',
421
+ strict: true,
422
+ noUnusedLocals: true,
423
+ noUnusedParameters:true,
424
+ exactOptionalPropertyTypes: true,
425
+ lib: ['ES2022', 'DOM', 'DOM.Iterable'],
426
+ types: ['@ozsarman/clarityjs'],
427
+ paths: {
428
+ '@ozsarman/clarityjs': ['./node_modules/@ozsarman/clarityjs/src/index.js'],
429
+ '@ozsarman/clarityjs/*': ['./node_modules/@ozsarman/clarityjs/src/*.js'],
430
+ },
431
+ plugins: [{ name: '@ozsarman/clarityjs/ts-plugin' }],
432
+ },
433
+ include: ['src/**/*.ts', 'src/**/*.clarity', 'types/**/*.d.ts'],
434
+ exclude: ['node_modules', 'dist'],
435
+ };
436
+
437
+ // ─── Volar plugin (Vue Language Tools compatible) ────────────────────────────
438
+
439
+ /**
440
+ * Volar / Vue Language Tools plugin factory for .clarity files.
441
+ *
442
+ * Register this in volar.config.js (or .volarrc.js):
443
+ *
444
+ * import { createClarityVolarPlugin } from '@ozsarman/clarityjs/ts-plugin';
445
+ * export default { plugins: [createClarityVolarPlugin()] };
446
+ *
447
+ * @returns {VolarPlugin}
448
+ */
449
+ export function createClarityVolarPlugin() {
450
+ return {
451
+ name: '@ozsarman/clarityjs',
452
+
453
+ /** Volar calls this to determine if this plugin handles a given file. */
454
+ resolveEmbeddedFile(fileName, sfc, embeddedFile) {
455
+ if (!fileName.endsWith('.clarity')) return;
456
+ // Tell Volar to treat the .clarity file as a virtual TS module
457
+ embeddedFile.kind = 1 /* TypeScript */;
458
+ embeddedFile.capabilities = {
459
+ diagnostic: true,
460
+ foldingRange: true,
461
+ documentFormatting: true,
462
+ documentSymbol: true,
463
+ completion: { triggerCharacters: ['.', ':', '<', '"', "'", '/'] },
464
+ referencesCodeLens: true,
465
+ };
466
+ },
467
+
468
+ /** Transform .clarity source to virtual TypeScript for type checking. */
469
+ resolveVirtualFile(fileName, source) {
470
+ if (!fileName.endsWith('.clarity')) return;
471
+
472
+ try {
473
+ const tokens = tokenize(source, fileName);
474
+ const ast = parse(tokens, source);
475
+ const dts = generateTypes(ast, { filename: fileName });
476
+
477
+ // Return a virtual .ts that TS can check
478
+ return {
479
+ fileName: fileName + '.ts',
480
+ content: dts,
481
+ kind: 'typescript',
482
+ };
483
+ } catch (err) {
484
+ return {
485
+ fileName: fileName + '.ts',
486
+ content: `// Clarity parse error: ${err.message}\nexport {};\n`,
487
+ kind: 'typescript',
488
+ };
489
+ }
490
+ },
491
+ };
492
+ }
493
+
494
+ // ─── Ambient type declarations (written to types/clarity.d.ts) ────────────────
495
+
496
+ /**
497
+ * Ambient TypeScript declarations for Clarity's runtime types.
498
+ * These are automatically included when users add "@ozsarman/clarityjs" to types[].
499
+ */
500
+ export const AMBIENT_DECLARATIONS = `
501
+ // Clarity.js — Ambient Type Declarations
502
+ // Auto-generated — do not edit manually.
503
+
504
+ declare module '@ozsarman/clarityjs' {
505
+ // ── Reactive primitives ────────────────────────────────────────────────────
506
+ export interface Signal<T> {
507
+ get(): T;
508
+ set(value: T): void;
509
+ peek(): T;
510
+ subscribe(fn: (value: T) => void): () => void;
511
+ }
512
+
513
+ export function signal<T>(value: T): Signal<T>;
514
+ export function computed<T>(fn: () => T): Signal<T>;
515
+ export function effect(fn: () => void | (() => void)): () => void;
516
+ export function batch(fn: () => void): void;
517
+
518
+ // ── Ref ───────────────────────────────────────────────────────────────────
519
+ export interface Ref<T = unknown> {
520
+ current: T | null;
521
+ readonly __isRef: true;
522
+ }
523
+ export function createRef<T = unknown>(initialValue?: T): Ref<T>;
524
+
525
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
526
+ export function onMount(fn: () => void | (() => void)): void;
527
+ export function onCleanup(fn: () => void): void;
528
+ export function onUpdate(fn: () => void): void;
529
+
530
+ // ── Context ───────────────────────────────────────────────────────────────
531
+ export interface Context<T> { readonly __contextId: symbol; defaultValue: T | undefined; }
532
+ export function createContext<T>(defaultValue?: T): Context<T>;
533
+ export function useContext<T>(ctx: Context<T>): T;
534
+
535
+ // ── DOM helpers ───────────────────────────────────────────────────────────
536
+ export function h(tag: string, attrs?: Record<string, unknown> | null, ...children: (Node | string | number | boolean | null | undefined)[]): Element;
537
+ export function mount(componentFn: (...args: unknown[]) => Node, target: Element | string, props?: Record<string, unknown>): void;
538
+ export function when(condition: () => boolean, then: () => Node, otherwise?: () => Node): Node;
539
+ export function list<T>(items: () => T[], render: (item: T, index: number) => Node, key?: (item: T, index: number) => string | number): Node;
540
+
541
+ // ── Router ────────────────────────────────────────────────────────────────
542
+ export interface NavigateOptions { state?: unknown; scroll?: boolean; viewTransition?: boolean; }
543
+ export function navigate(path: string, opts?: NavigateOptions): Promise<boolean>;
544
+ export function navigateReplace(path: string, opts?: NavigateOptions): Promise<boolean>;
545
+ export function back(): void;
546
+ export function forward(): void;
547
+ export const currentPath: Signal<string> & (() => string);
548
+ export const currentQuery: Signal<Record<string, string>> & (() => Record<string, string>);
549
+ export const routeParams: Signal<Record<string, string>> & (() => Record<string, string>);
550
+ export function matchRoute(pattern: string, path: string): Record<string, string> | null;
551
+ export function useViewTransition(): { isTransitioning: Signal<boolean>; startTransition(cb: () => void): void };
552
+
553
+ // ── Store ─────────────────────────────────────────────────────────────────
554
+ export interface StoreConfig<S> {
555
+ state: () => S;
556
+ getters?: Record<string, (state: S) => unknown>;
557
+ actions?: Record<string, (this: Store<S>, ...args: unknown[]) => unknown>;
558
+ }
559
+ export interface Store<S> {
560
+ state: S;
561
+ $patch(partial: Partial<S>): void;
562
+ $reset(): void;
563
+ $subscribe(fn: (state: S) => void): () => void;
564
+ [key: string]: unknown;
565
+ }
566
+ export function createStore<S>(config: StoreConfig<S>): Store<S>;
567
+
568
+ // ── Server Actions ────────────────────────────────────────────────────────
569
+ export interface ServerClient { call<O>(name: string, input?: unknown): Promise<O>; }
570
+ export function createServerClient(opts?: { baseUrl?: string; prefix?: string; headers?: Record<string, string> }): ServerClient;
571
+ export interface UseServerActionResult<I, O> {
572
+ execute(input?: I): Promise<O>;
573
+ loading: Signal<boolean>;
574
+ error: Signal<Error | null>;
575
+ data: Signal<O | null>;
576
+ reset(): void;
577
+ }
578
+ export function useServerAction<I = unknown, O = unknown>(
579
+ client: ServerClient,
580
+ name: string,
581
+ opts?: { initialData?: O; optimistic?: (input: I, current: O | null) => O; onSuccess?: (data: O) => void; onError?: (err: Error) => void }
582
+ ): UseServerActionResult<I, O>;
583
+
584
+ // ── Head management ───────────────────────────────────────────────────────
585
+ export interface HeadConfig {
586
+ title?: string;
587
+ titleTemplate?: string;
588
+ meta?: Array<Record<string, string>>;
589
+ link?: Array<Record<string, string>>;
590
+ script?: Array<Record<string, string>>;
591
+ }
592
+ export function useHead(config: HeadConfig): void;
593
+
594
+ // ── SSR ───────────────────────────────────────────────────────────────────
595
+ export function renderToString(componentFn: (...args: unknown[]) => Node, props?: Record<string, unknown>): { html: string; state: Record<string, unknown> };
596
+ export function renderToStringAsync(componentFn: (...args: unknown[]) => Node, props?: Record<string, unknown>): Promise<{ html: string; state: Record<string, unknown> }>;
597
+ export function hydrateRoot(componentFn: (...args: unknown[]) => Node, target?: Element, props?: Record<string, unknown>): void;
598
+
599
+ // ── Edge ─────────────────────────────────────────────────────────────────
600
+ export function createEdgeHandler(opts: { render: (req: unknown) => unknown; actions?: () => Promise<unknown>; }): { fetch: (req: Request, env?: unknown, ctx?: unknown) => Promise<Response> };
601
+ export function detectEdgeRuntime(): 'cloudflare' | 'deno' | 'bun' | 'vercel-edge' | 'node' | 'unknown';
602
+
603
+ // ── i18n ─────────────────────────────────────────────────────────────────
604
+ export function createI18n(opts: { locale: string; messages: Record<string, Record<string, string>>; fallbackLocale?: string }): unknown;
605
+ export function useI18n(): { t: (key: string, params?: Record<string, unknown>) => string; locale: Signal<string>; dir: Signal<'ltr' | 'rtl'> };
606
+ }
607
+
608
+ declare module '*.clarity' {
609
+ import type { Signal } from '@ozsarman/clarityjs';
610
+ const component: (...args: unknown[]) => Node;
611
+ export default component;
612
+ }
613
+ `;