@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/layout.js
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clarity.js — Layout System
|
|
3
|
+
*
|
|
4
|
+
* Next.js app/ layout.js / Nuxt layouts/ eşdeğeri.
|
|
5
|
+
* Dosya tabanlı nested layout konvansiyonu + programatik API.
|
|
6
|
+
*
|
|
7
|
+
* ── Dosya konvansiyonu ────────────────────────────────────────────────────────
|
|
8
|
+
*
|
|
9
|
+
* pages/
|
|
10
|
+
* ├── _layout.js ← root layout (tüm sayfaları sarar)
|
|
11
|
+
* ├── index.js
|
|
12
|
+
* ├── about.js
|
|
13
|
+
* └── blog/
|
|
14
|
+
* ├── _layout.js ← /blog/* için layout (root layout'u da devralır)
|
|
15
|
+
* └── [slug].js
|
|
16
|
+
*
|
|
17
|
+
* ── _layout.js örneği ────────────────────────────────────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* // pages/_layout.js
|
|
20
|
+
* export default function RootLayout({ children }) {
|
|
21
|
+
* return (
|
|
22
|
+
* <html lang="tr">
|
|
23
|
+
* <body>
|
|
24
|
+
* <Nav />
|
|
25
|
+
* <main>{children}</main>
|
|
26
|
+
* <Footer />
|
|
27
|
+
* </body>
|
|
28
|
+
* </html>
|
|
29
|
+
* );
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* ── Programatik API ───────────────────────────────────────────────────────────
|
|
33
|
+
*
|
|
34
|
+
* import { defineLayout, applyLayouts, renderWithLayouts } from '@ozsarman/clarityjs/layout'
|
|
35
|
+
*
|
|
36
|
+
* defineLayout('root', RootLayout);
|
|
37
|
+
* defineLayout('blog', BlogLayout, { parent: 'root' });
|
|
38
|
+
*
|
|
39
|
+
* // Bir sayfayı layout'larıyla render et:
|
|
40
|
+
* const html = renderWithLayouts(BlogPost, { slug: 'hello' }, 'blog');
|
|
41
|
+
*
|
|
42
|
+
* ── definePageMeta ────────────────────────────────────────────────────────────
|
|
43
|
+
*
|
|
44
|
+
* // pages/blog/[slug].js
|
|
45
|
+
* export const pageMeta = definePageMeta({
|
|
46
|
+
* layout: 'blog', // kullanılacak layout
|
|
47
|
+
* title: 'Blog Yazısı',
|
|
48
|
+
* auth: true, // route guard
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* ── SSR entegrasyonu ──────────────────────────────────────────────────────────
|
|
52
|
+
*
|
|
53
|
+
* import { wrapWithLayouts } from '@ozsarman/clarityjs/layout'
|
|
54
|
+
*
|
|
55
|
+
* // SSR pipeline'da:
|
|
56
|
+
* const html = renderToString(wrapWithLayouts(PageComponent, layoutChain), { props });
|
|
57
|
+
*
|
|
58
|
+
* Author: Claude (Anthropic) + Özdemir Sarman
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
import { renderToString } from './ssr.js';
|
|
62
|
+
|
|
63
|
+
// ─── Layout Registry ──────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
const _layoutRegistry = new Map();
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Programatik layout tanımı.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} name – Layout adı (benzersiz)
|
|
71
|
+
* @param {Function} ComponentFn – Layout bileşeni ({ children, ...props }) => node
|
|
72
|
+
* @param {object} [opts]
|
|
73
|
+
* @param {string} [opts.parent] – Ebeveyn layout adı (nested layouts)
|
|
74
|
+
*/
|
|
75
|
+
export function defineLayout(name, ComponentFn, opts = {}) {
|
|
76
|
+
const { parent = null } = opts;
|
|
77
|
+
_layoutRegistry.set(name, { name, ComponentFn, parent });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Kayıtlı layout'u al.
|
|
82
|
+
* @param {string} name
|
|
83
|
+
* @returns {{ name: string, ComponentFn: Function, parent: string|null } | null}
|
|
84
|
+
*/
|
|
85
|
+
export function getLayout(name) {
|
|
86
|
+
return _layoutRegistry.get(name) ?? null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Tüm layout kayıtlarını sıfırla (test / SSR yeniden başlatma için).
|
|
91
|
+
*/
|
|
92
|
+
export function resetLayoutRegistry() {
|
|
93
|
+
_layoutRegistry.clear();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── definePageMeta ───────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Sayfa metadata tanımla — layout seçimi, auth guard, title vb.
|
|
100
|
+
* Sayfa dosyasında `export const pageMeta = definePageMeta({...})` şeklinde kullanılır.
|
|
101
|
+
*
|
|
102
|
+
* @param {object} opts
|
|
103
|
+
* @param {string} [opts.layout] – Kullanılacak layout adı (yoksa 'default')
|
|
104
|
+
* @param {string} [opts.title] – Sayfa başlığı
|
|
105
|
+
* @param {boolean} [opts.auth] – Kimlik doğrulama gerektiriyor mu?
|
|
106
|
+
* @param {string} [opts.middleware] – Middleware adı
|
|
107
|
+
* @param {object} [opts.meta] – Ek metadata
|
|
108
|
+
* @returns {PageMeta}
|
|
109
|
+
*/
|
|
110
|
+
export function definePageMeta(opts = {}) {
|
|
111
|
+
return {
|
|
112
|
+
__clarity_page_meta__: true,
|
|
113
|
+
layout: opts.layout ?? 'default',
|
|
114
|
+
title: opts.title ?? null,
|
|
115
|
+
auth: opts.auth ?? false,
|
|
116
|
+
middleware: opts.middleware ?? null,
|
|
117
|
+
meta: opts.meta ?? {},
|
|
118
|
+
...opts,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Layout zinciri oluştur ───────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Bir layout adından başlayarak en üst layout'a kadar zincir oluşturur.
|
|
126
|
+
* ['root', 'blog'] gibi dıştan içe sıralı döner.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} layoutName
|
|
129
|
+
* @returns {Array<{ name: string, ComponentFn: Function }>}
|
|
130
|
+
*/
|
|
131
|
+
export function buildLayoutChain(layoutName) {
|
|
132
|
+
const chain = [];
|
|
133
|
+
let current = _layoutRegistry.get(layoutName) ?? null;
|
|
134
|
+
|
|
135
|
+
while (current) {
|
|
136
|
+
chain.unshift(current); // en dıştan başla
|
|
137
|
+
current = current.parent ? _layoutRegistry.get(current.parent) ?? null : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return chain;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── Runtime: children injection ─────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Sayfa bileşenini layout zinciriyle sar.
|
|
147
|
+
* En dış layout en üstte, sayfa bileşeni en içte.
|
|
148
|
+
*
|
|
149
|
+
* @param {Function} PageComponent
|
|
150
|
+
* @param {Array} layoutChain – buildLayoutChain() çıktısı
|
|
151
|
+
* @returns {Function} Sarılmış bileşen (render için kullanılır)
|
|
152
|
+
*/
|
|
153
|
+
export function wrapWithLayouts(PageComponent, layoutChain) {
|
|
154
|
+
if (!layoutChain || layoutChain.length === 0) return PageComponent;
|
|
155
|
+
|
|
156
|
+
// Layout zincirini içten dışa sarar: page → innerLayout → ... → rootLayout
|
|
157
|
+
let Wrapped = PageComponent;
|
|
158
|
+
|
|
159
|
+
for (let i = layoutChain.length - 1; i >= 0; i--) {
|
|
160
|
+
const { ComponentFn } = layoutChain[i];
|
|
161
|
+
const Inner = Wrapped;
|
|
162
|
+
Wrapped = function LayoutWrapper(props) {
|
|
163
|
+
// children olarak Inner bileşenini ilet
|
|
164
|
+
return ComponentFn({ ...props, children: Inner(props) });
|
|
165
|
+
};
|
|
166
|
+
Wrapped.displayName = `Layout(${ComponentFn.name || '?'})`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return Wrapped;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ─── SSR: layout'larla render ─────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Bir sayfayı layout zinciriyle SSR render et.
|
|
176
|
+
*
|
|
177
|
+
* @param {Function} PageComponent
|
|
178
|
+
* @param {object} [props={}]
|
|
179
|
+
* @param {string} [layoutName] – layout adı; yoksa pageMeta.layout veya 'default'
|
|
180
|
+
* @returns {string} HTML string
|
|
181
|
+
*/
|
|
182
|
+
export function renderWithLayouts(PageComponent, props = {}, layoutName = null) {
|
|
183
|
+
// Sayfa meta'sından layout adını belirle
|
|
184
|
+
const metaLayout = PageComponent.pageMeta?.layout
|
|
185
|
+
?? PageComponent.__clarity_page_meta__?.layout
|
|
186
|
+
?? null;
|
|
187
|
+
|
|
188
|
+
const finalLayout = layoutName ?? metaLayout ?? 'default';
|
|
189
|
+
const chain = buildLayoutChain(finalLayout);
|
|
190
|
+
|
|
191
|
+
const Wrapped = wrapWithLayouts(PageComponent, chain);
|
|
192
|
+
return renderToString(Wrapped, { props });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── Dosya sistemi layout tarayıcı ───────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* `pages/` dizin yapısını tarayıp _layout.js dosyalarını bulur,
|
|
199
|
+
* otomatik olarak defineLayout() ile kaydeder.
|
|
200
|
+
*
|
|
201
|
+
* File-router ve SSG pipeline'da çağrılır.
|
|
202
|
+
*
|
|
203
|
+
* @param {string} pagesDir – Mutlak yol
|
|
204
|
+
* @param {string} [rootName='default']
|
|
205
|
+
* @returns {Promise<string[]>} Kaydedilen layout adları
|
|
206
|
+
*/
|
|
207
|
+
export async function scanAndRegisterLayouts(pagesDir, rootName = 'default') {
|
|
208
|
+
const { readdir, stat } = await import('node:fs/promises');
|
|
209
|
+
const { existsSync } = await import('node:fs');
|
|
210
|
+
const { join, relative, dirname } = await import('node:path');
|
|
211
|
+
const { pathToFileURL } = await import('node:url');
|
|
212
|
+
|
|
213
|
+
const registered = [];
|
|
214
|
+
|
|
215
|
+
async function scan(dir, parentName) {
|
|
216
|
+
if (!existsSync(dir)) return;
|
|
217
|
+
|
|
218
|
+
const entries = await readdir(dir).catch(() => []);
|
|
219
|
+
|
|
220
|
+
// _layout.js bu dizinde var mı?
|
|
221
|
+
const layoutFile = entries.find(e => e === '_layout.js' || e === '_layout.mjs');
|
|
222
|
+
if (layoutFile) {
|
|
223
|
+
const fullPath = join(dir, layoutFile);
|
|
224
|
+
const relPath = relative(pagesDir, dir) || '.';
|
|
225
|
+
const layoutName = relPath === '.' ? rootName : relPath.replace(/\\/g, '/');
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const mod = await import(pathToFileURL(fullPath).href + `?t=${Date.now()}`);
|
|
229
|
+
const ComponentFn = mod.default;
|
|
230
|
+
if (typeof ComponentFn === 'function') {
|
|
231
|
+
defineLayout(layoutName, ComponentFn, { parent: parentName !== layoutName ? parentName : null });
|
|
232
|
+
registered.push(layoutName);
|
|
233
|
+
console.log(`[clarity/layout] Layout kaydedildi: '${layoutName}' (parent: ${parentName ?? 'none'})`);
|
|
234
|
+
}
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.warn(`[clarity/layout] Layout yüklenemedi (${fullPath}): ${err.message}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Alt dizinleri bu layout'u parent olarak kullanarak tara
|
|
240
|
+
for (const entry of entries) {
|
|
241
|
+
const fullEntry = join(dir, entry);
|
|
242
|
+
const info = await stat(fullEntry).catch(() => null);
|
|
243
|
+
if (info?.isDirectory()) {
|
|
244
|
+
await scan(fullEntry, layoutName);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
// Bu dizinde layout yok — alt dizinleri parent ile tara
|
|
249
|
+
for (const entry of entries) {
|
|
250
|
+
const fullEntry = join(dir, entry);
|
|
251
|
+
const info = await stat(fullEntry).catch(() => null);
|
|
252
|
+
if (info?.isDirectory()) {
|
|
253
|
+
await scan(fullEntry, parentName);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
await scan(pagesDir, null);
|
|
260
|
+
return registered;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ─── Client-side: layout route değişimlerinde uygula ─────────────────────────
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Client router ile entegrasyon — route değiştiğinde layout'u güncelle.
|
|
267
|
+
* `hydrateRoot` ve router `navigate` ile birlikte çalışır.
|
|
268
|
+
*
|
|
269
|
+
* @param {object} opts
|
|
270
|
+
* @param {HTMLElement} opts.container – Layout wrapper DOM node
|
|
271
|
+
* @param {Function} opts.getPage – (path: string) => { ComponentFn, layoutName }
|
|
272
|
+
*/
|
|
273
|
+
export function initLayoutRouter(opts = {}) {
|
|
274
|
+
const { container, getPage } = opts;
|
|
275
|
+
if (typeof window === 'undefined') return;
|
|
276
|
+
|
|
277
|
+
function handleRouteChange() {
|
|
278
|
+
const path = window.location.pathname;
|
|
279
|
+
const page = getPage(path);
|
|
280
|
+
if (!page) return;
|
|
281
|
+
|
|
282
|
+
const { ComponentFn, layoutName } = page;
|
|
283
|
+
const chain = buildLayoutChain(layoutName ?? 'default');
|
|
284
|
+
const Wrapped = wrapWithLayouts(ComponentFn, chain);
|
|
285
|
+
|
|
286
|
+
// Container'ı temizle ve yeniden render et
|
|
287
|
+
if (container) {
|
|
288
|
+
// Clarity runtime ile hydrate
|
|
289
|
+
import('./hydrate.js').then(({ hydrateRoot }) => {
|
|
290
|
+
hydrateRoot(Wrapped, container, {});
|
|
291
|
+
}).catch(() => {
|
|
292
|
+
// Fallback: innerHTML (SSR olmadan)
|
|
293
|
+
const html = renderWithLayouts(ComponentFn, {}, layoutName);
|
|
294
|
+
container.innerHTML = html;
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
window.addEventListener('popstate', handleRouteChange);
|
|
300
|
+
window.addEventListener('clarity:navigate', handleRouteChange);
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
destroy: () => {
|
|
304
|
+
window.removeEventListener('popstate', handleRouteChange);
|
|
305
|
+
window.removeEventListener('clarity:navigate', handleRouteChange);
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ─── Built-in layout bileşenleri ─────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Boş layout — sadece children render eder, ek sarmalama yok.
|
|
314
|
+
* `layout: 'none'` veya doğrudan kullananlar için.
|
|
315
|
+
*/
|
|
316
|
+
export function BlankLayout({ children }) {
|
|
317
|
+
return children;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Gerektiğinde kullanılabilecek basit HTML shell layout.
|
|
322
|
+
* SSR root layout olarak kullanılabilir.
|
|
323
|
+
*
|
|
324
|
+
* @param {object} props
|
|
325
|
+
* @param {*} props.children
|
|
326
|
+
* @param {string} [props.lang='tr']
|
|
327
|
+
* @param {string} [props.title]
|
|
328
|
+
* @param {string} [props.bodyClass]
|
|
329
|
+
*/
|
|
330
|
+
export function HTMLShellLayout({ children, lang = 'tr', title = '', bodyClass = '' }) {
|
|
331
|
+
// Bu bileşen SSR ortamında kullanılır — string tabanlı çıktı için basit tag üretimi
|
|
332
|
+
if (typeof document === 'undefined') {
|
|
333
|
+
// SSR ortam
|
|
334
|
+
return {
|
|
335
|
+
__clarity_html_shell__: true,
|
|
336
|
+
lang, title, bodyClass,
|
|
337
|
+
content: children,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
// Client: sadece children
|
|
341
|
+
return children;
|
|
342
|
+
}
|