rainbowindex 0.3.0 → 0.4.1

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.
@@ -5,6 +5,7 @@ import {
5
5
  RI_IMPORT_SPECIFIER_ALTERNATION,
6
6
  SAFE_FONT_FAMILY_RE,
7
7
  SHADOW_VAR_REF_RE,
8
+ analyzeProjectCSS,
8
9
  codepointCompare,
9
10
  compileCSSFunctions,
10
11
  createCompiler,
@@ -18,341 +19,12 @@ import {
18
19
  renderCSS,
19
20
  scanCSSForTokenUsage,
20
21
  withTimeout
21
- } from "./chunk-IE6N76PW.mjs";
22
+ } from "./chunk-6GTG5SZJ.mjs";
22
23
  import {
23
24
  checkPaletteContrast,
24
25
  generateAllColorVariables,
25
26
  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
- }
27
+ } from "./chunk-KRZL4IDK.mjs";
356
28
 
357
29
  // src/integrations/font-providers/google/state.ts
358
30
  var googleFontInternals = {
@@ -366,13 +38,13 @@ var googleFontInternals = {
366
38
  // src/integrations/font-providers/google/cache.ts
367
39
  import { createHash, randomUUID } from "crypto";
368
40
  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";
41
+ import { resolve, dirname, join, isAbsolute } from "path";
370
42
  import { isAbsolute as win32IsAbsolute } from "path/win32";
371
43
  var MAX_FONT_CACHE_TTL_SECONDS = 30 * 24 * 60 * 60;
372
44
  function getFontCacheDir() {
373
45
  if (googleFontInternals.validatedCacheDir !== null) return googleFontInternals.validatedCacheDir;
374
46
  const raw = process.env.RI_CACHE_DIR || "node_modules/.cache/rainbowindex";
375
- if (isAbsolute2(raw) || win32IsAbsolute(raw)) {
47
+ if (isAbsolute(raw) || win32IsAbsolute(raw)) {
376
48
  throw new Error(
377
49
  `[RI-1208] RI_CACHE_DIR must be a relative path, got absolute path: "${raw}". Use a relative path like "node_modules/.cache/rainbowindex".`
378
50
  );
@@ -392,7 +64,7 @@ function getResolvedCachePath() {
392
64
  if (googleFontInternals.resolvedCachePath !== null && googleFontInternals.validatedCacheDir !== null) {
393
65
  return googleFontInternals.resolvedCachePath;
394
66
  }
395
- googleFontInternals.resolvedCachePath = resolve3(process.cwd(), getFontCacheFile());
67
+ googleFontInternals.resolvedCachePath = resolve(process.cwd(), getFontCacheFile());
396
68
  return googleFontInternals.resolvedCachePath;
397
69
  }
398
70
  function getFontCacheTTL() {
@@ -475,12 +147,12 @@ async function loadFontCache(ignoreExpiry = false) {
475
147
  async function saveFontCache() {
476
148
  try {
477
149
  const cachePath = getResolvedCachePath();
478
- await mkdir(dirname2(cachePath), { recursive: true });
150
+ await mkdir(dirname(cachePath), { recursive: true });
479
151
  const entries = Array.from(googleFontInternals.googleFontState.cache.values());
480
152
  const entriesJson = JSON.stringify(entries);
481
153
  const checksum = createHash("sha256").update(entriesJson).digest("hex");
482
154
  const payload = `{"checksum":${JSON.stringify(checksum)},"entries":${entriesJson}}`;
483
- const tmpPath = join2(dirname2(cachePath), `.google-${process.pid}-${randomUUID()}.tmp`);
155
+ const tmpPath = join(dirname(cachePath), `.google-${process.pid}-${randomUUID()}.tmp`);
484
156
  try {
485
157
  await writeFile(tmpPath, payload);
486
158
  await rename(tmpPath, cachePath);
@@ -583,272 +255,763 @@ async function fetchGoogleFontMetadata() {
583
255
  clearTimeout(timeout);
584
256
  }
585
257
  }
586
- throw new Error("[RI-1205] Exhausted Google Fonts metadata fetch retries.");
258
+ throw new Error("[RI-1205] Exhausted Google Fonts metadata fetch retries.");
259
+ }
260
+
261
+ // src/integrations/font-providers/google/index.ts
262
+ var FETCH_RETRY_COOLDOWN_MS = 3e4;
263
+ async function fetchGoogleFontList() {
264
+ if (googleFontInternals.googleFontState.fetched) return;
265
+ if (googleFontInternals.googleFontListPromise) return googleFontInternals.googleFontListPromise;
266
+ if (googleFontInternals.lastFetchFailureMs > 0 && Date.now() - googleFontInternals.lastFetchFailureMs < FETCH_RETRY_COOLDOWN_MS) {
267
+ return;
268
+ }
269
+ const localPromise = (async () => {
270
+ try {
271
+ const isOffline = process.env.RI_OFFLINE === "1" || process.env.RI_OFFLINE === "true";
272
+ if (isOffline) {
273
+ if (await loadFontCache(true)) return;
274
+ console.warn(
275
+ `[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.`
276
+ );
277
+ return;
278
+ }
279
+ if (await loadFontCache()) return;
280
+ const fetchDisabled = process.env.RI_FETCH_FONTS === "0" || process.env.RI_FETCH_FONTS === "false";
281
+ if (fetchDisabled) {
282
+ if (await loadFontCache(true)) return;
283
+ return;
284
+ }
285
+ if (typeof globalThis.fetch !== "function") {
286
+ console.warn(
287
+ "[RI-1212] Global fetch() is not available. Google Fonts metadata requires Node.js >= 18. Skipping font fetch."
288
+ );
289
+ return;
290
+ }
291
+ if (isRIDebug()) {
292
+ console.warn("[RI-DEBUG] Fetching Google Fonts metadata...");
293
+ }
294
+ try {
295
+ const newCache = await fetchGoogleFontMetadata();
296
+ googleFontInternals.googleFontState = { cache: newCache, fetched: true };
297
+ await saveFontCache();
298
+ return;
299
+ } catch (err) {
300
+ const message = err instanceof Error ? err.message : String(err);
301
+ if (message.startsWith("[RI-1207]")) {
302
+ console.warn(`${message} Skipping.`);
303
+ if (await loadFontCache(true)) return;
304
+ return;
305
+ }
306
+ if (await loadFontCache(true)) return;
307
+ console.warn(
308
+ `[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.`
309
+ );
310
+ }
311
+ } finally {
312
+ if (!googleFontInternals.googleFontState.fetched) {
313
+ googleFontInternals.lastFetchFailureMs = Date.now();
314
+ }
315
+ googleFontInternals.googleFontListPromise = null;
316
+ }
317
+ })();
318
+ googleFontInternals.googleFontListPromise = localPromise;
319
+ return localPromise;
320
+ }
321
+ var refreshMemo = /* @__PURE__ */ new WeakMap();
322
+ function refreshFontWeightDefaults(fonts) {
323
+ const state = googleFontInternals.googleFontState;
324
+ const memo = refreshMemo.get(fonts);
325
+ if (memo && memo.state === state) return memo.result;
326
+ let anyChanged = false;
327
+ const refreshed = fonts.map((slot) => {
328
+ if (slot.kind !== "google") return slot;
329
+ const meta = googleFontInternals.googleFontState.cache.get(slot.family);
330
+ if (!meta) return slot;
331
+ let changed = false;
332
+ const faces = slot.faces.map((f) => {
333
+ let next = f;
334
+ if (!f._weightExplicit) {
335
+ const wghtAxis = meta.axes?.find((a) => a.tag === "wght");
336
+ const axisWeight = wghtAxis && wghtAxis.start !== wghtAxis.end ? `${wghtAxis.start} ${wghtAxis.end}` : "400";
337
+ if (next.weight !== axisWeight) next = { ...next, weight: axisWeight };
338
+ }
339
+ if (!f._styleExplicit) {
340
+ const italAxis = meta.axes?.find((a) => a.tag === "ital");
341
+ const axisStyle = italAxis ? "normal italic" : "normal";
342
+ if (next.style !== axisStyle) next = { ...next, style: axisStyle };
343
+ }
344
+ if (next !== f) changed = true;
345
+ return next;
346
+ });
347
+ if (changed) anyChanged = true;
348
+ return changed ? { ...slot, faces } : slot;
349
+ });
350
+ const result = anyChanged ? refreshed : fonts;
351
+ refreshMemo.set(fonts, { state, result });
352
+ return result;
353
+ }
354
+ var FONT_FETCH_TIMEOUT_MS = 1e4;
355
+ async function resolveGoogleFonts(fonts) {
356
+ if (!fonts.some((slot) => slot.kind === "google")) return fonts;
357
+ try {
358
+ await withTimeout(
359
+ fetchGoogleFontList(),
360
+ FONT_FETCH_TIMEOUT_MS,
361
+ "[RI-1213] Google Fonts metadata fetch timed out"
362
+ );
363
+ } catch {
364
+ console.warn(
365
+ `[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.`
366
+ );
367
+ }
368
+ return refreshFontWeightDefaults(fonts);
369
+ }
370
+
371
+ // src/integrations/font-providers/metrics-data.ts
372
+ var FONT_METRICS_TABLE = {
373
+ abel: [2006, -604, 0, 2048, 783, "sans-serif"],
374
+ alegreya: [1016, -345, 0, 1e3, 410, "serif"],
375
+ "anonymous pro": [1675, -373, 0, 2048, 1118, "monospace"],
376
+ archivo: [878, -210, 0, 1e3, 440, "sans-serif"],
377
+ arial: [1854, -434, 67, 2048, 913, "sans-serif"],
378
+ asap: [934, -212, 0, 1e3, 442, "sans-serif"],
379
+ barlow: [1e3, -200, 0, 1e3, 431, "sans-serif"],
380
+ bitter: [935, -265, 0, 1e3, 465, "serif"],
381
+ "bricolage grotesque": [930, -270, 0, 1e3, 470, "sans-serif"],
382
+ cabin: [1930, -500, 0, 2e3, 844, "sans-serif"],
383
+ catamaran: [1100, -540, 0, 1e3, 411, "sans-serif"],
384
+ chivo: [940, -250, 0, 1e3, 478, "sans-serif"],
385
+ "cormorant garamond": [924, -287, 0, 1e3, 394, "serif"],
386
+ "courier new": [1705, -615, 0, 2048, 1229, "monospace"],
387
+ "crimson pro": [918, -220, 0, 1024, 410, "serif"],
388
+ "crimson text": [972, -359, 0, 1024, 405, "serif"],
389
+ "dm sans": [992, -310, 0, 1e3, 466, "sans-serif"],
390
+ "dm serif display": [1036, -335, 0, 1e3, 446, "serif"],
391
+ dosis: [1027, -237, 0, 1e3, 377, "sans-serif"],
392
+ "eb garamond": [1007, -298, 0, 1e3, 385, "serif"],
393
+ epilogue: [1580, -470, 0, 2e3, 990, "sans-serif"],
394
+ "exo 2": [999, -201, 0, 1e3, 455, "sans-serif"],
395
+ figtree: [950, -250, 0, 1e3, 449, "sans-serif"],
396
+ "fira code": [1980, -644, 0, 2e3, 1200, "monospace"],
397
+ "fira sans": [935, -265, 0, 1e3, 458, "sans-serif"],
398
+ fraunces: [1956, -510, 0, 2e3, 938, "serif"],
399
+ gabarito: [940, -260, 0, 1e3, 442, "display"],
400
+ "geist mono": [1005, -295, 0, 1e3, 600, "monospace"],
401
+ geist: [1005, -295, 0, 1e3, 467, "sans-serif"],
402
+ georgia: [1878, -449, 0, 2048, 913, "serif"],
403
+ heebo: [2146, -862, 0, 2048, 912, "sans-serif"],
404
+ "helvetica neue": [952, -213, 28, 1e3, 450, "sans-serif"],
405
+ helvetica: [1577, -471, 0, 2048, 913, "sans-serif"],
406
+ hind: [1055, -546, 0, 1e3, 429, "sans-serif"],
407
+ "ibm plex mono": [1025, -275, 0, 1e3, 600, "monospace"],
408
+ "ibm plex sans": [1025, -275, 0, 1e3, 451, "sans-serif"],
409
+ "ibm plex serif": [1025, -275, 0, 1e3, 473, "serif"],
410
+ inconsolata: [859, -190, 0, 1e3, 500, "monospace"],
411
+ "instrument sans": [970, -250, 0, 1e3, 458, "sans-serif"],
412
+ "instrument serif": [990, -310, 0, 1e3, 341, "serif"],
413
+ inter: [1984, -494, 0, 2048, 978, "sans-serif"],
414
+ "jetbrains mono": [1020, -300, 0, 1e3, 600, "monospace"],
415
+ "josefin sans": [750, -250, 0, 1e3, 456, "sans-serif"],
416
+ kanit: [1100, -395, 0, 1e3, 452, "sans-serif"],
417
+ karla: [1834, -504, 0, 2e3, 913, "sans-serif"],
418
+ lato: [1974, -426, 0, 2e3, 871, "sans-serif"],
419
+ lexend: [1e3, -250, 0, 1e3, 490, "sans-serif"],
420
+ "libre baskerville": [970, -270, 0, 1e3, 517, "serif"],
421
+ "libre franklin": [966, -246, 0, 1e3, 465, "sans-serif"],
422
+ lora: [1006, -274, 0, 1e3, 468, "serif"],
423
+ manrope: [2132, -600, 0, 2e3, 920, "sans-serif"],
424
+ "maven pro": [965, -210, 0, 1e3, 462, "sans-serif"],
425
+ merriweather: [1968, -546, 0, 2e3, 970, "serif"],
426
+ montserrat: [968, -251, 0, 1e3, 503, "sans-serif"],
427
+ mulish: [1005, -250, 0, 1e3, 464, "sans-serif"],
428
+ newsreader: [1470, -530, 0, 2e3, 857, "serif"],
429
+ "noto sans": [1069, -293, 0, 1e3, 474, "sans-serif"],
430
+ "noto serif": [1069, -293, 0, 1e3, 481, "serif"],
431
+ "nunito sans": [1011, -353, 0, 1e3, 452, "sans-serif"],
432
+ nunito: [1011, -353, 0, 1e3, 452, "sans-serif"],
433
+ onest: [970, -305, 0, 1e3, 469, "sans-serif"],
434
+ "open sans": [2189, -600, 0, 2048, 960, "sans-serif"],
435
+ oswald: [1193, -289, 0, 1e3, 363, "sans-serif"],
436
+ outfit: [1e3, -260, 0, 1e3, 445, "sans-serif"],
437
+ overpass: [1766, -766, 0, 2e3, 898, "sans-serif"],
438
+ oxygen: [2103, -483, 0, 2048, 923, "sans-serif"],
439
+ "playfair display": [1082, -251, 0, 1e3, 452, "serif"],
440
+ "plus jakarta sans": [1038, -222, 0, 1e3, 468, "sans-serif"],
441
+ poppins: [1050, -350, 100, 1e3, 500, "sans-serif"],
442
+ "pt sans": [1018, -276, 0, 1e3, 431, "sans-serif"],
443
+ "pt serif": [1039, -286, 0, 1e3, 448, "serif"],
444
+ "public sans": [1900, -450, 0, 2e3, 935, "sans-serif"],
445
+ quicksand: [1e3, -250, 0, 1e3, 465, "sans-serif"],
446
+ raleway: [940, -234, 0, 1e3, 463, "sans-serif"],
447
+ "red hat display": [1018, -305, 0, 1e3, 442, "sans-serif"],
448
+ "red hat text": [1018, -305, 0, 1e3, 447, "sans-serif"],
449
+ "roboto condensed": [1900, -500, 0, 2048, 811, "sans-serif"],
450
+ "roboto flex": [1900, -500, 0, 2048, 908, "sans-serif"],
451
+ "roboto mono": [2146, -555, 0, 2048, 1229, "monospace"],
452
+ "roboto slab": [2146, -555, 0, 2048, 972, "serif"],
453
+ roboto: [1900, -500, 0, 2048, 911, "sans-serif"],
454
+ rubik: [935, -250, 0, 1e3, 468, "sans-serif"],
455
+ "schibsted grotesk": [2e3, -528, 0, 2048, 954, "sans-serif"],
456
+ "segoe ui": [2210, -514, 0, 2048, 908, "sans-serif"],
457
+ sora: [970, -290, 0, 1e3, 507, "sans-serif"],
458
+ "source code pro": [984, -273, 0, 1e3, 600, "monospace"],
459
+ "source sans 3": [1024, -400, 0, 1e3, 418, "sans-serif"],
460
+ "source serif 4": [1036, -335, 0, 1e3, 479, "serif"],
461
+ "space grotesk": [984, -292, 0, 1e3, 489, "sans-serif"],
462
+ "space mono": [1120, -361, 0, 1e3, 612, "monospace"],
463
+ spectral: [1059, -463, 0, 1e3, 446, "serif"],
464
+ tahoma: [2049, -423, 0, 2048, 917, "sans-serif"],
465
+ "times new roman": [1825, -443, 87, 2048, 832, "serif"],
466
+ "titillium web": [1133, -388, 0, 1e3, 421, "sans-serif"],
467
+ "trebuchet ms": [1923, -455, 0, 2048, 934, "sans-serif"],
468
+ "ubuntu mono": [830, -170, 0, 1e3, 500, "monospace"],
469
+ ubuntu: [932, -189, 28, 1e3, 455, "sans-serif"],
470
+ urbanist: [1900, -500, 0, 2e3, 883, "sans-serif"],
471
+ "varela round": [918, -286, 0, 1e3, 478, "sans-serif"],
472
+ verdana: [2059, -430, 0, 2048, 1049, "sans-serif"],
473
+ "victor mono": [1100, -250, 0, 1e3, 600, "monospace"],
474
+ vollkorn: [952, -441, 0, 1e3, 438, "serif"],
475
+ "work sans": [930, -243, 0, 1e3, 499, "sans-serif"],
476
+ "zilla slab": [944, -256, 0, 1e3, 434, "serif"]
477
+ };
478
+
479
+ // src/integrations/font-providers/metrics.ts
480
+ var CATEGORY_FALLBACK = {
481
+ "sans-serif": "Arial",
482
+ serif: "Times New Roman",
483
+ monospace: "Courier New"
484
+ };
485
+ function lookupFontMetrics(family) {
486
+ return FONT_METRICS_TABLE[family.trim().toLowerCase()];
487
+ }
488
+ var round4 = (n) => Math.round(n * 1e4) / 1e4;
489
+ function computeFallbackMetrics(fallbackName, font, fallbackFont) {
490
+ const [ascent, descent, lineGap, unitsPerEm, xWidthAvg] = font;
491
+ const [, , , fbUnitsPerEm, fbXWidthAvg] = fallbackFont;
492
+ const sizeAdjust = xWidthAvg / unitsPerEm / (fbXWidthAvg / fbUnitsPerEm);
493
+ return {
494
+ fallback: fallbackName,
495
+ sizeAdjust: round4(sizeAdjust * 100),
496
+ ascent: round4(ascent / unitsPerEm / sizeAdjust * 100),
497
+ descent: round4(Math.abs(descent) / unitsPerEm / sizeAdjust * 100),
498
+ lineGap: round4(lineGap / unitsPerEm / sizeAdjust * 100)
499
+ };
500
+ }
501
+ function resolveAutoMetrics(family, fallbackStack, explicitFallback) {
502
+ const font = lookupFontMetrics(family);
503
+ if (!font) return null;
504
+ const fallbackName = explicitFallback ?? fallbackStack.find((f) => lookupFontMetrics(f)) ?? CATEGORY_FALLBACK[font[5]] ?? "Arial";
505
+ const fallbackFont = lookupFontMetrics(fallbackName);
506
+ if (!fallbackFont) return null;
507
+ return computeFallbackMetrics(fallbackName, font, fallbackFont);
508
+ }
509
+
510
+ // src/integrations/font-providers/index.ts
511
+ var SYSTEM_STACKS = Object.freeze({
512
+ sans: 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
513
+ serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
514
+ 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"'
515
+ });
516
+ function getFallbackStack(slot) {
517
+ if (!SYSTEM_STACKS[slot] && isRIDebug()) {
518
+ console.warn(`[RI-DEBUG] Unknown font slot "${slot}" \u2014 falling back to sans stack.`);
519
+ }
520
+ return SYSTEM_STACKS[slot] || SYSTEM_STACKS.sans;
521
+ }
522
+ function googleFontsUrl(family, face) {
523
+ const encodedFamily = encodeURIComponent(family).replace(/%20/g, "+");
524
+ let axisParam;
525
+ if (face.weight.includes(",")) {
526
+ const weights = face.weight.split(",").map((w) => w.trim());
527
+ if (face.style.includes("italic")) {
528
+ const tuples = weights.flatMap((w) => [`0,${w}`, `1,${w}`]);
529
+ axisParam = `ital,wght@${tuples.join(";")}`;
530
+ } else {
531
+ axisParam = `wght@${weights.join(";")}`;
532
+ }
533
+ } else if (face.weight.includes(" ")) {
534
+ const range = face.weight.replace(" ", "..");
535
+ axisParam = face.style.includes("italic") ? `ital,wght@0,${range};1,${range}` : `wght@${range}`;
536
+ } else {
537
+ const w = face.weight || "400";
538
+ axisParam = face.style.includes("italic") ? `ital,wght@0,${w};1,${w}` : `wght@${w}`;
539
+ }
540
+ const display = face.display || "swap";
541
+ return `https://fonts.googleapis.com/css2?family=${encodedFamily}:${axisParam}&display=${display}`;
542
+ }
543
+ function escapeFontFamily(name) {
544
+ return name.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\0/g, "\\0").replace(/\n/g, "\\a ").replace(/\r/g, "\\d ");
545
+ }
546
+ function generateFallbackFontFace(family, metrics) {
547
+ const safeFamily = escapeFontFamily(family);
548
+ return `@font-face {
549
+ font-family: "${safeFamily} Fallback";
550
+ src: local("${escapeFontFamily(metrics.fallback)}");
551
+ size-adjust: ${metrics.sizeAdjust}%;
552
+ ascent-override: ${metrics.ascent}%;
553
+ descent-override: ${metrics.descent}%;
554
+ line-gap-override: ${metrics.lineGap}%;
555
+ }`;
556
+ }
557
+ var FONT_FORMAT_MIME = {
558
+ woff2: "font/woff2",
559
+ woff: "font/woff",
560
+ truetype: "font/ttf",
561
+ opentype: "font/otf"
562
+ };
563
+ function inferFontFormat(path) {
564
+ if (path.endsWith(".woff2")) return "woff2";
565
+ if (path.endsWith(".woff")) return "woff";
566
+ if (path.endsWith(".ttf")) return "truetype";
567
+ if (path.endsWith(".otf")) return "opentype";
568
+ return "woff2";
569
+ }
570
+ function generateWebFontFace(family, face) {
571
+ if (face.provider === "system" || !face.provider) return null;
572
+ if (face.provider === "google") {
573
+ return { type: "import", css: `@import url("${googleFontsUrl(family, face)}");` };
574
+ }
575
+ const safeProvider = escapeFontFamily(face.provider);
576
+ const format = inferFontFormat(face.provider);
577
+ const src = `url("${safeProvider}") format("${format}")`;
578
+ const declarations = [` font-family: "${escapeFontFamily(family)}";`, ` src: ${src};`];
579
+ if (face.weight) declarations.push(` font-weight: ${face.weight};`);
580
+ if (face.style && face.style !== "normal") declarations.push(` font-style: ${face.style};`);
581
+ if (face.display) declarations.push(` font-display: ${face.display};`);
582
+ if (face.unicodeRange) declarations.push(` unicode-range: ${face.unicodeRange};`);
583
+ return { type: "font-face", css: `@font-face {
584
+ ${declarations.join("\n")}
585
+ }` };
586
+ }
587
+ function normalizeLocalStyle(style, family, warnings) {
588
+ if (style.includes(" ") && !style.startsWith("oblique")) {
589
+ const first = style.split(/\s+/)[0];
590
+ warnings.push(
591
+ `[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}".`
592
+ );
593
+ return first;
594
+ }
595
+ return style;
596
+ }
597
+ function resolveSlotMetrics(slot, warnings) {
598
+ const cfg = slot.metrics;
599
+ if (cfg === null) return null;
600
+ if (cfg?.sizeAdjust !== void 0) {
601
+ return {
602
+ fallback: cfg.fallback || slot.fallback[0] || "Arial",
603
+ sizeAdjust: cfg.sizeAdjust,
604
+ ascent: cfg.ascent,
605
+ descent: cfg.descent,
606
+ lineGap: cfg.lineGap
607
+ };
608
+ }
609
+ const resolved = resolveAutoMetrics(slot.family, slot.fallback, cfg?.fallback);
610
+ if (!resolved && cfg?.fallback) {
611
+ const missing = lookupFontMetrics(slot.family) ? cfg.fallback : slot.family;
612
+ warnings.push(
613
+ `[RI-1220] @font slot "${slot.slot}" requests metrics matching against "${cfg.fallback}", but "${missing}" is not in the built-in metrics table \u2014 no fallback @font-face was generated. Provide the four percentages explicitly (metrics: "${cfg.fallback}" <size-adjust> <ascent> <descent> <line-gap>) or use \`metrics: none\`.`
614
+ );
615
+ }
616
+ return resolved;
617
+ }
618
+ function generateFontCSS(slot) {
619
+ const imports = [];
620
+ const fontFaces = [];
621
+ const variables = [];
622
+ const warnings = [];
623
+ const pushFeatureVars = () => {
624
+ if (slot.features) variables.push(`--font-${slot.slot}--features: ${slot.features};`);
625
+ if (slot.variation) variables.push(`--font-${slot.slot}--variations: ${slot.variation};`);
626
+ };
627
+ if (slot.kind === "system") {
628
+ variables.push(`--font-${slot.slot}: ${getFallbackStack(slot.slot)};`);
629
+ return { imports, fontFaces, variables, warnings };
630
+ }
631
+ if (slot.kind === "manual") {
632
+ const stack = [`"${escapeFontFamily(slot.family)}"`, ...slot.fallback].join(", ");
633
+ variables.push(`--font-${slot.slot}: ${stack};`);
634
+ pushFeatureVars();
635
+ return { imports, fontFaces, variables, warnings };
636
+ }
637
+ const metrics = resolveSlotMetrics(slot, warnings);
638
+ if (metrics) fontFaces.push(generateFallbackFontFace(slot.family, metrics));
639
+ for (const face of slot.faces) {
640
+ if (face.provider !== "google" && !face.provider.startsWith("/") && !face.provider.startsWith("http") && !face.provider.startsWith(".")) {
641
+ warnings.push(
642
+ `[RI-1201] Unknown font provider "${face.provider}" for "${slot.family}" \u2014 supported: google, or a file path/URL.`
643
+ );
644
+ continue;
645
+ }
646
+ const emitFace = face.provider === "google" ? face : { ...face, style: normalizeLocalStyle(face.style, slot.family, warnings) };
647
+ const webFont = generateWebFontFace(slot.family, emitFace);
648
+ if (!webFont) continue;
649
+ if (webFont.type === "import") imports.push(webFont.css);
650
+ else fontFaces.push(webFont.css);
651
+ }
652
+ const fallbackStack = slot.fallback.length > 0 ? slot.fallback.join(", ") : getFallbackStack(slot.slot);
653
+ const safeFamily = escapeFontFamily(slot.family);
654
+ const stackParts = [`"${safeFamily}"`];
655
+ if (metrics) stackParts.push(`"${safeFamily} Fallback"`);
656
+ stackParts.push(fallbackStack);
657
+ variables.push(`--font-${slot.slot}: ${stackParts.join(", ")};`);
658
+ pushFeatureVars();
659
+ return { imports, fontFaces, variables, warnings };
660
+ }
661
+ function getFontPreloadLinks(slots) {
662
+ const links = [];
663
+ const seen = /* @__PURE__ */ new Set();
664
+ for (const slot of slots) {
665
+ for (const face of slot.faces) {
666
+ if (!face.preload) continue;
667
+ if (face.provider === "system" || face.provider === "google" || !face.provider) continue;
668
+ if (seen.has(face.provider)) continue;
669
+ seen.add(face.provider);
670
+ links.push({
671
+ href: face.provider,
672
+ as: "font",
673
+ type: FONT_FORMAT_MIME[inferFontFormat(face.provider)],
674
+ crossorigin: true
675
+ });
676
+ }
677
+ }
678
+ return links;
587
679
  }
588
680
 
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;
681
+ // src/scanner/sources.ts
682
+ import { readFile, stat } from "fs/promises";
683
+ import { resolve as resolve3 } from "path";
684
+ import { glob } from "tinyglobby";
685
+
686
+ // src/scanner/glob-utils.ts
687
+ import { isAbsolute as isAbsolute2, win32 } from "path";
688
+ function validateGlobPattern(pattern) {
689
+ if (!pattern?.trim()) {
690
+ return "Glob pattern is empty.";
596
691
  }
597
- const localPromise = (async () => {
692
+ if (pattern.includes("\0")) {
693
+ return "Glob pattern contains a null byte, which is invalid in file paths.";
694
+ }
695
+ if (isAbsolute2(pattern) || win32.isAbsolute(pattern)) {
696
+ return `Glob pattern "${pattern}" must be relative, not absolute.`;
697
+ }
698
+ const segments = pattern.split(/[\\/]+/);
699
+ for (const seg of segments) {
700
+ if (seg === "..") {
701
+ 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.`;
702
+ }
703
+ }
704
+ return null;
705
+ }
706
+
707
+ // src/scanner/package-discovery.ts
708
+ import { existsSync, readFileSync, realpathSync, statSync } from "fs";
709
+ import { dirname as dirname2, join as join2, posix, resolve as resolve2 } from "path";
710
+ var EMPTY = Object.freeze({ sources: [], warnings: [] });
711
+ var discoveryCache = /* @__PURE__ */ new Map();
712
+ function discoverPackageSafelistSources(cwd) {
713
+ const cwdAbs = resolve2(cwd);
714
+ let mtimeMs;
715
+ try {
716
+ mtimeMs = statSync(join2(cwdAbs, "package.json")).mtimeMs;
717
+ } catch {
718
+ return EMPTY;
719
+ }
720
+ const cached = discoveryCache.get(cwdAbs);
721
+ if (cached && cached.mtimeMs === mtimeMs) return cached.result;
722
+ const result = runDiscovery(cwdAbs);
723
+ discoveryCache.set(cwdAbs, { mtimeMs, result });
724
+ return result;
725
+ }
726
+ function runDiscovery(cwdAbs) {
727
+ let consumer;
728
+ try {
729
+ consumer = readPackageJson(join2(cwdAbs, "package.json"));
730
+ } catch {
731
+ return EMPTY;
732
+ }
733
+ const deps = [
734
+ ...Object.keys(consumer.dependencies ?? {}),
735
+ ...Object.keys(consumer.peerDependencies ?? {})
736
+ ];
737
+ if (deps.length === 0) return EMPTY;
738
+ const sources = [];
739
+ const warnings = [];
740
+ for (const depName of deps) {
741
+ const depPkgPath = findDepPackageJson(cwdAbs, depName);
742
+ if (!depPkgPath) {
743
+ continue;
744
+ }
745
+ let depPkg;
598
746
  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.`
747
+ depPkg = readPackageJson(depPkgPath);
748
+ } catch (err) {
749
+ warnings.push(
750
+ `[RI-1410] Could not read ${depName}/package.json during safelist discovery: ${errMessage(err)}`
751
+ );
752
+ continue;
753
+ }
754
+ const patterns = depPkg.rainbowindex?.safelistSources;
755
+ if (patterns == null) continue;
756
+ if (!Array.isArray(patterns)) {
757
+ warnings.push(
758
+ `[RI-1410] Invalid rainbowindex.safelistSources in ${depName} \u2014 expected an array of glob strings.`
759
+ );
760
+ continue;
761
+ }
762
+ if (patterns.length === 0) continue;
763
+ const depRoot = realpathOrFallback(dirname2(depPkgPath)).replace(/\\/g, "/");
764
+ for (const pattern of patterns) {
765
+ if (typeof pattern !== "string" || !pattern) {
766
+ warnings.push(
767
+ `[RI-1410] Invalid rainbowindex.safelistSources entry in ${depName} \u2014 expected a non-empty glob string.`
604
768
  );
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;
769
+ continue;
612
770
  }
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."
771
+ if (validateGlobPattern(pattern) !== null) {
772
+ warnings.push(
773
+ `[RI-1410] Invalid rainbowindex.safelistSources entry "${pattern}" in ${depName} \u2014 patterns must be relative to the package root and must not traverse parent directories.`
616
774
  );
617
- return;
618
- }
619
- if (isRIDebug()) {
620
- console.warn("[RI-DEBUG] Fetching Google Fonts metadata...");
775
+ continue;
621
776
  }
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.`
777
+ const absolute = posix.normalize(`${depRoot}/${pattern.replace(/^\.\//, "")}`);
778
+ if (absolute !== depRoot && !absolute.startsWith(`${depRoot}/`)) {
779
+ warnings.push(
780
+ `[RI-1410] Invalid rainbowindex.safelistSources entry "${pattern}" in ${depName} \u2014 resolved outside the package root.`
637
781
  );
782
+ continue;
638
783
  }
639
- } finally {
640
- if (!googleFontInternals.googleFontState.fetched) {
641
- googleFontInternals.lastFetchFailureMs = Date.now();
642
- }
643
- googleFontInternals.googleFontListPromise = null;
784
+ sources.push({ pattern: absolute, negated: false, inline: false, absolute: true });
644
785
  }
645
- })();
646
- googleFontInternals.googleFontListPromise = localPromise;
647
- return localPromise;
786
+ }
787
+ return { sources, warnings };
648
788
  }
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
- });
789
+ function readPackageJson(path) {
790
+ const raw = readFileSync(path, "utf8");
791
+ return JSON.parse(raw);
672
792
  }
673
- var FONT_FETCH_TIMEOUT_MS = 1e4;
674
- async function resolveGoogleFonts(fonts) {
675
- if (!fonts.some((slot) => slot.kind === "google")) return fonts;
793
+ function findDepPackageJson(cwd, depName) {
794
+ let dir = cwd;
795
+ while (true) {
796
+ const candidate = join2(dir, "node_modules", depName, "package.json");
797
+ if (existsSync(candidate)) return candidate;
798
+ const parent = dirname2(dir);
799
+ if (parent === dir) return null;
800
+ dir = parent;
801
+ }
802
+ }
803
+ function realpathOrFallback(path) {
676
804
  try {
677
- await withTimeout(
678
- fetchGoogleFontList(),
679
- FONT_FETCH_TIMEOUT_MS,
680
- "[RI-1213] Google Fonts metadata fetch timed out"
681
- );
805
+ return realpathSync(path);
682
806
  } 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
- );
807
+ return path;
686
808
  }
687
- return refreshFontWeightDefaults(fonts);
809
+ }
810
+ function errMessage(err) {
811
+ return err instanceof Error ? err.message : String(err);
688
812
  }
689
813
 
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.`);
814
+ // src/scanner/sources.ts
815
+ var DEFAULT_PATTERNS = Object.freeze([
816
+ "index.html",
817
+ "src/**/*.{html,js,jsx,ts,tsx,mdx,vue,svelte}"
818
+ ]);
819
+ var DEFAULT_EXCLUDES = Object.freeze([
820
+ "node_modules/**",
821
+ "dist/**",
822
+ "build/**",
823
+ "coverage/**",
824
+ "public/**",
825
+ "**/*.config.*",
826
+ "**/*.d.ts"
827
+ ]);
828
+ var MAX_FILE_SIZE = 1048576;
829
+ var MAX_INLINE_SOURCE_SIZE = 102400;
830
+ var GLOB_TIMEOUT_MS = 3e4;
831
+ var GLOB_TIMEOUT_MESSAGE = `glob() timed out after ${GLOB_TIMEOUT_MS / 1e3}s \u2014 check for network filesystem issues or overly broad @source patterns`;
832
+ function collectPatterns(sources) {
833
+ const includePatterns = [];
834
+ const nodeModulesIncludePatterns = [];
835
+ const excludePatterns = [...DEFAULT_EXCLUDES];
836
+ const hasUserPositiveGlobs = sources.some((s) => !s.inline && !s.negated && !s.absolute);
837
+ if (!hasUserPositiveGlobs) {
838
+ includePatterns.push(...DEFAULT_PATTERNS);
699
839
  }
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(";")}`;
840
+ const warnings = [];
841
+ for (const src of sources) {
842
+ if (src.inline) continue;
843
+ if (!src.absolute) {
844
+ const err = validateGlobPattern(src.pattern);
845
+ if (err) {
846
+ warnings.push(`[RI-1404] @source pattern rejected: ${err}`);
847
+ continue;
848
+ }
849
+ }
850
+ if (src.negated) {
851
+ excludePatterns.push(src.pattern);
852
+ } else if (src.absolute || src.pattern.replace(/^\.\//, "").startsWith("node_modules/")) {
853
+ nodeModulesIncludePatterns.push(src.pattern);
710
854
  } else {
711
- axisParam = `wght@${weights.join(";")}`;
855
+ includePatterns.push(src.pattern);
712
856
  }
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
857
  }
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
- }` };
858
+ return {
859
+ includePatterns,
860
+ nodeModulesIncludePatterns,
861
+ excludePatterns,
862
+ warnings,
863
+ hasUserPositiveGlobs
864
+ };
767
865
  }
768
- function normalizeLocalStyle(style, family, warnings) {
769
- if (style.includes(" ") && !style.startsWith("oblique")) {
770
- const first = style.split(/\s+/)[0];
866
+ async function resolveSourceFilesAsync(sources, cwd, hasInlineClasses = false) {
867
+ const {
868
+ includePatterns,
869
+ nodeModulesIncludePatterns,
870
+ excludePatterns,
871
+ warnings,
872
+ hasUserPositiveGlobs
873
+ } = collectPatterns(sources);
874
+ const allIncludes = [...includePatterns, ...nodeModulesIncludePatterns];
875
+ if (allIncludes.length === 0) return { files: [], warnings };
876
+ try {
877
+ const globPasses = [];
878
+ if (includePatterns.length > 0) {
879
+ globPasses.push(
880
+ withTimeout(
881
+ glob(includePatterns, { cwd, ignore: excludePatterns }),
882
+ GLOB_TIMEOUT_MS,
883
+ GLOB_TIMEOUT_MESSAGE
884
+ )
885
+ );
886
+ }
887
+ if (nodeModulesIncludePatterns.length > 0) {
888
+ const relaxedExcludes = excludePatterns.filter((p) => p !== "node_modules/**");
889
+ globPasses.push(
890
+ withTimeout(
891
+ glob(nodeModulesIncludePatterns, { cwd, ignore: relaxedExcludes }),
892
+ GLOB_TIMEOUT_MS,
893
+ GLOB_TIMEOUT_MESSAGE
894
+ )
895
+ );
896
+ }
897
+ const matched = (await Promise.all(globPasses)).flat();
898
+ const files = [...new Set(matched.map((f) => resolve3(cwd, f)))].sort(codepointCompare);
899
+ if (files.length === 0 && !(hasInlineClasses && !hasUserPositiveGlobs)) {
900
+ warnings.push(
901
+ `[RI-1401] No source files found matching ${allIncludes.map((p) => `"${p}"`).join(", ")} \u2014 check @source paths or project structure.`
902
+ );
903
+ }
904
+ return { files, warnings };
905
+ } catch (err) {
771
906
  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}".`
907
+ `[RI-1402] Invalid glob pattern \u2014 skipping. Check @source syntax. (${err instanceof Error ? err.message : String(err)})`
773
908
  );
774
- return first;
909
+ return { files: [], warnings };
775
910
  }
776
- return style;
777
911
  }
778
- function generateFontCSS(slot) {
779
- const imports = [];
780
- const fontFaces = [];
781
- const variables = [];
912
+ function collectInlineClasses(sources) {
913
+ const classes = /* @__PURE__ */ new Set();
782
914
  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(".")) {
915
+ for (const src of sources) {
916
+ if (!src.inline) continue;
917
+ const items = src.classes ?? [];
918
+ let contentLength = 0;
919
+ for (const cls of items) contentLength += cls.length;
920
+ if (contentLength > MAX_INLINE_SOURCE_SIZE) {
813
921
  warnings.push(
814
- `[RI-1201] Unknown font provider "${face.provider}" for "${slot.family}" \u2014 supported: google, or a file path/URL.`
922
+ `[RI-1406] Inline @source content exceeds ${MAX_INLINE_SOURCE_SIZE} byte limit (${contentLength} bytes) \u2014 skipping.`
815
923
  );
816
924
  continue;
817
925
  }
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);
926
+ for (const cls of items) {
927
+ if (cls) classes.add(cls);
928
+ }
823
929
  }
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 };
930
+ return { classes, warnings };
832
931
  }
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
- });
932
+ var FILE_IO_TIMEOUT_MS = 1e4;
933
+ var SCAN_CACHE_MAX_ENTRIES = 2e4;
934
+ var scanCache = /* @__PURE__ */ new Map();
935
+ async function scanOneFile(file) {
936
+ const warnings = [];
937
+ try {
938
+ const stats = await withTimeout(stat(file), FILE_IO_TIMEOUT_MS, "stat() timed out");
939
+ const cached = scanCache.get(file);
940
+ if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
941
+ return cached.result;
942
+ }
943
+ let result;
944
+ if (stats.size > MAX_FILE_SIZE) {
945
+ result = {
946
+ classes: null,
947
+ warnings,
948
+ failure: `[RI-1405] Skipping source file "${file}" (${stats.size} bytes) \u2014 exceeds ${MAX_FILE_SIZE} byte limit.`
949
+ };
950
+ } else {
951
+ const content = await withTimeout(
952
+ readFile(file, "utf-8"),
953
+ FILE_IO_TIMEOUT_MS,
954
+ "readFile() timed out"
955
+ );
956
+ result = {
957
+ classes: extractClassesFromSource({ path: file, content }, warnings),
958
+ warnings,
959
+ failure: null
960
+ };
849
961
  }
962
+ if (scanCache.size >= SCAN_CACHE_MAX_ENTRIES) scanCache.clear();
963
+ scanCache.set(file, { mtimeMs: stats.mtimeMs, size: stats.size, result });
964
+ return result;
965
+ } catch (err) {
966
+ return {
967
+ classes: null,
968
+ warnings,
969
+ failure: `[RI-1403] Could not read source file "${file}" \u2014 skipping. (${err instanceof Error ? err.message : String(err)})`
970
+ };
850
971
  }
851
- return links;
972
+ }
973
+ async function scanSourceFilesAsync(sources, cwd) {
974
+ const { classes: allClasses, warnings: inlineWarnings } = collectInlineClasses(sources);
975
+ const allWarnings = [...inlineWarnings];
976
+ const { files, warnings: resolveWarnings } = await resolveSourceFilesAsync(
977
+ sources,
978
+ cwd,
979
+ allClasses.size > 0
980
+ );
981
+ allWarnings.push(...resolveWarnings);
982
+ const CONCURRENCY_LIMIT = 32;
983
+ const results = new Array(files.length);
984
+ let nextIndex = 0;
985
+ const worker = async () => {
986
+ while (nextIndex < files.length) {
987
+ const index = nextIndex++;
988
+ results[index] = await scanOneFile(files[index]);
989
+ }
990
+ };
991
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY_LIMIT, files.length) }, worker));
992
+ const seen = new Set(allWarnings);
993
+ for (const result of results) {
994
+ if (result.failure) {
995
+ allWarnings.push(result.failure);
996
+ seen.add(result.failure);
997
+ continue;
998
+ }
999
+ if (result.classes) {
1000
+ for (const cls of result.classes) {
1001
+ allClasses.add(cls);
1002
+ }
1003
+ }
1004
+ pushWarningsDeduped(allWarnings, result.warnings, seen);
1005
+ }
1006
+ return { classes: allClasses, warnings: allWarnings };
1007
+ }
1008
+ async function collectProjectClasses(themeSources, surfaceSources, cwd, warnings, warningSeen) {
1009
+ const discovered = discoverPackageSafelistSources(cwd);
1010
+ pushWarningsDeduped(warnings, discovered.warnings, warningSeen);
1011
+ const allSources = [...themeSources, ...surfaceSources, ...discovered.sources];
1012
+ const scanResult = await scanSourceFilesAsync(allSources, cwd);
1013
+ pushWarningsDeduped(warnings, scanResult.warnings, warningSeen);
1014
+ return scanResult.classes;
852
1015
  }
853
1016
 
854
1017
  // src/css/strip.ts
@@ -1459,7 +1622,7 @@ function collectPropertyBlocks(registered, engineProperties) {
1459
1622
  return blocks;
1460
1623
  }
1461
1624
  function assembleSections(compilation, theme, fontOutputCache) {
1462
- const sections = [];
1625
+ const baseSections = [];
1463
1626
  const fontImports = [];
1464
1627
  const fontFaceBlocks = [];
1465
1628
  const assemblyWarnings = [];
@@ -1480,18 +1643,16 @@ function assembleSections(compilation, theme, fontOutputCache) {
1480
1643
  }
1481
1644
  }
1482
1645
  }
1483
- if (fontImports.length > 0) {
1484
- sections.push(fontImports.join("\n"));
1485
- }
1646
+ const importsSection = fontImports.length > 0 ? fontImports.join("\n") : null;
1486
1647
  const propertyBlocks = collectPropertyBlocks(theme.registeredProperties, compilation.properties);
1487
1648
  if (propertyBlocks.length > 0) {
1488
- sections.push(propertyBlocks.join("\n\n"));
1649
+ baseSections.push(propertyBlocks.join("\n\n"));
1489
1650
  }
1490
1651
  if (fontFaceBlocks.length > 0) {
1491
- sections.push(fontFaceBlocks.join("\n\n"));
1652
+ baseSections.push(fontFaceBlocks.join("\n\n"));
1492
1653
  }
1493
1654
  const tokenLayer = generateTokenLayer(theme, compilation, fontOutputCache);
1494
- if (tokenLayer) sections.push(tokenLayer);
1655
+ if (tokenLayer) baseSections.push(tokenLayer);
1495
1656
  for (const w of checkPaletteContrast(theme.colors, compilation.usedColorStops)) {
1496
1657
  assemblyWarnings.push(w);
1497
1658
  }
@@ -1500,25 +1661,32 @@ function assembleSections(compilation, theme, fontOutputCache) {
1500
1661
  compilation.usedColorStops.get("theme")
1501
1662
  );
1502
1663
  if (themeOverrides.length > 0) {
1503
- sections.push(themeOverrides.join("\n\n"));
1664
+ baseSections.push(themeOverrides.join("\n\n"));
1504
1665
  }
1505
1666
  const cornerShape = generateCornerShapeBlock(theme);
1506
- if (cornerShape) sections.push(cornerShape);
1667
+ if (cornerShape) baseSections.push(cornerShape);
1507
1668
  if (compilation.keyframes.length > 0) {
1508
- sections.push(compilation.keyframes.join("\n\n"));
1669
+ baseSections.push(compilation.keyframes.join("\n\n"));
1509
1670
  }
1510
1671
  const preflight = generatePreflight(theme.preflight);
1511
- if (preflight) sections.push(preflight);
1672
+ if (preflight) baseSections.push(preflight);
1512
1673
  const utilityCSS = renderCSS({
1513
1674
  ...compilation,
1514
1675
  properties: [],
1515
1676
  keyframes: []
1516
1677
  });
1517
- if (utilityCSS) sections.push(utilityCSS);
1518
- if (theme.layer) {
1519
- applyLayerWrapping(sections, fontImports, theme.layer, utilityCSS !== "");
1520
- }
1521
- return { sections, fontImports, warnings: assemblyWarnings };
1678
+ const utilitiesSection = utilityCSS !== "" ? utilityCSS : null;
1679
+ const parts = {
1680
+ imports: importsSection,
1681
+ base: baseSections,
1682
+ utilities: utilitiesSection
1683
+ };
1684
+ const sections = theme.layer ? applyLayerWrapping(parts, theme.layer) : [
1685
+ ...importsSection !== null ? [importsSection] : [],
1686
+ ...baseSections,
1687
+ ...utilitiesSection !== null ? [utilitiesSection] : []
1688
+ ];
1689
+ return { sections, warnings: assemblyWarnings };
1522
1690
  }
1523
1691
  function wrapInLayer(content, layerName) {
1524
1692
  const indented = content.replace(/^(?=.)/gm, " ");
@@ -1526,42 +1694,36 @@ function wrapInLayer(content, layerName) {
1526
1694
  ${indented}
1527
1695
  }`;
1528
1696
  }
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];
1697
+ function applyLayerWrapping(parts, layer) {
1698
+ const sections = [];
1699
+ if (parts.imports !== null) sections.push(parts.imports);
1533
1700
  if (layer.wrapAll) {
1534
- const joined = contentSections.join("\n\n");
1535
- sections.length = 0;
1536
- if (importSection) sections.push(importSection);
1537
1701
  sections.push(`@layer ${layer.wrapAll};`);
1702
+ const joined = [...parts.base, ...parts.utilities !== null ? [parts.utilities] : []].join(
1703
+ "\n\n"
1704
+ );
1538
1705
  if (joined) sections.push(wrapInLayer(joined, layer.wrapAll));
1539
- return;
1706
+ return sections;
1540
1707
  }
1541
- const utilitySection = hasUtilitySection ? contentSections.pop() ?? null : null;
1542
- const baseSections = contentSections;
1543
- const finalSections = [];
1544
- if (importSection) finalSections.push(importSection);
1545
1708
  if (layer.order && layer.order.length > 0) {
1546
- finalSections.push(`@layer ${layer.order.join(", ")};`);
1709
+ sections.push(`@layer ${layer.order.join(", ")};`);
1547
1710
  }
1548
- if (baseSections.length > 0) {
1549
- const baseJoined = baseSections.join("\n\n");
1711
+ if (parts.base.length > 0) {
1712
+ const baseJoined = parts.base.join("\n\n");
1550
1713
  if (layer.base) {
1551
- finalSections.push(wrapInLayer(baseJoined, layer.base));
1714
+ sections.push(wrapInLayer(baseJoined, layer.base));
1552
1715
  } else {
1553
- finalSections.push(baseJoined);
1716
+ sections.push(baseJoined);
1554
1717
  }
1555
1718
  }
1556
- if (utilitySection) {
1719
+ if (parts.utilities !== null) {
1557
1720
  if (layer.utilities) {
1558
- finalSections.push(wrapInLayer(utilitySection, layer.utilities));
1721
+ sections.push(wrapInLayer(parts.utilities, layer.utilities));
1559
1722
  } else {
1560
- finalSections.push(utilitySection);
1723
+ sections.push(parts.utilities);
1561
1724
  }
1562
1725
  }
1563
- sections.length = 0;
1564
- sections.push(...finalSections);
1726
+ return sections;
1565
1727
  }
1566
1728
 
1567
1729
  // src/project/pipeline.ts
@@ -1575,13 +1737,20 @@ function collectApplyClassNames(css, warnings) {
1575
1737
  }
1576
1738
  return classes;
1577
1739
  }
1740
+ var effectiveThemeMemo = /* @__PURE__ */ new WeakMap();
1578
1741
  async function finalizeProjectCompilation(options) {
1579
1742
  const { analysis } = options;
1580
1743
  let effectiveTheme = analysis.theme;
1581
1744
  if (options.resolveFonts) {
1582
1745
  const resolvedFonts = await options.resolveFonts(analysis.theme.fonts);
1583
1746
  if (resolvedFonts !== analysis.theme.fonts) {
1584
- effectiveTheme = { ...analysis.theme, fonts: [...resolvedFonts] };
1747
+ const memo = effectiveThemeMemo.get(analysis.theme);
1748
+ if (memo && memo.fonts === resolvedFonts) {
1749
+ effectiveTheme = memo.theme;
1750
+ } else {
1751
+ effectiveTheme = { ...analysis.theme, fonts: [...resolvedFonts] };
1752
+ effectiveThemeMemo.set(analysis.theme, { fonts: resolvedFonts, theme: effectiveTheme });
1753
+ }
1585
1754
  }
1586
1755
  }
1587
1756
  const expansionWarnings = [];
@@ -1624,12 +1793,59 @@ async function finalizeProjectCompilation(options) {
1624
1793
  };
1625
1794
  }
1626
1795
 
1796
+ // src/project/scan.ts
1797
+ var lastAnalysis = null;
1798
+ function analyzeProjectCSSMemo(css) {
1799
+ if (lastAnalysis === null || lastAnalysis.css !== css) {
1800
+ lastAnalysis = { css, analysis: analyzeProjectCSS(css) };
1801
+ }
1802
+ const cached = lastAnalysis.analysis;
1803
+ return {
1804
+ ...cached,
1805
+ warnings: [...cached.warnings],
1806
+ warningSeen: new Set(cached.warningSeen),
1807
+ diagnostics: [...cached.diagnostics]
1808
+ };
1809
+ }
1810
+ async function compileScannedProject(options) {
1811
+ const analysis = analyzeProjectCSSMemo(options.css);
1812
+ const resolveFonts = options.resolveFonts ?? resolveGoogleFonts;
1813
+ const fontsReady = Promise.resolve(resolveFonts(analysis.theme.fonts));
1814
+ fontsReady.catch(() => {
1815
+ });
1816
+ const surfaceSources = [];
1817
+ for (const pattern of options.surfacePatterns ?? []) {
1818
+ const error = validateGlobPattern(pattern);
1819
+ if (error) {
1820
+ const warning = options.onInvalidPattern(error);
1821
+ if (warning !== void 0) {
1822
+ pushWarningsDeduped(analysis.warnings, [warning], analysis.warningSeen);
1823
+ }
1824
+ continue;
1825
+ }
1826
+ surfaceSources.push({ pattern, negated: false, inline: false });
1827
+ }
1828
+ const classNames = await collectProjectClasses(
1829
+ analysis.theme.sources,
1830
+ surfaceSources,
1831
+ options.cwd,
1832
+ analysis.warnings,
1833
+ analysis.warningSeen
1834
+ );
1835
+ const compiled = await finalizeProjectCompilation({
1836
+ css: options.css,
1837
+ classNames,
1838
+ analysis,
1839
+ resolveFonts: () => fontsReady
1840
+ });
1841
+ return { compiled, warningSeen: analysis.warningSeen };
1842
+ }
1843
+
1627
1844
  export {
1628
- validateGlobPattern,
1629
- DEFAULT_PATTERNS,
1630
- DEFAULT_EXCLUDES,
1631
- collectProjectClasses,
1632
1845
  resolveGoogleFonts,
1633
1846
  getFontPreloadLinks,
1634
- finalizeProjectCompilation
1847
+ DEFAULT_PATTERNS,
1848
+ DEFAULT_EXCLUDES,
1849
+ finalizeProjectCompilation,
1850
+ compileScannedProject
1635
1851
  };