rainbowindex 0.5.0 → 0.5.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.
- package/CHANGELOG.md +197 -0
- package/README.md +66 -37
- package/dist/browser.d.ts +2 -2
- package/dist/browser.mjs +1 -1
- package/dist/{chunk-CMB6BHVE.mjs → chunk-4CTJLMYM.mjs} +47 -29
- package/dist/{chunk-KRZL4IDK.mjs → chunk-4UKFK2GE.mjs} +62 -47
- package/dist/{chunk-DT5HYIM3.mjs → chunk-F4VCBISU.mjs} +454 -432
- package/dist/{chunk-ZI5ZYNSU.mjs → chunk-PDORZSQX.mjs} +205 -138
- package/dist/{chunk-NXJZX6KI.mjs → chunk-RU4756NG.mjs} +5 -6
- package/dist/cli.mjs +8 -7
- package/dist/{context-ruu2x_jR.d.ts → context-B9yhJxd5.d.ts} +26 -8
- package/dist/editor.d.ts +9 -6
- package/dist/editor.mjs +9 -5
- package/dist/{index-Dp6i5TSv.d.ts → index-Dx-NpFFx.d.ts} +5 -5
- package/dist/index.d.ts +4 -4
- package/dist/index.mjs +6 -4
- package/dist/oxlint.d.ts +55 -0
- package/dist/oxlint.mjs +38 -0
- package/dist/{safelist-D9-Plqta.d.ts → safelist-DAkKuxCk.d.ts} +2 -15
- package/dist/vite.mjs +53 -12
- package/package.json +7 -1
|
@@ -19,12 +19,374 @@ import {
|
|
|
19
19
|
renderCSS,
|
|
20
20
|
scanCSSForTokenUsage,
|
|
21
21
|
withTimeout
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-PDORZSQX.mjs";
|
|
23
23
|
import {
|
|
24
24
|
checkPaletteContrast,
|
|
25
25
|
generateAllColorVariables,
|
|
26
26
|
generateThemeOverrides
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-4UKFK2GE.mjs";
|
|
28
|
+
|
|
29
|
+
// src/scanner/sources.ts
|
|
30
|
+
import { readFile, stat } from "fs/promises";
|
|
31
|
+
import { resolve as resolve2 } from "path";
|
|
32
|
+
import { glob } from "tinyglobby";
|
|
33
|
+
|
|
34
|
+
// src/scanner/glob-utils.ts
|
|
35
|
+
import { isAbsolute, win32 } from "path";
|
|
36
|
+
function validateGlobPattern(pattern) {
|
|
37
|
+
if (!pattern?.trim()) {
|
|
38
|
+
return "Glob pattern is empty.";
|
|
39
|
+
}
|
|
40
|
+
if (pattern.includes("\0")) {
|
|
41
|
+
return "Glob pattern contains a null byte, which is invalid in file paths.";
|
|
42
|
+
}
|
|
43
|
+
if (isAbsolute(pattern) || win32.isAbsolute(pattern)) {
|
|
44
|
+
return `Glob pattern "${pattern}" must be relative, not absolute.`;
|
|
45
|
+
}
|
|
46
|
+
const segments = pattern.split(/[\\/]+/);
|
|
47
|
+
for (const seg of segments) {
|
|
48
|
+
if (seg === "..") {
|
|
49
|
+
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.`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/scanner/package-discovery.ts
|
|
56
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "fs";
|
|
57
|
+
import { dirname, join, posix, resolve } from "path";
|
|
58
|
+
var EMPTY = Object.freeze({ sources: [], warnings: [] });
|
|
59
|
+
var discoveryCache = /* @__PURE__ */ new Map();
|
|
60
|
+
function discoverPackageSafelistSources(cwd) {
|
|
61
|
+
const cwdAbs = resolve(cwd);
|
|
62
|
+
let mtimeMs;
|
|
63
|
+
try {
|
|
64
|
+
mtimeMs = statSync(join(cwdAbs, "package.json")).mtimeMs;
|
|
65
|
+
} catch {
|
|
66
|
+
return EMPTY;
|
|
67
|
+
}
|
|
68
|
+
const cached = discoveryCache.get(cwdAbs);
|
|
69
|
+
if (cached && cached.mtimeMs === mtimeMs) return cached.result;
|
|
70
|
+
const result = runDiscovery(cwdAbs);
|
|
71
|
+
discoveryCache.set(cwdAbs, { mtimeMs, result });
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
function runDiscovery(cwdAbs) {
|
|
75
|
+
let consumer;
|
|
76
|
+
try {
|
|
77
|
+
consumer = readPackageJson(join(cwdAbs, "package.json"));
|
|
78
|
+
} catch {
|
|
79
|
+
return EMPTY;
|
|
80
|
+
}
|
|
81
|
+
const deps = [
|
|
82
|
+
...Object.keys(consumer.dependencies ?? {}),
|
|
83
|
+
...Object.keys(consumer.peerDependencies ?? {})
|
|
84
|
+
];
|
|
85
|
+
if (deps.length === 0) return EMPTY;
|
|
86
|
+
const sources = [];
|
|
87
|
+
const warnings = [];
|
|
88
|
+
for (const depName of deps) {
|
|
89
|
+
const depPkgPath = findDepPackageJson(cwdAbs, depName);
|
|
90
|
+
if (!depPkgPath) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
let depPkg;
|
|
94
|
+
try {
|
|
95
|
+
depPkg = readPackageJson(depPkgPath);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
warnings.push(
|
|
98
|
+
`[RI-1410] Could not read ${depName}/package.json during safelist discovery: ${errMessage(err)}`
|
|
99
|
+
);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const patterns = depPkg.rainbowindex?.safelistSources;
|
|
103
|
+
if (patterns == null) continue;
|
|
104
|
+
if (!Array.isArray(patterns)) {
|
|
105
|
+
warnings.push(
|
|
106
|
+
`[RI-1410] Invalid rainbowindex.safelistSources in ${depName} \u2014 expected an array of glob strings.`
|
|
107
|
+
);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (patterns.length === 0) continue;
|
|
111
|
+
const depRoot = realpathOrFallback(dirname(depPkgPath)).replace(/\\/g, "/");
|
|
112
|
+
for (const pattern of patterns) {
|
|
113
|
+
if (typeof pattern !== "string" || !pattern) {
|
|
114
|
+
warnings.push(
|
|
115
|
+
`[RI-1410] Invalid rainbowindex.safelistSources entry in ${depName} \u2014 expected a non-empty glob string.`
|
|
116
|
+
);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (validateGlobPattern(pattern) !== null) {
|
|
120
|
+
warnings.push(
|
|
121
|
+
`[RI-1410] Invalid rainbowindex.safelistSources entry "${pattern}" in ${depName} \u2014 patterns must be relative to the package root and must not traverse parent directories.`
|
|
122
|
+
);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const absolute = posix.normalize(`${depRoot}/${pattern.replace(/^\.\//, "")}`);
|
|
126
|
+
if (absolute !== depRoot && !absolute.startsWith(`${depRoot}/`)) {
|
|
127
|
+
warnings.push(
|
|
128
|
+
`[RI-1410] Invalid rainbowindex.safelistSources entry "${pattern}" in ${depName} \u2014 resolved outside the package root.`
|
|
129
|
+
);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
sources.push({ pattern: absolute, negated: false, inline: false, absolute: true });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { sources, warnings };
|
|
136
|
+
}
|
|
137
|
+
function readPackageJson(path) {
|
|
138
|
+
const raw = readFileSync(path, "utf8");
|
|
139
|
+
return JSON.parse(raw);
|
|
140
|
+
}
|
|
141
|
+
function findDepPackageJson(cwd, depName) {
|
|
142
|
+
let dir = cwd;
|
|
143
|
+
while (true) {
|
|
144
|
+
const candidate = join(dir, "node_modules", depName, "package.json");
|
|
145
|
+
if (existsSync(candidate)) return candidate;
|
|
146
|
+
const parent = dirname(dir);
|
|
147
|
+
if (parent === dir) return null;
|
|
148
|
+
dir = parent;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function realpathOrFallback(path) {
|
|
152
|
+
try {
|
|
153
|
+
return realpathSync(path);
|
|
154
|
+
} catch {
|
|
155
|
+
return path;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function errMessage(err) {
|
|
159
|
+
return err instanceof Error ? err.message : String(err);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/scanner/sources.ts
|
|
163
|
+
var DEFAULT_PATTERNS = Object.freeze([
|
|
164
|
+
"*.html",
|
|
165
|
+
"src/**/*.{html,js,jsx,ts,tsx,mdx,vue,svelte}"
|
|
166
|
+
]);
|
|
167
|
+
var DEFAULT_EXCLUDES = Object.freeze([
|
|
168
|
+
"node_modules/**",
|
|
169
|
+
"dist/**",
|
|
170
|
+
"build/**",
|
|
171
|
+
"coverage/**",
|
|
172
|
+
"public/**",
|
|
173
|
+
"**/*.config.*",
|
|
174
|
+
"**/*.d.ts"
|
|
175
|
+
]);
|
|
176
|
+
var MAX_FILE_SIZE = 1048576;
|
|
177
|
+
var MAX_INLINE_SOURCE_SIZE = 102400;
|
|
178
|
+
var GLOB_TIMEOUT_MS = 3e4;
|
|
179
|
+
var GLOB_TIMEOUT_MESSAGE = `glob() timed out after ${GLOB_TIMEOUT_MS / 1e3}s \u2014 check for network filesystem issues or overly broad @source patterns`;
|
|
180
|
+
var SOURCE_LIST_CACHE_MAX_ENTRIES = 50;
|
|
181
|
+
var sourceListCacheEnabled = false;
|
|
182
|
+
var sourceListCache = /* @__PURE__ */ new Map();
|
|
183
|
+
var sourceListCacheGeneration = 0;
|
|
184
|
+
function enableSourceFileListCache() {
|
|
185
|
+
sourceListCacheEnabled = true;
|
|
186
|
+
}
|
|
187
|
+
function invalidateSourceFileListCache() {
|
|
188
|
+
sourceListCacheGeneration++;
|
|
189
|
+
sourceListCache.clear();
|
|
190
|
+
}
|
|
191
|
+
function collectPatterns(sources) {
|
|
192
|
+
const includePatterns = [];
|
|
193
|
+
const nodeModulesIncludePatterns = [];
|
|
194
|
+
const excludePatterns = [...DEFAULT_EXCLUDES];
|
|
195
|
+
const hasUserPositiveGlobs = sources.some((s) => !s.inline && !s.negated && !s.absolute);
|
|
196
|
+
if (!hasUserPositiveGlobs) {
|
|
197
|
+
includePatterns.push(...DEFAULT_PATTERNS);
|
|
198
|
+
}
|
|
199
|
+
const warnings = [];
|
|
200
|
+
for (const src of sources) {
|
|
201
|
+
if (src.inline) continue;
|
|
202
|
+
if (!src.absolute) {
|
|
203
|
+
const err = validateGlobPattern(src.pattern);
|
|
204
|
+
if (err) {
|
|
205
|
+
warnings.push(`[RI-1404] @source pattern rejected: ${err}`);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (src.negated) {
|
|
210
|
+
excludePatterns.push(src.pattern);
|
|
211
|
+
} else if (src.absolute || src.pattern.replace(/^\.\//, "").startsWith("node_modules/")) {
|
|
212
|
+
nodeModulesIncludePatterns.push(src.pattern);
|
|
213
|
+
} else {
|
|
214
|
+
includePatterns.push(src.pattern);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
includePatterns,
|
|
219
|
+
nodeModulesIncludePatterns,
|
|
220
|
+
excludePatterns,
|
|
221
|
+
warnings,
|
|
222
|
+
hasUserPositiveGlobs
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
async function resolveSourceFilesAsync(sources, cwd, hasInlineClasses = false) {
|
|
226
|
+
const {
|
|
227
|
+
includePatterns,
|
|
228
|
+
nodeModulesIncludePatterns,
|
|
229
|
+
excludePatterns,
|
|
230
|
+
warnings,
|
|
231
|
+
hasUserPositiveGlobs
|
|
232
|
+
} = collectPatterns(sources);
|
|
233
|
+
const allIncludes = [...includePatterns, ...nodeModulesIncludePatterns];
|
|
234
|
+
if (allIncludes.length === 0) return { files: [], warnings };
|
|
235
|
+
const cacheKey = sourceListCacheEnabled ? [
|
|
236
|
+
cwd,
|
|
237
|
+
includePatterns.join("\0"),
|
|
238
|
+
nodeModulesIncludePatterns.join("\0"),
|
|
239
|
+
excludePatterns.join("\0")
|
|
240
|
+
].join("") : null;
|
|
241
|
+
try {
|
|
242
|
+
const generationAtStart = sourceListCacheGeneration;
|
|
243
|
+
let files = cacheKey !== null ? sourceListCache.get(cacheKey) : void 0;
|
|
244
|
+
if (files === void 0) {
|
|
245
|
+
const globPasses = [];
|
|
246
|
+
if (includePatterns.length > 0) {
|
|
247
|
+
globPasses.push(
|
|
248
|
+
withTimeout(
|
|
249
|
+
glob(includePatterns, { cwd, ignore: excludePatterns }),
|
|
250
|
+
GLOB_TIMEOUT_MS,
|
|
251
|
+
GLOB_TIMEOUT_MESSAGE
|
|
252
|
+
)
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
if (nodeModulesIncludePatterns.length > 0) {
|
|
256
|
+
const relaxedExcludes = excludePatterns.filter((p) => p !== "node_modules/**");
|
|
257
|
+
globPasses.push(
|
|
258
|
+
withTimeout(
|
|
259
|
+
glob(nodeModulesIncludePatterns, { cwd, ignore: relaxedExcludes }),
|
|
260
|
+
GLOB_TIMEOUT_MS,
|
|
261
|
+
GLOB_TIMEOUT_MESSAGE
|
|
262
|
+
)
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
const matched = (await Promise.all(globPasses)).flat();
|
|
266
|
+
files = [...new Set(matched.map((f) => resolve2(cwd, f)))].sort(codepointCompare);
|
|
267
|
+
if (cacheKey !== null && sourceListCacheGeneration === generationAtStart) {
|
|
268
|
+
if (sourceListCache.size >= SOURCE_LIST_CACHE_MAX_ENTRIES) sourceListCache.clear();
|
|
269
|
+
sourceListCache.set(cacheKey, files);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (files.length === 0 && !(hasInlineClasses && !hasUserPositiveGlobs)) {
|
|
273
|
+
warnings.push(
|
|
274
|
+
`[RI-1401] No source files found matching ${allIncludes.map((p) => `"${p}"`).join(", ")} \u2014 check @source paths or project structure.`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return { files, warnings };
|
|
278
|
+
} catch (err) {
|
|
279
|
+
warnings.push(
|
|
280
|
+
`[RI-1402] Invalid glob pattern \u2014 skipping. Check @source syntax. (${err instanceof Error ? err.message : String(err)})`
|
|
281
|
+
);
|
|
282
|
+
return { files: [], warnings };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function collectInlineClasses(sources) {
|
|
286
|
+
const classes = /* @__PURE__ */ new Set();
|
|
287
|
+
const warnings = [];
|
|
288
|
+
for (const src of sources) {
|
|
289
|
+
if (!src.inline) continue;
|
|
290
|
+
const items = src.classes ?? [];
|
|
291
|
+
let contentLength = 0;
|
|
292
|
+
for (const cls of items) contentLength += cls.length;
|
|
293
|
+
if (contentLength > MAX_INLINE_SOURCE_SIZE) {
|
|
294
|
+
warnings.push(
|
|
295
|
+
`[RI-1406] Inline @source content exceeds ${MAX_INLINE_SOURCE_SIZE} byte limit (${contentLength} bytes) \u2014 skipping.`
|
|
296
|
+
);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
for (const cls of items) {
|
|
300
|
+
if (cls) classes.add(cls);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return { classes, warnings };
|
|
304
|
+
}
|
|
305
|
+
var FILE_IO_TIMEOUT_MS = 1e4;
|
|
306
|
+
var SCAN_CACHE_MAX_ENTRIES = 2e4;
|
|
307
|
+
var scanCache = /* @__PURE__ */ new Map();
|
|
308
|
+
async function scanOneFile(file) {
|
|
309
|
+
const warnings = [];
|
|
310
|
+
try {
|
|
311
|
+
const stats = await withTimeout(stat(file), FILE_IO_TIMEOUT_MS, "stat() timed out");
|
|
312
|
+
const cached = scanCache.get(file);
|
|
313
|
+
if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
|
|
314
|
+
return cached.result;
|
|
315
|
+
}
|
|
316
|
+
let result;
|
|
317
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
318
|
+
result = {
|
|
319
|
+
classes: null,
|
|
320
|
+
warnings,
|
|
321
|
+
failure: `[RI-1405] Skipping source file "${file}" (${stats.size} bytes) \u2014 exceeds ${MAX_FILE_SIZE} byte limit.`
|
|
322
|
+
};
|
|
323
|
+
} else {
|
|
324
|
+
const content = await withTimeout(
|
|
325
|
+
readFile(file, "utf-8"),
|
|
326
|
+
FILE_IO_TIMEOUT_MS,
|
|
327
|
+
"readFile() timed out"
|
|
328
|
+
);
|
|
329
|
+
result = {
|
|
330
|
+
classes: extractClassesFromSource({ path: file, content }, warnings),
|
|
331
|
+
warnings,
|
|
332
|
+
failure: null
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
if (scanCache.size >= SCAN_CACHE_MAX_ENTRIES) scanCache.clear();
|
|
336
|
+
scanCache.set(file, { mtimeMs: stats.mtimeMs, size: stats.size, result });
|
|
337
|
+
return result;
|
|
338
|
+
} catch (err) {
|
|
339
|
+
return {
|
|
340
|
+
classes: null,
|
|
341
|
+
warnings,
|
|
342
|
+
failure: `[RI-1403] Could not read source file "${file}" \u2014 skipping. (${err instanceof Error ? err.message : String(err)})`
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async function scanSourceFilesAsync(sources, cwd) {
|
|
347
|
+
const { classes: allClasses, warnings: inlineWarnings } = collectInlineClasses(sources);
|
|
348
|
+
const authored = new Set(allClasses);
|
|
349
|
+
const allWarnings = [...inlineWarnings];
|
|
350
|
+
const { files, warnings: resolveWarnings } = await resolveSourceFilesAsync(
|
|
351
|
+
sources,
|
|
352
|
+
cwd,
|
|
353
|
+
allClasses.size > 0
|
|
354
|
+
);
|
|
355
|
+
allWarnings.push(...resolveWarnings);
|
|
356
|
+
const CONCURRENCY_LIMIT = 32;
|
|
357
|
+
const results = new Array(files.length);
|
|
358
|
+
let nextIndex = 0;
|
|
359
|
+
const worker = async () => {
|
|
360
|
+
while (nextIndex < files.length) {
|
|
361
|
+
const index = nextIndex++;
|
|
362
|
+
results[index] = await scanOneFile(files[index]);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
await Promise.all(Array.from({ length: Math.min(CONCURRENCY_LIMIT, files.length) }, worker));
|
|
366
|
+
const seen = new Set(allWarnings);
|
|
367
|
+
for (const result of results) {
|
|
368
|
+
if (result.failure) {
|
|
369
|
+
allWarnings.push(result.failure);
|
|
370
|
+
seen.add(result.failure);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (result.classes) {
|
|
374
|
+
for (const cls of result.classes) {
|
|
375
|
+
allClasses.add(cls);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
pushWarningsDeduped(allWarnings, result.warnings, seen);
|
|
379
|
+
}
|
|
380
|
+
return { classes: allClasses, authored, warnings: allWarnings };
|
|
381
|
+
}
|
|
382
|
+
async function collectProjectClasses(themeSources, surfaceSources, cwd, warnings, warningSeen) {
|
|
383
|
+
const discovered = discoverPackageSafelistSources(cwd);
|
|
384
|
+
pushWarningsDeduped(warnings, discovered.warnings, warningSeen);
|
|
385
|
+
const allSources = [...themeSources, ...surfaceSources, ...discovered.sources];
|
|
386
|
+
const scanResult = await scanSourceFilesAsync(allSources, cwd);
|
|
387
|
+
pushWarningsDeduped(warnings, scanResult.warnings, warningSeen);
|
|
388
|
+
return { classes: scanResult.classes, authored: scanResult.authored };
|
|
389
|
+
}
|
|
28
390
|
|
|
29
391
|
// src/integrations/font-providers/google/state.ts
|
|
30
392
|
var googleFontInternals = {
|
|
@@ -38,13 +400,13 @@ var googleFontInternals = {
|
|
|
38
400
|
// src/integrations/font-providers/google/cache.ts
|
|
39
401
|
import { createHash, randomUUID } from "crypto";
|
|
40
402
|
import { open as fsOpen, writeFile, rename, unlink, mkdir } from "fs/promises";
|
|
41
|
-
import { resolve, dirname, join, isAbsolute } from "path";
|
|
403
|
+
import { resolve as resolve3, dirname as dirname2, join as join2, isAbsolute as isAbsolute2 } from "path";
|
|
42
404
|
import { isAbsolute as win32IsAbsolute } from "path/win32";
|
|
43
405
|
var MAX_FONT_CACHE_TTL_SECONDS = 30 * 24 * 60 * 60;
|
|
44
406
|
function getFontCacheDir() {
|
|
45
407
|
if (googleFontInternals.validatedCacheDir !== null) return googleFontInternals.validatedCacheDir;
|
|
46
408
|
const raw = process.env.RI_CACHE_DIR || "node_modules/.cache/rainbowindex";
|
|
47
|
-
if (
|
|
409
|
+
if (isAbsolute2(raw) || win32IsAbsolute(raw)) {
|
|
48
410
|
throw new Error(
|
|
49
411
|
`[RI-1208] RI_CACHE_DIR must be a relative path, got absolute path: "${raw}". Use a relative path like "node_modules/.cache/rainbowindex".`
|
|
50
412
|
);
|
|
@@ -64,7 +426,7 @@ function getResolvedCachePath() {
|
|
|
64
426
|
if (googleFontInternals.resolvedCachePath !== null && googleFontInternals.validatedCacheDir !== null) {
|
|
65
427
|
return googleFontInternals.resolvedCachePath;
|
|
66
428
|
}
|
|
67
|
-
googleFontInternals.resolvedCachePath =
|
|
429
|
+
googleFontInternals.resolvedCachePath = resolve3(process.cwd(), getFontCacheFile());
|
|
68
430
|
return googleFontInternals.resolvedCachePath;
|
|
69
431
|
}
|
|
70
432
|
function getFontCacheTTL() {
|
|
@@ -147,12 +509,12 @@ async function loadFontCache(ignoreExpiry = false) {
|
|
|
147
509
|
async function saveFontCache() {
|
|
148
510
|
try {
|
|
149
511
|
const cachePath = getResolvedCachePath();
|
|
150
|
-
await mkdir(
|
|
512
|
+
await mkdir(dirname2(cachePath), { recursive: true });
|
|
151
513
|
const entries = Array.from(googleFontInternals.googleFontState.cache.values());
|
|
152
514
|
const entriesJson = JSON.stringify(entries);
|
|
153
515
|
const checksum = createHash("sha256").update(entriesJson).digest("hex");
|
|
154
516
|
const payload = `{"checksum":${JSON.stringify(checksum)},"entries":${entriesJson}}`;
|
|
155
|
-
const tmpPath =
|
|
517
|
+
const tmpPath = join2(dirname2(cachePath), `.google-${process.pid}-${randomUUID()}.tmp`);
|
|
156
518
|
try {
|
|
157
519
|
await writeFile(tmpPath, payload);
|
|
158
520
|
await rename(tmpPath, cachePath);
|
|
@@ -582,437 +944,101 @@ function generateWebFontFace(family, face) {
|
|
|
582
944
|
if (face.unicodeRange) declarations.push(` unicode-range: ${face.unicodeRange};`);
|
|
583
945
|
return { type: "font-face", css: `@font-face {
|
|
584
946
|
${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 fallback = slot.fallback.length > 0 ? slot.fallback.join(", ") : getFallbackStack(slot.slot);
|
|
633
|
-
const stack = [`"${escapeFontFamily(slot.family)}"`, fallback].join(", ");
|
|
634
|
-
variables.push(`--font-${slot.slot}: ${stack};`);
|
|
635
|
-
pushFeatureVars();
|
|
636
|
-
return { imports, fontFaces, variables, warnings };
|
|
637
|
-
}
|
|
638
|
-
const metrics = resolveSlotMetrics(slot, warnings);
|
|
639
|
-
if (metrics) fontFaces.push(generateFallbackFontFace(slot.family, metrics));
|
|
640
|
-
for (const face of slot.faces) {
|
|
641
|
-
if (face.provider !== "google" && !face.provider.startsWith("/") && !face.provider.startsWith("http") && !face.provider.startsWith(".")) {
|
|
642
|
-
warnings.push(
|
|
643
|
-
`[RI-1201] Unknown font provider "${face.provider}" for "${slot.family}" \u2014 supported: google, or a file path/URL.`
|
|
644
|
-
);
|
|
645
|
-
continue;
|
|
646
|
-
}
|
|
647
|
-
const emitFace = face.provider === "google" ? face : { ...face, style: normalizeLocalStyle(face.style, slot.family, warnings) };
|
|
648
|
-
const webFont = generateWebFontFace(slot.family, emitFace);
|
|
649
|
-
if (!webFont) continue;
|
|
650
|
-
if (webFont.type === "import") imports.push(webFont.css);
|
|
651
|
-
else fontFaces.push(webFont.css);
|
|
652
|
-
}
|
|
653
|
-
const fallbackStack = slot.fallback.length > 0 ? slot.fallback.join(", ") : getFallbackStack(slot.slot);
|
|
654
|
-
const safeFamily = escapeFontFamily(slot.family);
|
|
655
|
-
const stackParts = [`"${safeFamily}"`];
|
|
656
|
-
if (metrics) stackParts.push(`"${safeFamily} Fallback"`);
|
|
657
|
-
stackParts.push(fallbackStack);
|
|
658
|
-
variables.push(`--font-${slot.slot}: ${stackParts.join(", ")};`);
|
|
659
|
-
pushFeatureVars();
|
|
660
|
-
return { imports, fontFaces, variables, warnings };
|
|
661
|
-
}
|
|
662
|
-
function getFontPreloadLinks(slots) {
|
|
663
|
-
const links = [];
|
|
664
|
-
const seen = /* @__PURE__ */ new Set();
|
|
665
|
-
for (const slot of slots) {
|
|
666
|
-
for (const face of slot.faces) {
|
|
667
|
-
if (!face.preload) continue;
|
|
668
|
-
if (face.provider === "system" || face.provider === "google" || !face.provider) continue;
|
|
669
|
-
if (seen.has(face.provider)) continue;
|
|
670
|
-
seen.add(face.provider);
|
|
671
|
-
links.push({
|
|
672
|
-
href: face.provider,
|
|
673
|
-
as: "font",
|
|
674
|
-
type: FONT_FORMAT_MIME[inferFontFormat(face.provider)],
|
|
675
|
-
crossorigin: true
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
return links;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
// src/scanner/sources.ts
|
|
683
|
-
import { readFile, stat } from "fs/promises";
|
|
684
|
-
import { resolve as resolve3 } from "path";
|
|
685
|
-
import { glob } from "tinyglobby";
|
|
686
|
-
|
|
687
|
-
// src/scanner/glob-utils.ts
|
|
688
|
-
import { isAbsolute as isAbsolute2, win32 } from "path";
|
|
689
|
-
function validateGlobPattern(pattern) {
|
|
690
|
-
if (!pattern?.trim()) {
|
|
691
|
-
return "Glob pattern is empty.";
|
|
692
|
-
}
|
|
693
|
-
if (pattern.includes("\0")) {
|
|
694
|
-
return "Glob pattern contains a null byte, which is invalid in file paths.";
|
|
695
|
-
}
|
|
696
|
-
if (isAbsolute2(pattern) || win32.isAbsolute(pattern)) {
|
|
697
|
-
return `Glob pattern "${pattern}" must be relative, not absolute.`;
|
|
698
|
-
}
|
|
699
|
-
const segments = pattern.split(/[\\/]+/);
|
|
700
|
-
for (const seg of segments) {
|
|
701
|
-
if (seg === "..") {
|
|
702
|
-
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.`;
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
return null;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// src/scanner/package-discovery.ts
|
|
709
|
-
import { existsSync, readFileSync, realpathSync, statSync } from "fs";
|
|
710
|
-
import { dirname as dirname2, join as join2, posix, resolve as resolve2 } from "path";
|
|
711
|
-
var EMPTY = Object.freeze({ sources: [], warnings: [] });
|
|
712
|
-
var discoveryCache = /* @__PURE__ */ new Map();
|
|
713
|
-
function discoverPackageSafelistSources(cwd) {
|
|
714
|
-
const cwdAbs = resolve2(cwd);
|
|
715
|
-
let mtimeMs;
|
|
716
|
-
try {
|
|
717
|
-
mtimeMs = statSync(join2(cwdAbs, "package.json")).mtimeMs;
|
|
718
|
-
} catch {
|
|
719
|
-
return EMPTY;
|
|
720
|
-
}
|
|
721
|
-
const cached = discoveryCache.get(cwdAbs);
|
|
722
|
-
if (cached && cached.mtimeMs === mtimeMs) return cached.result;
|
|
723
|
-
const result = runDiscovery(cwdAbs);
|
|
724
|
-
discoveryCache.set(cwdAbs, { mtimeMs, result });
|
|
725
|
-
return result;
|
|
726
|
-
}
|
|
727
|
-
function runDiscovery(cwdAbs) {
|
|
728
|
-
let consumer;
|
|
729
|
-
try {
|
|
730
|
-
consumer = readPackageJson(join2(cwdAbs, "package.json"));
|
|
731
|
-
} catch {
|
|
732
|
-
return EMPTY;
|
|
733
|
-
}
|
|
734
|
-
const deps = [
|
|
735
|
-
...Object.keys(consumer.dependencies ?? {}),
|
|
736
|
-
...Object.keys(consumer.peerDependencies ?? {})
|
|
737
|
-
];
|
|
738
|
-
if (deps.length === 0) return EMPTY;
|
|
739
|
-
const sources = [];
|
|
740
|
-
const warnings = [];
|
|
741
|
-
for (const depName of deps) {
|
|
742
|
-
const depPkgPath = findDepPackageJson(cwdAbs, depName);
|
|
743
|
-
if (!depPkgPath) {
|
|
744
|
-
continue;
|
|
745
|
-
}
|
|
746
|
-
let depPkg;
|
|
747
|
-
try {
|
|
748
|
-
depPkg = readPackageJson(depPkgPath);
|
|
749
|
-
} catch (err) {
|
|
750
|
-
warnings.push(
|
|
751
|
-
`[RI-1410] Could not read ${depName}/package.json during safelist discovery: ${errMessage(err)}`
|
|
752
|
-
);
|
|
753
|
-
continue;
|
|
754
|
-
}
|
|
755
|
-
const patterns = depPkg.rainbowindex?.safelistSources;
|
|
756
|
-
if (patterns == null) continue;
|
|
757
|
-
if (!Array.isArray(patterns)) {
|
|
758
|
-
warnings.push(
|
|
759
|
-
`[RI-1410] Invalid rainbowindex.safelistSources in ${depName} \u2014 expected an array of glob strings.`
|
|
760
|
-
);
|
|
761
|
-
continue;
|
|
762
|
-
}
|
|
763
|
-
if (patterns.length === 0) continue;
|
|
764
|
-
const depRoot = realpathOrFallback(dirname2(depPkgPath)).replace(/\\/g, "/");
|
|
765
|
-
for (const pattern of patterns) {
|
|
766
|
-
if (typeof pattern !== "string" || !pattern) {
|
|
767
|
-
warnings.push(
|
|
768
|
-
`[RI-1410] Invalid rainbowindex.safelistSources entry in ${depName} \u2014 expected a non-empty glob string.`
|
|
769
|
-
);
|
|
770
|
-
continue;
|
|
771
|
-
}
|
|
772
|
-
if (validateGlobPattern(pattern) !== null) {
|
|
773
|
-
warnings.push(
|
|
774
|
-
`[RI-1410] Invalid rainbowindex.safelistSources entry "${pattern}" in ${depName} \u2014 patterns must be relative to the package root and must not traverse parent directories.`
|
|
775
|
-
);
|
|
776
|
-
continue;
|
|
777
|
-
}
|
|
778
|
-
const absolute = posix.normalize(`${depRoot}/${pattern.replace(/^\.\//, "")}`);
|
|
779
|
-
if (absolute !== depRoot && !absolute.startsWith(`${depRoot}/`)) {
|
|
780
|
-
warnings.push(
|
|
781
|
-
`[RI-1410] Invalid rainbowindex.safelistSources entry "${pattern}" in ${depName} \u2014 resolved outside the package root.`
|
|
782
|
-
);
|
|
783
|
-
continue;
|
|
784
|
-
}
|
|
785
|
-
sources.push({ pattern: absolute, negated: false, inline: false, absolute: true });
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
return { sources, warnings };
|
|
789
|
-
}
|
|
790
|
-
function readPackageJson(path) {
|
|
791
|
-
const raw = readFileSync(path, "utf8");
|
|
792
|
-
return JSON.parse(raw);
|
|
793
|
-
}
|
|
794
|
-
function findDepPackageJson(cwd, depName) {
|
|
795
|
-
let dir = cwd;
|
|
796
|
-
while (true) {
|
|
797
|
-
const candidate = join2(dir, "node_modules", depName, "package.json");
|
|
798
|
-
if (existsSync(candidate)) return candidate;
|
|
799
|
-
const parent = dirname2(dir);
|
|
800
|
-
if (parent === dir) return null;
|
|
801
|
-
dir = parent;
|
|
802
|
-
}
|
|
947
|
+
}` };
|
|
803
948
|
}
|
|
804
|
-
function
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
949
|
+
function normalizeLocalStyle(style, family, warnings) {
|
|
950
|
+
if (style.includes(" ") && !style.startsWith("oblique")) {
|
|
951
|
+
const first = style.split(/\s+/)[0];
|
|
952
|
+
warnings.push(
|
|
953
|
+
`[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}".`
|
|
954
|
+
);
|
|
955
|
+
return first;
|
|
809
956
|
}
|
|
957
|
+
return style;
|
|
810
958
|
}
|
|
811
|
-
function
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
"dist/**",
|
|
823
|
-
"build/**",
|
|
824
|
-
"coverage/**",
|
|
825
|
-
"public/**",
|
|
826
|
-
"**/*.config.*",
|
|
827
|
-
"**/*.d.ts"
|
|
828
|
-
]);
|
|
829
|
-
var MAX_FILE_SIZE = 1048576;
|
|
830
|
-
var MAX_INLINE_SOURCE_SIZE = 102400;
|
|
831
|
-
var GLOB_TIMEOUT_MS = 3e4;
|
|
832
|
-
var GLOB_TIMEOUT_MESSAGE = `glob() timed out after ${GLOB_TIMEOUT_MS / 1e3}s \u2014 check for network filesystem issues or overly broad @source patterns`;
|
|
833
|
-
function collectPatterns(sources) {
|
|
834
|
-
const includePatterns = [];
|
|
835
|
-
const nodeModulesIncludePatterns = [];
|
|
836
|
-
const excludePatterns = [...DEFAULT_EXCLUDES];
|
|
837
|
-
const hasUserPositiveGlobs = sources.some((s) => !s.inline && !s.negated && !s.absolute);
|
|
838
|
-
if (!hasUserPositiveGlobs) {
|
|
839
|
-
includePatterns.push(...DEFAULT_PATTERNS);
|
|
840
|
-
}
|
|
841
|
-
const warnings = [];
|
|
842
|
-
for (const src of sources) {
|
|
843
|
-
if (src.inline) continue;
|
|
844
|
-
if (!src.absolute) {
|
|
845
|
-
const err = validateGlobPattern(src.pattern);
|
|
846
|
-
if (err) {
|
|
847
|
-
warnings.push(`[RI-1404] @source pattern rejected: ${err}`);
|
|
848
|
-
continue;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
if (src.negated) {
|
|
852
|
-
excludePatterns.push(src.pattern);
|
|
853
|
-
} else if (src.absolute || src.pattern.replace(/^\.\//, "").startsWith("node_modules/")) {
|
|
854
|
-
nodeModulesIncludePatterns.push(src.pattern);
|
|
855
|
-
} else {
|
|
856
|
-
includePatterns.push(src.pattern);
|
|
857
|
-
}
|
|
959
|
+
function resolveSlotMetrics(slot, warnings) {
|
|
960
|
+
const cfg = slot.metrics;
|
|
961
|
+
if (cfg === null) return null;
|
|
962
|
+
if (cfg?.sizeAdjust !== void 0) {
|
|
963
|
+
return {
|
|
964
|
+
fallback: cfg.fallback || slot.fallback[0] || "Arial",
|
|
965
|
+
sizeAdjust: cfg.sizeAdjust,
|
|
966
|
+
ascent: cfg.ascent,
|
|
967
|
+
descent: cfg.descent,
|
|
968
|
+
lineGap: cfg.lineGap
|
|
969
|
+
};
|
|
858
970
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
excludePatterns,
|
|
863
|
-
warnings,
|
|
864
|
-
hasUserPositiveGlobs
|
|
865
|
-
};
|
|
866
|
-
}
|
|
867
|
-
async function resolveSourceFilesAsync(sources, cwd, hasInlineClasses = false) {
|
|
868
|
-
const {
|
|
869
|
-
includePatterns,
|
|
870
|
-
nodeModulesIncludePatterns,
|
|
871
|
-
excludePatterns,
|
|
872
|
-
warnings,
|
|
873
|
-
hasUserPositiveGlobs
|
|
874
|
-
} = collectPatterns(sources);
|
|
875
|
-
const allIncludes = [...includePatterns, ...nodeModulesIncludePatterns];
|
|
876
|
-
if (allIncludes.length === 0) return { files: [], warnings };
|
|
877
|
-
try {
|
|
878
|
-
const globPasses = [];
|
|
879
|
-
if (includePatterns.length > 0) {
|
|
880
|
-
globPasses.push(
|
|
881
|
-
withTimeout(
|
|
882
|
-
glob(includePatterns, { cwd, ignore: excludePatterns }),
|
|
883
|
-
GLOB_TIMEOUT_MS,
|
|
884
|
-
GLOB_TIMEOUT_MESSAGE
|
|
885
|
-
)
|
|
886
|
-
);
|
|
887
|
-
}
|
|
888
|
-
if (nodeModulesIncludePatterns.length > 0) {
|
|
889
|
-
const relaxedExcludes = excludePatterns.filter((p) => p !== "node_modules/**");
|
|
890
|
-
globPasses.push(
|
|
891
|
-
withTimeout(
|
|
892
|
-
glob(nodeModulesIncludePatterns, { cwd, ignore: relaxedExcludes }),
|
|
893
|
-
GLOB_TIMEOUT_MS,
|
|
894
|
-
GLOB_TIMEOUT_MESSAGE
|
|
895
|
-
)
|
|
896
|
-
);
|
|
897
|
-
}
|
|
898
|
-
const matched = (await Promise.all(globPasses)).flat();
|
|
899
|
-
const files = [...new Set(matched.map((f) => resolve3(cwd, f)))].sort(codepointCompare);
|
|
900
|
-
if (files.length === 0 && !(hasInlineClasses && !hasUserPositiveGlobs)) {
|
|
901
|
-
warnings.push(
|
|
902
|
-
`[RI-1401] No source files found matching ${allIncludes.map((p) => `"${p}"`).join(", ")} \u2014 check @source paths or project structure.`
|
|
903
|
-
);
|
|
904
|
-
}
|
|
905
|
-
return { files, warnings };
|
|
906
|
-
} catch (err) {
|
|
971
|
+
const resolved = resolveAutoMetrics(slot.family, slot.fallback, cfg?.fallback);
|
|
972
|
+
if (!resolved && cfg?.fallback) {
|
|
973
|
+
const missing = lookupFontMetrics(slot.family) ? cfg.fallback : slot.family;
|
|
907
974
|
warnings.push(
|
|
908
|
-
`[RI-
|
|
975
|
+
`[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\`.`
|
|
909
976
|
);
|
|
910
|
-
return { files: [], warnings };
|
|
911
977
|
}
|
|
978
|
+
return resolved;
|
|
912
979
|
}
|
|
913
|
-
function
|
|
914
|
-
const
|
|
980
|
+
function generateFontCSS(slot) {
|
|
981
|
+
const imports = [];
|
|
982
|
+
const fontFaces = [];
|
|
983
|
+
const variables = [];
|
|
915
984
|
const warnings = [];
|
|
916
|
-
|
|
917
|
-
if (
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
985
|
+
const pushFeatureVars = () => {
|
|
986
|
+
if (slot.features) variables.push(`--font-${slot.slot}--features: ${slot.features};`);
|
|
987
|
+
if (slot.variation) variables.push(`--font-${slot.slot}--variations: ${slot.variation};`);
|
|
988
|
+
};
|
|
989
|
+
if (slot.kind === "system") {
|
|
990
|
+
variables.push(`--font-${slot.slot}: ${getFallbackStack(slot.slot)};`);
|
|
991
|
+
return { imports, fontFaces, variables, warnings };
|
|
992
|
+
}
|
|
993
|
+
if (slot.kind === "manual") {
|
|
994
|
+
const fallback = slot.fallback.length > 0 ? slot.fallback.join(", ") : getFallbackStack(slot.slot);
|
|
995
|
+
const stack = [`"${escapeFontFamily(slot.family)}"`, fallback].join(", ");
|
|
996
|
+
variables.push(`--font-${slot.slot}: ${stack};`);
|
|
997
|
+
pushFeatureVars();
|
|
998
|
+
return { imports, fontFaces, variables, warnings };
|
|
999
|
+
}
|
|
1000
|
+
const metrics = resolveSlotMetrics(slot, warnings);
|
|
1001
|
+
if (metrics) fontFaces.push(generateFallbackFontFace(slot.family, metrics));
|
|
1002
|
+
for (const face of slot.faces) {
|
|
1003
|
+
if (face.provider !== "google" && !face.provider.startsWith("/") && !face.provider.startsWith("http") && !face.provider.startsWith(".")) {
|
|
922
1004
|
warnings.push(
|
|
923
|
-
`[RI-
|
|
1005
|
+
`[RI-1201] Unknown font provider "${face.provider}" for "${slot.family}" \u2014 supported: google, or a file path/URL.`
|
|
924
1006
|
);
|
|
925
1007
|
continue;
|
|
926
1008
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
}
|
|
933
|
-
var FILE_IO_TIMEOUT_MS = 1e4;
|
|
934
|
-
var SCAN_CACHE_MAX_ENTRIES = 2e4;
|
|
935
|
-
var scanCache = /* @__PURE__ */ new Map();
|
|
936
|
-
async function scanOneFile(file) {
|
|
937
|
-
const warnings = [];
|
|
938
|
-
try {
|
|
939
|
-
const stats = await withTimeout(stat(file), FILE_IO_TIMEOUT_MS, "stat() timed out");
|
|
940
|
-
const cached = scanCache.get(file);
|
|
941
|
-
if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
|
|
942
|
-
return cached.result;
|
|
943
|
-
}
|
|
944
|
-
let result;
|
|
945
|
-
if (stats.size > MAX_FILE_SIZE) {
|
|
946
|
-
result = {
|
|
947
|
-
classes: null,
|
|
948
|
-
warnings,
|
|
949
|
-
failure: `[RI-1405] Skipping source file "${file}" (${stats.size} bytes) \u2014 exceeds ${MAX_FILE_SIZE} byte limit.`
|
|
950
|
-
};
|
|
951
|
-
} else {
|
|
952
|
-
const content = await withTimeout(
|
|
953
|
-
readFile(file, "utf-8"),
|
|
954
|
-
FILE_IO_TIMEOUT_MS,
|
|
955
|
-
"readFile() timed out"
|
|
956
|
-
);
|
|
957
|
-
result = {
|
|
958
|
-
classes: extractClassesFromSource({ path: file, content }, warnings),
|
|
959
|
-
warnings,
|
|
960
|
-
failure: null
|
|
961
|
-
};
|
|
962
|
-
}
|
|
963
|
-
if (scanCache.size >= SCAN_CACHE_MAX_ENTRIES) scanCache.clear();
|
|
964
|
-
scanCache.set(file, { mtimeMs: stats.mtimeMs, size: stats.size, result });
|
|
965
|
-
return result;
|
|
966
|
-
} catch (err) {
|
|
967
|
-
return {
|
|
968
|
-
classes: null,
|
|
969
|
-
warnings,
|
|
970
|
-
failure: `[RI-1403] Could not read source file "${file}" \u2014 skipping. (${err instanceof Error ? err.message : String(err)})`
|
|
971
|
-
};
|
|
1009
|
+
const emitFace = face.provider === "google" ? face : { ...face, style: normalizeLocalStyle(face.style, slot.family, warnings) };
|
|
1010
|
+
const webFont = generateWebFontFace(slot.family, emitFace);
|
|
1011
|
+
if (!webFont) continue;
|
|
1012
|
+
if (webFont.type === "import") imports.push(webFont.css);
|
|
1013
|
+
else fontFaces.push(webFont.css);
|
|
972
1014
|
}
|
|
1015
|
+
const fallbackStack = slot.fallback.length > 0 ? slot.fallback.join(", ") : getFallbackStack(slot.slot);
|
|
1016
|
+
const safeFamily = escapeFontFamily(slot.family);
|
|
1017
|
+
const stackParts = [`"${safeFamily}"`];
|
|
1018
|
+
if (metrics) stackParts.push(`"${safeFamily} Fallback"`);
|
|
1019
|
+
stackParts.push(fallbackStack);
|
|
1020
|
+
variables.push(`--font-${slot.slot}: ${stackParts.join(", ")};`);
|
|
1021
|
+
pushFeatureVars();
|
|
1022
|
+
return { imports, fontFaces, variables, warnings };
|
|
973
1023
|
}
|
|
974
|
-
|
|
975
|
-
const
|
|
976
|
-
const
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
results[index] = await scanOneFile(files[index]);
|
|
990
|
-
}
|
|
991
|
-
};
|
|
992
|
-
await Promise.all(Array.from({ length: Math.min(CONCURRENCY_LIMIT, files.length) }, worker));
|
|
993
|
-
const seen = new Set(allWarnings);
|
|
994
|
-
for (const result of results) {
|
|
995
|
-
if (result.failure) {
|
|
996
|
-
allWarnings.push(result.failure);
|
|
997
|
-
seen.add(result.failure);
|
|
998
|
-
continue;
|
|
999
|
-
}
|
|
1000
|
-
if (result.classes) {
|
|
1001
|
-
for (const cls of result.classes) {
|
|
1002
|
-
allClasses.add(cls);
|
|
1003
|
-
}
|
|
1024
|
+
function getFontPreloadLinks(slots) {
|
|
1025
|
+
const links = [];
|
|
1026
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1027
|
+
for (const slot of slots) {
|
|
1028
|
+
for (const face of slot.faces) {
|
|
1029
|
+
if (!face.preload) continue;
|
|
1030
|
+
if (face.provider === "system" || face.provider === "google" || !face.provider) continue;
|
|
1031
|
+
if (seen.has(face.provider)) continue;
|
|
1032
|
+
seen.add(face.provider);
|
|
1033
|
+
links.push({
|
|
1034
|
+
href: face.provider,
|
|
1035
|
+
as: "font",
|
|
1036
|
+
type: FONT_FORMAT_MIME[inferFontFormat(face.provider)],
|
|
1037
|
+
crossorigin: true
|
|
1038
|
+
});
|
|
1004
1039
|
}
|
|
1005
|
-
pushWarningsDeduped(allWarnings, result.warnings, seen);
|
|
1006
1040
|
}
|
|
1007
|
-
return
|
|
1008
|
-
}
|
|
1009
|
-
async function collectProjectClasses(themeSources, surfaceSources, cwd, warnings, warningSeen) {
|
|
1010
|
-
const discovered = discoverPackageSafelistSources(cwd);
|
|
1011
|
-
pushWarningsDeduped(warnings, discovered.warnings, warningSeen);
|
|
1012
|
-
const allSources = [...themeSources, ...surfaceSources, ...discovered.sources];
|
|
1013
|
-
const scanResult = await scanSourceFilesAsync(allSources, cwd);
|
|
1014
|
-
pushWarningsDeduped(warnings, scanResult.warnings, warningSeen);
|
|
1015
|
-
return scanResult.classes;
|
|
1041
|
+
return links;
|
|
1016
1042
|
}
|
|
1017
1043
|
|
|
1018
1044
|
// src/css/strip.ts
|
|
@@ -1505,15 +1531,6 @@ function generateTokenLayer(theme, usage, fontOutputCache) {
|
|
|
1505
1531
|
vars.push(`--font-${slot}: ${stack};`);
|
|
1506
1532
|
}
|
|
1507
1533
|
}
|
|
1508
|
-
if (usage.usedRounded.size > 0) {
|
|
1509
|
-
vars.push(`--rounded-roof: ${theme.roundedRoof};`);
|
|
1510
|
-
for (const [name, val] of Object.entries(theme.rounded).sort(
|
|
1511
|
-
([a], [b]) => codepointCompare(a, b)
|
|
1512
|
-
)) {
|
|
1513
|
-
if (!usage.usedRounded.has(name)) continue;
|
|
1514
|
-
vars.push(`--rounded-${name}: ${val};`);
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
1534
|
const shadowsToEmit = resolveTransitiveShadowDeps(theme.shadows, usage.usedShadows);
|
|
1518
1535
|
for (const [name, val] of Object.entries(theme.shadows).sort(
|
|
1519
1536
|
([a], [b]) => codepointCompare(a, b)
|
|
@@ -1748,13 +1765,15 @@ async function finalizeProjectCompilation(options) {
|
|
|
1748
1765
|
}
|
|
1749
1766
|
const expansionWarnings = [];
|
|
1750
1767
|
const classNameSet = new Set(options.classNames);
|
|
1768
|
+
const authored = options.authoredClassNames && new Set(options.authoredClassNames);
|
|
1751
1769
|
for (const cls of collectApplyClassNames(options.css, expansionWarnings)) {
|
|
1752
1770
|
classNameSet.add(cls);
|
|
1771
|
+
authored?.add(cls);
|
|
1753
1772
|
}
|
|
1754
1773
|
const classNames = [...classNameSet];
|
|
1755
1774
|
pushWarningsDeduped(analysis.warnings, expansionWarnings, analysis.warningSeen);
|
|
1756
1775
|
const compiler = createCompiler();
|
|
1757
|
-
const compilation = compiler.compile(classNames, effectiveTheme);
|
|
1776
|
+
const compilation = compiler.compile(classNames, effectiveTheme, authored);
|
|
1758
1777
|
let userCSS = stripRIDirectives(options.css);
|
|
1759
1778
|
if ((options.processCssFunctions ?? true) && userCSS && hasCSSFunctions(userCSS)) {
|
|
1760
1779
|
userCSS = compileCSSFunctions(userCSS, effectiveTheme, analysis.warnings);
|
|
@@ -1818,7 +1837,7 @@ async function compileScannedProject(options) {
|
|
|
1818
1837
|
}
|
|
1819
1838
|
surfaceSources.push({ pattern, negated: false, inline: false });
|
|
1820
1839
|
}
|
|
1821
|
-
const classNames = await collectProjectClasses(
|
|
1840
|
+
const { classes: classNames, authored } = await collectProjectClasses(
|
|
1822
1841
|
analysis.theme.sources,
|
|
1823
1842
|
surfaceSources,
|
|
1824
1843
|
options.cwd,
|
|
@@ -1828,6 +1847,7 @@ async function compileScannedProject(options) {
|
|
|
1828
1847
|
const compiled = await finalizeProjectCompilation({
|
|
1829
1848
|
css: options.css,
|
|
1830
1849
|
classNames,
|
|
1850
|
+
authoredClassNames: authored,
|
|
1831
1851
|
analysis,
|
|
1832
1852
|
resolveFonts: () => fontsReady
|
|
1833
1853
|
});
|
|
@@ -1839,6 +1859,8 @@ export {
|
|
|
1839
1859
|
getFontPreloadLinks,
|
|
1840
1860
|
DEFAULT_PATTERNS,
|
|
1841
1861
|
DEFAULT_EXCLUDES,
|
|
1862
|
+
enableSourceFileListCache,
|
|
1863
|
+
invalidateSourceFileListCache,
|
|
1842
1864
|
finalizeProjectCompilation,
|
|
1843
1865
|
compileScannedProject
|
|
1844
1866
|
};
|