@hyperframes/core 0.8.183 → 0.8.184
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/dist/compiler/compositionScoping.d.ts +7 -2
- package/dist/compiler/compositionScoping.js +68 -4
- package/dist/compiler/htmlBundler.d.ts +1 -0
- package/dist/compiler/htmlBundler.js +119 -45
- package/dist/compiler/inlineSubCompositions.js +34 -5
- package/dist/fonts/deterministicFonts.d.ts +1 -1
- package/dist/fonts/deterministicFonts.js +53 -5
- package/dist/generated/runtime-inline.js +1 -1
- package/dist/hyperframe.runtime.iife.js +59 -59
- package/dist/runtime/types.d.ts +1 -0
- package/package.json +4 -4
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
type ScopeOptions = {
|
|
2
2
|
compoundAuthoredRoot?: boolean;
|
|
3
3
|
scopeRootSelectors?: boolean;
|
|
4
|
-
}
|
|
4
|
+
};
|
|
5
|
+
export declare function scopeCssToComposition(css: string, compositionId: string, scopeSelectorOverride?: string, authoredRootId?: string | null, options?: ScopeOptions): string;
|
|
6
|
+
/** `scopeCssToComposition` for sheets mounted many times: each sheet is scoped once around a
|
|
7
|
+
* placeholder scope, which each mount's own scope then replaces. */
|
|
8
|
+
export declare function createCompositionCssScoper(): typeof scopeCssToComposition;
|
|
5
9
|
/** Drops repeats of an identical `@font-face` across the given style texts, keeping the last copy:
|
|
6
10
|
* the last matching rule is the one the browser uses, so a rule in between never gains precedence. */
|
|
7
11
|
export declare const dedupeFontFaceRules: (styleTexts: string[]) => string[];
|
|
@@ -30,3 +34,4 @@ export declare function wrapInlineScriptWithErrorBoundary(source: string, errorL
|
|
|
30
34
|
* table goes through `jsonScriptLiteral` — see there for why.
|
|
31
35
|
*/
|
|
32
36
|
export declare function buildVariablesByCompScript(variablesByComp: Record<string, Record<string, unknown>>): string | null;
|
|
37
|
+
export {};
|
|
@@ -132,22 +132,86 @@ export function scopeCssToComposition(css, compositionId, scopeSelectorOverride,
|
|
|
132
132
|
return css;
|
|
133
133
|
const scope = scopeSelectorOverride ||
|
|
134
134
|
`[data-composition-id="${escapeCssAttributeValue(trimmedCompositionId)}"]`;
|
|
135
|
-
|
|
135
|
+
const root = parseCss(css);
|
|
136
|
+
return root ? scopeParsedCss(root, scope, trimmedCompositionId, authoredRootId, options) : "";
|
|
137
|
+
}
|
|
138
|
+
function parseCss(css) {
|
|
136
139
|
try {
|
|
137
|
-
|
|
140
|
+
return postcss.parse(css);
|
|
138
141
|
}
|
|
139
142
|
catch {
|
|
140
|
-
return
|
|
143
|
+
return null;
|
|
141
144
|
}
|
|
145
|
+
}
|
|
146
|
+
function forEachScopedRule(root, visit) {
|
|
142
147
|
root.walkRules((rule) => {
|
|
143
148
|
if (isInsideGlobalAtRule(rule))
|
|
144
149
|
return;
|
|
145
150
|
if (isNestedInsideAnotherRule(rule))
|
|
146
151
|
return;
|
|
147
|
-
rule
|
|
152
|
+
visit(rule);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function scopeParsedCss(root, scope, compositionId, authoredRootId, options) {
|
|
156
|
+
forEachScopedRule(root, (rule) => {
|
|
157
|
+
rule.selectors = rule.selectors.map((selector) => scopeSelector(selector, scope, compositionId, authoredRootId, options?.compoundAuthoredRoot, options?.scopeRootSelectors));
|
|
148
158
|
});
|
|
149
159
|
return root.toResult({ map: false }).css;
|
|
150
160
|
}
|
|
161
|
+
// Shaped like every scope it stands in for, so each selector test treats the two alike; its only `[` is
|
|
162
|
+
// its first character, so no occurrence of it overlaps another or the text around it.
|
|
163
|
+
const PLACEHOLDER_ID = "hf-scope-placeholder";
|
|
164
|
+
const PLACEHOLDER_SCOPE = `[data-composition-id="${PLACEHOLDER_ID}"]`;
|
|
165
|
+
// No quote, bracket, backslash, space or `$` (a String.replace pattern): such a scope lands verbatim.
|
|
166
|
+
const PLAIN_ID = /^[\w.:-]+$/;
|
|
167
|
+
const PLAIN_SCOPE = /^\[data-composition-id="([\w.:-]+)"\]$/;
|
|
168
|
+
/** `scopeCssToComposition` for sheets mounted many times: each sheet is scoped once around a
|
|
169
|
+
* placeholder scope, which each mount's own scope then replaces. */
|
|
170
|
+
export function createCompositionCssScoper() {
|
|
171
|
+
const templates = new Map();
|
|
172
|
+
return (css, compositionId, scopeSelectorOverride, authoredRootId, options) => {
|
|
173
|
+
const trimmedId = compositionId.trim();
|
|
174
|
+
const scope = scopeSelectorOverride ?? "";
|
|
175
|
+
const scopeId = PLAIN_SCOPE.exec(scope)?.[1];
|
|
176
|
+
if (!scopeId ||
|
|
177
|
+
scopeId === trimmedId ||
|
|
178
|
+
!PLAIN_ID.test(trimmedId) ||
|
|
179
|
+
trimmedId === PLACEHOLDER_ID)
|
|
180
|
+
return scopeCssToComposition(css, compositionId, scopeSelectorOverride, authoredRootId, options);
|
|
181
|
+
const key = JSON.stringify([
|
|
182
|
+
trimmedId,
|
|
183
|
+
authoredRootId ?? null,
|
|
184
|
+
Boolean(options?.compoundAuthoredRoot),
|
|
185
|
+
Boolean(options?.scopeRootSelectors),
|
|
186
|
+
css,
|
|
187
|
+
]);
|
|
188
|
+
let template = templates.get(key);
|
|
189
|
+
if (template === undefined) {
|
|
190
|
+
template = scopeTemplate(css, trimmedId, authoredRootId, options);
|
|
191
|
+
templates.set(key, template);
|
|
192
|
+
}
|
|
193
|
+
return template === null
|
|
194
|
+
? scopeCssToComposition(css, compositionId, scopeSelectorOverride, authoredRootId, options)
|
|
195
|
+
: template.split(PLACEHOLDER_SCOPE).join(scope);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/** Null when the placeholder is already in the sheet, or a selector keeps its raw text (it holds a comment). */
|
|
199
|
+
function scopeTemplate(css, compositionId, authoredRootId, options) {
|
|
200
|
+
if (css.includes(PLACEHOLDER_SCOPE))
|
|
201
|
+
return null;
|
|
202
|
+
if (!css)
|
|
203
|
+
return css;
|
|
204
|
+
const root = parseCss(css);
|
|
205
|
+
if (!root)
|
|
206
|
+
return "";
|
|
207
|
+
let rawSelector = false;
|
|
208
|
+
forEachScopedRule(root, (rule) => {
|
|
209
|
+
rawSelector ||= Boolean(rule.raws.selector);
|
|
210
|
+
});
|
|
211
|
+
return rawSelector
|
|
212
|
+
? null
|
|
213
|
+
: scopeParsedCss(root, PLACEHOLDER_SCOPE, compositionId, authoredRootId, options);
|
|
214
|
+
}
|
|
151
215
|
function isFontFaceAtRule(node) {
|
|
152
216
|
return node.type === "atrule" && node.name.toLowerCase() === "font-face";
|
|
153
217
|
}
|
|
@@ -55,6 +55,7 @@ export interface BundleOptions {
|
|
|
55
55
|
sceneParts?: boolean;
|
|
56
56
|
/** Warn when the compiled HTML breaks the HyperFrames contract (default true). */
|
|
57
57
|
staticGuard?: boolean;
|
|
58
|
+
/** Called once per build for each project file the build reads or looks for. */
|
|
58
59
|
onRead?: (filePath: string) => void;
|
|
59
60
|
}
|
|
60
61
|
export declare function bundleToSingleHtml(projectDir: string, options?: BundleOptions): Promise<string>;
|
|
@@ -312,33 +312,49 @@ function warnAssetTooLargeToInline(assetPath, byteLength) {
|
|
|
312
312
|
const mb = (byteLength / (1024 * 1024)).toFixed(1);
|
|
313
313
|
console.warn(`[HyperFrames] Not inlining "${assetPath}" (${mb} MB exceeds the ${MAX_INLINE_ASSET_BYTES / (1024 * 1024)} MB inline limit). The bundle may not be self-contained.`);
|
|
314
314
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
315
|
+
/** Inlines relative asset URLs as data: URLs, reading each project file once per build. */
|
|
316
|
+
function createAssetInliner(resolveProjectPath, inlineAssets) {
|
|
317
|
+
const assets = new Map();
|
|
318
|
+
const readAsset = (filePath, mimeType) => {
|
|
319
|
+
// Size-check before reading: an oversized asset must not be pulled into memory
|
|
320
|
+
// just to be discarded.
|
|
321
|
+
const byteLength = safeStatSize(filePath);
|
|
322
|
+
if (byteLength !== null && byteLength > MAX_INLINE_ASSET_BYTES)
|
|
323
|
+
return { tooLarge: byteLength };
|
|
324
|
+
const content = safeReadFileBuffer(filePath);
|
|
325
|
+
return {
|
|
326
|
+
dataUrl: content === null ? null : `data:${mimeType};base64,${content.toString("base64")}`,
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
return (urlValue) => {
|
|
330
|
+
if (!inlineAssets)
|
|
331
|
+
return null;
|
|
332
|
+
if (!urlValue || !isRelativeUrl(urlValue))
|
|
333
|
+
return null;
|
|
334
|
+
const { basePath, suffix } = splitUrlSuffix(urlValue.trim());
|
|
335
|
+
if (!basePath)
|
|
336
|
+
return null;
|
|
337
|
+
const filePath = resolveProjectPath(decodeWellFormedEscapes(basePath));
|
|
338
|
+
if (!filePath)
|
|
339
|
+
return null;
|
|
340
|
+
const ext = filePath.toLowerCase().match(/\.[^.]+$/)?.[0] ?? "";
|
|
341
|
+
const mimeType = INLINE_MIME[ext];
|
|
342
|
+
if (!mimeType)
|
|
343
|
+
return null;
|
|
344
|
+
let asset = assets.get(filePath);
|
|
345
|
+
if (asset === undefined) {
|
|
346
|
+
asset = readAsset(filePath, mimeType);
|
|
347
|
+
assets.set(filePath, asset);
|
|
348
|
+
}
|
|
349
|
+
else if ("dataUrl" in asset) {
|
|
350
|
+
noteRead(filePath);
|
|
351
|
+
}
|
|
352
|
+
if ("tooLarge" in asset) {
|
|
353
|
+
warnAssetTooLargeToInline(basePath, asset.tooLarge);
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
return asset.dataUrl === null ? null : appendSuffixToUrl(asset.dataUrl, suffix);
|
|
357
|
+
};
|
|
342
358
|
}
|
|
343
359
|
function isExternalSvgFragmentUse(el, attr, urlValue) {
|
|
344
360
|
if (el.tagName.toLowerCase() !== "use")
|
|
@@ -360,7 +376,7 @@ function warnColorGradingLutNotInlined(lutSrc) {
|
|
|
360
376
|
console.warn(`[HyperFrames] Could not inline color grading LUT "${trimmed}". The rendered bundle may not be self-contained.`);
|
|
361
377
|
}
|
|
362
378
|
// fallow-ignore-next-line complexity
|
|
363
|
-
function rewriteColorGradingLutWithInlinedAssets(value,
|
|
379
|
+
function rewriteColorGradingLutWithInlinedAssets(value, inlineAsset) {
|
|
364
380
|
if (!value.trim().startsWith("{"))
|
|
365
381
|
return value;
|
|
366
382
|
let parsed;
|
|
@@ -376,7 +392,7 @@ function rewriteColorGradingLutWithInlinedAssets(value, projectDir) {
|
|
|
376
392
|
if (typeof lut === "string") {
|
|
377
393
|
// Gated by inlineAssets and inlineColorGradingLuts at the call site above;
|
|
378
394
|
// this call only runs once both have already passed.
|
|
379
|
-
const inlined =
|
|
395
|
+
const inlined = inlineAsset(lut);
|
|
380
396
|
if (!inlined) {
|
|
381
397
|
warnColorGradingLutNotInlined(lut);
|
|
382
398
|
return value;
|
|
@@ -389,7 +405,7 @@ function rewriteColorGradingLutWithInlinedAssets(value, projectDir) {
|
|
|
389
405
|
const lutSrc = Reflect.get(lut, "src");
|
|
390
406
|
if (typeof lutSrc !== "string")
|
|
391
407
|
return value;
|
|
392
|
-
const inlined =
|
|
408
|
+
const inlined = inlineAsset(lutSrc);
|
|
393
409
|
if (!inlined) {
|
|
394
410
|
warnColorGradingLutNotInlined(lutSrc);
|
|
395
411
|
return value;
|
|
@@ -397,7 +413,7 @@ function rewriteColorGradingLutWithInlinedAssets(value, projectDir) {
|
|
|
397
413
|
Reflect.set(lut, "src", inlined);
|
|
398
414
|
return JSON.stringify(parsed);
|
|
399
415
|
}
|
|
400
|
-
function rewriteSrcsetWithInlinedAssets(srcsetValue,
|
|
416
|
+
function rewriteSrcsetWithInlinedAssets(srcsetValue, inlineAsset) {
|
|
401
417
|
if (!srcsetValue)
|
|
402
418
|
return srcsetValue;
|
|
403
419
|
return srcsetValue
|
|
@@ -409,18 +425,18 @@ function rewriteSrcsetWithInlinedAssets(srcsetValue, projectDir, inlineAssets) {
|
|
|
409
425
|
const parts = candidate.split(/\s+/);
|
|
410
426
|
if (parts.length === 0)
|
|
411
427
|
return candidate;
|
|
412
|
-
const maybeInlined =
|
|
428
|
+
const maybeInlined = inlineAsset(parts[0] ?? "");
|
|
413
429
|
if (maybeInlined)
|
|
414
430
|
parts[0] = maybeInlined;
|
|
415
431
|
return parts.join(" ");
|
|
416
432
|
})
|
|
417
433
|
.join(", ");
|
|
418
434
|
}
|
|
419
|
-
function rewriteCssUrlsWithInlinedAssets(cssText,
|
|
435
|
+
function rewriteCssUrlsWithInlinedAssets(cssText, inlineAsset) {
|
|
420
436
|
if (!cssText)
|
|
421
437
|
return cssText;
|
|
422
438
|
return cssText.replace(/\burl\(\s*(["']?)([^)"']+)\1\s*\)/g, (_full, quote, rawUrl) => {
|
|
423
|
-
const maybeInlined =
|
|
439
|
+
const maybeInlined = inlineAsset((rawUrl || "").trim());
|
|
424
440
|
if (!maybeInlined)
|
|
425
441
|
return _full;
|
|
426
442
|
return `url(${quote || ""}${maybeInlined}${quote || ""})`;
|
|
@@ -830,7 +846,17 @@ function hoistCompositionScripts(container, opts) {
|
|
|
830
846
|
}
|
|
831
847
|
export function bundleToSingleHtml(projectDir, options) {
|
|
832
848
|
const bundle = () => bundleProject(projectDir, options);
|
|
833
|
-
|
|
849
|
+
const onRead = options?.onRead;
|
|
850
|
+
if (!onRead)
|
|
851
|
+
return bundle();
|
|
852
|
+
const noted = new Set();
|
|
853
|
+
const noteOnce = (filePath) => {
|
|
854
|
+
if (noted.has(filePath))
|
|
855
|
+
return;
|
|
856
|
+
noted.add(filePath);
|
|
857
|
+
onRead(filePath);
|
|
858
|
+
};
|
|
859
|
+
return bundleReads.run(noteOnce, bundle);
|
|
834
860
|
}
|
|
835
861
|
// A <template> scene has no root or GSAP of its own: bundle it inside the index that mounts it, as render does.
|
|
836
862
|
function readMountedSceneShell(projectDir, entryFile, entryHtml) {
|
|
@@ -851,9 +877,29 @@ async function bundleProject(projectDir, options) {
|
|
|
851
877
|
const entryHtml = readFileSync(indexPath, "utf-8");
|
|
852
878
|
const mountedShell = readMountedSceneShell(projectDir, entryFile, entryHtml);
|
|
853
879
|
const sourceDir = mountedShell ? projectDir : dirname(indexPath);
|
|
880
|
+
// Every mount of a sub-composition asks again for each of its url()s; isSafePath realpaths the whole chain.
|
|
881
|
+
const safePaths = new Map();
|
|
882
|
+
const isSafeProjectPath = (resolved) => {
|
|
883
|
+
let safe = safePaths.get(resolved);
|
|
884
|
+
if (safe === undefined) {
|
|
885
|
+
safe = isSafePath(projectDir, resolved);
|
|
886
|
+
safePaths.set(resolved, safe);
|
|
887
|
+
}
|
|
888
|
+
return safe;
|
|
889
|
+
};
|
|
890
|
+
const entryPaths = new Map();
|
|
854
891
|
const resolveEntryPath = (relativePath) => {
|
|
855
|
-
|
|
856
|
-
|
|
892
|
+
let entry = entryPaths.get(relativePath);
|
|
893
|
+
if (entry === undefined) {
|
|
894
|
+
const resolved = resolve(sourceDir, relativePath);
|
|
895
|
+
entry = isSafeProjectPath(resolved) ? resolved : null;
|
|
896
|
+
entryPaths.set(relativePath, entry);
|
|
897
|
+
}
|
|
898
|
+
return entry;
|
|
899
|
+
};
|
|
900
|
+
const resolveProjectPath = (relativePath) => {
|
|
901
|
+
const resolved = resolve(projectDir, relativePath);
|
|
902
|
+
return isSafeProjectPath(resolved) ? resolved : null;
|
|
857
903
|
};
|
|
858
904
|
const resolveEntryUrl = (url) => resolveEntryPath(decodedUrlPath(url));
|
|
859
905
|
const readSource = options?.stampHfIds ? ensureHfIds : (html) => html;
|
|
@@ -908,13 +954,24 @@ async function bundleProject(projectDir, options) {
|
|
|
908
954
|
const trackedCompositionHosts = getBundledTrackedCompositionHosts(document);
|
|
909
955
|
const hostIdentityByElement = assignBundledRuntimeCompositionIds(trackedCompositionHosts);
|
|
910
956
|
const subCompositionHosts = trackedCompositionHosts.filter((host) => host.hasAttribute("data-composition-src"));
|
|
957
|
+
const sceneSources = new Map();
|
|
911
958
|
const subCompResult = inlineSubCompositions(document, subCompositionHosts, {
|
|
912
959
|
resolveHtml: (srcPath) => {
|
|
913
960
|
if (!isRelativeUrl(srcPath))
|
|
914
961
|
return null;
|
|
915
962
|
const compPath = resolveEntryPath(srcPath);
|
|
916
|
-
|
|
917
|
-
|
|
963
|
+
if (!compPath)
|
|
964
|
+
return null;
|
|
965
|
+
let source = sceneSources.get(compPath);
|
|
966
|
+
if (source === undefined) {
|
|
967
|
+
const html = safeReadFile(compPath);
|
|
968
|
+
source = html === null ? null : readSource(html);
|
|
969
|
+
sceneSources.set(compPath, source);
|
|
970
|
+
}
|
|
971
|
+
else {
|
|
972
|
+
noteRead(compPath);
|
|
973
|
+
}
|
|
974
|
+
return source;
|
|
918
975
|
},
|
|
919
976
|
parseHtml: parseHTMLContent,
|
|
920
977
|
hostIdentityMap: hostIdentityByElement,
|
|
@@ -1150,6 +1207,7 @@ async function bundleProject(projectDir, options) {
|
|
|
1150
1207
|
injectTextRenderingRule(document);
|
|
1151
1208
|
// Inline textual assets
|
|
1152
1209
|
const inlineAssets = options?.inlineAssets !== false;
|
|
1210
|
+
const inlineAsset = createAssetInliner(resolveProjectPath, inlineAssets);
|
|
1153
1211
|
for (const el of [...document.querySelectorAll("[src], [href], [poster], [xlink\\:href]")]) {
|
|
1154
1212
|
for (const attr of ["src", "href", "poster", "xlink:href"]) {
|
|
1155
1213
|
const value = el.getAttribute(attr);
|
|
@@ -1161,7 +1219,7 @@ async function bundleProject(projectDir, options) {
|
|
|
1161
1219
|
// Keep the project-relative URL; render/check servers already expose it.
|
|
1162
1220
|
if (isExternalSvgFragmentUse(el, attr, value))
|
|
1163
1221
|
continue;
|
|
1164
|
-
const inlined =
|
|
1222
|
+
const inlined = inlineAsset(value);
|
|
1165
1223
|
if (inlined)
|
|
1166
1224
|
el.setAttribute(attr, inlined);
|
|
1167
1225
|
}
|
|
@@ -1169,7 +1227,7 @@ async function bundleProject(projectDir, options) {
|
|
|
1169
1227
|
for (const el of [...document.querySelectorAll("[srcset]")]) {
|
|
1170
1228
|
const srcset = el.getAttribute("srcset");
|
|
1171
1229
|
if (srcset)
|
|
1172
|
-
el.setAttribute("srcset", rewriteSrcsetWithInlinedAssets(srcset,
|
|
1230
|
+
el.setAttribute("srcset", rewriteSrcsetWithInlinedAssets(srcset, inlineAsset));
|
|
1173
1231
|
}
|
|
1174
1232
|
// Before inlining, so postcss reads paths not font bytes.
|
|
1175
1233
|
const liveStyles = [...document.querySelectorAll("style")].filter(isAlwaysAppliedStyle);
|
|
@@ -1187,16 +1245,16 @@ async function bundleProject(projectDir, options) {
|
|
|
1187
1245
|
document.head.prepend(holder);
|
|
1188
1246
|
}
|
|
1189
1247
|
for (const styleEl of document.querySelectorAll("style")) {
|
|
1190
|
-
styleEl.textContent = rewriteCssUrlsWithInlinedAssets(styleEl.textContent || "",
|
|
1248
|
+
styleEl.textContent = rewriteCssUrlsWithInlinedAssets(styleEl.textContent || "", inlineAsset);
|
|
1191
1249
|
}
|
|
1192
1250
|
for (const el of [...document.querySelectorAll("[style]")]) {
|
|
1193
|
-
el.setAttribute("style", rewriteCssUrlsWithInlinedAssets(el.getAttribute("style") || "",
|
|
1251
|
+
el.setAttribute("style", rewriteCssUrlsWithInlinedAssets(el.getAttribute("style") || "", inlineAsset));
|
|
1194
1252
|
}
|
|
1195
1253
|
if (inlineAssets && options?.inlineColorGradingLuts !== false) {
|
|
1196
1254
|
for (const el of [...document.querySelectorAll(`[${HF_COLOR_GRADING_ATTR}]`)]) {
|
|
1197
1255
|
const value = el.getAttribute(HF_COLOR_GRADING_ATTR);
|
|
1198
1256
|
if (value) {
|
|
1199
|
-
el.setAttribute(HF_COLOR_GRADING_ATTR, rewriteColorGradingLutWithInlinedAssets(value,
|
|
1257
|
+
el.setAttribute(HF_COLOR_GRADING_ATTR, rewriteColorGradingLutWithInlinedAssets(value, inlineAsset));
|
|
1200
1258
|
}
|
|
1201
1259
|
}
|
|
1202
1260
|
}
|
|
@@ -1277,7 +1335,23 @@ function authoredDefinesPredicate(document) {
|
|
|
1277
1335
|
const authoredCss = [...document.querySelectorAll("style:not([data-hf-composition-variables])")]
|
|
1278
1336
|
.map((s) => s.textContent || "")
|
|
1279
1337
|
.join("\n");
|
|
1280
|
-
|
|
1338
|
+
const defined = namesBeforeAColon(authoredCss);
|
|
1339
|
+
return (id) => defined.has(cssVariableName(id));
|
|
1340
|
+
}
|
|
1341
|
+
/** Every `--name` written right before a `:`, including the tail of a longer word (`.a--b:hover`), in one pass. */
|
|
1342
|
+
function namesBeforeAColon(css) {
|
|
1343
|
+
const names = new Set();
|
|
1344
|
+
const word = /[\w-]+/g;
|
|
1345
|
+
const colonNext = /\s*:/y;
|
|
1346
|
+
for (let match = word.exec(css); match; match = word.exec(css)) {
|
|
1347
|
+
colonNext.lastIndex = word.lastIndex;
|
|
1348
|
+
if (!colonNext.test(css))
|
|
1349
|
+
continue;
|
|
1350
|
+
const text = match[0];
|
|
1351
|
+
for (let at = text.indexOf("--"); at !== -1; at = text.indexOf("--", at + 1))
|
|
1352
|
+
names.add(text.slice(at));
|
|
1353
|
+
}
|
|
1354
|
+
return names;
|
|
1281
1355
|
}
|
|
1282
1356
|
/**
|
|
1283
1357
|
* Layering for one declarer: authored stylesheet definitions win over
|
|
@@ -13,7 +13,7 @@ import { compositionStyle, cssStyleMergeKey, hasSameLink, } from "./scriptRuns.j
|
|
|
13
13
|
*/
|
|
14
14
|
import { rewriteAssetPath, rewriteAssetPaths, rewriteCssAssetUrls, rewriteInlineStyleAssetUrls, } from "./rewriteSubCompPaths.js";
|
|
15
15
|
import { warnUnknownEnumValues } from "../runtime/getVariables.js";
|
|
16
|
-
import {
|
|
16
|
+
import { createCompositionCssScoper, wrapInlineScriptWithErrorBoundary, scopedModulePrelude, wrapScopedCompositionScript, } from "./compositionScoping.js";
|
|
17
17
|
import { namespaceCollidingSvgIds, rewriteSvgIdReferencesInCss, } from "./svgIdNamespacing.js";
|
|
18
18
|
import { checkSubCompositionUsability, resolveSubCompositionContent, } from "@hyperframes/parsers/sub-composition-validity";
|
|
19
19
|
import { enumerateNestedCompositionHosts, planCompositionAssembly, extractedCompositionAssets, } from "./compositionAssembly.js";
|
|
@@ -110,6 +110,7 @@ export function ensureExternalLinkTag(doc, link) {
|
|
|
110
110
|
// ---------------------------------------------------------------------------
|
|
111
111
|
// Default helpers
|
|
112
112
|
// ---------------------------------------------------------------------------
|
|
113
|
+
const isDocument = (node) => node.nodeType === 9;
|
|
113
114
|
function defaultBuildScopeSelector(compId) {
|
|
114
115
|
const escaped = compId.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
115
116
|
return `[data-composition-id="${escaped}"]`;
|
|
@@ -146,6 +147,24 @@ export function inlineSubCompositions(document, hosts, options) {
|
|
|
146
147
|
const externalLinks = [];
|
|
147
148
|
const variablesByComp = {};
|
|
148
149
|
const svgIdScopes = [];
|
|
150
|
+
// A file mounted many times reads the same; only its scope, ids and variables differ per mount.
|
|
151
|
+
// Each file is parsed once and only read; a mount edits its own copy.
|
|
152
|
+
const parsed = new Map();
|
|
153
|
+
const parseOnce = (html) => {
|
|
154
|
+
let doc = parsed.get(html);
|
|
155
|
+
if (!doc) {
|
|
156
|
+
doc = parseHtml(html);
|
|
157
|
+
parsed.set(html, doc);
|
|
158
|
+
}
|
|
159
|
+
return doc;
|
|
160
|
+
};
|
|
161
|
+
const parseCopy = (html) => {
|
|
162
|
+
const copy = parseOnce(html).cloneNode(true);
|
|
163
|
+
return isDocument(copy) ? copy : parseHtml(html);
|
|
164
|
+
};
|
|
165
|
+
const usability = new Map();
|
|
166
|
+
const sheetsWithAssetUrls = new Map();
|
|
167
|
+
const scopeCssToComposition = createCompositionCssScoper();
|
|
149
168
|
const sceneHosts = new Map();
|
|
150
169
|
const queue = hosts.map((element) => ({
|
|
151
170
|
element,
|
|
@@ -164,7 +183,12 @@ export function inlineSubCompositions(document, hosts, options) {
|
|
|
164
183
|
// tolerant (skip, don't throw) — preview and studio must keep bundling
|
|
165
184
|
// around a scene that's still being authored. Lint and the render
|
|
166
185
|
// pre-flight check use the same helper to fail loudly instead.
|
|
167
|
-
|
|
186
|
+
let validity = compHtml == null ? undefined : usability.get(compHtml);
|
|
187
|
+
if (!validity) {
|
|
188
|
+
validity = checkSubCompositionUsability(compHtml, parseOnce);
|
|
189
|
+
if (compHtml != null)
|
|
190
|
+
usability.set(compHtml, validity);
|
|
191
|
+
}
|
|
168
192
|
if (!validity.ok) {
|
|
169
193
|
onMissingComposition?.(src, validity.detail);
|
|
170
194
|
continue;
|
|
@@ -176,7 +200,7 @@ export function inlineSubCompositions(document, hosts, options) {
|
|
|
176
200
|
onMissingComposition?.(src);
|
|
177
201
|
continue;
|
|
178
202
|
}
|
|
179
|
-
const compDoc =
|
|
203
|
+
const compDoc = parseCopy(compHtml);
|
|
180
204
|
// Determine composition IDs
|
|
181
205
|
let compId;
|
|
182
206
|
let runtimeCompId;
|
|
@@ -194,7 +218,7 @@ export function inlineSubCompositions(document, hosts, options) {
|
|
|
194
218
|
onMissingComposition?.(src);
|
|
195
219
|
continue;
|
|
196
220
|
}
|
|
197
|
-
const contentDoc =
|
|
221
|
+
const contentDoc = parseCopy(contentHtml);
|
|
198
222
|
if (!contentDoc.documentElement) {
|
|
199
223
|
onMissingComposition?.(src);
|
|
200
224
|
continue;
|
|
@@ -259,7 +283,12 @@ export function inlineSubCompositions(document, hosts, options) {
|
|
|
259
283
|
// sub-comp's html/body/:root rules from clobbering the host document (they
|
|
260
284
|
// are remapped to the composition box); see compositionScoping.
|
|
261
285
|
const scopeSubStyle = (raw) => {
|
|
262
|
-
const
|
|
286
|
+
const sheetKey = JSON.stringify([src, raw]);
|
|
287
|
+
let css = sheetsWithAssetUrls.get(sheetKey);
|
|
288
|
+
if (css === undefined) {
|
|
289
|
+
css = rewriteCssAssetUrls(raw, src, assetExists);
|
|
290
|
+
sheetsWithAssetUrls.set(sheetKey, css);
|
|
291
|
+
}
|
|
263
292
|
return scopeCompId
|
|
264
293
|
? scopeCssToComposition(css, scopeCompId, runtimeScope || undefined, authoredRootId, {
|
|
265
294
|
compoundAuthoredRoot: compoundAuthoredRoot === true,
|
|
@@ -105,7 +105,7 @@ export interface FontFetchRetryPolicy {
|
|
|
105
105
|
/** Initial full-jitter backoff ceiling. Default: 250 ms. */
|
|
106
106
|
baseDelayMs: number;
|
|
107
107
|
}
|
|
108
|
-
/** Test-only reset
|
|
108
|
+
/** Test-only reset of the process and disk caches, so the next lookup reaches `fetchImpl`. */
|
|
109
109
|
export declare function _clearGoogleFontCssCacheForTests(): void;
|
|
110
110
|
/**
|
|
111
111
|
* Options for {@link injectDeterministicFontFaces}.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { FONT_ALIAS_MAP, resolveAliasDisplayName } from "./aliases.js";
|
|
@@ -1131,9 +1131,44 @@ async function ensureWoff2DataUri(cachePath, woff2Url, familyName, weight, style
|
|
|
1131
1131
|
return `data:font/woff2;base64,${readFileSync(cachePath).toString("base64")}`;
|
|
1132
1132
|
}
|
|
1133
1133
|
const googleFontCssCache = new Map();
|
|
1134
|
-
|
|
1134
|
+
const GOOGLE_FONT_CSS_CACHE_DIR = "_css";
|
|
1135
|
+
/** Test-only reset of the process and disk caches, so the next lookup reaches `fetchImpl`. */
|
|
1135
1136
|
export function _clearGoogleFontCssCacheForTests() {
|
|
1136
1137
|
googleFontCssCache.clear();
|
|
1138
|
+
rmSync(join(resolveFontCacheRoot(), GOOGLE_FONT_CSS_CACHE_DIR), { recursive: true, force: true });
|
|
1139
|
+
}
|
|
1140
|
+
// The lookup names the woff2 files, so it is cached on disk beside them; otherwise each new process
|
|
1141
|
+
// asks the network first and, offline, never reaches them. No family slug has an underscore.
|
|
1142
|
+
function googleFontCssCachePath(url) {
|
|
1143
|
+
return join(fontCacheDir(GOOGLE_FONT_CSS_CACHE_DIR), `${createHash("sha256").update(url).digest("hex")}.json`);
|
|
1144
|
+
}
|
|
1145
|
+
function readCachedGoogleFontCss(url) {
|
|
1146
|
+
let cached;
|
|
1147
|
+
try {
|
|
1148
|
+
cached = JSON.parse(readFileSync(googleFontCssCachePath(url), "utf8"));
|
|
1149
|
+
}
|
|
1150
|
+
catch {
|
|
1151
|
+
return null;
|
|
1152
|
+
}
|
|
1153
|
+
if (typeof cached !== "object" || cached === null)
|
|
1154
|
+
return null;
|
|
1155
|
+
const body = Reflect.get(cached, "body");
|
|
1156
|
+
const status = Reflect.get(cached, "status");
|
|
1157
|
+
return typeof body === "string" && typeof status === "number" ? { ok: true, body, status } : null;
|
|
1158
|
+
}
|
|
1159
|
+
/** Keeps only a lookup that named faces; written aside and renamed, so a reader never sees half of it. */
|
|
1160
|
+
function writeCachedGoogleFontCss(url, body, status) {
|
|
1161
|
+
if (!/@font-face/i.test(body))
|
|
1162
|
+
return;
|
|
1163
|
+
const path = googleFontCssCachePath(url);
|
|
1164
|
+
const partial = `${path}.${process.pid}.tmp`;
|
|
1165
|
+
try {
|
|
1166
|
+
writeFileSync(partial, JSON.stringify({ status, body }), { mode: 0o644 });
|
|
1167
|
+
renameSync(partial, path);
|
|
1168
|
+
}
|
|
1169
|
+
catch {
|
|
1170
|
+
rmSync(partial, { force: true });
|
|
1171
|
+
}
|
|
1137
1172
|
}
|
|
1138
1173
|
/** Rejects with `signal`'s own abort reason without cancelling `promise` itself. */
|
|
1139
1174
|
function raceAgainstAbort(promise, signal) {
|
|
@@ -1152,13 +1187,20 @@ function fetchGoogleFontCss(url, familyName, options, attempt) {
|
|
|
1152
1187
|
const mode = options.failClosedFontFetch ? `closed${options.retryPolicy.maxAttempts}` : "open";
|
|
1153
1188
|
const key = `${mode}:${url}`;
|
|
1154
1189
|
let shared = googleFontCssCache.get(key);
|
|
1190
|
+
const cached = shared ? null : readCachedGoogleFontCss(url);
|
|
1191
|
+
if (cached) {
|
|
1192
|
+
shared = Promise.resolve(cached);
|
|
1193
|
+
googleFontCssCache.set(key, shared);
|
|
1194
|
+
}
|
|
1155
1195
|
attempt.cssCache = shared ? "hit" : "fresh";
|
|
1156
1196
|
if (!shared) {
|
|
1157
1197
|
// The shared fetch must not carry any one caller's abortSignal, or that
|
|
1158
1198
|
// caller cancelling would fail the lookup for every other waiter.
|
|
1159
1199
|
shared = fetchFontResource(url, { headers: { "User-Agent": WOFF2_USER_AGENT } }, (response) => response.text(), familyName, "Google Fonts CSS", { ...options, abortSignal: undefined }).then((result) => {
|
|
1160
|
-
if (result.ok)
|
|
1200
|
+
if (result.ok) {
|
|
1201
|
+
writeCachedGoogleFontCss(url, result.body, result.response.status);
|
|
1161
1202
|
return { ok: true, body: result.body, status: result.response.status };
|
|
1203
|
+
}
|
|
1162
1204
|
// A transient status must not stick for the process lifetime.
|
|
1163
1205
|
if (isRetryableFontFetchStatus(result.response.status))
|
|
1164
1206
|
googleFontCssCache.delete(key);
|
|
@@ -1415,8 +1457,14 @@ function extractGoogleFontsText(html) {
|
|
|
1415
1457
|
// Intentional over-approximation: raw html includes base64, scripts, and
|
|
1416
1458
|
// class names, but they collapse in the Set and the budget gate catches bloat.
|
|
1417
1459
|
const uniqueCharacters = new Set();
|
|
1418
|
-
|
|
1419
|
-
|
|
1460
|
+
const seen = new Set();
|
|
1461
|
+
for (const text of [html, decodedBodyText]) {
|
|
1462
|
+
for (const character of text) {
|
|
1463
|
+
if (seen.has(character))
|
|
1464
|
+
continue;
|
|
1465
|
+
seen.add(character);
|
|
1466
|
+
addCaseClosure(uniqueCharacters, character, locales);
|
|
1467
|
+
}
|
|
1420
1468
|
}
|
|
1421
1469
|
if (hasTextTransformKeyword(html, FULL_WIDTH_KEYWORD_RE))
|
|
1422
1470
|
addFullwidthVariants(uniqueCharacters);
|