@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.
- package/README.md +178 -0
- package/package.json +168 -0
- package/src/analyze.js +534 -0
- package/src/async-state.js +555 -0
- package/src/bundle-runtime.js +35 -0
- package/src/clarity-bundle.js +332 -0
- package/src/clarity-test.js +622 -0
- package/src/cli.js +453 -0
- package/src/codegen.js +1934 -0
- package/src/dev-server.js +362 -0
- package/src/devtools.js +765 -0
- package/src/edge.js +606 -0
- package/src/error-overlay.js +535 -0
- package/src/file-conventions.js +472 -0
- package/src/font.js +513 -0
- package/src/game-loop.js +106 -0
- package/src/head.js +393 -0
- package/src/hydrate.js +292 -0
- package/src/i18n.js +403 -0
- package/src/image.js +352 -0
- package/src/index.js +193 -0
- package/src/islands.js +284 -0
- package/src/isr.js +306 -0
- package/src/layout.js +342 -0
- package/src/lexer.js +572 -0
- package/src/linter.js +547 -0
- package/src/pages-router.js +229 -0
- package/src/parser.js +1108 -0
- package/src/router.js +732 -0
- package/src/runtime.js +1465 -0
- package/src/scoped-css.js +641 -0
- package/src/server-actions.js +439 -0
- package/src/server-data.js +225 -0
- package/src/sourcemap.js +130 -0
- package/src/ssg.js +310 -0
- package/src/ssr.js +621 -0
- package/src/store.js +276 -0
- package/src/transitions.js +438 -0
- package/src/ts-plugin.js +613 -0
- package/src/typegen.js +240 -0
- package/src/vite-plugin.js +447 -0
- package/types/index.d.ts +366 -0
package/src/ts-plugin.js
ADDED
|
@@ -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
|
+
`;
|