@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,641 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clarity.js — Scoped CSS Processor
|
|
3
|
+
*
|
|
4
|
+
* Extracts <style scoped> blocks from .clarity source files and transforms
|
|
5
|
+
* them into component-scoped CSS using a unique data attribute.
|
|
6
|
+
*
|
|
7
|
+
* How it works:
|
|
8
|
+
* 1. Extract the <style scoped> block from source
|
|
9
|
+
* 2. Generate a stable hash (e.g. "data-c-3f8a1b") from the file path
|
|
10
|
+
* 3. Rewrite every CSS selector to include the scoped attribute:
|
|
11
|
+
* .card { ... } → .card[data-c-3f8a1b] { ... }
|
|
12
|
+
* 4. Inject the data attribute into the root element of the compiled component
|
|
13
|
+
*
|
|
14
|
+
* The Vite plugin (vite-plugin.js) calls this during transform.
|
|
15
|
+
* The CodeGenerator receives the scopeId and injects data-c-{id} automatically.
|
|
16
|
+
*
|
|
17
|
+
* Non-scoped <style> blocks are returned as-is (global CSS).
|
|
18
|
+
*
|
|
19
|
+
* Usage (from Vite plugin / build tool):
|
|
20
|
+
*
|
|
21
|
+
* import { extractStyles, scopeCSS, hashId } from '@ozsarman/clarityjs/scoped-css';
|
|
22
|
+
*
|
|
23
|
+
* const { scoped, global, scopeId } = extractStyles(source, filename);
|
|
24
|
+
* const transformedCSS = scopeCSS(scoped, scopeId);
|
|
25
|
+
* // Inject scopeId into codegen options so root element gets data-c-{scopeId}
|
|
26
|
+
*
|
|
27
|
+
* Author: Claude (Anthropic) + Özdemir Sarman
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// ─── hashId ──────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generate a short, stable 6-char hex hash from a string (e.g. file path).
|
|
34
|
+
* Uses a simple djb2-style hash — collision probability is negligible for
|
|
35
|
+
* the number of components in a typical application.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} str
|
|
38
|
+
* @returns {string} 6-character hex string e.g. "3f8a1b"
|
|
39
|
+
*/
|
|
40
|
+
export function hashId(str) {
|
|
41
|
+
let h = 5381;
|
|
42
|
+
for (let i = 0; i < str.length; i++) {
|
|
43
|
+
h = ((h << 5) + h) ^ str.charCodeAt(i);
|
|
44
|
+
h = h >>> 0; // keep unsigned 32-bit
|
|
45
|
+
}
|
|
46
|
+
return (h >>> 0).toString(16).padStart(8, '0').slice(0, 6);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── extractStyles ────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extract <style> and <style scoped> blocks from Clarity source.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} source - Full .clarity file source
|
|
55
|
+
* @param {string} filename - File path (used to generate the scope id)
|
|
56
|
+
* @returns {{
|
|
57
|
+
* scoped: string, // CSS from <style scoped> (or '')
|
|
58
|
+
* global: string, // CSS from <style> (or '')
|
|
59
|
+
* scopeId: string, // e.g. "3f8a1b"
|
|
60
|
+
* source: string, // source with style blocks stripped
|
|
61
|
+
* }}
|
|
62
|
+
*/
|
|
63
|
+
export function extractStyles(source, filename = '<anonymous>') {
|
|
64
|
+
const scopeId = hashId(filename);
|
|
65
|
+
|
|
66
|
+
let scoped = '';
|
|
67
|
+
let global = '';
|
|
68
|
+
let module_ = ''; // <style module> content
|
|
69
|
+
|
|
70
|
+
// Match <style scoped> ... </style>
|
|
71
|
+
const scopedRE = /<style\s+scoped\s*>([\s\S]*?)<\/style>/gi;
|
|
72
|
+
// Match <style module> ... </style>
|
|
73
|
+
const moduleRE = /<style\s+module\s*>([\s\S]*?)<\/style>/gi;
|
|
74
|
+
// Match <style> ... </style> (without scoped/module)
|
|
75
|
+
const globalRE = /<style(?!\s+(?:scoped|module))\s*>([\s\S]*?)<\/style>/gi;
|
|
76
|
+
|
|
77
|
+
let stripped = source;
|
|
78
|
+
let m;
|
|
79
|
+
|
|
80
|
+
while ((m = scopedRE.exec(source)) !== null) scoped += m[1];
|
|
81
|
+
while ((m = moduleRE.exec(source)) !== null) module_ += m[1];
|
|
82
|
+
while ((m = globalRE.exec(source)) !== null) global += m[1];
|
|
83
|
+
|
|
84
|
+
// Strip all kinds of <style> from source before compilation
|
|
85
|
+
stripped = stripped
|
|
86
|
+
.replace(/<style\s+scoped\s*>[\s\S]*?<\/style>/gi, '')
|
|
87
|
+
.replace(/<style\s+module\s*>[\s\S]*?<\/style>/gi, '')
|
|
88
|
+
.replace(/<style\s*>[\s\S]*?<\/style>/gi, '');
|
|
89
|
+
|
|
90
|
+
return { scoped, global, module: module_, scopeId, source: stripped.trim() };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── scopeCSS ────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Transform CSS by adding a scoped data-attribute to every selector.
|
|
97
|
+
*
|
|
98
|
+
* Examples:
|
|
99
|
+
* .card { ... } → .card[data-c-3f8a1b] { ... }
|
|
100
|
+
* h1, p { ... } → h1[data-c-3f8a1b], p[data-c-3f8a1b] { ... }
|
|
101
|
+
* .parent .child { ... } → .parent .child[data-c-3f8a1b] { ... }
|
|
102
|
+
* :root { ... } → :root { ... } (left untouched)
|
|
103
|
+
* @keyframes spin { ... } → @keyframes spin { ... } (keyframes untouched)
|
|
104
|
+
* @media (…) { .x { } } → @media (…) { .x[data-c-…] { } }
|
|
105
|
+
*
|
|
106
|
+
* @param {string} css - Raw CSS string
|
|
107
|
+
* @param {string} scopeId - e.g. "3f8a1b"
|
|
108
|
+
* @returns {string} - Scoped CSS
|
|
109
|
+
*/
|
|
110
|
+
export function scopeCSS(css, scopeId) {
|
|
111
|
+
if (!css.trim()) return '';
|
|
112
|
+
const attr = `[data-c-${scopeId}]`;
|
|
113
|
+
return _transformCSS(css, attr);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── Internal CSS transformer ─────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
function _transformCSS(css, attr) {
|
|
119
|
+
const result = [];
|
|
120
|
+
let i = 0;
|
|
121
|
+
|
|
122
|
+
while (i < css.length) {
|
|
123
|
+
// Skip comments
|
|
124
|
+
if (css[i] === '/' && css[i + 1] === '*') {
|
|
125
|
+
const end = css.indexOf('*/', i + 2);
|
|
126
|
+
if (end === -1) {
|
|
127
|
+
result.push(css.slice(i));
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
result.push(css.slice(i, end + 2));
|
|
131
|
+
i = end + 2;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// At-rules with blocks: @media, @supports, @layer, @container
|
|
136
|
+
if (css[i] === '@') {
|
|
137
|
+
const { rule, rest, isBlock } = _readAtRule(css, i);
|
|
138
|
+
if (isBlock) {
|
|
139
|
+
// Recurse into the block content to scope nested selectors
|
|
140
|
+
const blockMatch = rule.match(/^(@[\w-]+[^{]*)\{([\s\S]*)\}$/s);
|
|
141
|
+
if (blockMatch) {
|
|
142
|
+
const inner = _transformCSS(blockMatch[2], attr);
|
|
143
|
+
result.push(`${blockMatch[1]}{${inner}}`);
|
|
144
|
+
} else {
|
|
145
|
+
result.push(rule);
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
// @import, @charset, @keyframes (no inner selectors to scope)
|
|
149
|
+
result.push(rule);
|
|
150
|
+
}
|
|
151
|
+
i += rule.length;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Skip whitespace
|
|
156
|
+
if (/\s/.test(css[i])) {
|
|
157
|
+
result.push(css[i]);
|
|
158
|
+
i++;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Read selector + block
|
|
163
|
+
const { selector, block, total } = _readSelectorBlock(css, i);
|
|
164
|
+
if (total === 0) {
|
|
165
|
+
// Malformed — pass through
|
|
166
|
+
result.push(css[i]);
|
|
167
|
+
i++;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const scopedSelector = _scopeSelector(selector, attr);
|
|
172
|
+
result.push(`${scopedSelector}${block}`);
|
|
173
|
+
i += total;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return result.join('');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function _readAtRule(css, start) {
|
|
180
|
+
// Find the end of the at-rule (either ; or a {…} block)
|
|
181
|
+
let i = start + 1;
|
|
182
|
+
let depth = 0;
|
|
183
|
+
let inStr = false;
|
|
184
|
+
let strChar = '';
|
|
185
|
+
|
|
186
|
+
while (i < css.length) {
|
|
187
|
+
const c = css[i];
|
|
188
|
+
|
|
189
|
+
if (inStr) {
|
|
190
|
+
if (c === strChar && css[i - 1] !== '\\') inStr = false;
|
|
191
|
+
i++;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (c === '"' || c === "'") { inStr = true; strChar = c; i++; continue; }
|
|
196
|
+
|
|
197
|
+
if (c === '{') {
|
|
198
|
+
depth++;
|
|
199
|
+
if (depth === 1 && _isAtRuleWithBlock(css, start, i)) {
|
|
200
|
+
// Read until matching }
|
|
201
|
+
i++;
|
|
202
|
+
while (i < css.length) {
|
|
203
|
+
if (css[i] === '{') depth++;
|
|
204
|
+
else if (css[i] === '}') {
|
|
205
|
+
depth--;
|
|
206
|
+
if (depth === 0) { i++; break; }
|
|
207
|
+
}
|
|
208
|
+
i++;
|
|
209
|
+
}
|
|
210
|
+
return { rule: css.slice(start, i), rest: css.slice(i), isBlock: true };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (c === ';' && depth === 0) {
|
|
215
|
+
i++;
|
|
216
|
+
return { rule: css.slice(start, i), rest: css.slice(i), isBlock: false };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
i++;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { rule: css.slice(start), rest: '', isBlock: false };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function _isAtRuleWithBlock(css, atStart, bracePos) {
|
|
226
|
+
const name = css.slice(atStart, bracePos).match(/@([\w-]+)/)?.[1] ?? '';
|
|
227
|
+
// Keyframes, charset, import — do not recurse
|
|
228
|
+
const noRecurse = new Set(['keyframes', 'charset', 'import', 'font-face', 'namespace']);
|
|
229
|
+
return !noRecurse.has(name);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function _readSelectorBlock(css, start) {
|
|
233
|
+
// Read up to the first {
|
|
234
|
+
let i = start;
|
|
235
|
+
let inStr = false;
|
|
236
|
+
let strChar = '';
|
|
237
|
+
|
|
238
|
+
while (i < css.length) {
|
|
239
|
+
const c = css[i];
|
|
240
|
+
if (inStr) {
|
|
241
|
+
if (c === strChar && css[i - 1] !== '\\') inStr = false;
|
|
242
|
+
i++;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (c === '"' || c === "'") { inStr = true; strChar = c; i++; continue; }
|
|
246
|
+
if (c === '{') break;
|
|
247
|
+
i++;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (i >= css.length) return { selector: '', block: '', total: 0 };
|
|
251
|
+
|
|
252
|
+
const selector = css.slice(start, i).trim();
|
|
253
|
+
// Now read the block {…}
|
|
254
|
+
let depth = 0;
|
|
255
|
+
let blockStart = i;
|
|
256
|
+
i++;
|
|
257
|
+
depth = 1;
|
|
258
|
+
while (i < css.length && depth > 0) {
|
|
259
|
+
if (css[i] === '{') depth++;
|
|
260
|
+
else if (css[i] === '}') depth--;
|
|
261
|
+
i++;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const block = css.slice(blockStart, i);
|
|
265
|
+
return { selector, block, total: i - start };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Add the scope attribute to the last simple selector in each compound selector.
|
|
270
|
+
*
|
|
271
|
+
* Selector list (comma-separated) → each selector gets scoped independently.
|
|
272
|
+
*
|
|
273
|
+
* We append to the LAST simple selector in a compound so that:
|
|
274
|
+
* .parent .child → .parent .child[attr] (only the leaf is scoped)
|
|
275
|
+
*
|
|
276
|
+
* Special cases left untouched:
|
|
277
|
+
* :root, html, body, @keyframes stop selectors, :global(...)
|
|
278
|
+
*/
|
|
279
|
+
function _scopeSelector(selector, attr) {
|
|
280
|
+
if (!selector.trim()) return selector;
|
|
281
|
+
|
|
282
|
+
// Split on top-level commas (not inside parens)
|
|
283
|
+
const parts = _splitSelectorList(selector);
|
|
284
|
+
|
|
285
|
+
return parts.map(part => {
|
|
286
|
+
const t = part.trim();
|
|
287
|
+
|
|
288
|
+
// :root, html, body — never scoped
|
|
289
|
+
if (/^(:root|html|body)$/i.test(t)) return t;
|
|
290
|
+
|
|
291
|
+
// :global(…) — strip the wrapper, output the inner selector as-is (not scoped)
|
|
292
|
+
if (t.startsWith(':global(') && t.endsWith(')')) {
|
|
293
|
+
return t.slice(':global('.length, -1).trim();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// :local(…) — explicitly scope the inner selector
|
|
297
|
+
if (t.startsWith(':local(') && t.endsWith(')')) {
|
|
298
|
+
const inner = t.slice(':local('.length, -1).trim();
|
|
299
|
+
return _appendToLastSimple(inner, attr);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Mixed selectors may contain :global() or :local() inline fragments:
|
|
303
|
+
// .parent :global(.child) → .parent[attr] .child
|
|
304
|
+
// :global(.outer) .inner → .outer .inner[attr]
|
|
305
|
+
// Handle these by splitting on :global()/`:local()` pseudo-functions.
|
|
306
|
+
if (t.includes(':global(') || t.includes(':local(')) {
|
|
307
|
+
return _processLocalGlobalFragments(t, attr);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Default: scope the whole selector
|
|
311
|
+
return _appendToLastSimple(t, attr);
|
|
312
|
+
}).join(', ');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Handle selectors that mix :local() and :global() fragments inline.
|
|
317
|
+
* E.g.: ".parent :global(.child)" → ".parent[attr] .child"
|
|
318
|
+
* ":global(.header) .body" → ".header .body[attr]"
|
|
319
|
+
*/
|
|
320
|
+
function _processLocalGlobalFragments(selector, attr) {
|
|
321
|
+
// Tokenise the selector into scoped and unscoped segments
|
|
322
|
+
const result = [];
|
|
323
|
+
let i = 0;
|
|
324
|
+
let lastWasGlobal = false;
|
|
325
|
+
let buf = '';
|
|
326
|
+
|
|
327
|
+
while (i < selector.length) {
|
|
328
|
+
// Detect :global(...)
|
|
329
|
+
if (selector.startsWith(':global(', i)) {
|
|
330
|
+
// Flush buffered scoped text
|
|
331
|
+
if (buf.trim()) {
|
|
332
|
+
result.push(_appendToLastSimple(buf.trim(), attr));
|
|
333
|
+
buf = '';
|
|
334
|
+
lastWasGlobal = false;
|
|
335
|
+
}
|
|
336
|
+
const end = _findClosingParen(selector, i + ':global('.length - 1);
|
|
337
|
+
const inner = selector.slice(i + ':global('.length, end);
|
|
338
|
+
result.push(inner);
|
|
339
|
+
i = end + 1;
|
|
340
|
+
lastWasGlobal = true;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
// Detect :local(...)
|
|
344
|
+
if (selector.startsWith(':local(', i)) {
|
|
345
|
+
if (buf.trim()) {
|
|
346
|
+
result.push(_appendToLastSimple(buf.trim(), attr));
|
|
347
|
+
buf = '';
|
|
348
|
+
}
|
|
349
|
+
const end = _findClosingParen(selector, i + ':local('.length - 1);
|
|
350
|
+
const inner = selector.slice(i + ':local('.length, end);
|
|
351
|
+
result.push(_appendToLastSimple(inner.trim(), attr));
|
|
352
|
+
i = end + 1;
|
|
353
|
+
lastWasGlobal = false;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
// Accumulate combinator/whitespace between segments
|
|
357
|
+
if (lastWasGlobal && /[\s>~+]/.test(selector[i])) {
|
|
358
|
+
result.push(selector[i]);
|
|
359
|
+
i++;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
buf += selector[i];
|
|
363
|
+
i++;
|
|
364
|
+
}
|
|
365
|
+
if (buf.trim()) result.push(_appendToLastSimple(buf.trim(), attr));
|
|
366
|
+
return result.join('');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function _findClosingParen(str, openPos) {
|
|
370
|
+
let depth = 0;
|
|
371
|
+
for (let i = openPos; i < str.length; i++) {
|
|
372
|
+
if (str[i] === '(') depth++;
|
|
373
|
+
else if (str[i] === ')') {
|
|
374
|
+
depth--;
|
|
375
|
+
if (depth === 0) return i;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return str.length - 1;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function _appendToLastSimple(selector, attr) {
|
|
382
|
+
// Find the last segment (not a combinator: space, >, ~, +)
|
|
383
|
+
// Split on combinators while keeping them
|
|
384
|
+
const parts = selector.split(/([\s>~+]+)/);
|
|
385
|
+
// last non-combinator part
|
|
386
|
+
let lastIdx = parts.length - 1;
|
|
387
|
+
while (lastIdx > 0 && /^[\s>~+]+$/.test(parts[lastIdx])) lastIdx--;
|
|
388
|
+
|
|
389
|
+
const lastPart = parts[lastIdx];
|
|
390
|
+
|
|
391
|
+
// Insert attr before pseudo-elements (::before, ::after)
|
|
392
|
+
const pseudoElRE = /:{1,2}[a-z-]+(\([^)]*\))?$/i;
|
|
393
|
+
const peMatch = lastPart.match(pseudoElRE);
|
|
394
|
+
|
|
395
|
+
if (peMatch) {
|
|
396
|
+
const idx = lastPart.lastIndexOf(peMatch[0]);
|
|
397
|
+
parts[lastIdx] = lastPart.slice(0, idx) + attr + lastPart.slice(idx);
|
|
398
|
+
} else {
|
|
399
|
+
parts[lastIdx] = lastPart + attr;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return parts.join('');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function _splitSelectorList(selector) {
|
|
406
|
+
const parts = [];
|
|
407
|
+
let depth = 0;
|
|
408
|
+
let start = 0;
|
|
409
|
+
let inStr = false;
|
|
410
|
+
let strCh = '';
|
|
411
|
+
|
|
412
|
+
for (let i = 0; i < selector.length; i++) {
|
|
413
|
+
const c = selector[i];
|
|
414
|
+
if (inStr) {
|
|
415
|
+
if (c === strCh && selector[i - 1] !== '\\') inStr = false;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (c === '"' || c === "'") { inStr = true; strCh = c; continue; }
|
|
419
|
+
if (c === '(') { depth++; continue; }
|
|
420
|
+
if (c === ')') { depth--; continue; }
|
|
421
|
+
if (c === ',' && depth === 0) {
|
|
422
|
+
parts.push(selector.slice(start, i));
|
|
423
|
+
start = i + 1;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
parts.push(selector.slice(start));
|
|
428
|
+
return parts;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ─── injectScopeAttr ─────────────────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Given generated JavaScript code for a component, inject the scope data
|
|
435
|
+
* attribute onto the root element.
|
|
436
|
+
*
|
|
437
|
+
* The codegen emits something like:
|
|
438
|
+
* const _root = h('div', { class: 'card' }, [...]);
|
|
439
|
+
*
|
|
440
|
+
* We patch it to:
|
|
441
|
+
* const _root = h('div', { class: 'card', 'data-c-3f8a1b': '' }, [...]);
|
|
442
|
+
*
|
|
443
|
+
* This is called by the Vite plugin / compiler after codegen, before writing
|
|
444
|
+
* the final JS output.
|
|
445
|
+
*
|
|
446
|
+
* @param {string} code - Generated JS
|
|
447
|
+
* @param {string} scopeId - e.g. "3f8a1b"
|
|
448
|
+
* @returns {string} - Patched JS
|
|
449
|
+
*/
|
|
450
|
+
export function injectScopeAttr(code, scopeId) {
|
|
451
|
+
if (!scopeId) return code;
|
|
452
|
+
const attr = `'data-c-${scopeId}': ''`;
|
|
453
|
+
// The codegen always names the root element variable _root in the component factory.
|
|
454
|
+
// We insert the attr into the first h(...) call props object.
|
|
455
|
+
// Pattern: h('tag', { → h('tag', { 'data-c-xxx': '',
|
|
456
|
+
return code.replace(
|
|
457
|
+
/\bh\((['"`][^'"` ]+['"`]),\s*\{/,
|
|
458
|
+
`h($1, { ${attr}, `
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ─── CSS Modules support ──────────────────────────────────────────────────────
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Process a `<style module>` block.
|
|
466
|
+
*
|
|
467
|
+
* In CSS Modules mode:
|
|
468
|
+
* - All class selectors are renamed: .card → .card_3f8a1b
|
|
469
|
+
* - :global(.foo) opts out of renaming
|
|
470
|
+
* - :local(.foo) opts in (always renamed, even if global mode is used)
|
|
471
|
+
* - Returns { css: transformedCSS, classMap: { card: 'card_3f8a1b' } }
|
|
472
|
+
*
|
|
473
|
+
* Usage:
|
|
474
|
+
* const { css, classMap } = cssModules(raw, scopeId);
|
|
475
|
+
* // classMap.card === 'card_3f8a1b'
|
|
476
|
+
* // Inject classMap as `styles` into the component props
|
|
477
|
+
*
|
|
478
|
+
* @param {string} css - Raw CSS from <style module>
|
|
479
|
+
* @param {string} scopeId - Scope hash e.g. "3f8a1b"
|
|
480
|
+
* @param {object} [opts]
|
|
481
|
+
* @param {boolean} [opts.globalDefault=false] - If true, classes are global by default (only :local() scopes)
|
|
482
|
+
* @returns {{ css: string, classMap: object }}
|
|
483
|
+
*/
|
|
484
|
+
export function cssModules(css, scopeId, { globalDefault = false } = {}) {
|
|
485
|
+
if (!css.trim()) return { css: '', classMap: {} };
|
|
486
|
+
|
|
487
|
+
const classMap = {};
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Rename a class name: .card → .card_3f8a1b
|
|
491
|
+
* Side effect: records the mapping in classMap.
|
|
492
|
+
*/
|
|
493
|
+
function rename(cls) {
|
|
494
|
+
const key = cls.slice(1); // strip leading '.'
|
|
495
|
+
const renamed = `${key}_${scopeId}`;
|
|
496
|
+
classMap[key] = renamed;
|
|
497
|
+
return `.${renamed}`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const transformed = _transformCSSModules(css, { rename, globalDefault });
|
|
501
|
+
return { css: transformed, classMap };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Transform CSS for modules mode: rename .class selectors unless :global() wrapped.
|
|
506
|
+
*/
|
|
507
|
+
function _transformCSSModules(css, { rename, globalDefault }) {
|
|
508
|
+
const result = [];
|
|
509
|
+
let i = 0;
|
|
510
|
+
|
|
511
|
+
while (i < css.length) {
|
|
512
|
+
// Skip comments
|
|
513
|
+
if (css[i] === '/' && css[i + 1] === '*') {
|
|
514
|
+
const end = css.indexOf('*/', i + 2);
|
|
515
|
+
if (end === -1) { result.push(css.slice(i)); break; }
|
|
516
|
+
result.push(css.slice(i, end + 2));
|
|
517
|
+
i = end + 2;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// At-rules with blocks
|
|
522
|
+
if (css[i] === '@') {
|
|
523
|
+
const { rule, isBlock } = _readAtRule(css, i);
|
|
524
|
+
if (isBlock) {
|
|
525
|
+
const blockMatch = rule.match(/^(@[\w-]+[^{]*)\{([\s\S]*)\}$/s);
|
|
526
|
+
if (blockMatch) {
|
|
527
|
+
const inner = _transformCSSModules(blockMatch[2], { rename, globalDefault });
|
|
528
|
+
result.push(`${blockMatch[1]}{${inner}}`);
|
|
529
|
+
} else {
|
|
530
|
+
result.push(rule);
|
|
531
|
+
}
|
|
532
|
+
} else {
|
|
533
|
+
result.push(rule);
|
|
534
|
+
}
|
|
535
|
+
i += rule.length;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Whitespace pass-through
|
|
540
|
+
if (/\s/.test(css[i])) { result.push(css[i]); i++; continue; }
|
|
541
|
+
|
|
542
|
+
// Selector block
|
|
543
|
+
const { selector, block, total } = _readSelectorBlock(css, i);
|
|
544
|
+
if (total === 0) { result.push(css[i]); i++; continue; }
|
|
545
|
+
|
|
546
|
+
const transformedSelector = _transformModuleSelector(selector, { rename, globalDefault });
|
|
547
|
+
result.push(`${transformedSelector}${block}`);
|
|
548
|
+
i += total;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return result.join('');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function _transformModuleSelector(selector, { rename, globalDefault }) {
|
|
555
|
+
if (!selector.trim()) return selector;
|
|
556
|
+
const parts = _splitSelectorList(selector);
|
|
557
|
+
|
|
558
|
+
return parts.map(part => {
|
|
559
|
+
const t = part.trim();
|
|
560
|
+
return _renameClassesInSelector(t, { rename, globalDefault });
|
|
561
|
+
}).join(', ');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Walk through a selector string, renaming class tokens.
|
|
566
|
+
*
|
|
567
|
+
* Handles:
|
|
568
|
+
* .card → .card_xxx (scoped)
|
|
569
|
+
* :global(.card) → .card (not renamed)
|
|
570
|
+
* :local(.card) → .card_xxx (always renamed)
|
|
571
|
+
* .parent :global(.sub) → .parent_xxx .sub (mixed)
|
|
572
|
+
*/
|
|
573
|
+
function _renameClassesInSelector(selector, { rename, globalDefault }) {
|
|
574
|
+
let result = '';
|
|
575
|
+
let i = 0;
|
|
576
|
+
|
|
577
|
+
while (i < selector.length) {
|
|
578
|
+
// :global(...) — pass through inner content unchanged
|
|
579
|
+
if (selector.startsWith(':global(', i)) {
|
|
580
|
+
const end = _findClosingParen(selector, i + ':global('.length - 1);
|
|
581
|
+
const inner = selector.slice(i + ':global('.length, end);
|
|
582
|
+
// Keep as-is — don't rename inside :global()
|
|
583
|
+
result += inner;
|
|
584
|
+
i = end + 1;
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// :local(...) — always rename inner classes
|
|
589
|
+
if (selector.startsWith(':local(', i)) {
|
|
590
|
+
const end = _findClosingParen(selector, i + ':local('.length - 1);
|
|
591
|
+
const inner = selector.slice(i + ':local('.length, end);
|
|
592
|
+
result += _renameRawClasses(inner, rename);
|
|
593
|
+
i = end + 1;
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Class selector token: .foo
|
|
598
|
+
if (selector[i] === '.' && (i === 0 || /[\s>~+:(]/.test(selector[i - 1]))) {
|
|
599
|
+
// Read class name
|
|
600
|
+
let j = i + 1;
|
|
601
|
+
while (j < selector.length && /[\w-]/.test(selector[j])) j++;
|
|
602
|
+
const cls = selector.slice(i, j);
|
|
603
|
+
if (!globalDefault) {
|
|
604
|
+
result += rename(cls);
|
|
605
|
+
} else {
|
|
606
|
+
result += cls; // global-default: leave class as-is (only :local() renames)
|
|
607
|
+
}
|
|
608
|
+
i = j;
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
result += selector[i];
|
|
613
|
+
i++;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return result;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function _renameRawClasses(selector, rename) {
|
|
620
|
+
return selector.replace(/\.([\w-]+)/g, (_, name) => rename(`.${name}`));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ─── collectStyles (for SSR / build pipeline) ─────────────────────────────────
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Aggregate all scoped + global CSS from multiple processed files.
|
|
627
|
+
* Returns a single CSS string suitable for injection into <head>.
|
|
628
|
+
*
|
|
629
|
+
* @param {Array<{ scoped: string, global: string, scopeId: string }>} entries
|
|
630
|
+
* @returns {string}
|
|
631
|
+
*/
|
|
632
|
+
export function collectStyles(entries) {
|
|
633
|
+
const parts = [];
|
|
634
|
+
for (const { scoped, global, scopeId } of entries) {
|
|
635
|
+
if (global.trim()) parts.push(global.trim());
|
|
636
|
+
if (scoped.trim() && scopeId) {
|
|
637
|
+
parts.push(scopeCSS(scoped, scopeId));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return parts.join('\n');
|
|
641
|
+
}
|