@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,130 @@
1
+ /**
2
+ * Clarity.js Source Map Generator
3
+ *
4
+ * Produces V3 source maps that map generated JS lines back to .clarity source.
5
+ * The codegen embeds "//@ SM:N" marker comments at key points.
6
+ * This module strips those markers and builds the final source map.
7
+ *
8
+ * V3 source map format: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/
9
+ *
10
+ * Author: Claude (Anthropic)
11
+ */
12
+
13
+ // ─── VLQ Encoding ─────────────────────────────────────────────────────────────
14
+ // Base64 alphabet
15
+ const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
16
+
17
+ /**
18
+ * Encode a single signed integer as VLQ.
19
+ * Each digit is 5 bits; the high bit is a "continue" flag.
20
+ * The lowest bit of the first digit encodes the sign.
21
+ */
22
+ function vlqEncode(n) {
23
+ // Convert to sign-magnitude VLQ format
24
+ let vlq = n < 0 ? ((-n) << 1) | 1 : (n << 1);
25
+ let out = '';
26
+ do {
27
+ let digit = vlq & 0x1F; // take 5 bits
28
+ vlq >>>= 5;
29
+ if (vlq > 0) digit |= 0x20; // set continuation bit
30
+ out += B64[digit];
31
+ } while (vlq > 0);
32
+ return out;
33
+ }
34
+
35
+ /**
36
+ * Encode an array of segments (one generated line's mappings).
37
+ * Each segment is [genCol, srcFileIdx, srcLine, srcCol] (all 0-indexed, relative).
38
+ */
39
+ function encodeSegments(segments) {
40
+ let out = '';
41
+ let prevGenCol = 0, prevSrcFile = 0, prevSrcLine = 0, prevSrcCol = 0;
42
+
43
+ for (const [genCol, srcFile, srcLine, srcCol] of segments) {
44
+ out += vlqEncode(genCol - prevGenCol);
45
+ out += vlqEncode(srcFile - prevSrcFile);
46
+ out += vlqEncode(srcLine - prevSrcLine);
47
+ out += vlqEncode(srcCol - prevSrcCol);
48
+ prevGenCol = genCol;
49
+ prevSrcFile = srcFile;
50
+ prevSrcLine = srcLine;
51
+ prevSrcCol = srcCol;
52
+ }
53
+ return out;
54
+ }
55
+
56
+ // ─── Source Map Builder ───────────────────────────────────────────────────────
57
+
58
+ /**
59
+ * Process code that contains "//@ SM:N" marker lines.
60
+ *
61
+ * The codegen embeds these markers right before lines that trace to source line N.
62
+ * This function:
63
+ * 1. Strips all SM markers from the code
64
+ * 2. Uses them to build a V3 source map (line-level precision)
65
+ * 3. Optionally inlines the source map as a data URL
66
+ *
67
+ * Returns: { code, map } where map is the V3 source map object.
68
+ */
69
+ export function processSourceMarkers(annotatedCode, options = {}) {
70
+ const {
71
+ filename = '<anonymous>', // output .js filename
72
+ sourceFile = '<source>', // input .clarity filename
73
+ sourceContent = '', // input source text (for sourcesContent)
74
+ inline = true, // embed map as data URL at bottom of code
75
+ } = options;
76
+
77
+ const MARKER_RE = /^\s*\/\/@ SM:(\d+)\s*$/; // allow indented markers
78
+ const inputLines = annotatedCode.split('\n');
79
+ const cleanLines = []; // output lines (markers removed)
80
+ const lineMappings = []; // cleanLine index → source line (1-indexed), -1 = no mapping
81
+
82
+ let currentSrcLine = -1; // last seen SM marker value
83
+
84
+ for (const line of inputLines) {
85
+ const m = line.match(MARKER_RE);
86
+ if (m) {
87
+ currentSrcLine = parseInt(m[1], 10);
88
+ // Don't emit marker line to output
89
+ } else {
90
+ cleanLines.push(line);
91
+ lineMappings.push(currentSrcLine);
92
+ }
93
+ }
94
+
95
+ // ── Build V3 mappings string ────────────────────────────────────────────────
96
+ // For each output line, emit one segment (column 0 mapped to source line).
97
+ // Lines with no mapping emit an empty group.
98
+ const mappingGroups = lineMappings.map((srcLine) => {
99
+ if (srcLine < 0) return '';
100
+ // Segment: [genCol=0, srcFile=0, srcLine (0-indexed), srcCol=0]
101
+ return encodeSegments([[0, 0, srcLine - 1, 0]]);
102
+ });
103
+
104
+ const map = {
105
+ version: 3,
106
+ file: filename,
107
+ sourceRoot: '',
108
+ sources: [sourceFile],
109
+ sourcesContent: [sourceContent],
110
+ mappings: mappingGroups.join(';'),
111
+ };
112
+
113
+ const cleanCode = cleanLines.join('\n');
114
+
115
+ if (inline) {
116
+ const b64 = Buffer.from(JSON.stringify(map)).toString('base64');
117
+ const annotated = cleanCode + `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
118
+ return { code: annotated, map };
119
+ }
120
+
121
+ return { code: cleanCode, map };
122
+ }
123
+
124
+ /**
125
+ * Write a .map sidecar file for a given JS file.
126
+ * Usage: writeMapFile('/path/to/out.js', map)
127
+ */
128
+ export function buildMapComment(mapFilename) {
129
+ return `//# sourceMappingURL=${mapFilename}`;
130
+ }
package/src/ssg.js ADDED
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Clarity.js — Static Site Generation (SSG)
3
+ *
4
+ * Build-time HTML üretimi. Next.js getStaticProps / Nuxt generate eşdeğeri.
5
+ *
6
+ * ── Kullanım (pages/ konvansiyonu ile) ───────────────────────────────────────
7
+ *
8
+ * // pages/blog/[slug].js
9
+ * export default function BlogPost({ slug, post }) { ... }
10
+ *
11
+ * export async function getStaticPaths() {
12
+ * const posts = await db.posts.findAll();
13
+ * return posts.map(p => ({ params: { slug: p.slug } }));
14
+ * }
15
+ *
16
+ * export async function getStaticProps({ params }) {
17
+ * const post = await db.posts.findBySlug(params.slug);
18
+ * return { props: { post }, revalidate: 3600 }; // ISR için revalidate
19
+ * }
20
+ *
21
+ * ── CLI kullanımı ─────────────────────────────────────────────────────────────
22
+ *
23
+ * clarity ssg # dist/ klasörüne statik HTML üret
24
+ * clarity ssg ./my-app --out dist # özel çıktı klasörü
25
+ *
26
+ * ── Programatik kullanım ──────────────────────────────────────────────────────
27
+ *
28
+ * import { generateStaticSite } from '@ozsarman/clarityjs/ssg';
29
+ *
30
+ * await generateStaticSite({
31
+ * pagesDir: './pages',
32
+ * outDir: './dist',
33
+ * baseUrl: 'https://mysite.com',
34
+ * concurrent: 4,
35
+ * });
36
+ *
37
+ * Author: Claude (Anthropic) + Özdemir Sarman
38
+ */
39
+
40
+ import { readdir, stat, mkdir, writeFile, copyFile, readFile } from 'node:fs/promises';
41
+ import { existsSync, mkdirSync } from 'node:fs';
42
+ import { join, relative, extname, basename, dirname, resolve } from 'node:path';
43
+ import { pathToFileURL } from 'node:url';
44
+
45
+ // ─── Public API ───────────────────────────────────────────────────────────────
46
+
47
+ /**
48
+ * Statik site oluştur — pages/ dizinini tarar, her sayfa için HTML üretir.
49
+ *
50
+ * @param {object} opts
51
+ * @param {string} opts.pagesDir – sayfa dosyalarının dizini (default: 'pages')
52
+ * @param {string} opts.outDir – HTML çıktı dizini (default: 'dist')
53
+ * @param {string} opts.publicDir – kopyalanacak statik dosyalar (default: 'public')
54
+ * @param {string} [opts.baseUrl] – canonical URL tabanı
55
+ * @param {string} [opts.title] – varsayılan sayfa başlığı
56
+ * @param {string} [opts.clientScript] – client JS bundle yolu (dist/app.js gibi)
57
+ * @param {number} [opts.concurrent=4] – eş zamanlı render sayısı
58
+ * @param {boolean} [opts.verbose] – ayrıntılı log
59
+ * @returns {Promise<SSGResult>}
60
+ */
61
+ export async function generateStaticSite(opts = {}) {
62
+ const {
63
+ pagesDir = 'pages',
64
+ outDir = 'dist',
65
+ publicDir = 'public',
66
+ baseUrl = '',
67
+ title = '',
68
+ clientScript = null,
69
+ concurrent = 4,
70
+ verbose = false,
71
+ } = opts;
72
+
73
+ const pagesDirAbs = resolve(pagesDir);
74
+ const outDirAbs = resolve(outDir);
75
+ const publicDirAbs = resolve(publicDir);
76
+
77
+ const log = (...args) => verbose && console.log('[clarity/ssg]', ...args);
78
+ const t0 = Date.now();
79
+
80
+ // 1. Çıktı dizinini oluştur
81
+ await mkdir(outDirAbs, { recursive: true });
82
+
83
+ // 2. pages/ altındaki tüm dosyaları tara
84
+ const pageFiles = await _scanPages(pagesDirAbs);
85
+ log(`${pageFiles.length} sayfa bulundu`);
86
+
87
+ // 3. Her sayfa dosyasını yükle ve statik yolları topla
88
+ const renderJobs = [];
89
+
90
+ for (const file of pageFiles) {
91
+ const mod = await import(pathToFileURL(file).href + `?bust=${Date.now()}`);
92
+ const routePath = _fileToRoutePath(pagesDirAbs, file);
93
+ const isApiFile = routePath.startsWith('/api/') ||
94
+ ['GET','POST','PUT','PATCH','DELETE'].some(m => typeof mod[m] === 'function');
95
+
96
+ if (isApiFile) continue; // API route'ları dahil etme
97
+
98
+ if (typeof mod.getStaticPaths === 'function') {
99
+ // Dinamik rota — her path için ayrı job
100
+ const paths = await mod.getStaticPaths();
101
+ for (const entry of paths) {
102
+ const params = entry.params ?? {};
103
+ const resolved = _resolveRoute(routePath, params);
104
+ renderJobs.push({ file, mod, routePath: resolved, params, isDynamic: true });
105
+ }
106
+ } else {
107
+ // Statik rota
108
+ renderJobs.push({ file, mod, routePath, params: {}, isDynamic: false });
109
+ }
110
+ }
111
+
112
+ log(`${renderJobs.length} HTML dosyası oluşturulacak`);
113
+
114
+ // 4. Paralel render — max `concurrent` eş zamanlı
115
+ const results = [];
116
+ const isr_data = {}; // revalidate için metadata
117
+
118
+ for (let i = 0; i < renderJobs.length; i += concurrent) {
119
+ const batch = renderJobs.slice(i, i + concurrent);
120
+ const batch_results = await Promise.all(batch.map(job =>
121
+ _renderJob(job, { outDirAbs, baseUrl, title, clientScript, log, isr_data })
122
+ ));
123
+ results.push(...batch_results);
124
+ }
125
+
126
+ // 5. ISR metadata dosyasını yaz (ISR modülü okur)
127
+ if (Object.keys(isr_data).length > 0) {
128
+ await writeFile(
129
+ join(outDirAbs, '_clarity_isr.json'),
130
+ JSON.stringify(isr_data, null, 2)
131
+ );
132
+ log('ISR metadata yazıldı → _clarity_isr.json');
133
+ }
134
+
135
+ // 6. public/ klasörünü kopyala
136
+ if (existsSync(publicDirAbs)) {
137
+ await _copyDir(publicDirAbs, outDirAbs, log);
138
+ }
139
+
140
+ const elapsed = Date.now() - t0;
141
+ const ok = results.filter(r => r.success).length;
142
+ const fail = results.filter(r => !r.success).length;
143
+
144
+ console.log(`[clarity/ssg] ✅ ${ok} sayfa oluşturuldu${fail ? `, ❌ ${fail} hata` : ''} (${elapsed}ms)`);
145
+
146
+ return { pages: results, isrData: isr_data, outDir: outDirAbs, elapsed };
147
+ }
148
+
149
+ // ─── getStaticProps / getStaticPaths helpers ──────────────────────────────────
150
+
151
+ /**
152
+ * Tek bir sayfa için statik props çek.
153
+ * Sayfa dosyasında `export async function getStaticProps({ params })` beklenir.
154
+ *
155
+ * @param {string} file – sayfa dosyasının mutlak yolu
156
+ * @param {object} params – URL parametreleri (dinamik rotalar için)
157
+ * @returns {Promise<{ props: object, revalidate?: number, notFound?: boolean }>}
158
+ */
159
+ export async function fetchStaticProps(file, params = {}) {
160
+ const mod = await import(pathToFileURL(file).href);
161
+ if (typeof mod.getStaticProps !== 'function') return { props: {} };
162
+
163
+ const result = await mod.getStaticProps({ params });
164
+ return result ?? { props: {} };
165
+ }
166
+
167
+ /**
168
+ * Dinamik rota için statik yolları çek.
169
+ * Sayfa dosyasında `export async function getStaticPaths()` beklenir.
170
+ *
171
+ * @param {string} file – sayfa dosyasının mutlak yolu
172
+ * @returns {Promise<Array<{ params: object }>>}
173
+ */
174
+ export async function fetchStaticPaths(file) {
175
+ const mod = await import(pathToFileURL(file).href);
176
+ if (typeof mod.getStaticPaths !== 'function') return [];
177
+ return await mod.getStaticPaths();
178
+ }
179
+
180
+ // ─── SSG Context helper ───────────────────────────────────────────────────────
181
+
182
+ /**
183
+ * Server tarafında SSG context'i oluştur.
184
+ * Component'ler içinde kullanılabilir (server-side only).
185
+ *
186
+ * @param {object} params – URL parametreleri
187
+ * @param {object} props – getStaticProps'tan gelen props
188
+ * @returns {object}
189
+ */
190
+ export function createSSGContext(params = {}, props = {}) {
191
+ return { params, props, isSSG: true, isSSR: false };
192
+ }
193
+
194
+ // ─── Internal: render tek bir sayfa ──────────────────────────────────────────
195
+
196
+ async function _renderJob(job, { outDirAbs, baseUrl, title, clientScript, log, isr_data }) {
197
+ const { file, mod, routePath, params } = job;
198
+
199
+ try {
200
+ // getStaticProps'u çağır
201
+ let pageProps = params;
202
+ let revalidate = null;
203
+ let notFound = false;
204
+
205
+ if (typeof mod.getStaticProps === 'function') {
206
+ const result = await mod.getStaticProps({ params });
207
+ if (result?.notFound) {
208
+ notFound = true;
209
+ } else {
210
+ pageProps = { ...params, ...(result?.props ?? {}) };
211
+ revalidate = result?.revalidate ?? null;
212
+ }
213
+ }
214
+
215
+ if (notFound) {
216
+ log(`SKIP (notFound) → ${routePath}`);
217
+ return { routePath, success: true, notFound: true };
218
+ }
219
+
220
+ // Component'i render et
221
+ const { renderToDocument } = await import('./ssr.js');
222
+ const pageTitle = mod.meta?.title ?? title ?? '';
223
+ const html = renderToDocument(mod.default, {
224
+ props: pageProps,
225
+ title: pageTitle,
226
+ clientScript,
227
+ lang: 'tr',
228
+ // SSG metadata: initial data gömülü
229
+ inlineData: { __SSG_PROPS__: pageProps, __SSG_PATH__: routePath },
230
+ });
231
+
232
+ // Dosya yolunu belirle
233
+ const filePath = _routeToFilePath(outDirAbs, routePath);
234
+ await mkdir(dirname(filePath), { recursive: true });
235
+ await writeFile(filePath, html, 'utf8');
236
+
237
+ // ISR metadata
238
+ if (revalidate) {
239
+ isr_data[routePath] = {
240
+ revalidate,
241
+ file: relative(process.cwd(), file),
242
+ params,
243
+ generatedAt: Date.now(),
244
+ expiresAt: Date.now() + revalidate * 1000,
245
+ };
246
+ }
247
+
248
+ log(`✅ ${routePath} → ${relative(outDirAbs, filePath)}`);
249
+ return { routePath, success: true, filePath, revalidate };
250
+
251
+ } catch (err) {
252
+ console.error(`[clarity/ssg] ❌ ${routePath}: ${err.message}`);
253
+ return { routePath, success: false, error: err.message };
254
+ }
255
+ }
256
+
257
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
258
+
259
+ async function _scanPages(dir, files = []) {
260
+ if (!existsSync(dir)) return files;
261
+ const entries = await readdir(dir);
262
+ for (const entry of entries) {
263
+ if (entry.startsWith('_') || entry.startsWith('.')) continue;
264
+ const full = join(dir, entry);
265
+ const info = await stat(full);
266
+ if (info.isDirectory()) {
267
+ await _scanPages(full, files);
268
+ } else if (/\.(js|mjs|clarity)$/.test(entry)) {
269
+ files.push(full);
270
+ }
271
+ }
272
+ return files;
273
+ }
274
+
275
+ function _fileToRoutePath(pagesDir, file) {
276
+ const rel = relative(pagesDir, file).replace(/\\/g, '/');
277
+ const name = rel.replace(/\.(js|mjs|clarity)$/, '');
278
+ if (name === 'index') return '/';
279
+ return '/' + name
280
+ .split('/')
281
+ .map(seg => seg === 'index' ? '' : seg.replace(/^\[\.\.\.(.+)\]$/, ':$1*').replace(/^\[(.+)\]$/, ':$1'))
282
+ .filter(Boolean)
283
+ .join('/');
284
+ }
285
+
286
+ function _resolveRoute(routePattern, params) {
287
+ return routePattern.replace(/:(\w+)\*?/g, (_, key) => params[key] ?? `[${key}]`);
288
+ }
289
+
290
+ function _routeToFilePath(outDir, routePath) {
291
+ if (routePath === '/') return join(outDir, 'index.html');
292
+ const clean = routePath.replace(/^\//, '');
293
+ return join(outDir, clean, 'index.html');
294
+ }
295
+
296
+ async function _copyDir(src, dest, log) {
297
+ const entries = await readdir(src);
298
+ for (const entry of entries) {
299
+ const srcFull = join(src, entry);
300
+ const destFull = join(dest, entry);
301
+ const info = await stat(srcFull);
302
+ if (info.isDirectory()) {
303
+ await mkdir(destFull, { recursive: true });
304
+ await _copyDir(srcFull, destFull, log);
305
+ } else {
306
+ await copyFile(srcFull, destFull);
307
+ log(`copy public/ → ${entry}`);
308
+ }
309
+ }
310
+ }