rainbowindex 0.2.1 → 0.2.2

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,1635 @@
1
+ import {
2
+ APPLY_LIKE_MATCH_RE,
3
+ COLOR_STOP_REF_RE,
4
+ EXTRACTABLE_DIRECTIVE_NAMES,
5
+ RI_IMPORT_SPECIFIER_ALTERNATION,
6
+ SAFE_FONT_FAMILY_RE,
7
+ SHADOW_VAR_REF_RE,
8
+ codepointCompare,
9
+ compileCSSFunctions,
10
+ createCompiler,
11
+ directiveAtRulePattern,
12
+ expandVariantGroups,
13
+ extractClassesFromSource,
14
+ hasCSSFunctions,
15
+ isAtRuleBoundary,
16
+ isRIDebug,
17
+ pushWarningsDeduped,
18
+ renderCSS,
19
+ scanCSSForTokenUsage,
20
+ withTimeout
21
+ } from "./chunk-W6XIBM4M.mjs";
22
+ import {
23
+ checkPaletteContrast,
24
+ generateAllColorVariables,
25
+ generateThemeOverrides
26
+ } from "./chunk-SOMDX7V6.mjs";
27
+
28
+ // src/scanner/glob-utils.ts
29
+ import { isAbsolute, win32 } from "path";
30
+ function validateGlobPattern(pattern) {
31
+ if (!pattern?.trim()) {
32
+ return "Glob pattern is empty.";
33
+ }
34
+ if (pattern.includes("\0")) {
35
+ return "Glob pattern contains a null byte, which is invalid in file paths.";
36
+ }
37
+ if (isAbsolute(pattern) || win32.isAbsolute(pattern)) {
38
+ return `Glob pattern "${pattern}" must be relative, not absolute.`;
39
+ }
40
+ const segments = pattern.split(/[\\/]+/);
41
+ for (const seg of segments) {
42
+ if (seg === "..") {
43
+ return `Glob pattern "${pattern}" must not traverse parent directories (".."). Restructure your project layout so source files are within the project root, or use @source with a pattern rooted at the project directory.`;
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+
49
+ // src/scanner/sources.ts
50
+ import { readFile, stat } from "fs/promises";
51
+ import { resolve as resolve2 } from "path";
52
+ import { glob } from "tinyglobby";
53
+
54
+ // src/scanner/package-discovery.ts
55
+ import { existsSync, readFileSync, realpathSync, statSync } from "fs";
56
+ import { dirname, join, posix, resolve } from "path";
57
+ var EMPTY = Object.freeze({ sources: [], warnings: [] });
58
+ var discoveryCache = /* @__PURE__ */ new Map();
59
+ function discoverPackageSafelistSources(cwd) {
60
+ const cwdAbs = resolve(cwd);
61
+ let mtimeMs;
62
+ try {
63
+ mtimeMs = statSync(join(cwdAbs, "package.json")).mtimeMs;
64
+ } catch {
65
+ return EMPTY;
66
+ }
67
+ const cached = discoveryCache.get(cwdAbs);
68
+ if (cached && cached.mtimeMs === mtimeMs) return cached.result;
69
+ const result = runDiscovery(cwdAbs);
70
+ discoveryCache.set(cwdAbs, { mtimeMs, result });
71
+ return result;
72
+ }
73
+ function runDiscovery(cwdAbs) {
74
+ let consumer;
75
+ try {
76
+ consumer = readPackageJson(join(cwdAbs, "package.json"));
77
+ } catch {
78
+ return EMPTY;
79
+ }
80
+ const deps = [
81
+ ...Object.keys(consumer.dependencies ?? {}),
82
+ ...Object.keys(consumer.peerDependencies ?? {})
83
+ ];
84
+ if (deps.length === 0) return EMPTY;
85
+ const sources = [];
86
+ const warnings = [];
87
+ for (const depName of deps) {
88
+ const depPkgPath = findDepPackageJson(cwdAbs, depName);
89
+ if (!depPkgPath) {
90
+ continue;
91
+ }
92
+ let depPkg;
93
+ try {
94
+ depPkg = readPackageJson(depPkgPath);
95
+ } catch (err) {
96
+ warnings.push(
97
+ `[RI-1410] Could not read ${depName}/package.json during safelist discovery: ${errMessage(err)}`
98
+ );
99
+ continue;
100
+ }
101
+ const patterns = depPkg.rainbowindex?.safelistSources;
102
+ if (patterns == null) continue;
103
+ if (!Array.isArray(patterns)) {
104
+ warnings.push(
105
+ `[RI-1410] Invalid rainbowindex.safelistSources in ${depName} \u2014 expected an array of glob strings.`
106
+ );
107
+ continue;
108
+ }
109
+ if (patterns.length === 0) continue;
110
+ const depRoot = realpathOrFallback(dirname(depPkgPath)).replace(/\\/g, "/");
111
+ for (const pattern of patterns) {
112
+ if (typeof pattern !== "string" || !pattern) {
113
+ warnings.push(
114
+ `[RI-1410] Invalid rainbowindex.safelistSources entry in ${depName} \u2014 expected a non-empty glob string.`
115
+ );
116
+ continue;
117
+ }
118
+ if (validateGlobPattern(pattern) !== null) {
119
+ warnings.push(
120
+ `[RI-1410] Invalid rainbowindex.safelistSources entry "${pattern}" in ${depName} \u2014 patterns must be relative to the package root and must not traverse parent directories.`
121
+ );
122
+ continue;
123
+ }
124
+ const absolute = posix.normalize(`${depRoot}/${pattern.replace(/^\.\//, "")}`);
125
+ if (absolute !== depRoot && !absolute.startsWith(`${depRoot}/`)) {
126
+ warnings.push(
127
+ `[RI-1410] Invalid rainbowindex.safelistSources entry "${pattern}" in ${depName} \u2014 resolved outside the package root.`
128
+ );
129
+ continue;
130
+ }
131
+ sources.push({ pattern: absolute, negated: false, inline: false, absolute: true });
132
+ }
133
+ }
134
+ return { sources, warnings };
135
+ }
136
+ function readPackageJson(path) {
137
+ const raw = readFileSync(path, "utf8");
138
+ return JSON.parse(raw);
139
+ }
140
+ function findDepPackageJson(cwd, depName) {
141
+ let dir = cwd;
142
+ while (true) {
143
+ const candidate = join(dir, "node_modules", depName, "package.json");
144
+ if (existsSync(candidate)) return candidate;
145
+ const parent = dirname(dir);
146
+ if (parent === dir) return null;
147
+ dir = parent;
148
+ }
149
+ }
150
+ function realpathOrFallback(path) {
151
+ try {
152
+ return realpathSync(path);
153
+ } catch {
154
+ return path;
155
+ }
156
+ }
157
+ function errMessage(err) {
158
+ return err instanceof Error ? err.message : String(err);
159
+ }
160
+
161
+ // src/scanner/sources.ts
162
+ var DEFAULT_PATTERNS = Object.freeze([
163
+ "index.html",
164
+ "src/**/*.{html,js,jsx,ts,tsx,mdx,vue,svelte}"
165
+ ]);
166
+ var DEFAULT_EXCLUDES = Object.freeze([
167
+ "node_modules/**",
168
+ "dist/**",
169
+ "build/**",
170
+ "coverage/**",
171
+ "public/**",
172
+ "**/*.config.*",
173
+ "**/*.d.ts"
174
+ ]);
175
+ var MAX_FILE_SIZE = 1048576;
176
+ var MAX_INLINE_SOURCE_SIZE = 102400;
177
+ var GLOB_TIMEOUT_MS = 3e4;
178
+ var GLOB_TIMEOUT_MESSAGE = `glob() timed out after ${GLOB_TIMEOUT_MS / 1e3}s \u2014 check for network filesystem issues or overly broad @source patterns`;
179
+ function collectPatterns(sources) {
180
+ const includePatterns = [];
181
+ const nodeModulesIncludePatterns = [];
182
+ const excludePatterns = [...DEFAULT_EXCLUDES];
183
+ const hasUserPositiveGlobs = sources.some((s) => !s.inline && !s.negated && !s.absolute);
184
+ if (!hasUserPositiveGlobs) {
185
+ includePatterns.push(...DEFAULT_PATTERNS);
186
+ }
187
+ const warnings = [];
188
+ for (const src of sources) {
189
+ if (src.inline) continue;
190
+ if (!src.absolute) {
191
+ const err = validateGlobPattern(src.pattern);
192
+ if (err) {
193
+ warnings.push(`[RI-1404] @source pattern rejected: ${err}`);
194
+ continue;
195
+ }
196
+ }
197
+ if (src.negated) {
198
+ excludePatterns.push(src.pattern);
199
+ } else if (src.absolute || src.pattern.replace(/^\.\//, "").startsWith("node_modules/")) {
200
+ nodeModulesIncludePatterns.push(src.pattern);
201
+ } else {
202
+ includePatterns.push(src.pattern);
203
+ }
204
+ }
205
+ return {
206
+ includePatterns,
207
+ nodeModulesIncludePatterns,
208
+ excludePatterns,
209
+ warnings,
210
+ hasUserPositiveGlobs
211
+ };
212
+ }
213
+ async function resolveSourceFilesAsync(sources, cwd, hasInlineClasses = false) {
214
+ const {
215
+ includePatterns,
216
+ nodeModulesIncludePatterns,
217
+ excludePatterns,
218
+ warnings,
219
+ hasUserPositiveGlobs
220
+ } = collectPatterns(sources);
221
+ const allIncludes = [...includePatterns, ...nodeModulesIncludePatterns];
222
+ if (allIncludes.length === 0) return { files: [], warnings };
223
+ try {
224
+ const globPasses = [];
225
+ if (includePatterns.length > 0) {
226
+ globPasses.push(
227
+ withTimeout(
228
+ glob(includePatterns, { cwd, ignore: excludePatterns }),
229
+ GLOB_TIMEOUT_MS,
230
+ GLOB_TIMEOUT_MESSAGE
231
+ )
232
+ );
233
+ }
234
+ if (nodeModulesIncludePatterns.length > 0) {
235
+ const relaxedExcludes = excludePatterns.filter((p) => p !== "node_modules/**");
236
+ globPasses.push(
237
+ withTimeout(
238
+ glob(nodeModulesIncludePatterns, { cwd, ignore: relaxedExcludes }),
239
+ GLOB_TIMEOUT_MS,
240
+ GLOB_TIMEOUT_MESSAGE
241
+ )
242
+ );
243
+ }
244
+ const matched = (await Promise.all(globPasses)).flat();
245
+ const files = [...new Set(matched.map((f) => resolve2(cwd, f)))].sort(codepointCompare);
246
+ if (files.length === 0 && !(hasInlineClasses && !hasUserPositiveGlobs)) {
247
+ warnings.push(
248
+ `[RI-1401] No source files found matching ${allIncludes.map((p) => `"${p}"`).join(", ")} \u2014 check @source paths or project structure.`
249
+ );
250
+ }
251
+ return { files, warnings };
252
+ } catch (err) {
253
+ warnings.push(
254
+ `[RI-1402] Invalid glob pattern \u2014 skipping. Check @source syntax. (${err instanceof Error ? err.message : String(err)})`
255
+ );
256
+ return { files: [], warnings };
257
+ }
258
+ }
259
+ function collectInlineClasses(sources) {
260
+ const classes = /* @__PURE__ */ new Set();
261
+ const warnings = [];
262
+ for (const src of sources) {
263
+ if (!src.inline) continue;
264
+ const items = src.classes ?? [];
265
+ let contentLength = 0;
266
+ for (const cls of items) contentLength += cls.length;
267
+ if (contentLength > MAX_INLINE_SOURCE_SIZE) {
268
+ warnings.push(
269
+ `[RI-1406] Inline @source content exceeds ${MAX_INLINE_SOURCE_SIZE} byte limit (${contentLength} bytes) \u2014 skipping.`
270
+ );
271
+ continue;
272
+ }
273
+ for (const cls of items) {
274
+ if (cls) classes.add(cls);
275
+ }
276
+ }
277
+ return { classes, warnings };
278
+ }
279
+ var FILE_IO_TIMEOUT_MS = 1e4;
280
+ async function scanOneFile(file) {
281
+ const warnings = [];
282
+ try {
283
+ const fileSize = await withTimeout(
284
+ stat(file).then((s) => s.size),
285
+ FILE_IO_TIMEOUT_MS,
286
+ "stat() timed out"
287
+ );
288
+ if (fileSize > MAX_FILE_SIZE) {
289
+ return {
290
+ classes: null,
291
+ warnings,
292
+ failure: `[RI-1405] Skipping source file "${file}" (${fileSize} bytes) \u2014 exceeds ${MAX_FILE_SIZE} byte limit.`
293
+ };
294
+ }
295
+ const content = await withTimeout(
296
+ readFile(file, "utf-8"),
297
+ FILE_IO_TIMEOUT_MS,
298
+ "readFile() timed out"
299
+ );
300
+ return {
301
+ classes: extractClassesFromSource({ path: file, content }, warnings),
302
+ warnings,
303
+ failure: null
304
+ };
305
+ } catch (err) {
306
+ return {
307
+ classes: null,
308
+ warnings,
309
+ failure: `[RI-1403] Could not read source file "${file}" \u2014 skipping. (${err instanceof Error ? err.message : String(err)})`
310
+ };
311
+ }
312
+ }
313
+ async function scanSourceFilesAsync(sources, cwd) {
314
+ const { classes: allClasses, warnings: inlineWarnings } = collectInlineClasses(sources);
315
+ const allWarnings = [...inlineWarnings];
316
+ const { files, warnings: resolveWarnings } = await resolveSourceFilesAsync(
317
+ sources,
318
+ cwd,
319
+ allClasses.size > 0
320
+ );
321
+ allWarnings.push(...resolveWarnings);
322
+ const CONCURRENCY_LIMIT = 32;
323
+ const results = new Array(files.length);
324
+ let nextIndex = 0;
325
+ const worker = async () => {
326
+ while (nextIndex < files.length) {
327
+ const index = nextIndex++;
328
+ results[index] = await scanOneFile(files[index]);
329
+ }
330
+ };
331
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY_LIMIT, files.length) }, worker));
332
+ const seen = new Set(allWarnings);
333
+ for (const result of results) {
334
+ if (result.failure) {
335
+ allWarnings.push(result.failure);
336
+ seen.add(result.failure);
337
+ continue;
338
+ }
339
+ if (result.classes) {
340
+ for (const cls of result.classes) {
341
+ allClasses.add(cls);
342
+ }
343
+ }
344
+ pushWarningsDeduped(allWarnings, result.warnings, seen);
345
+ }
346
+ return { classes: allClasses, warnings: allWarnings };
347
+ }
348
+ async function collectProjectClasses(themeSources, surfaceSources, cwd, warnings, warningSeen) {
349
+ const discovered = discoverPackageSafelistSources(cwd);
350
+ pushWarningsDeduped(warnings, discovered.warnings, warningSeen);
351
+ const allSources = [...themeSources, ...surfaceSources, ...discovered.sources];
352
+ const scanResult = await scanSourceFilesAsync(allSources, cwd);
353
+ pushWarningsDeduped(warnings, scanResult.warnings, warningSeen);
354
+ return scanResult.classes;
355
+ }
356
+
357
+ // src/integrations/font-providers/google/state.ts
358
+ var googleFontInternals = {
359
+ googleFontState: { cache: /* @__PURE__ */ new Map(), fetched: false },
360
+ googleFontListPromise: null,
361
+ lastFetchFailureMs: 0,
362
+ validatedCacheDir: null,
363
+ resolvedCachePath: null
364
+ };
365
+
366
+ // src/integrations/font-providers/google/cache.ts
367
+ import { createHash, randomUUID } from "crypto";
368
+ import { open as fsOpen, writeFile, rename, unlink, mkdir } from "fs/promises";
369
+ import { resolve as resolve3, dirname as dirname2, join as join2, isAbsolute as isAbsolute2 } from "path";
370
+ import { isAbsolute as win32IsAbsolute } from "path/win32";
371
+ var MAX_FONT_CACHE_TTL_SECONDS = 30 * 24 * 60 * 60;
372
+ function getFontCacheDir() {
373
+ if (googleFontInternals.validatedCacheDir !== null) return googleFontInternals.validatedCacheDir;
374
+ const raw = process.env.RI_CACHE_DIR || "node_modules/.cache/rainbowindex";
375
+ if (isAbsolute2(raw) || win32IsAbsolute(raw)) {
376
+ throw new Error(
377
+ `[RI-1208] RI_CACHE_DIR must be a relative path, got absolute path: "${raw}". Use a relative path like "node_modules/.cache/rainbowindex".`
378
+ );
379
+ }
380
+ if (raw.split(/[\\/]/).some((s) => s === "..")) {
381
+ throw new Error(
382
+ `[RI-1209] RI_CACHE_DIR must not contain ".." segments: "${raw}". Use a direct relative path like "node_modules/.cache/rainbowindex".`
383
+ );
384
+ }
385
+ googleFontInternals.validatedCacheDir = raw;
386
+ return raw;
387
+ }
388
+ function getFontCacheFile() {
389
+ return `${getFontCacheDir()}/google.json`;
390
+ }
391
+ function getResolvedCachePath() {
392
+ if (googleFontInternals.resolvedCachePath !== null && googleFontInternals.validatedCacheDir !== null) {
393
+ return googleFontInternals.resolvedCachePath;
394
+ }
395
+ googleFontInternals.resolvedCachePath = resolve3(process.cwd(), getFontCacheFile());
396
+ return googleFontInternals.resolvedCachePath;
397
+ }
398
+ function getFontCacheTTL() {
399
+ const envVal = process.env.RI_FONT_CACHE_TTL;
400
+ if (envVal) {
401
+ const seconds = Number(envVal);
402
+ if (!Number.isNaN(seconds) && seconds >= 0) {
403
+ if (seconds > MAX_FONT_CACHE_TTL_SECONDS) {
404
+ console.warn(
405
+ `[RI-1211] RI_FONT_CACHE_TTL=${seconds} exceeds maximum of ${MAX_FONT_CACHE_TTL_SECONDS} seconds (30 days). Clamping to 30 days.`
406
+ );
407
+ return MAX_FONT_CACHE_TTL_SECONDS * 1e3;
408
+ }
409
+ return seconds * 1e3;
410
+ }
411
+ }
412
+ return 7 * 24 * 60 * 60 * 1e3;
413
+ }
414
+ function parseCachedMetaEntries(raw) {
415
+ const parsed = JSON.parse(raw);
416
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !("checksum" in parsed) || !("entries" in parsed)) {
417
+ return null;
418
+ }
419
+ if (!Array.isArray(parsed.entries)) return null;
420
+ const entriesJson = JSON.stringify(parsed.entries);
421
+ const expected = createHash("sha256").update(entriesJson).digest("hex");
422
+ if (typeof parsed.checksum !== "string" || parsed.checksum !== expected) return null;
423
+ const entries = parsed.entries;
424
+ const newCache = /* @__PURE__ */ new Map();
425
+ for (const rawItem of entries) {
426
+ if (!rawItem || typeof rawItem !== "object") continue;
427
+ const item = rawItem;
428
+ if (typeof item.family !== "string" || typeof item.variable !== "boolean" || typeof item.category !== "string" || item.axes !== void 0 && !Array.isArray(item.axes)) {
429
+ continue;
430
+ }
431
+ if (!SAFE_FONT_FAMILY_RE.test(item.family)) continue;
432
+ const axes = Array.isArray(item.axes) ? item.axes.filter((a) => {
433
+ if (!a || typeof a !== "object") return false;
434
+ const axis = a;
435
+ return typeof axis.tag === "string" && typeof axis.start === "number" && Number.isFinite(axis.start) && typeof axis.end === "number" && Number.isFinite(axis.end);
436
+ }).map((a) => {
437
+ const axis = { tag: a.tag, start: a.start, end: a.end };
438
+ Object.freeze(axis);
439
+ return axis;
440
+ }) : void 0;
441
+ if (axes) Object.freeze(axes);
442
+ const entry = {
443
+ family: item.family,
444
+ variable: item.variable,
445
+ axes,
446
+ category: item.category
447
+ };
448
+ Object.freeze(entry);
449
+ newCache.set(entry.family, entry);
450
+ }
451
+ return newCache.size > 0 ? newCache : null;
452
+ }
453
+ async function loadFontCache(ignoreExpiry = false) {
454
+ try {
455
+ const cachePath = getResolvedCachePath();
456
+ const fh = await fsOpen(cachePath, "r");
457
+ let raw;
458
+ try {
459
+ if (!ignoreExpiry) {
460
+ const st = await fh.stat();
461
+ if (Date.now() - st.mtimeMs > getFontCacheTTL()) return false;
462
+ }
463
+ raw = await fh.readFile("utf-8");
464
+ } finally {
465
+ await fh.close();
466
+ }
467
+ const newCache = parseCachedMetaEntries(raw);
468
+ if (!newCache) return false;
469
+ googleFontInternals.googleFontState = { cache: newCache, fetched: true };
470
+ return true;
471
+ } catch {
472
+ return false;
473
+ }
474
+ }
475
+ async function saveFontCache() {
476
+ try {
477
+ const cachePath = getResolvedCachePath();
478
+ await mkdir(dirname2(cachePath), { recursive: true });
479
+ const entries = Array.from(googleFontInternals.googleFontState.cache.values());
480
+ const entriesJson = JSON.stringify(entries);
481
+ const checksum = createHash("sha256").update(entriesJson).digest("hex");
482
+ const payload = `{"checksum":${JSON.stringify(checksum)},"entries":${entriesJson}}`;
483
+ const tmpPath = join2(dirname2(cachePath), `.google-${process.pid}-${randomUUID()}.tmp`);
484
+ try {
485
+ await writeFile(tmpPath, payload);
486
+ await rename(tmpPath, cachePath);
487
+ } finally {
488
+ await unlink(tmpPath).catch(() => {
489
+ });
490
+ }
491
+ } catch (err) {
492
+ const reason = err instanceof Error ? err.message : String(err);
493
+ console.warn(
494
+ `[RI-1210] Failed to write font cache to ${getFontCacheFile()}: ${reason}. Each build will fetch from Google Fonts. Set RI_CACHE_DIR to a writable path or use RI_OFFLINE=1 with a pre-populated cache.`
495
+ );
496
+ }
497
+ }
498
+
499
+ // src/integrations/font-providers/google/client.ts
500
+ var MAX_RESPONSE_SIZE = 10 * 1024 * 1024;
501
+ var MAX_RETRIES = 2;
502
+ var FETCH_TIMEOUT_MS = 5e3;
503
+ function wait(ms) {
504
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
505
+ }
506
+ function toGoogleFontMetaMap(data) {
507
+ const newCache = /* @__PURE__ */ new Map();
508
+ for (const font of data.familyMetadataList) {
509
+ const wghtAxis = font.axes.find((a) => a.tag === "wght");
510
+ const axes = font.axes.map((a) => {
511
+ const axis = { tag: a.tag, start: a.min, end: a.max };
512
+ Object.freeze(axis);
513
+ return axis;
514
+ });
515
+ Object.freeze(axes);
516
+ const entry = {
517
+ family: font.family,
518
+ variable: wghtAxis ? wghtAxis.min !== wghtAxis.max : false,
519
+ axes,
520
+ category: font.category
521
+ };
522
+ Object.freeze(entry);
523
+ newCache.set(font.family, entry);
524
+ }
525
+ return newCache;
526
+ }
527
+ async function readJsonResponse(res) {
528
+ const reader = res.body?.getReader();
529
+ if (!reader) {
530
+ throw new Error("[RI-1207] Google Fonts metadata response has no readable body.");
531
+ }
532
+ const chunks = [];
533
+ let totalBytes = 0;
534
+ for (; ; ) {
535
+ const { done, value } = await reader.read();
536
+ if (done) break;
537
+ totalBytes += value.byteLength;
538
+ if (totalBytes > MAX_RESPONSE_SIZE) {
539
+ reader.cancel();
540
+ throw new Error(
541
+ `[RI-1207] Google Fonts metadata response too large (>${MAX_RESPONSE_SIZE} bytes).`
542
+ );
543
+ }
544
+ chunks.push(value);
545
+ }
546
+ const text = new TextDecoder().decode(
547
+ chunks.length === 1 ? chunks[0] : await new Blob(chunks).arrayBuffer()
548
+ );
549
+ return JSON.parse(text);
550
+ }
551
+ async function fetchGoogleFontMetadata() {
552
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
553
+ const controller = new AbortController();
554
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
555
+ try {
556
+ const res = await fetch("https://fonts.google.com/metadata/fonts", {
557
+ signal: controller.signal
558
+ });
559
+ const contentLength = res.headers.get("content-length");
560
+ if (contentLength && Number(contentLength) > MAX_RESPONSE_SIZE) {
561
+ throw new Error(
562
+ `[RI-1207] Google Fonts metadata response too large (${contentLength} bytes).`
563
+ );
564
+ }
565
+ if (!res.ok) {
566
+ throw new Error(`[RI-1205] Google Fonts metadata request failed with HTTP ${res.status}.`);
567
+ }
568
+ const contentType = res.headers.get("content-type") ?? "";
569
+ if (contentType && !contentType.includes("json")) {
570
+ throw new Error(
571
+ `[RI-1207] Google Fonts metadata returned unexpected Content-Type "${contentType}" instead of JSON.`
572
+ );
573
+ }
574
+ const data = await readJsonResponse(res);
575
+ return toGoogleFontMetaMap(data);
576
+ } catch (err) {
577
+ if (attempt < MAX_RETRIES) {
578
+ await wait(1e3 * 2 ** attempt);
579
+ continue;
580
+ }
581
+ throw err;
582
+ } finally {
583
+ clearTimeout(timeout);
584
+ }
585
+ }
586
+ throw new Error("[RI-1205] Exhausted Google Fonts metadata fetch retries.");
587
+ }
588
+
589
+ // src/integrations/font-providers/google/index.ts
590
+ var FETCH_RETRY_COOLDOWN_MS = 3e4;
591
+ async function fetchGoogleFontList() {
592
+ if (googleFontInternals.googleFontState.fetched) return;
593
+ if (googleFontInternals.googleFontListPromise) return googleFontInternals.googleFontListPromise;
594
+ if (googleFontInternals.lastFetchFailureMs > 0 && Date.now() - googleFontInternals.lastFetchFailureMs < FETCH_RETRY_COOLDOWN_MS) {
595
+ return;
596
+ }
597
+ const localPromise = (async () => {
598
+ try {
599
+ const isOffline = process.env.RI_OFFLINE === "1" || process.env.RI_OFFLINE === "true";
600
+ if (isOffline) {
601
+ if (await loadFontCache(true)) return;
602
+ console.warn(
603
+ `[RI-1206] RI_OFFLINE is set but no local font cache found at ${getFontCacheFile()}. Run once with network access to populate it. Non-variable fonts will default to weight "100 900" and may produce broken Google Fonts URLs \u2014 set an explicit weight in the @font directive to avoid this.`
604
+ );
605
+ return;
606
+ }
607
+ if (await loadFontCache()) return;
608
+ const fetchDisabled = process.env.RI_FETCH_FONTS === "0" || process.env.RI_FETCH_FONTS === "false";
609
+ if (fetchDisabled) {
610
+ if (await loadFontCache(true)) return;
611
+ return;
612
+ }
613
+ if (typeof globalThis.fetch !== "function") {
614
+ console.warn(
615
+ "[RI-1212] Global fetch() is not available. Google Fonts metadata requires Node.js >= 18. Skipping font fetch."
616
+ );
617
+ return;
618
+ }
619
+ if (isRIDebug()) {
620
+ console.warn("[RI-DEBUG] Fetching Google Fonts metadata...");
621
+ }
622
+ try {
623
+ const newCache = await fetchGoogleFontMetadata();
624
+ googleFontInternals.googleFontState = { cache: newCache, fetched: true };
625
+ await saveFontCache();
626
+ return;
627
+ } catch (err) {
628
+ const message = err instanceof Error ? err.message : String(err);
629
+ if (message.startsWith("[RI-1207]")) {
630
+ console.warn(`${message} Skipping.`);
631
+ if (await loadFontCache(true)) return;
632
+ return;
633
+ }
634
+ if (await loadFontCache(true)) return;
635
+ console.warn(
636
+ `[RI-1205] Could not fetch Google Fonts metadata after 3 attempts (${message}). Fonts without explicit weight/style will default to "100 900" + "normal italic" \u2014 non-variable fonts may produce broken Google Fonts URLs. Run with network access to populate the metadata cache, or set an explicit weight/style in the @font directive.`
637
+ );
638
+ }
639
+ } finally {
640
+ if (!googleFontInternals.googleFontState.fetched) {
641
+ googleFontInternals.lastFetchFailureMs = Date.now();
642
+ }
643
+ googleFontInternals.googleFontListPromise = null;
644
+ }
645
+ })();
646
+ googleFontInternals.googleFontListPromise = localPromise;
647
+ return localPromise;
648
+ }
649
+ function refreshFontWeightDefaults(fonts) {
650
+ return fonts.map((slot) => {
651
+ if (slot.kind !== "google") return slot;
652
+ const meta = googleFontInternals.googleFontState.cache.get(slot.family);
653
+ if (!meta) return slot;
654
+ let changed = false;
655
+ const faces = slot.faces.map((f) => {
656
+ let next = f;
657
+ if (!f._weightExplicit) {
658
+ const wghtAxis = meta.axes?.find((a) => a.tag === "wght");
659
+ const axisWeight = wghtAxis && wghtAxis.start !== wghtAxis.end ? `${wghtAxis.start} ${wghtAxis.end}` : "400";
660
+ if (next.weight !== axisWeight) next = { ...next, weight: axisWeight };
661
+ }
662
+ if (!f._styleExplicit) {
663
+ const italAxis = meta.axes?.find((a) => a.tag === "ital");
664
+ const axisStyle = italAxis ? "normal italic" : "normal";
665
+ if (next.style !== axisStyle) next = { ...next, style: axisStyle };
666
+ }
667
+ if (next !== f) changed = true;
668
+ return next;
669
+ });
670
+ return changed ? { ...slot, faces } : slot;
671
+ });
672
+ }
673
+ var FONT_FETCH_TIMEOUT_MS = 1e4;
674
+ async function resolveGoogleFonts(fonts) {
675
+ if (!fonts.some((slot) => slot.kind === "google")) return fonts;
676
+ try {
677
+ await withTimeout(
678
+ fetchGoogleFontList(),
679
+ FONT_FETCH_TIMEOUT_MS,
680
+ "[RI-1213] Google Fonts metadata fetch timed out"
681
+ );
682
+ } catch {
683
+ console.warn(
684
+ `[RI-1213] Could not fetch Google Fonts metadata within ${FONT_FETCH_TIMEOUT_MS / 1e3}s \u2014 proceeding with default font weights. Variable-weight fonts may use "400" instead of their full range.`
685
+ );
686
+ }
687
+ return refreshFontWeightDefaults(fonts);
688
+ }
689
+
690
+ // src/integrations/font-providers/index.ts
691
+ var SYSTEM_STACKS = Object.freeze({
692
+ sans: 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
693
+ serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
694
+ mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'
695
+ });
696
+ function getFallbackStack(slot) {
697
+ if (!SYSTEM_STACKS[slot] && isRIDebug()) {
698
+ console.warn(`[RI-DEBUG] Unknown font slot "${slot}" \u2014 falling back to sans stack.`);
699
+ }
700
+ return SYSTEM_STACKS[slot] || SYSTEM_STACKS.sans;
701
+ }
702
+ function googleFontsUrl(family, face) {
703
+ const encodedFamily = encodeURIComponent(family).replace(/%20/g, "+");
704
+ let axisParam;
705
+ if (face.weight.includes(",")) {
706
+ const weights = face.weight.split(",").map((w) => w.trim());
707
+ if (face.style.includes("italic")) {
708
+ const tuples = weights.flatMap((w) => [`0,${w}`, `1,${w}`]);
709
+ axisParam = `ital,wght@${tuples.join(";")}`;
710
+ } else {
711
+ axisParam = `wght@${weights.join(";")}`;
712
+ }
713
+ } else if (face.weight.includes(" ")) {
714
+ const range = face.weight.replace(" ", "..");
715
+ axisParam = face.style.includes("italic") ? `ital,wght@0,${range};1,${range}` : `wght@${range}`;
716
+ } else {
717
+ const w = face.weight || "400";
718
+ axisParam = face.style.includes("italic") ? `ital,wght@0,${w};1,${w}` : `wght@${w}`;
719
+ }
720
+ const display = face.display || "swap";
721
+ const subset = face.subset && face.subset !== "latin" ? `&subset=${encodeURIComponent(face.subset)}` : "";
722
+ return `https://fonts.googleapis.com/css2?family=${encodedFamily}:${axisParam}&display=${display}${subset}`;
723
+ }
724
+ function escapeFontFamily(name) {
725
+ return name.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\0/g, "\\0").replace(/\n/g, "\\a ").replace(/\r/g, "\\d ");
726
+ }
727
+ function generateFallbackFontFace(family, metrics) {
728
+ const safeFamily = escapeFontFamily(family);
729
+ return `@font-face {
730
+ font-family: "${safeFamily} Fallback";
731
+ src: local("${metrics.fallback}");
732
+ size-adjust: ${metrics.sizeAdjust}%;
733
+ ascent-override: ${metrics.ascent}%;
734
+ descent-override: ${metrics.descent}%;
735
+ line-gap-override: ${metrics.lineGap}%;
736
+ }`;
737
+ }
738
+ var FONT_FORMAT_MIME = {
739
+ woff2: "font/woff2",
740
+ woff: "font/woff",
741
+ truetype: "font/ttf",
742
+ opentype: "font/otf"
743
+ };
744
+ function inferFontFormat(path) {
745
+ if (path.endsWith(".woff2")) return "woff2";
746
+ if (path.endsWith(".woff")) return "woff";
747
+ if (path.endsWith(".ttf")) return "truetype";
748
+ if (path.endsWith(".otf")) return "opentype";
749
+ return "woff2";
750
+ }
751
+ function generateWebFontFace(family, face) {
752
+ if (face.provider === "system" || !face.provider) return null;
753
+ if (face.provider === "google") {
754
+ return { type: "import", css: `@import url("${googleFontsUrl(family, face)}");` };
755
+ }
756
+ const safeProvider = face.provider.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
757
+ const format = inferFontFormat(face.provider);
758
+ const src = `url("${safeProvider}") format("${format}")`;
759
+ const declarations = [` font-family: "${escapeFontFamily(family)}";`, ` src: ${src};`];
760
+ if (face.weight) declarations.push(` font-weight: ${face.weight};`);
761
+ if (face.style && face.style !== "normal") declarations.push(` font-style: ${face.style};`);
762
+ if (face.display) declarations.push(` font-display: ${face.display};`);
763
+ if (face.unicodeRange) declarations.push(` unicode-range: ${face.unicodeRange};`);
764
+ return { type: "font-face", css: `@font-face {
765
+ ${declarations.join("\n")}
766
+ }` };
767
+ }
768
+ function normalizeLocalStyle(style, family, warnings) {
769
+ if (style.includes(" ") && !style.startsWith("oblique")) {
770
+ const first = style.split(/\s+/)[0];
771
+ warnings.push(
772
+ `[RI-1203] Local font "${family}" has a compound font-style "${style}" \u2014 a single @font-face takes one style. Split upright and italic into separate @face blocks (or use the italic: shorthand). Using "${first}".`
773
+ );
774
+ return first;
775
+ }
776
+ return style;
777
+ }
778
+ function generateFontCSS(slot) {
779
+ const imports = [];
780
+ const fontFaces = [];
781
+ const variables = [];
782
+ const warnings = [];
783
+ const pushFeatureVars = () => {
784
+ if (slot.features) variables.push(`--font-${slot.slot}--features: ${slot.features};`);
785
+ if (slot.variation) variables.push(`--font-${slot.slot}--variations: ${slot.variation};`);
786
+ };
787
+ if (slot.kind === "system") {
788
+ variables.push(`--font-${slot.slot}: ${getFallbackStack(slot.slot)};`);
789
+ return { imports, fontFaces, variables, warnings };
790
+ }
791
+ if (slot.kind === "manual") {
792
+ const stack = [`"${escapeFontFamily(slot.family)}"`, ...slot.fallback].join(", ");
793
+ variables.push(`--font-${slot.slot}: ${stack};`);
794
+ pushFeatureVars();
795
+ return { imports, fontFaces, variables, warnings };
796
+ }
797
+ const { sizeAdjust, ascent, descent, lineGap } = slot;
798
+ const hasMetrics = sizeAdjust !== void 0 && ascent !== void 0 && descent !== void 0 && lineGap !== void 0;
799
+ if (hasMetrics) {
800
+ const metricsFallbackFont = slot.metricsFallback || slot.fallback[0] || "Arial";
801
+ fontFaces.push(
802
+ generateFallbackFontFace(slot.family, {
803
+ fallback: metricsFallbackFont,
804
+ sizeAdjust,
805
+ ascent,
806
+ descent,
807
+ lineGap
808
+ })
809
+ );
810
+ }
811
+ for (const face of slot.faces) {
812
+ if (face.provider !== "google" && !face.provider.startsWith("/") && !face.provider.startsWith("http") && !face.provider.startsWith(".")) {
813
+ warnings.push(
814
+ `[RI-1201] Unknown font provider "${face.provider}" for "${slot.family}" \u2014 supported: google, or a file path/URL.`
815
+ );
816
+ continue;
817
+ }
818
+ const emitFace = face.provider === "google" ? face : { ...face, style: normalizeLocalStyle(face.style, slot.family, warnings) };
819
+ const webFont = generateWebFontFace(slot.family, emitFace);
820
+ if (!webFont) continue;
821
+ if (webFont.type === "import") imports.push(webFont.css);
822
+ else fontFaces.push(webFont.css);
823
+ }
824
+ const fallbackStack = slot.fallback.length > 0 ? slot.fallback.join(", ") : getFallbackStack(slot.slot);
825
+ const safeFamily = escapeFontFamily(slot.family);
826
+ const stackParts = [`"${safeFamily}"`];
827
+ if (hasMetrics) stackParts.push(`"${safeFamily} Fallback"`);
828
+ stackParts.push(fallbackStack);
829
+ variables.push(`--font-${slot.slot}: ${stackParts.join(", ")};`);
830
+ pushFeatureVars();
831
+ return { imports, fontFaces, variables, warnings };
832
+ }
833
+ function getFontPreloadLinks(slots) {
834
+ const links = [];
835
+ const seen = /* @__PURE__ */ new Set();
836
+ for (const slot of slots) {
837
+ for (const face of slot.faces) {
838
+ const preload = face.preload ?? slot.preload;
839
+ if (!preload) continue;
840
+ if (face.provider === "system" || face.provider === "google" || !face.provider) continue;
841
+ if (seen.has(face.provider)) continue;
842
+ seen.add(face.provider);
843
+ links.push({
844
+ href: face.provider,
845
+ as: "font",
846
+ type: FONT_FORMAT_MIME[inferFontFormat(face.provider)],
847
+ crossorigin: true
848
+ });
849
+ }
850
+ }
851
+ return links;
852
+ }
853
+
854
+ // src/css/strip.ts
855
+ var DIRECTIVE_AT_RULE_PATTERNS = [...EXTRACTABLE_DIRECTIVE_NAMES].map(directiveAtRulePattern);
856
+ function buildProtectedRanges(css) {
857
+ const ranges = [];
858
+ let i = 0;
859
+ while (i < css.length) {
860
+ if (css[i] === "/" && css[i + 1] === "*") {
861
+ const start = i;
862
+ const end = css.indexOf("*/", i + 2);
863
+ i = end === -1 ? css.length : end + 2;
864
+ ranges.push([start, i]);
865
+ continue;
866
+ }
867
+ if (css[i] === '"' || css[i] === "'") {
868
+ const quote = css[i];
869
+ const start = i;
870
+ i++;
871
+ while (i < css.length && css[i] !== quote) {
872
+ if (css[i] === "\\" && i + 1 < css.length) i++;
873
+ i++;
874
+ }
875
+ if (i < css.length) i++;
876
+ ranges.push([start, i]);
877
+ } else {
878
+ i++;
879
+ }
880
+ }
881
+ return ranges;
882
+ }
883
+ function isInsideProtectedRange(pos, ranges) {
884
+ let lo = 0;
885
+ let hi = ranges.length - 1;
886
+ while (lo <= hi) {
887
+ const mid = lo + hi >>> 1;
888
+ const [start, end] = ranges[mid];
889
+ if (pos < start) hi = mid - 1;
890
+ else if (pos >= end) lo = mid + 1;
891
+ else return true;
892
+ }
893
+ return false;
894
+ }
895
+ function replaceOutsideProtectedRanges(css, re, ranges) {
896
+ re.lastIndex = 0;
897
+ const parts = [];
898
+ let lastIdx = 0;
899
+ for (; ; ) {
900
+ const match = re.exec(css);
901
+ if (match === null) break;
902
+ if (isInsideProtectedRange(match.index, ranges) || !isAtRuleBoundary(css, match.index)) {
903
+ continue;
904
+ }
905
+ let end = match.index + match[0].length;
906
+ if (isInsideProtectedRange(end - 1, ranges)) {
907
+ let k = end;
908
+ let aborted = false;
909
+ while (k < css.length) {
910
+ if (!isInsideProtectedRange(k, ranges)) {
911
+ const ch = css[k];
912
+ if (ch === ";") break;
913
+ if (ch === "{" || ch === "}") {
914
+ aborted = true;
915
+ break;
916
+ }
917
+ }
918
+ k++;
919
+ }
920
+ if (aborted) continue;
921
+ end = k < css.length ? k + 1 : css.length;
922
+ re.lastIndex = end;
923
+ }
924
+ parts.push(css.slice(lastIdx, match.index));
925
+ lastIdx = end;
926
+ }
927
+ parts.push(css.slice(lastIdx));
928
+ return parts.join("");
929
+ }
930
+ function stripBalancedBlocks(css, re, protectedRanges) {
931
+ re.lastIndex = 0;
932
+ const ranges = [];
933
+ for (; ; ) {
934
+ const match = re.exec(css);
935
+ if (match === null) break;
936
+ const start = match.index;
937
+ if (isInsideProtectedRange(start, protectedRanges)) {
938
+ continue;
939
+ }
940
+ if (!isAtRuleBoundary(css, start)) {
941
+ continue;
942
+ }
943
+ let braceIdx = css.indexOf("{", start);
944
+ while (braceIdx !== -1 && isInsideProtectedRange(braceIdx, protectedRanges)) {
945
+ braceIdx = css.indexOf("{", braceIdx + 1);
946
+ }
947
+ const preludeEnd = braceIdx === -1 ? css.length : braceIdx;
948
+ let semiIdx = -1;
949
+ for (let k = start; k < preludeEnd; k++) {
950
+ if (css[k] === ";" && !isInsideProtectedRange(k, protectedRanges)) {
951
+ semiIdx = k;
952
+ break;
953
+ }
954
+ }
955
+ if (semiIdx !== -1) {
956
+ ranges.push([start, semiIdx + 1]);
957
+ re.lastIndex = semiIdx + 1;
958
+ continue;
959
+ }
960
+ if (braceIdx === -1) {
961
+ ranges.push([start, css.length]);
962
+ break;
963
+ }
964
+ let depth = 1;
965
+ let j = braceIdx + 1;
966
+ while (j < css.length && depth > 0) {
967
+ const ch = css[j];
968
+ if (ch === '"' || ch === "'") {
969
+ const quote = ch;
970
+ j++;
971
+ while (j < css.length && css[j] !== quote) {
972
+ if (css[j] === "\\" && j + 1 < css.length) j++;
973
+ j++;
974
+ }
975
+ j++;
976
+ continue;
977
+ }
978
+ if (ch === "/" && css[j + 1] === "*") {
979
+ const end = css.indexOf("*/", j + 2);
980
+ j = end === -1 ? css.length : end + 2;
981
+ continue;
982
+ }
983
+ if (ch === "{") depth++;
984
+ else if (ch === "}") depth--;
985
+ j++;
986
+ }
987
+ ranges.push([start, j]);
988
+ re.lastIndex = j;
989
+ }
990
+ if (ranges.length === 0) return css;
991
+ const parts = [];
992
+ let lastEnd = 0;
993
+ for (const [start, end] of ranges) {
994
+ parts.push(css.slice(lastEnd, start));
995
+ lastEnd = end;
996
+ }
997
+ parts.push(css.slice(lastEnd));
998
+ return parts.join("");
999
+ }
1000
+ var ALL_SEMI_DIRECTIVES_RE = new RegExp(
1001
+ DIRECTIVE_AT_RULE_PATTERNS.map((p) => `${p}[^{;]*;`).join("|"),
1002
+ "g"
1003
+ );
1004
+ var ALL_BRACE_DIRECTIVES_RE = new RegExp(
1005
+ DIRECTIVE_AT_RULE_PATTERNS.map((p) => `${p}[^{]*{`).join("|"),
1006
+ "g"
1007
+ );
1008
+ var RI_IMPORT_RE = new RegExp(
1009
+ `@import\\s+(?:url\\(\\s*)?["'](?:${RI_IMPORT_SPECIFIER_ALTERNATION})["']\\s*\\)?[^;]*;`,
1010
+ "g"
1011
+ );
1012
+ function stripRIDirectives(css) {
1013
+ let currentRanges = buildProtectedRanges(css);
1014
+ let result = replaceOutsideProtectedRanges(css, RI_IMPORT_RE, currentRanges);
1015
+ if (result !== css) {
1016
+ currentRanges = buildProtectedRanges(result);
1017
+ }
1018
+ const beforePhase1 = result;
1019
+ result = replaceOutsideProtectedRanges(result, ALL_SEMI_DIRECTIVES_RE, currentRanges);
1020
+ const dirty = result !== beforePhase1;
1021
+ if (dirty) {
1022
+ currentRanges = buildProtectedRanges(result);
1023
+ }
1024
+ result = stripBalancedBlocks(result, ALL_BRACE_DIRECTIVES_RE, currentRanges);
1025
+ return result.trim();
1026
+ }
1027
+
1028
+ // src/css/preflight.ts
1029
+ var DEFAULT_PREFLIGHT = {
1030
+ core: true,
1031
+ typography: true,
1032
+ content: true,
1033
+ forms: true,
1034
+ interactive: true,
1035
+ modern: true
1036
+ };
1037
+ var modules = [
1038
+ // ── Core ─────────────────────────────────────────────────
1039
+ {
1040
+ name: "box-sizing",
1041
+ category: "core",
1042
+ css: `*, *::before, *::after {
1043
+ box-sizing: border-box;
1044
+ }`
1045
+ },
1046
+ {
1047
+ name: "margins",
1048
+ category: "core",
1049
+ css: `body, h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd, pre {
1050
+ margin: 0;
1051
+ }`
1052
+ },
1053
+ {
1054
+ name: "borders",
1055
+ category: "core",
1056
+ css: `*, *::before, *::after {
1057
+ border-width: 0;
1058
+ border-style: solid;
1059
+ border-color: inherit;
1060
+ }`
1061
+ },
1062
+ {
1063
+ name: "root-defaults",
1064
+ category: "core",
1065
+ css: `:root {
1066
+ --sans-fallback: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
1067
+ --serif-fallback: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
1068
+ --mono-fallback: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
1069
+ line-height: 1.5;
1070
+ -webkit-text-size-adjust: 100%;
1071
+ tab-size: 4;
1072
+ }
1073
+ body {
1074
+ line-height: inherit;
1075
+ font-family: var(--font-sans);
1076
+ }
1077
+ code, kbd, samp, pre {
1078
+ font-family: var(--font-mono);
1079
+ font-size: 1em;
1080
+ }`
1081
+ },
1082
+ // ── Typography ───────────────────────────────────────────
1083
+ {
1084
+ name: "font-smoothing",
1085
+ category: "typography",
1086
+ css: `body {
1087
+ -webkit-font-smoothing: antialiased;
1088
+ -moz-osx-font-smoothing: grayscale;
1089
+ }`
1090
+ },
1091
+ {
1092
+ name: "font-inherit",
1093
+ category: "typography",
1094
+ css: `button, input, optgroup, select, textarea {
1095
+ font-family: inherit;
1096
+ font-feature-settings: inherit;
1097
+ font-variation-settings: inherit;
1098
+ font-size: 100%;
1099
+ font-weight: inherit;
1100
+ line-height: inherit;
1101
+ letter-spacing: inherit;
1102
+ color: inherit;
1103
+ }`
1104
+ },
1105
+ {
1106
+ name: "heading-sizes",
1107
+ category: "typography",
1108
+ css: `h1, h2, h3, h4, h5, h6 {
1109
+ font-size: inherit;
1110
+ font-weight: inherit;
1111
+ }`
1112
+ },
1113
+ {
1114
+ name: "link-reset",
1115
+ category: "typography",
1116
+ css: `a {
1117
+ color: inherit;
1118
+ text-decoration: inherit;
1119
+ }`
1120
+ },
1121
+ // ── Content ──────────────────────────────────────────────
1122
+ {
1123
+ name: "media-block",
1124
+ category: "content",
1125
+ css: `img, svg, video, canvas, audio, iframe, embed, object {
1126
+ display: block;
1127
+ max-inline-size: 100%;
1128
+ }
1129
+ img, video {
1130
+ block-size: auto;
1131
+ }`
1132
+ },
1133
+ {
1134
+ name: "list-reset",
1135
+ category: "content",
1136
+ css: `ol, ul, menu {
1137
+ list-style: none;
1138
+ padding: 0;
1139
+ }`
1140
+ },
1141
+ {
1142
+ name: "table-reset",
1143
+ category: "content",
1144
+ css: `table {
1145
+ text-indent: 0;
1146
+ border-color: inherit;
1147
+ border-collapse: collapse;
1148
+ }`
1149
+ },
1150
+ {
1151
+ name: "hr-reset",
1152
+ category: "content",
1153
+ css: `hr {
1154
+ block-size: 0;
1155
+ border-top-width: 1px;
1156
+ color: inherit;
1157
+ }`
1158
+ },
1159
+ // ── Forms ────────────────────────────────────────────────
1160
+ {
1161
+ name: "button-reset",
1162
+ category: "forms",
1163
+ css: `button, [role="button"] {
1164
+ cursor: pointer;
1165
+ padding: 0;
1166
+ }
1167
+ button {
1168
+ background-color: transparent;
1169
+ background-image: none;
1170
+ }`
1171
+ },
1172
+ {
1173
+ name: "input-reset",
1174
+ category: "forms",
1175
+ css: `input::placeholder, textarea::placeholder {
1176
+ opacity: 1;
1177
+ color: oklch(0.556 0 0);
1178
+ }
1179
+ input:where([type="button"], [type="reset"], [type="submit"]) {
1180
+ -webkit-appearance: button;
1181
+ appearance: button;
1182
+ }`
1183
+ },
1184
+ {
1185
+ name: "textarea-reset",
1186
+ category: "forms",
1187
+ css: `textarea {
1188
+ resize: vertical;
1189
+ }`
1190
+ },
1191
+ {
1192
+ name: "select-reset",
1193
+ category: "forms",
1194
+ css: `select {
1195
+ -webkit-appearance: none;
1196
+ appearance: none;
1197
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='currentColor'%3e%3cpath fill-rule='evenodd' d='M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06z' clip-rule='evenodd'/%3e%3c/svg%3e");
1198
+ background-position: right 0.5rem center;
1199
+ background-repeat: no-repeat;
1200
+ background-size: 1.5em 1.5em;
1201
+ padding-inline-end: 2.5rem;
1202
+ }`
1203
+ },
1204
+ // ── Interactive ──────────────────────────────────────────
1205
+ {
1206
+ name: "focus-visible",
1207
+ category: "interactive",
1208
+ css: `:focus-visible {
1209
+ outline-width: var(--spacing);
1210
+ outline-style: solid;
1211
+ outline-offset: calc(var(--spacing) * 0.5);
1212
+ outline-color: currentColor;
1213
+ }
1214
+ :focus:not(:focus-visible) {
1215
+ outline: none;
1216
+ }`
1217
+ },
1218
+ {
1219
+ name: "dialog-reset",
1220
+ category: "interactive",
1221
+ css: `dialog {
1222
+ padding: 0;
1223
+ }
1224
+ dialog::backdrop {
1225
+ background-color: oklch(0 0 0 / 0.5);
1226
+ }`
1227
+ },
1228
+ {
1229
+ name: "summary-reset",
1230
+ category: "interactive",
1231
+ css: `summary {
1232
+ display: list-item;
1233
+ cursor: pointer;
1234
+ }`
1235
+ },
1236
+ // ── Modern ───────────────────────────────────────────────
1237
+ {
1238
+ name: "interpolate-size",
1239
+ category: "modern",
1240
+ css: `@supports (interpolate-size: allow-keywords) {
1241
+ :root {
1242
+ interpolate-size: allow-keywords;
1243
+ }
1244
+ }`
1245
+ },
1246
+ {
1247
+ name: "overflow-wrap",
1248
+ category: "modern",
1249
+ css: `body {
1250
+ overflow-wrap: break-word;
1251
+ }`
1252
+ },
1253
+ {
1254
+ name: "color-scheme",
1255
+ category: "modern",
1256
+ css: `html {
1257
+ color-scheme: light dark;
1258
+ }
1259
+ html[data-appearance="dark"] {
1260
+ color-scheme: dark;
1261
+ }
1262
+ html[data-appearance="light"] {
1263
+ color-scheme: light;
1264
+ }`
1265
+ }
1266
+ ];
1267
+ function generatePreflight(config = DEFAULT_PREFLIGHT) {
1268
+ const enabledModules = modules.filter((m) => config[m.category]);
1269
+ if (enabledModules.length === 0) return "";
1270
+ return enabledModules.map((m) => `/* preflight: ${m.name} */
1271
+ ${m.css}`).join("\n\n");
1272
+ }
1273
+
1274
+ // src/assembly.ts
1275
+ function generateTokenLayer(theme, usage, fontOutputCache) {
1276
+ const vars = [];
1277
+ vars.push(`--spacing: ${theme.spacing.base};`);
1278
+ vars.push(`--fluid-min: ${theme.fluid.min};`);
1279
+ vars.push(`--fluid-max: ${theme.fluid.max};`);
1280
+ if (theme.textFluid) {
1281
+ vars.push(`--fluid-text-min: ${theme.textFluid.min};`);
1282
+ vars.push(`--fluid-text-max: ${theme.textFluid.max};`);
1283
+ }
1284
+ if (theme.spacingFluid) {
1285
+ vars.push(`--fluid-spacing-min: ${theme.spacingFluid.min};`);
1286
+ vars.push(`--fluid-spacing-max: ${theme.spacingFluid.max};`);
1287
+ }
1288
+ const effectiveStops = new Map(
1289
+ [...usage.usedColorStops].map(([k, v]) => [k, new Set(v)])
1290
+ );
1291
+ const themeDef = theme.colors.theme;
1292
+ const themeSuffixes = effectiveStops.get("theme");
1293
+ if (themeDef && themeSuffixes && themeDef.type === "alias") {
1294
+ let sourceSuffixes = effectiveStops.get(themeDef.source);
1295
+ if (!sourceSuffixes) {
1296
+ sourceSuffixes = /* @__PURE__ */ new Set();
1297
+ effectiveStops.set(themeDef.source, sourceSuffixes);
1298
+ }
1299
+ for (const s of themeSuffixes) sourceSuffixes.add(s);
1300
+ }
1301
+ if (themeDef && themeSuffixes) {
1302
+ for (const [name, def] of Object.entries(theme.colors)) {
1303
+ if (name === "theme" || def.type !== "generative" || !def.inline) continue;
1304
+ let stops = effectiveStops.get(name);
1305
+ if (!stops) {
1306
+ stops = /* @__PURE__ */ new Set();
1307
+ effectiveStops.set(name, stops);
1308
+ }
1309
+ for (const s of themeSuffixes) stops.add(s);
1310
+ }
1311
+ }
1312
+ for (const def of Object.values(theme.colors)) {
1313
+ const values = def.type === "explicit" ? [def.value] : def.type === "pair" ? [def.light, def.dark] : null;
1314
+ if (!values) continue;
1315
+ for (const v of values) {
1316
+ for (const m of v.matchAll(COLOR_STOP_REF_RE)) {
1317
+ if (!m[2]) continue;
1318
+ const hue = m[1];
1319
+ const stop = Number(m[2]);
1320
+ let set = effectiveStops.get(hue);
1321
+ if (!set) {
1322
+ set = /* @__PURE__ */ new Set();
1323
+ effectiveStops.set(hue, set);
1324
+ }
1325
+ set.add(stop);
1326
+ }
1327
+ }
1328
+ }
1329
+ const colorVars = generateAllColorVariables(theme.colors, theme.darkConfig, effectiveStops);
1330
+ vars.push(...colorVars);
1331
+ for (const [name, size] of Object.entries(theme.text).sort(
1332
+ ([a], [b]) => codepointCompare(a, b)
1333
+ )) {
1334
+ if (!usage.usedTextSizes.has(name)) continue;
1335
+ vars.push(`--text-${name}: ${size.fontSize};`);
1336
+ vars.push(`--text-${name}-leading: ${size.lineHeight};`);
1337
+ }
1338
+ const configuredSlots = /* @__PURE__ */ new Set();
1339
+ for (const fontConfig of theme.fonts) {
1340
+ if (!usage.usedFonts.has(fontConfig.slot)) continue;
1341
+ const output = getCachedFontOutput(fontConfig, fontOutputCache);
1342
+ vars.push(...output.variables);
1343
+ configuredSlots.add(fontConfig.slot);
1344
+ }
1345
+ for (const [slot, stack] of Object.entries(SYSTEM_STACKS).sort(
1346
+ ([a], [b]) => codepointCompare(a, b)
1347
+ )) {
1348
+ if (!configuredSlots.has(slot) && usage.usedFonts.has(slot)) {
1349
+ vars.push(`--font-${slot}: ${stack};`);
1350
+ }
1351
+ }
1352
+ if (usage.usedRounded.size > 0) {
1353
+ vars.push(`--rounded-roof: ${theme.roundedRoof};`);
1354
+ for (const [name, val] of Object.entries(theme.rounded).sort(
1355
+ ([a], [b]) => codepointCompare(a, b)
1356
+ )) {
1357
+ if (!usage.usedRounded.has(name)) continue;
1358
+ vars.push(`--rounded-${name}: ${val};`);
1359
+ }
1360
+ }
1361
+ const shadowsToEmit = resolveTransitiveShadowDeps(theme.shadows, usage.usedShadows);
1362
+ for (const [name, val] of Object.entries(theme.shadows).sort(
1363
+ ([a], [b]) => codepointCompare(a, b)
1364
+ )) {
1365
+ if (!shadowsToEmit.has(name)) continue;
1366
+ vars.push(`--shadow-${name}: ${val};`);
1367
+ }
1368
+ for (const [name, def] of Object.entries(theme.animations).sort(
1369
+ ([a], [b]) => codepointCompare(a, b)
1370
+ )) {
1371
+ if (!usage.usedAnimations.has(name)) continue;
1372
+ vars.push(`--animate-${name}: ${def.shorthand};`);
1373
+ }
1374
+ return `:root {
1375
+ ${vars.join("\n ")}
1376
+ }`;
1377
+ }
1378
+ function resolveTransitiveShadowDeps(shadows, seeds) {
1379
+ const out = /* @__PURE__ */ new Set();
1380
+ const worklist = [];
1381
+ for (const name of seeds) {
1382
+ if (Object.hasOwn(shadows, name)) {
1383
+ out.add(name);
1384
+ worklist.push(name);
1385
+ }
1386
+ }
1387
+ while (worklist.length > 0) {
1388
+ const name = worklist.pop();
1389
+ const value = shadows[name];
1390
+ if (!value?.includes("var(--shadow-")) continue;
1391
+ for (const m of value.matchAll(SHADOW_VAR_REF_RE)) {
1392
+ const dep = m[1];
1393
+ if (!out.has(dep) && Object.hasOwn(shadows, dep)) {
1394
+ out.add(dep);
1395
+ worklist.push(dep);
1396
+ }
1397
+ }
1398
+ }
1399
+ return out;
1400
+ }
1401
+ function cornerShapeValue(shape) {
1402
+ return typeof shape === "string" ? shape : `superellipse(${shape.superellipse})`;
1403
+ }
1404
+ function generateCornerShapeBlock(theme) {
1405
+ if (theme.roundedShape === null) return null;
1406
+ const value = cornerShapeValue(theme.roundedShape);
1407
+ const shapeRule = `:root {
1408
+ corner-shape: ${value};
1409
+ }
1410
+ *, ::before, ::after {
1411
+ corner-shape: inherit;
1412
+ }`;
1413
+ if (theme.roundedShapeScale === 1) return shapeRule;
1414
+ const scaleBlock = `@supports (corner-shape: ${value}) {
1415
+ :root {
1416
+ --ri-rounded-scale: ${theme.roundedShapeScale};
1417
+ }
1418
+ }`;
1419
+ return `${shapeRule}
1420
+
1421
+ ${scaleBlock}`;
1422
+ }
1423
+ function fontCacheKey(config) {
1424
+ return JSON.stringify(config);
1425
+ }
1426
+ function getCachedFontOutput(config, cache) {
1427
+ const key = fontCacheKey(config);
1428
+ let cached = cache.get(key);
1429
+ if (!cached) {
1430
+ cached = generateFontCSS(config);
1431
+ cache.set(key, cached);
1432
+ }
1433
+ return cached;
1434
+ }
1435
+ function renderRegisteredProperty(reg) {
1436
+ const lines = [
1437
+ `@property ${reg.name} {`,
1438
+ ` syntax: ${reg.syntax};`,
1439
+ ` inherits: ${reg.inherits};`
1440
+ ];
1441
+ if (reg.initialValue !== void 0) lines.push(` initial-value: ${reg.initialValue};`);
1442
+ lines.push("}");
1443
+ return lines.join("\n");
1444
+ }
1445
+ var PROPERTY_NAME_RE = /@property\s+(--[A-Za-z0-9_-]+)/;
1446
+ function collectPropertyBlocks(registered, engineProperties) {
1447
+ const blocks = [];
1448
+ const seen = /* @__PURE__ */ new Set();
1449
+ const push = (block) => {
1450
+ const name = PROPERTY_NAME_RE.exec(block)?.[1];
1451
+ if (name) {
1452
+ if (seen.has(name)) return;
1453
+ seen.add(name);
1454
+ }
1455
+ blocks.push(block);
1456
+ };
1457
+ for (const reg of registered) push(renderRegisteredProperty(reg));
1458
+ for (const block of engineProperties) push(block);
1459
+ return blocks;
1460
+ }
1461
+ function assembleSections(compilation, theme, fontOutputCache) {
1462
+ const sections = [];
1463
+ const fontImports = [];
1464
+ const fontFaceBlocks = [];
1465
+ const assemblyWarnings = [];
1466
+ const fontWarningsEmitted = /* @__PURE__ */ new Set();
1467
+ if (theme.preflight.core !== false) {
1468
+ compilation.usedFonts.add("sans");
1469
+ compilation.usedFonts.add("mono");
1470
+ }
1471
+ for (const fontConfig of theme.fonts) {
1472
+ if (!compilation.usedFonts.has(fontConfig.slot)) continue;
1473
+ const output = getCachedFontOutput(fontConfig, fontOutputCache);
1474
+ fontImports.push(...output.imports);
1475
+ fontFaceBlocks.push(...output.fontFaces);
1476
+ for (const w of output.warnings) {
1477
+ if (!fontWarningsEmitted.has(w)) {
1478
+ assemblyWarnings.push(w);
1479
+ fontWarningsEmitted.add(w);
1480
+ }
1481
+ }
1482
+ }
1483
+ if (fontImports.length > 0) {
1484
+ sections.push(fontImports.join("\n"));
1485
+ }
1486
+ const propertyBlocks = collectPropertyBlocks(theme.registeredProperties, compilation.properties);
1487
+ if (propertyBlocks.length > 0) {
1488
+ sections.push(propertyBlocks.join("\n\n"));
1489
+ }
1490
+ if (fontFaceBlocks.length > 0) {
1491
+ sections.push(fontFaceBlocks.join("\n\n"));
1492
+ }
1493
+ const tokenLayer = generateTokenLayer(theme, compilation, fontOutputCache);
1494
+ if (tokenLayer) sections.push(tokenLayer);
1495
+ for (const w of checkPaletteContrast(theme.colors, compilation.usedColorStops)) {
1496
+ assemblyWarnings.push(w);
1497
+ }
1498
+ const themeOverrides = generateThemeOverrides(
1499
+ theme.colors,
1500
+ compilation.usedColorStops.get("theme")
1501
+ );
1502
+ if (themeOverrides.length > 0) {
1503
+ sections.push(themeOverrides.join("\n\n"));
1504
+ }
1505
+ const cornerShape = generateCornerShapeBlock(theme);
1506
+ if (cornerShape) sections.push(cornerShape);
1507
+ if (compilation.keyframes.length > 0) {
1508
+ sections.push(compilation.keyframes.join("\n\n"));
1509
+ }
1510
+ const preflight = generatePreflight(theme.preflight);
1511
+ if (preflight) sections.push(preflight);
1512
+ const utilityCSS = renderCSS({
1513
+ ...compilation,
1514
+ properties: [],
1515
+ keyframes: []
1516
+ });
1517
+ if (utilityCSS) sections.push(utilityCSS);
1518
+ if (theme.layer) {
1519
+ applyLayerWrapping(sections, fontImports, theme.layer, utilityCSS !== "");
1520
+ }
1521
+ return { sections, fontImports, warnings: assemblyWarnings };
1522
+ }
1523
+ function wrapInLayer(content, layerName) {
1524
+ const indented = content.replace(/^(?=.)/gm, " ");
1525
+ return `@layer ${layerName} {
1526
+ ${indented}
1527
+ }`;
1528
+ }
1529
+ function applyLayerWrapping(sections, fontImports, layer, hasUtilitySection) {
1530
+ const hasImportSection = fontImports.length > 0 && sections.length > 0;
1531
+ const importSection = hasImportSection ? sections[0] : null;
1532
+ const contentSections = hasImportSection ? sections.slice(1) : [...sections];
1533
+ if (layer.wrapAll) {
1534
+ const joined = contentSections.join("\n\n");
1535
+ sections.length = 0;
1536
+ if (importSection) sections.push(importSection);
1537
+ sections.push(`@layer ${layer.wrapAll};`);
1538
+ if (joined) sections.push(wrapInLayer(joined, layer.wrapAll));
1539
+ return;
1540
+ }
1541
+ const utilitySection = hasUtilitySection ? contentSections.pop() ?? null : null;
1542
+ const baseSections = contentSections;
1543
+ const finalSections = [];
1544
+ if (importSection) finalSections.push(importSection);
1545
+ if (layer.order && layer.order.length > 0) {
1546
+ finalSections.push(`@layer ${layer.order.join(", ")};`);
1547
+ }
1548
+ if (baseSections.length > 0) {
1549
+ const baseJoined = baseSections.join("\n\n");
1550
+ if (layer.base) {
1551
+ finalSections.push(wrapInLayer(baseJoined, layer.base));
1552
+ } else {
1553
+ finalSections.push(baseJoined);
1554
+ }
1555
+ }
1556
+ if (utilitySection) {
1557
+ if (layer.utilities) {
1558
+ finalSections.push(wrapInLayer(utilitySection, layer.utilities));
1559
+ } else {
1560
+ finalSections.push(utilitySection);
1561
+ }
1562
+ }
1563
+ sections.length = 0;
1564
+ sections.push(...finalSections);
1565
+ }
1566
+
1567
+ // src/project/pipeline.ts
1568
+ function collectApplyClassNames(css, warnings) {
1569
+ const classes = [];
1570
+ for (const match of css.matchAll(APPLY_LIKE_MATCH_RE)) {
1571
+ const params = expandVariantGroups(match[1], warnings);
1572
+ for (const className of params.trim().split(/\s+/)) {
1573
+ if (className) classes.push(className);
1574
+ }
1575
+ }
1576
+ return classes;
1577
+ }
1578
+ async function finalizeProjectCompilation(options) {
1579
+ const { analysis } = options;
1580
+ let effectiveTheme = analysis.theme;
1581
+ if (options.resolveFonts) {
1582
+ const resolvedFonts = await options.resolveFonts(analysis.theme.fonts);
1583
+ if (resolvedFonts !== analysis.theme.fonts) {
1584
+ effectiveTheme = { ...analysis.theme, fonts: [...resolvedFonts] };
1585
+ }
1586
+ }
1587
+ const expansionWarnings = [];
1588
+ const classNameSet = new Set(options.classNames);
1589
+ for (const cls of collectApplyClassNames(options.css, expansionWarnings)) {
1590
+ classNameSet.add(cls);
1591
+ }
1592
+ const classNames = [...classNameSet];
1593
+ pushWarningsDeduped(analysis.warnings, expansionWarnings, analysis.warningSeen);
1594
+ const compiler = createCompiler();
1595
+ const compilation = compiler.compile(classNames, effectiveTheme);
1596
+ let userCSS = stripRIDirectives(options.css);
1597
+ if ((options.processCssFunctions ?? true) && userCSS && hasCSSFunctions(userCSS)) {
1598
+ userCSS = compileCSSFunctions(userCSS, effectiveTheme, analysis.warnings);
1599
+ }
1600
+ if (userCSS) {
1601
+ scanCSSForTokenUsage(userCSS, compilation);
1602
+ }
1603
+ const { sections, warnings: assemblyWarnings } = assembleSections(
1604
+ compilation,
1605
+ effectiveTheme,
1606
+ compiler.fontOutputCache
1607
+ );
1608
+ pushWarningsDeduped(analysis.warnings, assemblyWarnings, analysis.warningSeen);
1609
+ pushWarningsDeduped(analysis.warnings, compilation.warnings, analysis.warningSeen);
1610
+ let joinedCSS = null;
1611
+ return {
1612
+ get css() {
1613
+ if (joinedCSS === null) {
1614
+ joinedCSS = userCSS ? [...sections, userCSS].join("\n\n") : sections.join("\n\n");
1615
+ }
1616
+ return joinedCSS;
1617
+ },
1618
+ sections,
1619
+ userCSS,
1620
+ classNames,
1621
+ theme: effectiveTheme,
1622
+ directives: analysis.directives,
1623
+ warnings: analysis.warnings
1624
+ };
1625
+ }
1626
+
1627
+ export {
1628
+ validateGlobPattern,
1629
+ DEFAULT_PATTERNS,
1630
+ DEFAULT_EXCLUDES,
1631
+ collectProjectClasses,
1632
+ resolveGoogleFonts,
1633
+ getFontPreloadLinks,
1634
+ finalizeProjectCompilation
1635
+ };