@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/sourcemap.js
ADDED
|
@@ -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
|
+
}
|