rainbowindex 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +530 -0
- package/README.md +70 -39
- package/dist/browser.d.ts +2 -2
- package/dist/browser.mjs +1 -3
- package/dist/{chunk-CMB6BHVE.mjs → chunk-3LWJTLOJ.mjs} +62 -38
- package/dist/{chunk-KRZL4IDK.mjs → chunk-6U4IOFOS.mjs} +687 -859
- package/dist/{chunk-ZI5ZYNSU.mjs → chunk-KSNYSR3C.mjs} +5629 -5092
- package/dist/{chunk-DT5HYIM3.mjs → chunk-WK6S4HTC.mjs} +978 -901
- package/dist/{chunk-NXJZX6KI.mjs → chunk-ZR7XJMUN.mjs} +15 -8
- package/dist/cli.mjs +34 -11
- package/dist/editor.d.ts +35 -9
- package/dist/editor.mjs +34 -8
- package/dist/{index-Dp6i5TSv.d.ts → index-4Kyaq3IZ.d.ts} +29 -6
- package/dist/{context-ruu2x_jR.d.ts → index-DSgpB6bS.d.ts} +90 -69
- package/dist/index.d.ts +7 -4
- package/dist/index.mjs +14 -9
- package/dist/oxlint.d.ts +55 -0
- package/dist/oxlint.mjs +38 -0
- package/dist/{safelist-D9-Plqta.d.ts → safelist-CGCtF-Fr.d.ts} +2 -15
- package/dist/vite.mjs +67 -14
- package/package.json +7 -1
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
RI_IMPORT_SPECIFIER_ALTERNATION,
|
|
6
6
|
SAFE_FONT_FAMILY_RE,
|
|
7
7
|
SHADOW_VAR_REF_RE,
|
|
8
|
-
|
|
8
|
+
analyzeProjectCSSMemo,
|
|
9
9
|
codepointCompare,
|
|
10
10
|
compileCSSFunctions,
|
|
11
11
|
createCompiler,
|
|
@@ -19,367 +19,766 @@ import {
|
|
|
19
19
|
renderCSS,
|
|
20
20
|
scanCSSForTokenUsage,
|
|
21
21
|
withTimeout
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-KSNYSR3C.mjs";
|
|
23
23
|
import {
|
|
24
24
|
checkPaletteContrast,
|
|
25
25
|
generateAllColorVariables,
|
|
26
26
|
generateThemeOverrides
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-6U4IOFOS.mjs";
|
|
28
28
|
|
|
29
|
-
// src/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
lastFetchFailureMs: 0,
|
|
34
|
-
validatedCacheDir: null,
|
|
35
|
-
resolvedCachePath: null
|
|
36
|
-
};
|
|
29
|
+
// src/scanner/sources.ts
|
|
30
|
+
import { readFile, stat } from "fs/promises";
|
|
31
|
+
import { resolve as resolve2 } from "path";
|
|
32
|
+
import { glob } from "tinyglobby";
|
|
37
33
|
|
|
38
|
-
// src/
|
|
39
|
-
import {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
var MAX_FONT_CACHE_TTL_SECONDS = 30 * 24 * 60 * 60;
|
|
44
|
-
function getFontCacheDir() {
|
|
45
|
-
if (googleFontInternals.validatedCacheDir !== null) return googleFontInternals.validatedCacheDir;
|
|
46
|
-
const raw = process.env.RI_CACHE_DIR || "node_modules/.cache/rainbowindex";
|
|
47
|
-
if (isAbsolute(raw) || win32IsAbsolute(raw)) {
|
|
48
|
-
throw new Error(
|
|
49
|
-
`[RI-1208] RI_CACHE_DIR must be a relative path, got absolute path: "${raw}". Use a relative path like "node_modules/.cache/rainbowindex".`
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
if (raw.split(/[\\/]/).some((s) => s === "..")) {
|
|
53
|
-
throw new Error(
|
|
54
|
-
`[RI-1209] RI_CACHE_DIR must not contain ".." segments: "${raw}". Use a direct relative path like "node_modules/.cache/rainbowindex".`
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
googleFontInternals.validatedCacheDir = raw;
|
|
58
|
-
return raw;
|
|
59
|
-
}
|
|
60
|
-
function getFontCacheFile() {
|
|
61
|
-
return `${getFontCacheDir()}/google.json`;
|
|
62
|
-
}
|
|
63
|
-
function getResolvedCachePath() {
|
|
64
|
-
if (googleFontInternals.resolvedCachePath !== null && googleFontInternals.validatedCacheDir !== null) {
|
|
65
|
-
return googleFontInternals.resolvedCachePath;
|
|
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.";
|
|
66
39
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
function getFontCacheTTL() {
|
|
71
|
-
const envVal = process.env.RI_FONT_CACHE_TTL;
|
|
72
|
-
if (envVal) {
|
|
73
|
-
const seconds = Number(envVal);
|
|
74
|
-
if (!Number.isNaN(seconds) && seconds >= 0) {
|
|
75
|
-
if (seconds > MAX_FONT_CACHE_TTL_SECONDS) {
|
|
76
|
-
console.warn(
|
|
77
|
-
`[RI-1211] RI_FONT_CACHE_TTL=${seconds} exceeds maximum of ${MAX_FONT_CACHE_TTL_SECONDS} seconds (30 days). Clamping to 30 days.`
|
|
78
|
-
);
|
|
79
|
-
return MAX_FONT_CACHE_TTL_SECONDS * 1e3;
|
|
80
|
-
}
|
|
81
|
-
return seconds * 1e3;
|
|
82
|
-
}
|
|
40
|
+
if (pattern.includes("\0")) {
|
|
41
|
+
return "Glob pattern contains a null byte, which is invalid in file paths.";
|
|
83
42
|
}
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
function parseCachedMetaEntries(raw) {
|
|
87
|
-
const parsed = JSON.parse(raw);
|
|
88
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !("checksum" in parsed) || !("entries" in parsed)) {
|
|
89
|
-
return null;
|
|
43
|
+
if (isAbsolute(pattern) || win32.isAbsolute(pattern)) {
|
|
44
|
+
return `Glob pattern "${pattern}" must be relative, not absolute.`;
|
|
90
45
|
}
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const entries = parsed.entries;
|
|
96
|
-
const newCache = /* @__PURE__ */ new Map();
|
|
97
|
-
for (const rawItem of entries) {
|
|
98
|
-
if (!rawItem || typeof rawItem !== "object") continue;
|
|
99
|
-
const item = rawItem;
|
|
100
|
-
if (typeof item.family !== "string" || typeof item.variable !== "boolean" || typeof item.category !== "string" || item.axes !== void 0 && !Array.isArray(item.axes)) {
|
|
101
|
-
continue;
|
|
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.`;
|
|
102
50
|
}
|
|
103
|
-
if (!SAFE_FONT_FAMILY_RE.test(item.family)) continue;
|
|
104
|
-
const axes = Array.isArray(item.axes) ? item.axes.filter((a) => {
|
|
105
|
-
if (!a || typeof a !== "object") return false;
|
|
106
|
-
const axis = a;
|
|
107
|
-
return typeof axis.tag === "string" && typeof axis.start === "number" && Number.isFinite(axis.start) && typeof axis.end === "number" && Number.isFinite(axis.end);
|
|
108
|
-
}).map((a) => {
|
|
109
|
-
const axis = { tag: a.tag, start: a.start, end: a.end };
|
|
110
|
-
Object.freeze(axis);
|
|
111
|
-
return axis;
|
|
112
|
-
}) : void 0;
|
|
113
|
-
if (axes) Object.freeze(axes);
|
|
114
|
-
const entry = {
|
|
115
|
-
family: item.family,
|
|
116
|
-
variable: item.variable,
|
|
117
|
-
axes,
|
|
118
|
-
category: item.category
|
|
119
|
-
};
|
|
120
|
-
Object.freeze(entry);
|
|
121
|
-
newCache.set(entry.family, entry);
|
|
122
51
|
}
|
|
123
|
-
return
|
|
52
|
+
return null;
|
|
124
53
|
}
|
|
125
|
-
|
|
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;
|
|
126
63
|
try {
|
|
127
|
-
|
|
128
|
-
const fh = await fsOpen(cachePath, "r");
|
|
129
|
-
let raw;
|
|
130
|
-
try {
|
|
131
|
-
if (!ignoreExpiry) {
|
|
132
|
-
const st = await fh.stat();
|
|
133
|
-
if (Date.now() - st.mtimeMs > getFontCacheTTL()) return false;
|
|
134
|
-
}
|
|
135
|
-
raw = await fh.readFile("utf-8");
|
|
136
|
-
} finally {
|
|
137
|
-
await fh.close();
|
|
138
|
-
}
|
|
139
|
-
const newCache = parseCachedMetaEntries(raw);
|
|
140
|
-
if (!newCache) return false;
|
|
141
|
-
googleFontInternals.googleFontState = { cache: newCache, fetched: true };
|
|
142
|
-
return true;
|
|
64
|
+
mtimeMs = statSync(join(cwdAbs, "package.json")).mtimeMs;
|
|
143
65
|
} catch {
|
|
144
|
-
return
|
|
66
|
+
return EMPTY;
|
|
145
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;
|
|
146
73
|
}
|
|
147
|
-
|
|
74
|
+
function runDiscovery(cwdAbs) {
|
|
75
|
+
let consumer;
|
|
148
76
|
try {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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;
|
|
156
94
|
try {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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 });
|
|
162
133
|
}
|
|
163
|
-
} catch (err) {
|
|
164
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
165
|
-
console.warn(
|
|
166
|
-
`[RI-1210] Failed to write font cache to ${getFontCacheFile()}: ${reason}. Each build will fetch from Google Fonts. Set RI_CACHE_DIR to a writable path or use RI_OFFLINE=1 with a pre-populated cache.`
|
|
167
|
-
);
|
|
168
134
|
}
|
|
135
|
+
return { sources, warnings };
|
|
169
136
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
var MAX_RETRIES = 2;
|
|
174
|
-
var FETCH_TIMEOUT_MS = 5e3;
|
|
175
|
-
function wait(ms) {
|
|
176
|
-
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
137
|
+
function readPackageJson(path) {
|
|
138
|
+
const raw = readFileSync(path, "utf8");
|
|
139
|
+
return JSON.parse(raw);
|
|
177
140
|
}
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
});
|
|
187
|
-
Object.freeze(axes);
|
|
188
|
-
const entry = {
|
|
189
|
-
family: font.family,
|
|
190
|
-
variable: wghtAxis ? wghtAxis.min !== wghtAxis.max : false,
|
|
191
|
-
axes,
|
|
192
|
-
category: font.category
|
|
193
|
-
};
|
|
194
|
-
Object.freeze(entry);
|
|
195
|
-
newCache.set(font.family, entry);
|
|
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;
|
|
196
149
|
}
|
|
197
|
-
return newCache;
|
|
198
150
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
151
|
+
function realpathOrFallback(path) {
|
|
152
|
+
try {
|
|
153
|
+
return realpathSync(path);
|
|
154
|
+
} catch {
|
|
155
|
+
return path;
|
|
203
156
|
}
|
|
204
|
-
const chunks = [];
|
|
205
|
-
let totalBytes = 0;
|
|
206
|
-
for (; ; ) {
|
|
207
|
-
const { done, value } = await reader.read();
|
|
208
|
-
if (done) break;
|
|
209
|
-
totalBytes += value.byteLength;
|
|
210
|
-
if (totalBytes > MAX_RESPONSE_SIZE) {
|
|
211
|
-
reader.cancel();
|
|
212
|
-
throw new Error(
|
|
213
|
-
`[RI-1207] Google Fonts metadata response too large (>${MAX_RESPONSE_SIZE} bytes).`
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
chunks.push(value);
|
|
217
|
-
}
|
|
218
|
-
const text = new TextDecoder().decode(
|
|
219
|
-
chunks.length === 1 ? chunks[0] : await new Blob(chunks).arrayBuffer()
|
|
220
|
-
);
|
|
221
|
-
return JSON.parse(text);
|
|
222
157
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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}`);
|
|
251
206
|
continue;
|
|
252
207
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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);
|
|
256
215
|
}
|
|
257
216
|
}
|
|
258
|
-
|
|
217
|
+
return {
|
|
218
|
+
includePatterns,
|
|
219
|
+
nodeModulesIncludePatterns,
|
|
220
|
+
excludePatterns,
|
|
221
|
+
warnings,
|
|
222
|
+
hasUserPositiveGlobs
|
|
223
|
+
};
|
|
259
224
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
+
)
|
|
288
253
|
);
|
|
289
|
-
return;
|
|
290
254
|
}
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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.`
|
|
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
|
+
)
|
|
309
263
|
);
|
|
310
264
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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);
|
|
314
270
|
}
|
|
315
|
-
googleFontInternals.googleFontListPromise = null;
|
|
316
271
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+
}
|
|
320
284
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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;
|
|
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 };
|
|
353
304
|
}
|
|
354
|
-
var
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
305
|
+
var FILE_IO_TIMEOUT_MS = 1e4;
|
|
306
|
+
var SCAN_CACHE_MAX_ENTRIES = 2e4;
|
|
307
|
+
var scanCache = /* @__PURE__ */ new Map();
|
|
308
|
+
var scanChangeTrackingEnabled = false;
|
|
309
|
+
function enableScanChangeTracking() {
|
|
310
|
+
scanChangeTrackingEnabled = true;
|
|
311
|
+
}
|
|
312
|
+
function markSourceFileChanged(file, cwd) {
|
|
313
|
+
scanCache.delete(cwd === void 0 ? file : resolve2(cwd, file));
|
|
314
|
+
}
|
|
315
|
+
function disableScanChangeTracking() {
|
|
316
|
+
scanChangeTrackingEnabled = false;
|
|
317
|
+
scanCache.clear();
|
|
318
|
+
}
|
|
319
|
+
var fileUnion = null;
|
|
320
|
+
function applyToUnion(union, result, delta) {
|
|
321
|
+
if (!result?.classes) return;
|
|
322
|
+
for (const cls of result.classes) {
|
|
323
|
+
const next = (union.counts.get(cls) ?? 0) + delta;
|
|
324
|
+
if (next > 0) {
|
|
325
|
+
union.counts.set(cls, next);
|
|
326
|
+
if (delta === 1) union.classes.add(cls);
|
|
327
|
+
} else {
|
|
328
|
+
union.counts.delete(cls);
|
|
329
|
+
union.classes.delete(cls);
|
|
330
|
+
}
|
|
367
331
|
}
|
|
368
|
-
return refreshFontWeightDefaults(fonts);
|
|
369
332
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
333
|
+
async function scanOneFile(file) {
|
|
334
|
+
const warnings = [];
|
|
335
|
+
if (scanChangeTrackingEnabled) {
|
|
336
|
+
const tracked = scanCache.get(file);
|
|
337
|
+
if (tracked) return tracked.result;
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
const stats = await withTimeout(stat(file), FILE_IO_TIMEOUT_MS, "stat() timed out");
|
|
341
|
+
const cached = scanCache.get(file);
|
|
342
|
+
if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
|
|
343
|
+
return cached.result;
|
|
344
|
+
}
|
|
345
|
+
let result;
|
|
346
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
347
|
+
result = {
|
|
348
|
+
classes: null,
|
|
349
|
+
warnings,
|
|
350
|
+
failure: `[RI-1405] Skipping source file "${file}" (${stats.size} bytes) \u2014 exceeds ${MAX_FILE_SIZE} byte limit.`
|
|
351
|
+
};
|
|
352
|
+
} else {
|
|
353
|
+
const content = await withTimeout(
|
|
354
|
+
readFile(file, "utf-8"),
|
|
355
|
+
FILE_IO_TIMEOUT_MS,
|
|
356
|
+
"readFile() timed out"
|
|
357
|
+
);
|
|
358
|
+
result = {
|
|
359
|
+
classes: extractClassesFromSource({ path: file, content }, warnings),
|
|
360
|
+
warnings,
|
|
361
|
+
failure: null
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
if (scanCache.size >= SCAN_CACHE_MAX_ENTRIES) scanCache.clear();
|
|
365
|
+
scanCache.set(file, { mtimeMs: stats.mtimeMs, size: stats.size, result });
|
|
366
|
+
return result;
|
|
367
|
+
} catch (err) {
|
|
368
|
+
return {
|
|
369
|
+
classes: null,
|
|
370
|
+
warnings,
|
|
371
|
+
failure: `[RI-1403] Could not read source file "${file}" \u2014 skipping. (${err instanceof Error ? err.message : String(err)})`
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async function scanSourceFilesAsync(sources, cwd) {
|
|
376
|
+
const { classes: allClasses, warnings: inlineWarnings } = collectInlineClasses(sources);
|
|
377
|
+
const authored = new Set(allClasses);
|
|
378
|
+
const allWarnings = [...inlineWarnings];
|
|
379
|
+
const { files, warnings: resolveWarnings } = await resolveSourceFilesAsync(
|
|
380
|
+
sources,
|
|
381
|
+
cwd,
|
|
382
|
+
allClasses.size > 0
|
|
383
|
+
);
|
|
384
|
+
allWarnings.push(...resolveWarnings);
|
|
385
|
+
const CONCURRENCY_LIMIT = 32;
|
|
386
|
+
const results = new Array(files.length);
|
|
387
|
+
let nextIndex = 0;
|
|
388
|
+
const worker = async () => {
|
|
389
|
+
while (nextIndex < files.length) {
|
|
390
|
+
const index = nextIndex++;
|
|
391
|
+
results[index] = await scanOneFile(files[index]);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
await Promise.all(Array.from({ length: Math.min(CONCURRENCY_LIMIT, files.length) }, worker));
|
|
395
|
+
const seen = new Set(allWarnings);
|
|
396
|
+
for (const result of results) {
|
|
397
|
+
if (result.failure) {
|
|
398
|
+
allWarnings.push(result.failure);
|
|
399
|
+
seen.add(result.failure);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
pushWarningsDeduped(allWarnings, result.warnings, seen);
|
|
403
|
+
}
|
|
404
|
+
if (fileUnion === null || fileUnion.results.length !== results.length) {
|
|
405
|
+
fileUnion = { results, counts: /* @__PURE__ */ new Map(), classes: /* @__PURE__ */ new Set() };
|
|
406
|
+
for (const result of results) applyToUnion(fileUnion, result, 1);
|
|
407
|
+
} else {
|
|
408
|
+
const previous = fileUnion.results;
|
|
409
|
+
for (let i = 0; i < results.length; i++) {
|
|
410
|
+
if (previous[i] === results[i]) continue;
|
|
411
|
+
applyToUnion(fileUnion, previous[i], -1);
|
|
412
|
+
applyToUnion(fileUnion, results[i], 1);
|
|
413
|
+
}
|
|
414
|
+
fileUnion.results = results;
|
|
415
|
+
}
|
|
416
|
+
for (const cls of fileUnion.classes) allClasses.add(cls);
|
|
417
|
+
return { classes: allClasses, authored, warnings: allWarnings };
|
|
418
|
+
}
|
|
419
|
+
async function collectProjectClasses(themeSources, surfaceSources, cwd, warnings, warningSeen, suppressed) {
|
|
420
|
+
const discovered = discoverPackageSafelistSources(cwd);
|
|
421
|
+
pushWarningsDeduped(warnings, discovered.warnings, warningSeen, suppressed);
|
|
422
|
+
const allSources = [...themeSources, ...surfaceSources, ...discovered.sources];
|
|
423
|
+
const scanResult = await scanSourceFilesAsync(allSources, cwd);
|
|
424
|
+
pushWarningsDeduped(warnings, scanResult.warnings, warningSeen, suppressed);
|
|
425
|
+
return { classes: scanResult.classes, authored: scanResult.authored };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/integrations/font-providers/google/state.ts
|
|
429
|
+
var googleFontInternals = {
|
|
430
|
+
googleFontState: { cache: /* @__PURE__ */ new Map(), fetched: false },
|
|
431
|
+
googleFontListPromise: null,
|
|
432
|
+
lastFetchFailureMs: 0,
|
|
433
|
+
validatedCacheDir: null,
|
|
434
|
+
resolvedCachePath: null
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// src/integrations/font-providers/google/cache.ts
|
|
438
|
+
import { createHash, randomUUID } from "crypto";
|
|
439
|
+
import { open as fsOpen, writeFile, rename, unlink, mkdir } from "fs/promises";
|
|
440
|
+
import { resolve as resolve3, dirname as dirname2, join as join2, isAbsolute as isAbsolute2 } from "path";
|
|
441
|
+
import { isAbsolute as win32IsAbsolute } from "path/win32";
|
|
442
|
+
var MAX_FONT_CACHE_TTL_SECONDS = 30 * 24 * 60 * 60;
|
|
443
|
+
function getFontCacheDir() {
|
|
444
|
+
if (googleFontInternals.validatedCacheDir !== null) return googleFontInternals.validatedCacheDir;
|
|
445
|
+
const raw = process.env.RI_CACHE_DIR || "node_modules/.cache/rainbowindex";
|
|
446
|
+
if (isAbsolute2(raw) || win32IsAbsolute(raw)) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`[RI-1208] RI_CACHE_DIR must be a relative path, got absolute path: "${raw}". Use a relative path like "node_modules/.cache/rainbowindex".`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
if (raw.split(/[\\/]/).some((s) => s === "..")) {
|
|
452
|
+
throw new Error(
|
|
453
|
+
`[RI-1209] RI_CACHE_DIR must not contain ".." segments: "${raw}". Use a direct relative path like "node_modules/.cache/rainbowindex".`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
googleFontInternals.validatedCacheDir = raw;
|
|
457
|
+
return raw;
|
|
458
|
+
}
|
|
459
|
+
function getFontCacheFile() {
|
|
460
|
+
return `${getFontCacheDir()}/google.json`;
|
|
461
|
+
}
|
|
462
|
+
function getResolvedCachePath() {
|
|
463
|
+
if (googleFontInternals.resolvedCachePath !== null && googleFontInternals.validatedCacheDir !== null) {
|
|
464
|
+
return googleFontInternals.resolvedCachePath;
|
|
465
|
+
}
|
|
466
|
+
googleFontInternals.resolvedCachePath = resolve3(process.cwd(), getFontCacheFile());
|
|
467
|
+
return googleFontInternals.resolvedCachePath;
|
|
468
|
+
}
|
|
469
|
+
function getFontCacheTTL() {
|
|
470
|
+
const envVal = process.env.RI_FONT_CACHE_TTL;
|
|
471
|
+
if (envVal) {
|
|
472
|
+
const seconds = Number(envVal);
|
|
473
|
+
if (!Number.isNaN(seconds) && seconds >= 0) {
|
|
474
|
+
if (seconds > MAX_FONT_CACHE_TTL_SECONDS) {
|
|
475
|
+
console.warn(
|
|
476
|
+
`[RI-1211] RI_FONT_CACHE_TTL=${seconds} exceeds maximum of ${MAX_FONT_CACHE_TTL_SECONDS} seconds (30 days). Clamping to 30 days.`
|
|
477
|
+
);
|
|
478
|
+
return MAX_FONT_CACHE_TTL_SECONDS * 1e3;
|
|
479
|
+
}
|
|
480
|
+
return seconds * 1e3;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return 7 * 24 * 60 * 60 * 1e3;
|
|
484
|
+
}
|
|
485
|
+
function parseCachedMetaEntries(raw) {
|
|
486
|
+
const parsed = JSON.parse(raw);
|
|
487
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !("checksum" in parsed) || !("entries" in parsed)) {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
if (!Array.isArray(parsed.entries)) return null;
|
|
491
|
+
const entriesJson = JSON.stringify(parsed.entries);
|
|
492
|
+
const expected = createHash("sha256").update(entriesJson).digest("hex");
|
|
493
|
+
if (typeof parsed.checksum !== "string" || parsed.checksum !== expected) return null;
|
|
494
|
+
const entries = parsed.entries;
|
|
495
|
+
const newCache = /* @__PURE__ */ new Map();
|
|
496
|
+
for (const rawItem of entries) {
|
|
497
|
+
if (!rawItem || typeof rawItem !== "object") continue;
|
|
498
|
+
const item = rawItem;
|
|
499
|
+
if (typeof item.family !== "string" || typeof item.variable !== "boolean" || typeof item.category !== "string" || item.axes !== void 0 && !Array.isArray(item.axes)) {
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (!SAFE_FONT_FAMILY_RE.test(item.family)) continue;
|
|
503
|
+
const axes = Array.isArray(item.axes) ? item.axes.filter((a) => {
|
|
504
|
+
if (!a || typeof a !== "object") return false;
|
|
505
|
+
const axis = a;
|
|
506
|
+
return typeof axis.tag === "string" && typeof axis.start === "number" && Number.isFinite(axis.start) && typeof axis.end === "number" && Number.isFinite(axis.end);
|
|
507
|
+
}).map((a) => {
|
|
508
|
+
const axis = { tag: a.tag, start: a.start, end: a.end };
|
|
509
|
+
Object.freeze(axis);
|
|
510
|
+
return axis;
|
|
511
|
+
}) : void 0;
|
|
512
|
+
if (axes) Object.freeze(axes);
|
|
513
|
+
const entry = {
|
|
514
|
+
family: item.family,
|
|
515
|
+
variable: item.variable,
|
|
516
|
+
axes,
|
|
517
|
+
category: item.category
|
|
518
|
+
};
|
|
519
|
+
Object.freeze(entry);
|
|
520
|
+
newCache.set(entry.family, entry);
|
|
521
|
+
}
|
|
522
|
+
return newCache.size > 0 ? newCache : null;
|
|
523
|
+
}
|
|
524
|
+
async function loadFontCache(ignoreExpiry = false) {
|
|
525
|
+
try {
|
|
526
|
+
const cachePath = getResolvedCachePath();
|
|
527
|
+
const fh = await fsOpen(cachePath, "r");
|
|
528
|
+
let raw;
|
|
529
|
+
try {
|
|
530
|
+
if (!ignoreExpiry) {
|
|
531
|
+
const st = await fh.stat();
|
|
532
|
+
if (Date.now() - st.mtimeMs > getFontCacheTTL()) return false;
|
|
533
|
+
}
|
|
534
|
+
raw = await fh.readFile("utf-8");
|
|
535
|
+
} finally {
|
|
536
|
+
await fh.close();
|
|
537
|
+
}
|
|
538
|
+
const newCache = parseCachedMetaEntries(raw);
|
|
539
|
+
if (!newCache) return false;
|
|
540
|
+
googleFontInternals.googleFontState = { cache: newCache, fetched: true };
|
|
541
|
+
return true;
|
|
542
|
+
} catch {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async function saveFontCache() {
|
|
547
|
+
try {
|
|
548
|
+
const cachePath = getResolvedCachePath();
|
|
549
|
+
await mkdir(dirname2(cachePath), { recursive: true });
|
|
550
|
+
const entries = Array.from(googleFontInternals.googleFontState.cache.values());
|
|
551
|
+
const entriesJson = JSON.stringify(entries);
|
|
552
|
+
const checksum = createHash("sha256").update(entriesJson).digest("hex");
|
|
553
|
+
const payload = `{"checksum":${JSON.stringify(checksum)},"entries":${entriesJson}}`;
|
|
554
|
+
const tmpPath = join2(dirname2(cachePath), `.google-${process.pid}-${randomUUID()}.tmp`);
|
|
555
|
+
try {
|
|
556
|
+
await writeFile(tmpPath, payload);
|
|
557
|
+
await rename(tmpPath, cachePath);
|
|
558
|
+
} finally {
|
|
559
|
+
await unlink(tmpPath).catch(() => {
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
} catch (err) {
|
|
563
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
564
|
+
console.warn(
|
|
565
|
+
`[RI-1210] Failed to write font cache to ${getFontCacheFile()}: ${reason}. Each build will fetch from Google Fonts. Set RI_CACHE_DIR to a writable path or use RI_OFFLINE=1 with a pre-populated cache.`
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/integrations/font-providers/google/client.ts
|
|
571
|
+
var MAX_RESPONSE_SIZE = 10 * 1024 * 1024;
|
|
572
|
+
var MAX_RETRIES = 2;
|
|
573
|
+
var FETCH_TIMEOUT_MS = 5e3;
|
|
574
|
+
function wait(ms) {
|
|
575
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
576
|
+
}
|
|
577
|
+
function toGoogleFontMetaMap(data) {
|
|
578
|
+
const newCache = /* @__PURE__ */ new Map();
|
|
579
|
+
for (const font of data.familyMetadataList) {
|
|
580
|
+
const wghtAxis = font.axes.find((a) => a.tag === "wght");
|
|
581
|
+
const axes = font.axes.map((a) => {
|
|
582
|
+
const axis = { tag: a.tag, start: a.min, end: a.max };
|
|
583
|
+
Object.freeze(axis);
|
|
584
|
+
return axis;
|
|
585
|
+
});
|
|
586
|
+
Object.freeze(axes);
|
|
587
|
+
const entry = {
|
|
588
|
+
family: font.family,
|
|
589
|
+
variable: wghtAxis ? wghtAxis.min !== wghtAxis.max : false,
|
|
590
|
+
axes,
|
|
591
|
+
category: font.category
|
|
592
|
+
};
|
|
593
|
+
Object.freeze(entry);
|
|
594
|
+
newCache.set(font.family, entry);
|
|
595
|
+
}
|
|
596
|
+
return newCache;
|
|
597
|
+
}
|
|
598
|
+
async function readJsonResponse(res) {
|
|
599
|
+
const reader = res.body?.getReader();
|
|
600
|
+
if (!reader) {
|
|
601
|
+
throw new Error("[RI-1207] Google Fonts metadata response has no readable body.");
|
|
602
|
+
}
|
|
603
|
+
const chunks = [];
|
|
604
|
+
let totalBytes = 0;
|
|
605
|
+
for (; ; ) {
|
|
606
|
+
const { done, value } = await reader.read();
|
|
607
|
+
if (done) break;
|
|
608
|
+
totalBytes += value.byteLength;
|
|
609
|
+
if (totalBytes > MAX_RESPONSE_SIZE) {
|
|
610
|
+
reader.cancel();
|
|
611
|
+
throw new Error(
|
|
612
|
+
`[RI-1207] Google Fonts metadata response too large (>${MAX_RESPONSE_SIZE} bytes).`
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
chunks.push(value);
|
|
616
|
+
}
|
|
617
|
+
const text = new TextDecoder().decode(
|
|
618
|
+
chunks.length === 1 ? chunks[0] : await new Blob(chunks).arrayBuffer()
|
|
619
|
+
);
|
|
620
|
+
return JSON.parse(text);
|
|
621
|
+
}
|
|
622
|
+
async function fetchGoogleFontMetadata() {
|
|
623
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
624
|
+
const controller = new AbortController();
|
|
625
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
626
|
+
try {
|
|
627
|
+
const res = await fetch("https://fonts.google.com/metadata/fonts", {
|
|
628
|
+
signal: controller.signal
|
|
629
|
+
});
|
|
630
|
+
const contentLength = res.headers.get("content-length");
|
|
631
|
+
if (contentLength && Number(contentLength) > MAX_RESPONSE_SIZE) {
|
|
632
|
+
throw new Error(
|
|
633
|
+
`[RI-1207] Google Fonts metadata response too large (${contentLength} bytes).`
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
if (!res.ok) {
|
|
637
|
+
throw new Error(`[RI-1205] Google Fonts metadata request failed with HTTP ${res.status}.`);
|
|
638
|
+
}
|
|
639
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
640
|
+
if (contentType && !contentType.includes("json")) {
|
|
641
|
+
throw new Error(
|
|
642
|
+
`[RI-1207] Google Fonts metadata returned unexpected Content-Type "${contentType}" instead of JSON.`
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
const data = await readJsonResponse(res);
|
|
646
|
+
return toGoogleFontMetaMap(data);
|
|
647
|
+
} catch (err) {
|
|
648
|
+
if (attempt < MAX_RETRIES) {
|
|
649
|
+
await wait(1e3 * 2 ** attempt);
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
throw err;
|
|
653
|
+
} finally {
|
|
654
|
+
clearTimeout(timeout);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
throw new Error("[RI-1205] Exhausted Google Fonts metadata fetch retries.");
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// src/integrations/font-providers/google/index.ts
|
|
661
|
+
var FETCH_RETRY_COOLDOWN_MS = 3e4;
|
|
662
|
+
async function fetchGoogleFontList() {
|
|
663
|
+
if (googleFontInternals.googleFontState.fetched) return;
|
|
664
|
+
if (googleFontInternals.googleFontListPromise) return googleFontInternals.googleFontListPromise;
|
|
665
|
+
if (googleFontInternals.lastFetchFailureMs > 0 && Date.now() - googleFontInternals.lastFetchFailureMs < FETCH_RETRY_COOLDOWN_MS) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const localPromise = (async () => {
|
|
669
|
+
try {
|
|
670
|
+
const isOffline = process.env.RI_OFFLINE === "1" || process.env.RI_OFFLINE === "true";
|
|
671
|
+
if (isOffline) {
|
|
672
|
+
if (await loadFontCache(true)) return;
|
|
673
|
+
console.warn(
|
|
674
|
+
`[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.`
|
|
675
|
+
);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (await loadFontCache()) return;
|
|
679
|
+
const fetchDisabled = process.env.RI_FETCH_FONTS === "0" || process.env.RI_FETCH_FONTS === "false";
|
|
680
|
+
if (fetchDisabled) {
|
|
681
|
+
if (await loadFontCache(true)) return;
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (typeof globalThis.fetch !== "function") {
|
|
685
|
+
console.warn(
|
|
686
|
+
"[RI-1212] Global fetch() is not available. Google Fonts metadata requires Node.js >= 18. Skipping font fetch."
|
|
687
|
+
);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (isRIDebug()) {
|
|
691
|
+
console.warn("[RI-DEBUG] Fetching Google Fonts metadata...");
|
|
692
|
+
}
|
|
693
|
+
try {
|
|
694
|
+
const newCache = await fetchGoogleFontMetadata();
|
|
695
|
+
googleFontInternals.googleFontState = { cache: newCache, fetched: true };
|
|
696
|
+
await saveFontCache();
|
|
697
|
+
return;
|
|
698
|
+
} catch (err) {
|
|
699
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
700
|
+
if (message.startsWith("[RI-1207]")) {
|
|
701
|
+
console.warn(`${message} Skipping.`);
|
|
702
|
+
if (await loadFontCache(true)) return;
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (await loadFontCache(true)) return;
|
|
706
|
+
console.warn(
|
|
707
|
+
`[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.`
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
} finally {
|
|
711
|
+
if (!googleFontInternals.googleFontState.fetched) {
|
|
712
|
+
googleFontInternals.lastFetchFailureMs = Date.now();
|
|
713
|
+
}
|
|
714
|
+
googleFontInternals.googleFontListPromise = null;
|
|
715
|
+
}
|
|
716
|
+
})();
|
|
717
|
+
googleFontInternals.googleFontListPromise = localPromise;
|
|
718
|
+
return localPromise;
|
|
719
|
+
}
|
|
720
|
+
var refreshMemo = /* @__PURE__ */ new WeakMap();
|
|
721
|
+
function refreshFontWeightDefaults(fonts) {
|
|
722
|
+
const state = googleFontInternals.googleFontState;
|
|
723
|
+
const memo = refreshMemo.get(fonts);
|
|
724
|
+
if (memo && memo.state === state) return memo.result;
|
|
725
|
+
let anyChanged = false;
|
|
726
|
+
const refreshed = fonts.map((slot) => {
|
|
727
|
+
if (slot.kind !== "google") return slot;
|
|
728
|
+
const meta = googleFontInternals.googleFontState.cache.get(slot.family);
|
|
729
|
+
if (!meta) return slot;
|
|
730
|
+
let changed = false;
|
|
731
|
+
const faces = slot.faces.map((f) => {
|
|
732
|
+
let next = f;
|
|
733
|
+
if (!f._weightExplicit) {
|
|
734
|
+
const wghtAxis = meta.axes?.find((a) => a.tag === "wght");
|
|
735
|
+
const axisWeight = wghtAxis && wghtAxis.start !== wghtAxis.end ? `${wghtAxis.start} ${wghtAxis.end}` : "400";
|
|
736
|
+
if (next.weight !== axisWeight) next = { ...next, weight: axisWeight };
|
|
737
|
+
}
|
|
738
|
+
if (!f._styleExplicit) {
|
|
739
|
+
const italAxis = meta.axes?.find((a) => a.tag === "ital");
|
|
740
|
+
const axisStyle = italAxis ? "normal italic" : "normal";
|
|
741
|
+
if (next.style !== axisStyle) next = { ...next, style: axisStyle };
|
|
742
|
+
}
|
|
743
|
+
if (next !== f) changed = true;
|
|
744
|
+
return next;
|
|
745
|
+
});
|
|
746
|
+
if (changed) anyChanged = true;
|
|
747
|
+
return changed ? { ...slot, faces } : slot;
|
|
748
|
+
});
|
|
749
|
+
const result = anyChanged ? refreshed : fonts;
|
|
750
|
+
refreshMemo.set(fonts, { state, result });
|
|
751
|
+
return result;
|
|
752
|
+
}
|
|
753
|
+
var FONT_FETCH_TIMEOUT_MS = 1e4;
|
|
754
|
+
async function resolveGoogleFonts(fonts) {
|
|
755
|
+
if (!fonts.some((slot) => slot.kind === "google")) return fonts;
|
|
756
|
+
try {
|
|
757
|
+
await withTimeout(
|
|
758
|
+
fetchGoogleFontList(),
|
|
759
|
+
FONT_FETCH_TIMEOUT_MS,
|
|
760
|
+
"[RI-1213] Google Fonts metadata fetch timed out"
|
|
761
|
+
);
|
|
762
|
+
} catch {
|
|
763
|
+
console.warn(
|
|
764
|
+
`[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.`
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
return refreshFontWeightDefaults(fonts);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// src/integrations/font-providers/metrics-data.ts
|
|
771
|
+
var FONT_METRICS_TABLE = {
|
|
772
|
+
abel: [2006, -604, 0, 2048, 783, "sans-serif"],
|
|
773
|
+
alegreya: [1016, -345, 0, 1e3, 410, "serif"],
|
|
774
|
+
"anonymous pro": [1675, -373, 0, 2048, 1118, "monospace"],
|
|
775
|
+
archivo: [878, -210, 0, 1e3, 440, "sans-serif"],
|
|
776
|
+
arial: [1854, -434, 67, 2048, 913, "sans-serif"],
|
|
777
|
+
asap: [934, -212, 0, 1e3, 442, "sans-serif"],
|
|
778
|
+
barlow: [1e3, -200, 0, 1e3, 431, "sans-serif"],
|
|
779
|
+
bitter: [935, -265, 0, 1e3, 465, "serif"],
|
|
780
|
+
"bricolage grotesque": [930, -270, 0, 1e3, 470, "sans-serif"],
|
|
781
|
+
cabin: [1930, -500, 0, 2e3, 844, "sans-serif"],
|
|
383
782
|
catamaran: [1100, -540, 0, 1e3, 411, "sans-serif"],
|
|
384
783
|
chivo: [940, -250, 0, 1e3, 478, "sans-serif"],
|
|
385
784
|
"cormorant garamond": [924, -287, 0, 1e3, 394, "serif"],
|
|
@@ -448,571 +847,235 @@ var FONT_METRICS_TABLE = {
|
|
|
448
847
|
"red hat text": [1018, -305, 0, 1e3, 447, "sans-serif"],
|
|
449
848
|
"roboto condensed": [1900, -500, 0, 2048, 811, "sans-serif"],
|
|
450
849
|
"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 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 };
|
|
850
|
+
"roboto mono": [2146, -555, 0, 2048, 1229, "monospace"],
|
|
851
|
+
"roboto slab": [2146, -555, 0, 2048, 972, "serif"],
|
|
852
|
+
roboto: [1900, -500, 0, 2048, 911, "sans-serif"],
|
|
853
|
+
rubik: [935, -250, 0, 1e3, 468, "sans-serif"],
|
|
854
|
+
"schibsted grotesk": [2e3, -528, 0, 2048, 954, "sans-serif"],
|
|
855
|
+
"segoe ui": [2210, -514, 0, 2048, 908, "sans-serif"],
|
|
856
|
+
sora: [970, -290, 0, 1e3, 507, "sans-serif"],
|
|
857
|
+
"source code pro": [984, -273, 0, 1e3, 600, "monospace"],
|
|
858
|
+
"source sans 3": [1024, -400, 0, 1e3, 418, "sans-serif"],
|
|
859
|
+
"source serif 4": [1036, -335, 0, 1e3, 479, "serif"],
|
|
860
|
+
"space grotesk": [984, -292, 0, 1e3, 489, "sans-serif"],
|
|
861
|
+
"space mono": [1120, -361, 0, 1e3, 612, "monospace"],
|
|
862
|
+
spectral: [1059, -463, 0, 1e3, 446, "serif"],
|
|
863
|
+
tahoma: [2049, -423, 0, 2048, 917, "sans-serif"],
|
|
864
|
+
"times new roman": [1825, -443, 87, 2048, 832, "serif"],
|
|
865
|
+
"titillium web": [1133, -388, 0, 1e3, 421, "sans-serif"],
|
|
866
|
+
"trebuchet ms": [1923, -455, 0, 2048, 934, "sans-serif"],
|
|
867
|
+
"ubuntu mono": [830, -170, 0, 1e3, 500, "monospace"],
|
|
868
|
+
ubuntu: [932, -189, 28, 1e3, 455, "sans-serif"],
|
|
869
|
+
urbanist: [1900, -500, 0, 2e3, 883, "sans-serif"],
|
|
870
|
+
"varela round": [918, -286, 0, 1e3, 478, "sans-serif"],
|
|
871
|
+
verdana: [2059, -430, 0, 2048, 1049, "sans-serif"],
|
|
872
|
+
"victor mono": [1100, -250, 0, 1e3, 600, "monospace"],
|
|
873
|
+
vollkorn: [952, -441, 0, 1e3, 438, "serif"],
|
|
874
|
+
"work sans": [930, -243, 0, 1e3, 499, "sans-serif"],
|
|
875
|
+
"zilla slab": [944, -256, 0, 1e3, 434, "serif"]
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
// src/integrations/font-providers/metrics.ts
|
|
879
|
+
var CATEGORY_FALLBACK = {
|
|
880
|
+
"sans-serif": "Arial",
|
|
881
|
+
serif: "Times New Roman",
|
|
882
|
+
monospace: "Courier New"
|
|
883
|
+
};
|
|
884
|
+
function lookupFontMetrics(family) {
|
|
885
|
+
return FONT_METRICS_TABLE[family.trim().toLowerCase()];
|
|
661
886
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
type: FONT_FORMAT_MIME[inferFontFormat(face.provider)],
|
|
675
|
-
crossorigin: true
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
return links;
|
|
887
|
+
var round4 = (n) => Math.round(n * 1e4) / 1e4;
|
|
888
|
+
function computeFallbackMetrics(fallbackName, font, fallbackFont) {
|
|
889
|
+
const [ascent, descent, lineGap, unitsPerEm, xWidthAvg] = font;
|
|
890
|
+
const [, , , fbUnitsPerEm, fbXWidthAvg] = fallbackFont;
|
|
891
|
+
const sizeAdjust = xWidthAvg / unitsPerEm / (fbXWidthAvg / fbUnitsPerEm);
|
|
892
|
+
return {
|
|
893
|
+
fallback: fallbackName,
|
|
894
|
+
sizeAdjust: round4(sizeAdjust * 100),
|
|
895
|
+
ascent: round4(ascent / unitsPerEm / sizeAdjust * 100),
|
|
896
|
+
descent: round4(Math.abs(descent) / unitsPerEm / sizeAdjust * 100),
|
|
897
|
+
lineGap: round4(lineGap / unitsPerEm / sizeAdjust * 100)
|
|
898
|
+
};
|
|
680
899
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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;
|
|
900
|
+
function resolveAutoMetrics(family, fallbackStack, explicitFallback) {
|
|
901
|
+
const font = lookupFontMetrics(family);
|
|
902
|
+
if (!font) return null;
|
|
903
|
+
const fallbackName = explicitFallback ?? fallbackStack.find((f) => lookupFontMetrics(f)) ?? CATEGORY_FALLBACK[font[5]] ?? "Arial";
|
|
904
|
+
const fallbackFont = lookupFontMetrics(fallbackName);
|
|
905
|
+
if (!fallbackFont) return null;
|
|
906
|
+
return computeFallbackMetrics(fallbackName, font, fallbackFont);
|
|
706
907
|
}
|
|
707
908
|
|
|
708
|
-
// src/
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
mtimeMs = statSync(join2(cwdAbs, "package.json")).mtimeMs;
|
|
718
|
-
} catch {
|
|
719
|
-
return EMPTY;
|
|
909
|
+
// src/integrations/font-providers/index.ts
|
|
910
|
+
var SYSTEM_STACKS = Object.freeze({
|
|
911
|
+
sans: 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
|
|
912
|
+
serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
|
|
913
|
+
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"'
|
|
914
|
+
});
|
|
915
|
+
function getFallbackStack(slot) {
|
|
916
|
+
if (!SYSTEM_STACKS[slot] && isRIDebug()) {
|
|
917
|
+
console.warn(`[RI-DEBUG] Unknown font slot "${slot}" \u2014 falling back to sans stack.`);
|
|
720
918
|
}
|
|
721
|
-
|
|
722
|
-
if (cached && cached.mtimeMs === mtimeMs) return cached.result;
|
|
723
|
-
const result = runDiscovery(cwdAbs);
|
|
724
|
-
discoveryCache.set(cwdAbs, { mtimeMs, result });
|
|
725
|
-
return result;
|
|
919
|
+
return SYSTEM_STACKS[slot] || SYSTEM_STACKS.sans;
|
|
726
920
|
}
|
|
727
|
-
function
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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 });
|
|
921
|
+
function googleFontsUrl(family, face) {
|
|
922
|
+
const encodedFamily = encodeURIComponent(family).replace(/%20/g, "+");
|
|
923
|
+
let axisParam;
|
|
924
|
+
if (face.weight.includes(",")) {
|
|
925
|
+
const weights = face.weight.split(",").map((w) => w.trim());
|
|
926
|
+
if (face.style.includes("italic")) {
|
|
927
|
+
const tuples = weights.flatMap((w) => [`0,${w}`, `1,${w}`]);
|
|
928
|
+
axisParam = `ital,wght@${tuples.join(";")}`;
|
|
929
|
+
} else {
|
|
930
|
+
axisParam = `wght@${weights.join(";")}`;
|
|
786
931
|
}
|
|
932
|
+
} else if (face.weight.includes(" ")) {
|
|
933
|
+
const range = face.weight.replace(" ", "..");
|
|
934
|
+
axisParam = face.style.includes("italic") ? `ital,wght@0,${range};1,${range}` : `wght@${range}`;
|
|
935
|
+
} else {
|
|
936
|
+
const w = face.weight || "400";
|
|
937
|
+
axisParam = face.style.includes("italic") ? `ital,wght@0,${w};1,${w}` : `wght@${w}`;
|
|
787
938
|
}
|
|
788
|
-
|
|
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
|
-
}
|
|
939
|
+
const display = face.display || "swap";
|
|
940
|
+
return `https://fonts.googleapis.com/css2?family=${encodedFamily}:${axisParam}&display=${display}`;
|
|
803
941
|
}
|
|
804
|
-
function
|
|
805
|
-
|
|
806
|
-
return realpathSync(path);
|
|
807
|
-
} catch {
|
|
808
|
-
return path;
|
|
809
|
-
}
|
|
942
|
+
function escapeFontFamily(name) {
|
|
943
|
+
return name.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\0/g, "\\0").replace(/\n/g, "\\a ").replace(/\r/g, "\\d ");
|
|
810
944
|
}
|
|
811
|
-
function
|
|
812
|
-
|
|
945
|
+
function generateFallbackFontFace(family, metrics) {
|
|
946
|
+
const safeFamily = escapeFontFamily(family);
|
|
947
|
+
return `@font-face {
|
|
948
|
+
font-family: "${safeFamily} Fallback";
|
|
949
|
+
src: local("${escapeFontFamily(metrics.fallback)}");
|
|
950
|
+
size-adjust: ${metrics.sizeAdjust}%;
|
|
951
|
+
ascent-override: ${metrics.ascent}%;
|
|
952
|
+
descent-override: ${metrics.descent}%;
|
|
953
|
+
line-gap-override: ${metrics.lineGap}%;
|
|
954
|
+
}`;
|
|
813
955
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
"
|
|
818
|
-
"
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
"
|
|
822
|
-
"
|
|
823
|
-
"
|
|
824
|
-
"
|
|
825
|
-
"
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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);
|
|
956
|
+
var FONT_FORMAT_MIME = {
|
|
957
|
+
woff2: "font/woff2",
|
|
958
|
+
woff: "font/woff",
|
|
959
|
+
truetype: "font/ttf",
|
|
960
|
+
opentype: "font/otf"
|
|
961
|
+
};
|
|
962
|
+
function inferFontFormat(path) {
|
|
963
|
+
if (path.endsWith(".woff2")) return "woff2";
|
|
964
|
+
if (path.endsWith(".woff")) return "woff";
|
|
965
|
+
if (path.endsWith(".ttf")) return "truetype";
|
|
966
|
+
if (path.endsWith(".otf")) return "opentype";
|
|
967
|
+
return "woff2";
|
|
968
|
+
}
|
|
969
|
+
function generateWebFontFace(family, face) {
|
|
970
|
+
if (face.provider === "system" || !face.provider) return null;
|
|
971
|
+
if (face.provider === "google") {
|
|
972
|
+
return { type: "import", css: `@import url("${googleFontsUrl(family, face)}");` };
|
|
840
973
|
}
|
|
841
|
-
const
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
974
|
+
const safeProvider = escapeFontFamily(face.provider);
|
|
975
|
+
const format = inferFontFormat(face.provider);
|
|
976
|
+
const src = `url("${safeProvider}") format("${format}")`;
|
|
977
|
+
const declarations = [` font-family: "${escapeFontFamily(family)}";`, ` src: ${src};`];
|
|
978
|
+
if (face.weight) declarations.push(` font-weight: ${face.weight};`);
|
|
979
|
+
if (face.style && face.style !== "normal") declarations.push(` font-style: ${face.style};`);
|
|
980
|
+
if (face.display) declarations.push(` font-display: ${face.display};`);
|
|
981
|
+
if (face.unicodeRange) declarations.push(` unicode-range: ${face.unicodeRange};`);
|
|
982
|
+
return { type: "font-face", css: `@font-face {
|
|
983
|
+
${declarations.join("\n")}
|
|
984
|
+
}` };
|
|
985
|
+
}
|
|
986
|
+
function normalizeLocalStyle(style, family, warnings) {
|
|
987
|
+
if (style.includes(" ") && !style.startsWith("oblique")) {
|
|
988
|
+
const first = style.split(/\s+/)[0];
|
|
989
|
+
warnings.push(
|
|
990
|
+
`[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}".`
|
|
991
|
+
);
|
|
992
|
+
return first;
|
|
858
993
|
}
|
|
859
|
-
return
|
|
860
|
-
includePatterns,
|
|
861
|
-
nodeModulesIncludePatterns,
|
|
862
|
-
excludePatterns,
|
|
863
|
-
warnings,
|
|
864
|
-
hasUserPositiveGlobs
|
|
865
|
-
};
|
|
994
|
+
return style;
|
|
866
995
|
}
|
|
867
|
-
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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) {
|
|
996
|
+
function resolveSlotMetrics(slot, warnings) {
|
|
997
|
+
const cfg = slot.metrics;
|
|
998
|
+
if (cfg === null) return null;
|
|
999
|
+
if (cfg?.sizeAdjust !== void 0) {
|
|
1000
|
+
return {
|
|
1001
|
+
fallback: cfg.fallback || slot.fallback[0] || "Arial",
|
|
1002
|
+
sizeAdjust: cfg.sizeAdjust,
|
|
1003
|
+
ascent: cfg.ascent,
|
|
1004
|
+
descent: cfg.descent,
|
|
1005
|
+
lineGap: cfg.lineGap
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
const resolved = resolveAutoMetrics(slot.family, slot.fallback, cfg?.fallback);
|
|
1009
|
+
if (!resolved && cfg?.fallback) {
|
|
1010
|
+
const missing = lookupFontMetrics(slot.family) ? cfg.fallback : slot.family;
|
|
907
1011
|
warnings.push(
|
|
908
|
-
`[RI-
|
|
1012
|
+
`[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
1013
|
);
|
|
910
|
-
return { files: [], warnings };
|
|
911
1014
|
}
|
|
1015
|
+
return resolved;
|
|
912
1016
|
}
|
|
913
|
-
function
|
|
914
|
-
const
|
|
1017
|
+
function generateFontCSS(slot) {
|
|
1018
|
+
const imports = [];
|
|
1019
|
+
const fontFaces = [];
|
|
1020
|
+
const variables = [];
|
|
915
1021
|
const warnings = [];
|
|
916
|
-
|
|
917
|
-
if (
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1022
|
+
const pushFeatureVars = () => {
|
|
1023
|
+
if (slot.features) variables.push(`--font-${slot.slot}--features: ${slot.features};`);
|
|
1024
|
+
if (slot.variation) variables.push(`--font-${slot.slot}--variations: ${slot.variation};`);
|
|
1025
|
+
};
|
|
1026
|
+
if (slot.kind === "system") {
|
|
1027
|
+
variables.push(`--font-${slot.slot}: ${getFallbackStack(slot.slot)};`);
|
|
1028
|
+
return { imports, fontFaces, variables, warnings };
|
|
1029
|
+
}
|
|
1030
|
+
if (slot.kind === "manual") {
|
|
1031
|
+
const fallback = slot.fallback.length > 0 ? slot.fallback.join(", ") : getFallbackStack(slot.slot);
|
|
1032
|
+
const stack = [`"${escapeFontFamily(slot.family)}"`, fallback].join(", ");
|
|
1033
|
+
variables.push(`--font-${slot.slot}: ${stack};`);
|
|
1034
|
+
pushFeatureVars();
|
|
1035
|
+
return { imports, fontFaces, variables, warnings };
|
|
1036
|
+
}
|
|
1037
|
+
const metrics = resolveSlotMetrics(slot, warnings);
|
|
1038
|
+
if (metrics) fontFaces.push(generateFallbackFontFace(slot.family, metrics));
|
|
1039
|
+
for (const face of slot.faces) {
|
|
1040
|
+
if (face.provider !== "google" && !face.provider.startsWith("/") && !face.provider.startsWith("http") && !face.provider.startsWith(".")) {
|
|
922
1041
|
warnings.push(
|
|
923
|
-
`[RI-
|
|
1042
|
+
`[RI-1201] Unknown font provider "${face.provider}" for "${slot.family}" \u2014 supported: google, or a file path/URL.`
|
|
924
1043
|
);
|
|
925
1044
|
continue;
|
|
926
1045
|
}
|
|
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
|
-
};
|
|
1046
|
+
const emitFace = face.provider === "google" ? face : { ...face, style: normalizeLocalStyle(face.style, slot.family, warnings) };
|
|
1047
|
+
const webFont = generateWebFontFace(slot.family, emitFace);
|
|
1048
|
+
if (!webFont) continue;
|
|
1049
|
+
if (webFont.type === "import") imports.push(webFont.css);
|
|
1050
|
+
else fontFaces.push(webFont.css);
|
|
972
1051
|
}
|
|
1052
|
+
const fallbackStack = slot.fallback.length > 0 ? slot.fallback.join(", ") : getFallbackStack(slot.slot);
|
|
1053
|
+
const safeFamily = escapeFontFamily(slot.family);
|
|
1054
|
+
const stackParts = [`"${safeFamily}"`];
|
|
1055
|
+
if (metrics) stackParts.push(`"${safeFamily} Fallback"`);
|
|
1056
|
+
stackParts.push(fallbackStack);
|
|
1057
|
+
variables.push(`--font-${slot.slot}: ${stackParts.join(", ")};`);
|
|
1058
|
+
pushFeatureVars();
|
|
1059
|
+
return { imports, fontFaces, variables, warnings };
|
|
973
1060
|
}
|
|
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
|
-
}
|
|
1061
|
+
function getFontPreloadLinks(slots) {
|
|
1062
|
+
const links = [];
|
|
1063
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1064
|
+
for (const slot of slots) {
|
|
1065
|
+
for (const face of slot.faces) {
|
|
1066
|
+
if (!face.preload) continue;
|
|
1067
|
+
if (face.provider === "system" || face.provider === "google" || !face.provider) continue;
|
|
1068
|
+
if (seen.has(face.provider)) continue;
|
|
1069
|
+
seen.add(face.provider);
|
|
1070
|
+
links.push({
|
|
1071
|
+
href: face.provider,
|
|
1072
|
+
as: "font",
|
|
1073
|
+
type: FONT_FORMAT_MIME[inferFontFormat(face.provider)],
|
|
1074
|
+
crossorigin: true
|
|
1075
|
+
});
|
|
1004
1076
|
}
|
|
1005
|
-
pushWarningsDeduped(allWarnings, result.warnings, seen);
|
|
1006
1077
|
}
|
|
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;
|
|
1078
|
+
return links;
|
|
1016
1079
|
}
|
|
1017
1080
|
|
|
1018
1081
|
// src/css/strip.ts
|
|
@@ -1431,15 +1494,15 @@ ${m.css}`).join("\n\n");
|
|
|
1431
1494
|
function generateTokenLayer(theme, usage, fontOutputCache) {
|
|
1432
1495
|
const vars = [];
|
|
1433
1496
|
vars.push(`--spacing: ${theme.spacing.base};`);
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1497
|
+
const pushBounds = (prefix, config) => {
|
|
1498
|
+
if (config?.min !== void 0) vars.push(`${prefix}-min: ${config.min};`);
|
|
1499
|
+
if (config?.max !== void 0) vars.push(`${prefix}-max: ${config.max};`);
|
|
1500
|
+
};
|
|
1501
|
+
pushBounds("--fluid", theme.fluid);
|
|
1502
|
+
pushBounds("--fluid-text", theme.textFluid);
|
|
1503
|
+
pushBounds("--fluid-spacing", theme.spacingFluid);
|
|
1504
|
+
for (const [name, range] of Object.entries(theme.fluidRanges)) {
|
|
1505
|
+
pushBounds(`--fluid-${name}`, range);
|
|
1443
1506
|
}
|
|
1444
1507
|
const effectiveStops = new Map(
|
|
1445
1508
|
[...usage.usedColorStops].map(([k, v]) => [k, new Set(v)])
|
|
@@ -1505,15 +1568,6 @@ function generateTokenLayer(theme, usage, fontOutputCache) {
|
|
|
1505
1568
|
vars.push(`--font-${slot}: ${stack};`);
|
|
1506
1569
|
}
|
|
1507
1570
|
}
|
|
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
1571
|
const shadowsToEmit = resolveTransitiveShadowDeps(theme.shadows, usage.usedShadows);
|
|
1518
1572
|
for (const [name, val] of Object.entries(theme.shadows).sort(
|
|
1519
1573
|
([a], [b]) => codepointCompare(a, b)
|
|
@@ -1521,6 +1575,11 @@ function generateTokenLayer(theme, usage, fontOutputCache) {
|
|
|
1521
1575
|
if (!shadowsToEmit.has(name)) continue;
|
|
1522
1576
|
vars.push(`--shadow-${name}: ${val};`);
|
|
1523
1577
|
}
|
|
1578
|
+
for (const [name, val] of Object.entries(theme.radii).sort(
|
|
1579
|
+
([a], [b]) => codepointCompare(a, b)
|
|
1580
|
+
)) {
|
|
1581
|
+
vars.push(`--rounded-${name}: ${val};`);
|
|
1582
|
+
}
|
|
1524
1583
|
for (const [name, def] of Object.entries(theme.animations).sort(
|
|
1525
1584
|
([a], [b]) => codepointCompare(a, b)
|
|
1526
1585
|
)) {
|
|
@@ -1720,10 +1779,10 @@ function applyLayerWrapping(parts, layer) {
|
|
|
1720
1779
|
}
|
|
1721
1780
|
|
|
1722
1781
|
// src/project/pipeline.ts
|
|
1723
|
-
function collectApplyClassNames(css, warnings) {
|
|
1782
|
+
function collectApplyClassNames(css, warnings, cssPath) {
|
|
1724
1783
|
const classes = [];
|
|
1725
1784
|
for (const match of css.matchAll(APPLY_LIKE_MATCH_RE)) {
|
|
1726
|
-
const params = expandVariantGroups(match[1], warnings);
|
|
1785
|
+
const params = expandVariantGroups(match[1], warnings, cssPath);
|
|
1727
1786
|
for (const className of params.trim().split(/\s+/)) {
|
|
1728
1787
|
if (className) classes.push(className);
|
|
1729
1788
|
}
|
|
@@ -1748,13 +1807,20 @@ async function finalizeProjectCompilation(options) {
|
|
|
1748
1807
|
}
|
|
1749
1808
|
const expansionWarnings = [];
|
|
1750
1809
|
const classNameSet = new Set(options.classNames);
|
|
1751
|
-
|
|
1810
|
+
const authored = options.authoredClassNames && new Set(options.authoredClassNames);
|
|
1811
|
+
for (const cls of collectApplyClassNames(options.css, expansionWarnings, options.cssPath)) {
|
|
1752
1812
|
classNameSet.add(cls);
|
|
1813
|
+
authored?.add(cls);
|
|
1753
1814
|
}
|
|
1754
1815
|
const classNames = [...classNameSet];
|
|
1755
|
-
pushWarningsDeduped(
|
|
1816
|
+
pushWarningsDeduped(
|
|
1817
|
+
analysis.warnings,
|
|
1818
|
+
expansionWarnings,
|
|
1819
|
+
analysis.warningSeen,
|
|
1820
|
+
analysis.suppressed
|
|
1821
|
+
);
|
|
1756
1822
|
const compiler = createCompiler();
|
|
1757
|
-
const compilation = compiler.compile(classNames, effectiveTheme);
|
|
1823
|
+
const compilation = compiler.compile(classNames, effectiveTheme, authored);
|
|
1758
1824
|
let userCSS = stripRIDirectives(options.css);
|
|
1759
1825
|
if ((options.processCssFunctions ?? true) && userCSS && hasCSSFunctions(userCSS)) {
|
|
1760
1826
|
userCSS = compileCSSFunctions(userCSS, effectiveTheme, analysis.warnings);
|
|
@@ -1767,8 +1833,18 @@ async function finalizeProjectCompilation(options) {
|
|
|
1767
1833
|
effectiveTheme,
|
|
1768
1834
|
compiler.fontOutputCache
|
|
1769
1835
|
);
|
|
1770
|
-
pushWarningsDeduped(
|
|
1771
|
-
|
|
1836
|
+
pushWarningsDeduped(
|
|
1837
|
+
analysis.warnings,
|
|
1838
|
+
assemblyWarnings,
|
|
1839
|
+
analysis.warningSeen,
|
|
1840
|
+
analysis.suppressed
|
|
1841
|
+
);
|
|
1842
|
+
pushWarningsDeduped(
|
|
1843
|
+
analysis.warnings,
|
|
1844
|
+
compilation.warnings,
|
|
1845
|
+
analysis.warningSeen,
|
|
1846
|
+
analysis.suppressed
|
|
1847
|
+
);
|
|
1772
1848
|
let joinedCSS = null;
|
|
1773
1849
|
return {
|
|
1774
1850
|
get css() {
|
|
@@ -1782,24 +1858,12 @@ async function finalizeProjectCompilation(options) {
|
|
|
1782
1858
|
classNames,
|
|
1783
1859
|
theme: effectiveTheme,
|
|
1784
1860
|
directives: analysis.directives,
|
|
1785
|
-
warnings: analysis.warnings
|
|
1861
|
+
warnings: analysis.warnings,
|
|
1862
|
+
suppressed: analysis.suppressed
|
|
1786
1863
|
};
|
|
1787
1864
|
}
|
|
1788
1865
|
|
|
1789
1866
|
// src/project/scan.ts
|
|
1790
|
-
var lastAnalysis = null;
|
|
1791
|
-
function analyzeProjectCSSMemo(css) {
|
|
1792
|
-
if (lastAnalysis === null || lastAnalysis.css !== css) {
|
|
1793
|
-
lastAnalysis = { css, analysis: analyzeProjectCSS(css) };
|
|
1794
|
-
}
|
|
1795
|
-
const cached = lastAnalysis.analysis;
|
|
1796
|
-
return {
|
|
1797
|
-
...cached,
|
|
1798
|
-
warnings: [...cached.warnings],
|
|
1799
|
-
warningSeen: new Set(cached.warningSeen),
|
|
1800
|
-
diagnostics: [...cached.diagnostics]
|
|
1801
|
-
};
|
|
1802
|
-
}
|
|
1803
1867
|
async function compileScannedProject(options) {
|
|
1804
1868
|
const analysis = analyzeProjectCSSMemo(options.css);
|
|
1805
1869
|
const resolveFonts = options.resolveFonts ?? resolveGoogleFonts;
|
|
@@ -1812,22 +1876,30 @@ async function compileScannedProject(options) {
|
|
|
1812
1876
|
if (error) {
|
|
1813
1877
|
const warning = options.onInvalidPattern(error);
|
|
1814
1878
|
if (warning !== void 0) {
|
|
1815
|
-
pushWarningsDeduped(
|
|
1879
|
+
pushWarningsDeduped(
|
|
1880
|
+
analysis.warnings,
|
|
1881
|
+
[warning],
|
|
1882
|
+
analysis.warningSeen,
|
|
1883
|
+
analysis.suppressed
|
|
1884
|
+
);
|
|
1816
1885
|
}
|
|
1817
1886
|
continue;
|
|
1818
1887
|
}
|
|
1819
1888
|
surfaceSources.push({ pattern, negated: false, inline: false });
|
|
1820
1889
|
}
|
|
1821
|
-
const classNames = await collectProjectClasses(
|
|
1890
|
+
const { classes: classNames, authored } = await collectProjectClasses(
|
|
1822
1891
|
analysis.theme.sources,
|
|
1823
1892
|
surfaceSources,
|
|
1824
1893
|
options.cwd,
|
|
1825
1894
|
analysis.warnings,
|
|
1826
|
-
analysis.warningSeen
|
|
1895
|
+
analysis.warningSeen,
|
|
1896
|
+
analysis.suppressed
|
|
1827
1897
|
);
|
|
1828
1898
|
const compiled = await finalizeProjectCompilation({
|
|
1829
1899
|
css: options.css,
|
|
1900
|
+
cssPath: options.cssPath,
|
|
1830
1901
|
classNames,
|
|
1902
|
+
authoredClassNames: authored,
|
|
1831
1903
|
analysis,
|
|
1832
1904
|
resolveFonts: () => fontsReady
|
|
1833
1905
|
});
|
|
@@ -1839,6 +1911,11 @@ export {
|
|
|
1839
1911
|
getFontPreloadLinks,
|
|
1840
1912
|
DEFAULT_PATTERNS,
|
|
1841
1913
|
DEFAULT_EXCLUDES,
|
|
1914
|
+
enableSourceFileListCache,
|
|
1915
|
+
invalidateSourceFileListCache,
|
|
1916
|
+
enableScanChangeTracking,
|
|
1917
|
+
markSourceFileChanged,
|
|
1918
|
+
disableScanChangeTracking,
|
|
1842
1919
|
finalizeProjectCompilation,
|
|
1843
1920
|
compileScannedProject
|
|
1844
1921
|
};
|