@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
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clarity.js — File-Based Pages Router (browser runtime + pure helpers)
|
|
3
|
+
*
|
|
4
|
+
* This is the browser-side counterpart to the Vite `scanPages` integration.
|
|
5
|
+
* The Vite plugin scans `pages/` at dev/build time and emits a virtual module
|
|
6
|
+
* (`virtual:clarity-pages`) containing a lazy route table; this module turns
|
|
7
|
+
* that table into a working, code-split router.
|
|
8
|
+
*
|
|
9
|
+
* import { Routes } from 'virtual:clarity-pages';
|
|
10
|
+
* import { mount } from '@ozsarman/clarityjs';
|
|
11
|
+
* mount(Routes, document.getElementById('app'));
|
|
12
|
+
*
|
|
13
|
+
* File → route convention (Next.js / SvelteKit style):
|
|
14
|
+
*
|
|
15
|
+
* pages/index.clarity → /
|
|
16
|
+
* pages/about.clarity → /about
|
|
17
|
+
* pages/blog/index.clarity → /blog
|
|
18
|
+
* pages/blog/[slug].clarity → /blog/:slug
|
|
19
|
+
* pages/users/[id]/edit.clarity → /users/:id/edit
|
|
20
|
+
* pages/[...all].clarity → /* (catch-all)
|
|
21
|
+
*
|
|
22
|
+
* The pure helpers (filePathToRoutePattern, routeScore, sortRoutes,
|
|
23
|
+
* selectRoute) have no DOM or Node dependencies and are unit-tested directly.
|
|
24
|
+
*
|
|
25
|
+
* Author: Claude (Anthropic) + Özdemir Sarman
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { matchRoute, currentPath } from './router.js';
|
|
29
|
+
import { signal, effect, _runMountHooks, _runCleanupHooks } from './runtime.js';
|
|
30
|
+
|
|
31
|
+
// ─── Pure helpers ─────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Convert a page file path (relative to the pages dir) into a route pattern.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} relPath e.g. 'blog/[slug].clarity', 'index.js'
|
|
37
|
+
* @returns {string} e.g. '/blog/:slug', '/'
|
|
38
|
+
*/
|
|
39
|
+
export function filePathToRoutePattern(relPath) {
|
|
40
|
+
const stripped = String(relPath)
|
|
41
|
+
.replace(/\\/g, '/')
|
|
42
|
+
.replace(/^\.?\//, '')
|
|
43
|
+
.replace(/\.(clarity|jsx?|mjs|tsx?)$/i, '');
|
|
44
|
+
|
|
45
|
+
const segments = stripped.split('/').filter(Boolean);
|
|
46
|
+
const out = [];
|
|
47
|
+
|
|
48
|
+
for (const seg of segments) {
|
|
49
|
+
// index maps onto its parent directory (no extra segment)
|
|
50
|
+
if (seg === 'index') continue;
|
|
51
|
+
|
|
52
|
+
// Catch-all: [...name] → *
|
|
53
|
+
const rest = seg.match(/^\[\.\.\.(.+)\]$/);
|
|
54
|
+
if (rest) { out.push('*'); continue; }
|
|
55
|
+
|
|
56
|
+
// Dynamic: [name] → :name
|
|
57
|
+
const dyn = seg.match(/^\[(.+)\]$/);
|
|
58
|
+
if (dyn) { out.push(':' + dyn[1]); continue; }
|
|
59
|
+
|
|
60
|
+
out.push(seg);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const pattern = '/' + out.join('/');
|
|
64
|
+
return pattern === '/' ? '/' : pattern.replace(/\/$/, '');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Specificity score for a route pattern — higher = more specific.
|
|
69
|
+
* Static segments outrank dynamic params, which outrank catch-all wildcards.
|
|
70
|
+
* Used to decide which route wins when several could match.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} pattern
|
|
73
|
+
* @returns {number}
|
|
74
|
+
*/
|
|
75
|
+
export function routeScore(pattern) {
|
|
76
|
+
const segs = String(pattern).split('/').filter(Boolean);
|
|
77
|
+
let score = 0;
|
|
78
|
+
for (const s of segs) {
|
|
79
|
+
if (s === '*') score -= 1000; // catch-all — always least specific,
|
|
80
|
+
// even below the root '/' (score 0)
|
|
81
|
+
else if (s.startsWith(':')) score += 10; // dynamic param
|
|
82
|
+
else score += 100; // static segment — most specific
|
|
83
|
+
}
|
|
84
|
+
return score;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Return a new array of routes sorted most-specific first.
|
|
89
|
+
* @param {Array<{pattern: string}>} routes
|
|
90
|
+
*/
|
|
91
|
+
export function sortRoutes(routes) {
|
|
92
|
+
return [...routes].sort((a, b) => routeScore(b.pattern) - routeScore(a.pattern));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Select the best-matching route for a path.
|
|
97
|
+
* @param {Array<{pattern: string}>} routes
|
|
98
|
+
* @param {string} path
|
|
99
|
+
* @returns {{ route: object, params: Record<string,string> } | null}
|
|
100
|
+
*/
|
|
101
|
+
export function selectRoute(routes, path) {
|
|
102
|
+
for (const route of sortRoutes(routes)) {
|
|
103
|
+
const params = matchRoute(route.pattern, path);
|
|
104
|
+
if (params) return { route, params };
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build a sorted route table from a list of page file paths.
|
|
111
|
+
* @param {string[]} files page file paths (relative to pages dir)
|
|
112
|
+
* @param {(file: string) => any} [loaderFor] optional: returns a lazy loader per file
|
|
113
|
+
* @returns {Array<{ pattern: string, file: string, load?: Function }>}
|
|
114
|
+
*/
|
|
115
|
+
export function buildRouteTable(files, loaderFor) {
|
|
116
|
+
const routes = files.map(file => {
|
|
117
|
+
const route = { pattern: filePathToRoutePattern(file), file };
|
|
118
|
+
if (typeof loaderFor === 'function') route.load = loaderFor(file);
|
|
119
|
+
return route;
|
|
120
|
+
});
|
|
121
|
+
return sortRoutes(routes);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Browser router component ─────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
/** Pick the Clarity component export from a loaded page module. */
|
|
127
|
+
function _pickComponent(mod) {
|
|
128
|
+
if (!mod) return null;
|
|
129
|
+
if (typeof mod.default === 'function') return mod.default;
|
|
130
|
+
// Fall back to the first function export (named component)
|
|
131
|
+
for (const v of Object.values(mod)) {
|
|
132
|
+
if (typeof v === 'function') return v;
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Create a file-based pages router component.
|
|
139
|
+
*
|
|
140
|
+
* @param {object} options
|
|
141
|
+
* @param {Array<{ pattern, file, load }>} options.routes route table (each `load` returns Promise<module>)
|
|
142
|
+
* @param {Function} [options.loading] component shown while a page chunk loads
|
|
143
|
+
* @param {Function} [options.notFound] component shown when no route matches
|
|
144
|
+
* @param {Function} [options.error] component shown when a chunk fails to load — receives { error }
|
|
145
|
+
* @returns {Function} a Clarity component
|
|
146
|
+
*/
|
|
147
|
+
export function createPagesRouter(options = {}) {
|
|
148
|
+
const {
|
|
149
|
+
routes = [],
|
|
150
|
+
loading: LoadingComp = null,
|
|
151
|
+
notFound: NotFoundComp = null,
|
|
152
|
+
error: ErrorComp = null,
|
|
153
|
+
} = options;
|
|
154
|
+
|
|
155
|
+
const sorted = sortRoutes(routes);
|
|
156
|
+
const _moduleCache = new Map(); // pattern → resolved component
|
|
157
|
+
|
|
158
|
+
return function PagesRouter() {
|
|
159
|
+
const host = (typeof document !== 'undefined')
|
|
160
|
+
? document.createElement('div')
|
|
161
|
+
: { nodeType: 1, _children: [], replaceChildren(...n) { this._children = n; }, append(...n) { this._children.push(...n); } };
|
|
162
|
+
host.setAttribute?.('data-clarity-pages', '');
|
|
163
|
+
|
|
164
|
+
// Track the currently displayed nodes so we can run their cleanup hooks
|
|
165
|
+
// (stop game loops, dispose effects) before swapping in a new page.
|
|
166
|
+
let _current = [];
|
|
167
|
+
|
|
168
|
+
// Render a node (or component result) into the host, replacing prior content.
|
|
169
|
+
const renderInto = (node) => {
|
|
170
|
+
for (const n of _current) _runCleanupHooks(n);
|
|
171
|
+
_current = [];
|
|
172
|
+
|
|
173
|
+
if (node == null) { host.replaceChildren?.(); return; }
|
|
174
|
+
const resolved = typeof node === 'function' ? node() : node;
|
|
175
|
+
const arr = Array.isArray(resolved) ? resolved : [resolved];
|
|
176
|
+
host.replaceChildren?.(...arr);
|
|
177
|
+
|
|
178
|
+
// Fire onMount across the freshly-inserted subtree (nested components too).
|
|
179
|
+
for (const n of arr) {
|
|
180
|
+
if (n && typeof n === 'object' && n.nodeType) {
|
|
181
|
+
_runMountHooks(n);
|
|
182
|
+
_current.push(n);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const showLoading = () => { if (LoadingComp) renderInto(LoadingComp); };
|
|
188
|
+
const showNotFound = (path) => { if (NotFoundComp) renderInto(() => NotFoundComp({ path })); };
|
|
189
|
+
const showError = (err) => {
|
|
190
|
+
if (ErrorComp) renderInto(() => ErrorComp({ error: err }));
|
|
191
|
+
else console.error('[Clarity pages] failed to load route chunk:', err);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// React to path changes — select + lazy-load + render the matched page.
|
|
195
|
+
effect(() => {
|
|
196
|
+
const path = currentPath.get ? currentPath.get() : currentPath();
|
|
197
|
+
const match = selectRoute(sorted, path);
|
|
198
|
+
|
|
199
|
+
if (!match) { showNotFound(path); return; }
|
|
200
|
+
|
|
201
|
+
const { route, params } = match;
|
|
202
|
+
|
|
203
|
+
// Cached component → render synchronously.
|
|
204
|
+
if (_moduleCache.has(route.pattern)) {
|
|
205
|
+
const Comp = _moduleCache.get(route.pattern);
|
|
206
|
+
renderInto(() => Comp({ params }));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Not loaded yet → show loading, then lazy-import the chunk.
|
|
211
|
+
showLoading();
|
|
212
|
+
const loader = typeof route.load === 'function' ? route.load : null;
|
|
213
|
+
if (!loader) { showError(new Error(`Route ${route.pattern} has no loader`)); return; }
|
|
214
|
+
|
|
215
|
+
Promise.resolve(loader())
|
|
216
|
+
.then(mod => {
|
|
217
|
+
const Comp = _pickComponent(mod);
|
|
218
|
+
if (!Comp) throw new Error(`Route ${route.pattern} module has no component export`);
|
|
219
|
+
_moduleCache.set(route.pattern, Comp);
|
|
220
|
+
// Guard against a path change while we were loading.
|
|
221
|
+
const now = currentPath.get ? currentPath.get() : currentPath();
|
|
222
|
+
if (matchRoute(route.pattern, now)) renderInto(() => Comp({ params }));
|
|
223
|
+
})
|
|
224
|
+
.catch(showError);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return host;
|
|
228
|
+
};
|
|
229
|
+
}
|