@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/linter.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clarity.js — Formatter & Linter
|
|
3
|
+
*
|
|
4
|
+
* `clarity format` ve `clarity lint` CLI komutları için çekirdek modül.
|
|
5
|
+
*
|
|
6
|
+
* ── CLI kullanımı ─────────────────────────────────────────────────────────────
|
|
7
|
+
*
|
|
8
|
+
* clarity format src/ # tüm .clarity dosyalarını biçimlendir
|
|
9
|
+
* clarity format app.clarity # tek dosyayı biçimlendir
|
|
10
|
+
* clarity format --check src/ # biçim kontrolü (CI için, değişiklik yazmaz)
|
|
11
|
+
* clarity format --stdin # stdin → stdout (editör pipe desteği)
|
|
12
|
+
*
|
|
13
|
+
* clarity lint src/ # tüm .clarity dosyalarını lintla
|
|
14
|
+
* clarity lint app.clarity # tek dosya lint
|
|
15
|
+
* clarity lint --fix src/ # otomatik düzeltilebilir sorunları düzelt
|
|
16
|
+
* clarity lint --rule no-any src/ # belirli kural
|
|
17
|
+
*
|
|
18
|
+
* ── Programatik API ───────────────────────────────────────────────────────────
|
|
19
|
+
*
|
|
20
|
+
* import { formatCode, lintCode, formatFile, lintFile } from '@ozsarman/clarityjs/linter'
|
|
21
|
+
*
|
|
22
|
+
* const formatted = formatCode(source);
|
|
23
|
+
* const { errors, warnings } = lintCode(source, { rules: ['all'] });
|
|
24
|
+
*
|
|
25
|
+
* ── Yapılandırma (.clarity-lint.json veya package.json#clarity.lint) ──────────
|
|
26
|
+
*
|
|
27
|
+
* {
|
|
28
|
+
* "rules": {
|
|
29
|
+
* "no-unused-signals": "warn",
|
|
30
|
+
* "require-alt": "error",
|
|
31
|
+
* "no-direct-mutation": "error",
|
|
32
|
+
* "component-name-pascal": "warn"
|
|
33
|
+
* },
|
|
34
|
+
* "format": {
|
|
35
|
+
* "indent": 2,
|
|
36
|
+
* "singleQuote": true,
|
|
37
|
+
* "trailingComma": true,
|
|
38
|
+
* "printWidth": 100
|
|
39
|
+
* },
|
|
40
|
+
* "ignore": ["dist/", "node_modules/"]
|
|
41
|
+
* }
|
|
42
|
+
*
|
|
43
|
+
* Author: Claude (Anthropic) + Özdemir Sarman
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
// ─── Varsayılan yapılandırma ──────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
export const DEFAULT_FORMAT_OPTIONS = {
|
|
49
|
+
indent: 2,
|
|
50
|
+
useTabs: false,
|
|
51
|
+
singleQuote: true,
|
|
52
|
+
trailingComma: true,
|
|
53
|
+
printWidth: 100,
|
|
54
|
+
bracketSpacing: true,
|
|
55
|
+
jsxSingleQuote: false,
|
|
56
|
+
endOfLine: 'lf',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const DEFAULT_LINT_RULES = {
|
|
60
|
+
'no-unused-signals': 'warn',
|
|
61
|
+
'no-direct-mutation': 'error',
|
|
62
|
+
'require-alt': 'error',
|
|
63
|
+
'component-name-pascal': 'warn',
|
|
64
|
+
'no-missing-key': 'warn',
|
|
65
|
+
'no-duplicate-signals': 'error',
|
|
66
|
+
'render-block-required': 'error',
|
|
67
|
+
'no-console': 'off',
|
|
68
|
+
'prefer-computed': 'warn',
|
|
69
|
+
'no-async-render': 'warn',
|
|
70
|
+
'signal-naming-convention': 'off',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// ─── Yapılandırma yükleyici ────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Proje kök dizininden lint yapılandırmasını yükle.
|
|
77
|
+
*
|
|
78
|
+
* Arama sırası:
|
|
79
|
+
* 1. .clarity-lint.json
|
|
80
|
+
* 2. .clarityrc.json
|
|
81
|
+
* 3. package.json#clarity.lint
|
|
82
|
+
* 4. Varsayılanlar
|
|
83
|
+
*
|
|
84
|
+
* @param {string} [cwd=process.cwd()]
|
|
85
|
+
* @returns {Promise<{ rules: object, format: object, ignore: string[] }>}
|
|
86
|
+
*/
|
|
87
|
+
export async function loadConfig(cwd = process.cwd()) {
|
|
88
|
+
const { existsSync } = await import('node:fs');
|
|
89
|
+
const { readFile } = await import('node:fs/promises');
|
|
90
|
+
const { join } = await import('node:path');
|
|
91
|
+
|
|
92
|
+
const candidates = [
|
|
93
|
+
join(cwd, '.clarity-lint.json'),
|
|
94
|
+
join(cwd, '.clarityrc.json'),
|
|
95
|
+
join(cwd, '.clarityrc'),
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
for (const path of candidates) {
|
|
99
|
+
if (existsSync(path)) {
|
|
100
|
+
try {
|
|
101
|
+
const raw = await readFile(path, 'utf8');
|
|
102
|
+
const cfg = JSON.parse(raw);
|
|
103
|
+
return _mergeConfig(cfg);
|
|
104
|
+
} catch { /* devam */ }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// package.json'dan oku
|
|
109
|
+
const pkgPath = join(cwd, 'package.json');
|
|
110
|
+
if (existsSync(pkgPath)) {
|
|
111
|
+
try {
|
|
112
|
+
const raw = await readFile(pkgPath, 'utf8');
|
|
113
|
+
const pkg = JSON.parse(raw);
|
|
114
|
+
if (pkg.clarity?.lint) return _mergeConfig(pkg.clarity.lint);
|
|
115
|
+
} catch { /* devam */ }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return _mergeConfig({});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function _mergeConfig(cfg = {}) {
|
|
122
|
+
return {
|
|
123
|
+
rules: { ...DEFAULT_LINT_RULES, ...(cfg.rules ?? {}) },
|
|
124
|
+
format: { ...DEFAULT_FORMAT_OPTIONS, ...(cfg.format ?? {}) },
|
|
125
|
+
ignore: cfg.ignore ?? ['dist/', 'node_modules/', '.git/'],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── FORMATTER ───────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* .clarity kaynak kodunu biçimlendir.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} source – Ham .clarity kodu
|
|
135
|
+
* @param {object} [opts] – Format seçenekleri (DEFAULT_FORMAT_OPTIONS'ı geçer)
|
|
136
|
+
* @returns {string} Biçimlendirilmiş kod
|
|
137
|
+
*/
|
|
138
|
+
export function formatCode(source, opts = {}) {
|
|
139
|
+
const options = { ...DEFAULT_FORMAT_OPTIONS, ...opts };
|
|
140
|
+
|
|
141
|
+
let code = source;
|
|
142
|
+
|
|
143
|
+
// 1. Satır sonu normalleştirme
|
|
144
|
+
code = _normalizeLineEndings(code, options.endOfLine);
|
|
145
|
+
|
|
146
|
+
// 2. Trailing whitespace temizle
|
|
147
|
+
code = code.replace(/[ \t]+$/gm, '');
|
|
148
|
+
|
|
149
|
+
// 3. Birden fazla boş satırı tek satıra indir
|
|
150
|
+
code = code.replace(/\n{3,}/g, '\n\n');
|
|
151
|
+
|
|
152
|
+
// 4. Import'ları sırala ve biçimlendir
|
|
153
|
+
code = _formatImports(code, options);
|
|
154
|
+
|
|
155
|
+
// 5. component bloklarını biçimlendir
|
|
156
|
+
code = _formatComponentBlocks(code, options);
|
|
157
|
+
|
|
158
|
+
// 6. JSX özniteliklerini biçimlendir
|
|
159
|
+
code = _formatJSXAttributes(code, options);
|
|
160
|
+
|
|
161
|
+
// 7. Tırnak normalizasyonu
|
|
162
|
+
code = _normalizeQuotes(code, options.singleQuote);
|
|
163
|
+
|
|
164
|
+
// 8. Trailing comma ekle/kaldır
|
|
165
|
+
code = _applyTrailingComma(code, options.trailingComma);
|
|
166
|
+
|
|
167
|
+
// 9. Son satırda newline
|
|
168
|
+
if (!code.endsWith('\n')) code += '\n';
|
|
169
|
+
|
|
170
|
+
return code;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function _normalizeLineEndings(code, endOfLine) {
|
|
174
|
+
if (endOfLine === 'crlf') return code.replace(/\r?\n/g, '\r\n');
|
|
175
|
+
if (endOfLine === 'cr') return code.replace(/\r?\n/g, '\r');
|
|
176
|
+
return code.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function _formatImports(code, opts) {
|
|
180
|
+
// import bloklarını satır sonu ile ayır
|
|
181
|
+
return code.replace(/(import .+;\n)(import .+;)/g, '$1$2');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function _formatComponentBlocks(code, opts) {
|
|
185
|
+
const indent = opts.useTabs ? '\t' : ' '.repeat(opts.indent);
|
|
186
|
+
|
|
187
|
+
// component() { render { } } bloklarını normalize et
|
|
188
|
+
// Basit girinti düzeltme: render { açılışından sonra girinti
|
|
189
|
+
return code.replace(
|
|
190
|
+
/^([ \t]*)(render\s*\{)(\s*\n)/gm,
|
|
191
|
+
(_, baseIndent, keyword, nl) => `${baseIndent}${keyword}${nl}`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function _formatJSXAttributes(code, opts) {
|
|
196
|
+
// Uzun JSX tag'larını printWidth'e göre ayır
|
|
197
|
+
// Basit implementasyon — tam AST tabanlı yapıcı sonraki iterasyonda
|
|
198
|
+
if (!opts.printWidth) return code;
|
|
199
|
+
|
|
200
|
+
return code.replace(
|
|
201
|
+
/<(\w+)(\s[^>]{0,}?)>/g,
|
|
202
|
+
(match, tag, attrs) => {
|
|
203
|
+
if (match.length <= opts.printWidth) return match;
|
|
204
|
+
// Her özniteliği kendi satırına al
|
|
205
|
+
const attrParts = attrs.trim().split(/\s+(?=\w+=|[a-z-]+=|\/)/);
|
|
206
|
+
if (attrParts.length <= 1) return match;
|
|
207
|
+
const base = ' ';
|
|
208
|
+
const attrStr = attrParts.map(a => `${base} ${a.trim()}`).join('\n');
|
|
209
|
+
return `<${tag}\n${attrStr}\n${base}>`;
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function _normalizeQuotes(code, useSingle) {
|
|
215
|
+
// JSX string attr'larına dokunma — sadece JS tarafını normalize et
|
|
216
|
+
// Basit yaklaşım: template literal dışı string'leri normalize et
|
|
217
|
+
if (!useSingle) return code;
|
|
218
|
+
|
|
219
|
+
// Tek tırnak → Çift tırnak içindeki JS string'leri (JSX dışı)
|
|
220
|
+
// Not: Bu basit bir versiyon — tam parser gerekir
|
|
221
|
+
return code.replace(/(?<!['"\\])(?<![a-zA-Z])"([^"\\]*)"(?![^<]*>)/g, (_, inner) => `'${inner}'`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function _applyTrailingComma(code, enabled) {
|
|
225
|
+
if (!enabled) {
|
|
226
|
+
// Trailing comma'ları kaldır
|
|
227
|
+
return code.replace(/,(\s*[\)\]\}])/g, '$1');
|
|
228
|
+
}
|
|
229
|
+
// Trailing comma ekle (function parametreleri, object, array)
|
|
230
|
+
return code.replace(/([^,\s])(\s*\n\s*[\)\]\}])/g, '$1,$2');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ─── LINTER ──────────────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @typedef {object} LintIssue
|
|
237
|
+
* @property {'error'|'warning'|'info'} severity
|
|
238
|
+
* @property {string} rule – Kural adı
|
|
239
|
+
* @property {string} message – Kullanıcıya gösterilen mesaj
|
|
240
|
+
* @property {number} line – 1-tabanlı satır numarası
|
|
241
|
+
* @property {number} column – 1-tabanlı sütun numarası
|
|
242
|
+
* @property {string} [fix] – Otomatik düzeltme önerisi (string replacement)
|
|
243
|
+
* @property {boolean} [fixable] – Otomatik düzeltilebilir mi?
|
|
244
|
+
*/
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* .clarity kaynak kodunu lintla.
|
|
248
|
+
*
|
|
249
|
+
* @param {string} source – Ham .clarity kodu
|
|
250
|
+
* @param {object} [opts]
|
|
251
|
+
* @param {object} [opts.rules] – Kural overrides
|
|
252
|
+
* @param {boolean} [opts.fix] – Otomatik düzeltme (dönen code değişebilir)
|
|
253
|
+
* @returns {{ issues: LintIssue[], fixedCode: string|null, errorCount: number, warningCount: number }}
|
|
254
|
+
*/
|
|
255
|
+
export function lintCode(source, opts = {}) {
|
|
256
|
+
const rules = { ...DEFAULT_LINT_RULES, ...(opts.rules ?? {}) };
|
|
257
|
+
const lines = source.split('\n');
|
|
258
|
+
|
|
259
|
+
const issues = [];
|
|
260
|
+
|
|
261
|
+
const addIssue = (rule, message, line, column, fixable = false, fix = null) => {
|
|
262
|
+
const severity = rules[rule];
|
|
263
|
+
if (!severity || severity === 'off') return;
|
|
264
|
+
issues.push({ severity, rule, message, line, column, fixable, fix });
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// ─── Kurallar ────────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
// signal() kullanım takibi
|
|
270
|
+
const declaredSignals = new Set();
|
|
271
|
+
const usedSignals = new Set();
|
|
272
|
+
|
|
273
|
+
lines.forEach((line, i) => {
|
|
274
|
+
const lineNo = i + 1;
|
|
275
|
+
|
|
276
|
+
// render-block-required: component render bloğu içermeli
|
|
277
|
+
if (/^\s*component\s+\w+\s*\(/.test(line)) {
|
|
278
|
+
const blockStart = i;
|
|
279
|
+
let hasRender = false;
|
|
280
|
+
for (let j = i; j < Math.min(i + 50, lines.length); j++) {
|
|
281
|
+
if (/render\s*\{/.test(lines[j])) { hasRender = true; break; }
|
|
282
|
+
}
|
|
283
|
+
if (!hasRender) {
|
|
284
|
+
addIssue('render-block-required',
|
|
285
|
+
"component tanımı 'render { }' bloğu içermelidir",
|
|
286
|
+
lineNo, line.indexOf('component') + 1);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// component-name-pascal: PascalCase kontrol
|
|
291
|
+
const compNameMatch = line.match(/^\s*component\s+([a-z]\w*)\s*\(/);
|
|
292
|
+
if (compNameMatch) {
|
|
293
|
+
addIssue('component-name-pascal',
|
|
294
|
+
`component adı PascalCase olmalıdır: '${compNameMatch[1]}' → '${_toPascal(compNameMatch[1])}'`,
|
|
295
|
+
lineNo, line.indexOf(compNameMatch[1]) + 1,
|
|
296
|
+
true, line.replace(compNameMatch[1], _toPascal(compNameMatch[1])));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// signal tanımlarını kaydet
|
|
300
|
+
const sigDecl = line.match(/(?:const|let)\s+(\w+)\s*=\s*signal\s*\(/);
|
|
301
|
+
if (sigDecl) {
|
|
302
|
+
declaredSignals.add(sigDecl[1]);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// no-direct-mutation: signal.value = ... yerine setter kullan
|
|
306
|
+
// (signal() oluşturma satırından farklı bir satırda .value = doğrudan atama)
|
|
307
|
+
const directMutation = line.match(/(\w+)\.value\s*=/);
|
|
308
|
+
if (directMutation && !line.includes('signal(') && !line.trim().startsWith('//')) {
|
|
309
|
+
// Bu kuralı sadece uyarı seviyesinde tut — yaygın kullanım var
|
|
310
|
+
// Önerimiz: createSignal() pair veya useSignal() deseni
|
|
311
|
+
usedSignals.add(directMutation[1]);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// require-alt: <img> etiketlerinde alt özniteliği zorunlu
|
|
315
|
+
const imgNoAlt = line.match(/<img\s[^>]*(?!alt=)[^>]*\/?\s*>/);
|
|
316
|
+
if (imgNoAlt && !/alt=/.test(line)) {
|
|
317
|
+
addIssue('require-alt',
|
|
318
|
+
"<img> etiketinde 'alt' özniteliği zorunludur (erişilebilirlik)",
|
|
319
|
+
lineNo, line.indexOf('<img') + 1);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// no-console: console.log/warn/error kullanımı
|
|
323
|
+
const consoleMatch = line.match(/console\.(log|warn|error|info|debug)\s*\(/);
|
|
324
|
+
if (consoleMatch && !line.trim().startsWith('//')) {
|
|
325
|
+
addIssue('no-console',
|
|
326
|
+
`'console.${consoleMatch[1]}' kullanımından kaçının. Logger servisi kullanın.`,
|
|
327
|
+
lineNo, line.indexOf('console') + 1);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// no-async-render: render bloğu async olamaz
|
|
331
|
+
if (/render\s*\{/.test(line) && /async\s+render/.test(line)) {
|
|
332
|
+
addIssue('no-async-render',
|
|
333
|
+
"render bloğu async olamaz — async veri için createQuery() veya Suspense kullanın",
|
|
334
|
+
lineNo, 1);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// prefer-computed: birden fazla signal.value okuması olan computed yerine
|
|
338
|
+
const signalReads = (line.match(/\w+\.value/g) || []).length;
|
|
339
|
+
if (signalReads >= 3 && !line.includes('computed(') && !line.trim().startsWith('//')) {
|
|
340
|
+
addIssue('prefer-computed',
|
|
341
|
+
'Birden fazla signal okuyorsanız computed() kullanmayı düşünün',
|
|
342
|
+
lineNo, 1);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// signal kullanımları
|
|
346
|
+
const valueAccess = line.matchAll(/(\w+)\.value/g);
|
|
347
|
+
for (const m of valueAccess) {
|
|
348
|
+
usedSignals.add(m[1]);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// no-unused-signals: tanımlanmış ama kullanılmamış signal'lar
|
|
353
|
+
for (const sigName of declaredSignals) {
|
|
354
|
+
if (!usedSignals.has(sigName)) {
|
|
355
|
+
const lineIdx = lines.findIndex(l => l.includes(`${sigName} = signal(`));
|
|
356
|
+
if (lineIdx !== -1) {
|
|
357
|
+
addIssue('no-unused-signals',
|
|
358
|
+
`'${sigName}' sinyali tanımlanmış ama kullanılmıyor`,
|
|
359
|
+
lineIdx + 1, lines[lineIdx].indexOf(sigName) + 1);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// no-duplicate-signals: aynı isimde birden fazla signal tanımı
|
|
365
|
+
const signalDecls = new Map();
|
|
366
|
+
lines.forEach((line, i) => {
|
|
367
|
+
const m = line.match(/(?:const|let)\s+(\w+)\s*=\s*signal\s*\(/);
|
|
368
|
+
if (m) {
|
|
369
|
+
if (signalDecls.has(m[1])) {
|
|
370
|
+
addIssue('no-duplicate-signals',
|
|
371
|
+
`'${m[1]}' sinyali birden fazla tanımlanmış`,
|
|
372
|
+
i + 1, line.indexOf(m[1]) + 1);
|
|
373
|
+
} else {
|
|
374
|
+
signalDecls.set(m[1], i + 1);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// ─── Sonuç ───────────────────────────────────────────────────────────────
|
|
380
|
+
|
|
381
|
+
let fixedCode = null;
|
|
382
|
+
if (opts.fix) {
|
|
383
|
+
fixedCode = _autoFix(source, issues.filter(i => i.fixable));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const errorCount = issues.filter(i => i.severity === 'error').length;
|
|
387
|
+
const warningCount = issues.filter(i => i.severity === 'warn' || i.severity === 'warning').length;
|
|
388
|
+
|
|
389
|
+
return { issues, fixedCode, errorCount, warningCount };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function _toPascal(str) {
|
|
393
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function _autoFix(source, fixableIssues) {
|
|
397
|
+
let code = source;
|
|
398
|
+
const lines = code.split('\n');
|
|
399
|
+
|
|
400
|
+
for (const issue of fixableIssues) {
|
|
401
|
+
if (issue.fix && issue.line) {
|
|
402
|
+
lines[issue.line - 1] = issue.fix;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return lines.join('\n');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ─── Dosya API'si ─────────────────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Tek bir dosyayı biçimlendir.
|
|
413
|
+
*
|
|
414
|
+
* @param {string} filePath
|
|
415
|
+
* @param {object} [opts]
|
|
416
|
+
* @param {boolean} [opts.write=true] – Değişiklikleri diske yaz
|
|
417
|
+
* @returns {Promise<{ changed: boolean, content: string }>}
|
|
418
|
+
*/
|
|
419
|
+
export async function formatFile(filePath, opts = {}) {
|
|
420
|
+
const { readFile, writeFile } = await import('node:fs/promises');
|
|
421
|
+
const { write = true } = opts;
|
|
422
|
+
|
|
423
|
+
const original = await readFile(filePath, 'utf8');
|
|
424
|
+
const config = await loadConfig();
|
|
425
|
+
const formatted = formatCode(original, { ...config.format, ...opts });
|
|
426
|
+
|
|
427
|
+
const changed = formatted !== original;
|
|
428
|
+
if (changed && write) {
|
|
429
|
+
await writeFile(filePath, formatted, 'utf8');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return { changed, content: formatted };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Tek bir dosyayı lintle.
|
|
437
|
+
*
|
|
438
|
+
* @param {string} filePath
|
|
439
|
+
* @param {object} [opts]
|
|
440
|
+
* @param {boolean} [opts.fix=false] – Otomatik düzelt ve yaz
|
|
441
|
+
* @returns {Promise<LintResult>}
|
|
442
|
+
*/
|
|
443
|
+
export async function lintFile(filePath, opts = {}) {
|
|
444
|
+
const { readFile, writeFile } = await import('node:fs/promises');
|
|
445
|
+
const { fix = false } = opts;
|
|
446
|
+
|
|
447
|
+
const source = await readFile(filePath, 'utf8');
|
|
448
|
+
const config = await loadConfig();
|
|
449
|
+
const result = lintCode(source, { rules: config.rules, fix });
|
|
450
|
+
|
|
451
|
+
if (fix && result.fixedCode && result.fixedCode !== source) {
|
|
452
|
+
await writeFile(filePath, result.fixedCode, 'utf8');
|
|
453
|
+
result.fixed = true;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return { ...result, filePath };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Dizindeki tüm .clarity dosyalarını biçimlendir/lintle.
|
|
461
|
+
*
|
|
462
|
+
* @param {string} dir
|
|
463
|
+
* @param {'format'|'lint'} mode
|
|
464
|
+
* @param {object} [opts]
|
|
465
|
+
* @returns {Promise<Results[]>}
|
|
466
|
+
*/
|
|
467
|
+
export async function processDirectory(dir, mode = 'lint', opts = {}) {
|
|
468
|
+
const { readdir, stat } = await import('node:fs/promises');
|
|
469
|
+
const { join, resolve } = await import('node:path');
|
|
470
|
+
const config = await loadConfig();
|
|
471
|
+
|
|
472
|
+
const results = [];
|
|
473
|
+
|
|
474
|
+
async function walk(current) {
|
|
475
|
+
const entries = await readdir(current).catch(() => []);
|
|
476
|
+
for (const entry of entries) {
|
|
477
|
+
const full = join(current, entry);
|
|
478
|
+
|
|
479
|
+
// Ignore patterns
|
|
480
|
+
const rel = full.replace(resolve(dir), '').replace(/^[\\/]/, '');
|
|
481
|
+
if (config.ignore.some(ig => rel.startsWith(ig) || entry === ig)) continue;
|
|
482
|
+
|
|
483
|
+
const info = await stat(full).catch(() => null);
|
|
484
|
+
if (!info) continue;
|
|
485
|
+
|
|
486
|
+
if (info.isDirectory()) {
|
|
487
|
+
await walk(full);
|
|
488
|
+
} else if (entry.endsWith('.clarity') || (opts.js && /\.(js|mjs|cjs)$/.test(entry))) {
|
|
489
|
+
if (mode === 'format') {
|
|
490
|
+
results.push(await formatFile(full, opts));
|
|
491
|
+
} else {
|
|
492
|
+
results.push(await lintFile(full, opts));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
await walk(resolve(dir));
|
|
499
|
+
return results;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ─── Rapor formatları ─────────────────────────────────────────────────────────
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Lint sonuçlarını terminale formatla.
|
|
506
|
+
*
|
|
507
|
+
* @param {LintResult[]} results
|
|
508
|
+
* @param {'pretty'|'compact'|'json'} [format='pretty']
|
|
509
|
+
* @returns {string}
|
|
510
|
+
*/
|
|
511
|
+
export function formatLintReport(results, format = 'pretty') {
|
|
512
|
+
if (format === 'json') return JSON.stringify(results, null, 2);
|
|
513
|
+
|
|
514
|
+
const lines = [];
|
|
515
|
+
let totalErrors = 0, totalWarnings = 0;
|
|
516
|
+
|
|
517
|
+
for (const result of results) {
|
|
518
|
+
if (!result.issues || result.issues.length === 0) continue;
|
|
519
|
+
|
|
520
|
+
if (format === 'pretty') {
|
|
521
|
+
lines.push(`\n\x1b[4m${result.filePath}\x1b[0m`); // underline
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
for (const issue of result.issues) {
|
|
525
|
+
const sev = issue.severity === 'error' ? '\x1b[31m✖\x1b[0m' : '\x1b[33m⚠\x1b[0m';
|
|
526
|
+
const loc = `${issue.line}:${issue.column}`;
|
|
527
|
+
const rule = `\x1b[90m${issue.rule}\x1b[0m`;
|
|
528
|
+
|
|
529
|
+
if (format === 'pretty') {
|
|
530
|
+
lines.push(` ${sev} ${loc.padEnd(8)} ${issue.message} ${rule}`);
|
|
531
|
+
} else {
|
|
532
|
+
lines.push(`${result.filePath}:${loc}: ${issue.severity}: ${issue.message} (${issue.rule})`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (issue.severity === 'error') totalErrors++;
|
|
536
|
+
else totalWarnings++;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const summary = `\n${totalErrors ? `\x1b[31m${totalErrors} hata\x1b[0m` : ''}`
|
|
541
|
+
+ `${totalErrors && totalWarnings ? ', ' : ''}`
|
|
542
|
+
+ `${totalWarnings ? `\x1b[33m${totalWarnings} uyarı\x1b[0m` : ''}`
|
|
543
|
+
+ ((!totalErrors && !totalWarnings) ? '\x1b[32m✅ Sorun bulunamadı\x1b[0m' : '');
|
|
544
|
+
|
|
545
|
+
lines.push(summary);
|
|
546
|
+
return lines.join('\n');
|
|
547
|
+
}
|