@hyperframes/core 0.8.190 → 0.8.192

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.
@@ -1,7 +1,7 @@
1
1
  // Persistence for beat data: one JSON file per audio file, matched by the
2
2
  // audio's project-relative path. Lives under `beats/` in the project so it
3
3
  // survives the audio being removed and re-added.
4
- import { decodeAuthoredAttribute, scanHtmlOpeningTags, } from "@hyperframes/parsers";
4
+ import { decodeAuthoredAttribute, decodedUrlPath, scanHtmlOpeningTags, } from "@hyperframes/parsers";
5
5
  /** Project-relative path of the audio file behind a (possibly absolute) src URL. */
6
6
  export function audioRelPathForSrc(src) {
7
7
  if (!src)
@@ -23,16 +23,16 @@ export function audioRelPathForSrc(src) {
23
23
  after = after.slice(0, queryOrHash);
24
24
  if (after.startsWith("comp/"))
25
25
  after = after.slice("comp/".length);
26
- rel = after ? decodeURIComponent(after) : null;
26
+ rel = after ? decodedUrlPath(after) : null;
27
27
  }
28
28
  if (!rel) {
29
29
  // Fall back to the FULL pathname (not just basename) so two files with the
30
30
  // same name in different folders don't collide on one beat file.
31
31
  try {
32
- rel = decodeURIComponent(new URL(src, "http://_").pathname);
32
+ rel = decodedUrlPath(new URL(src, "http://_").pathname);
33
33
  }
34
34
  catch {
35
- rel = src;
35
+ rel = decodedUrlPath(src);
36
36
  }
37
37
  }
38
38
  if (!rel)
@@ -125,23 +125,42 @@ function rebaseRelativePath(urlValue, fromDir, toDir) {
125
125
  const rebased = encodeUrlPath(relative(resolve(toDir), absolutePath).split(sep).join("/"));
126
126
  return appendSuffixToUrl(rebased, suffix);
127
127
  }
128
- function rebaseSrcsetPaths(srcsetValue, fromDir, toDir) {
129
- if (!srcsetValue)
130
- return srcsetValue;
131
- return srcsetValue
132
- .split(",")
133
- .map((rawCandidate) => {
134
- const candidate = rawCandidate.trim();
135
- if (!candidate)
136
- return candidate;
137
- const parts = candidate.split(/\s+/);
138
- const first = parts[0] ?? "";
139
- if (parts.length === 0 || !isRelativeUrl(first))
140
- return candidate;
141
- parts[0] = rebaseRelativePath(first, fromDir, toDir);
142
- return parts.join(" ");
143
- })
144
- .join(", ");
128
+ function rewriteSrcsetUrls(srcset, rewriteUrl) {
129
+ const parts = [];
130
+ let position = 0;
131
+ let written = 0;
132
+ // Preserve URL commas per https://html.spec.whatwg.org/multipage/images.html#parse-a-srcset-attribute
133
+ while (position < srcset.length) {
134
+ while (position < srcset.length && /[ \t\n\r\f,]/.test(srcset.charAt(position)))
135
+ position++;
136
+ const start = position;
137
+ while (position < srcset.length && !/[ \t\n\r\f]/.test(srcset.charAt(position)))
138
+ position++;
139
+ let end = position;
140
+ while (end > start && srcset.charAt(end - 1) === ",")
141
+ end--;
142
+ if (end > start) {
143
+ const url = srcset.slice(start, end);
144
+ const rewritten = rewriteUrl(url);
145
+ if (rewritten !== url) {
146
+ parts.push(srcset.slice(written, start), rewritten);
147
+ written = end;
148
+ }
149
+ }
150
+ if (end < position)
151
+ continue;
152
+ let inParens = false;
153
+ while (position < srcset.length) {
154
+ const char = srcset.charAt(position++);
155
+ if (char === "(")
156
+ inParens = true;
157
+ else if (char === ")")
158
+ inParens = false;
159
+ else if (char === "," && !inParens)
160
+ break;
161
+ }
162
+ }
163
+ return parts.length > 0 ? parts.join("") + srcset.slice(written) : srcset;
145
164
  }
146
165
  function rebaseColorGradingLutPath(value, fromDir, toDir) {
147
166
  if (!value.trim().startsWith("{"))
@@ -194,7 +213,7 @@ function rebaseEntryAuthoredAssetPaths(document, sourceDir, projectDir) {
194
213
  for (const el of [...document.querySelectorAll("[srcset]")]) {
195
214
  const srcset = el.getAttribute("srcset");
196
215
  if (srcset)
197
- el.setAttribute("srcset", rebaseSrcsetPaths(srcset, sourceDir, projectDir));
216
+ el.setAttribute("srcset", rewriteSrcsetUrls(srcset, (url) => isRelativeUrl(url) ? rebaseRelativePath(url, sourceDir, projectDir) : url));
198
217
  }
199
218
  for (const el of [...document.querySelectorAll(`[${HF_COLOR_GRADING_ATTR}]`)]) {
200
219
  const value = el.getAttribute(HF_COLOR_GRADING_ATTR);
@@ -301,6 +320,8 @@ const INLINE_MIME = {
301
320
  * exactly where "self-contained" stops being true.
302
321
  */
303
322
  const MAX_INLINE_ASSET_BYTES = 2 * 1024 * 1024;
323
+ // An asset is inlined once per reference, so repeats can pass V8's ~2^29-char string limit.
324
+ const MAX_INLINED_BUNDLE_CHARS = 256 * 1024 * 1024;
304
325
  function safeStatSize(filePath) {
305
326
  try {
306
327
  return statSync(filePath).size;
@@ -313,9 +334,14 @@ function warnAssetTooLargeToInline(assetPath, byteLength) {
313
334
  const mb = (byteLength / (1024 * 1024)).toFixed(1);
314
335
  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.`);
315
336
  }
337
+ function warnInlineBudgetExhausted(assetPath) {
338
+ console.warn(`[HyperFrames] Not inlining "${assetPath}" and later assets: the bundle already inlines ${MAX_INLINED_BUNDLE_CHARS / (1024 * 1024)} MB of data URLs. The bundle may not be self-contained.`);
339
+ }
316
340
  /** Inlines relative asset URLs as data: URLs, reading each project file once per build. */
317
341
  function createAssetInliner(resolveProjectPath, inlineAssets) {
318
342
  const assets = new Map();
343
+ let inlinedChars = 0;
344
+ let budgetExhausted = false;
319
345
  const readAsset = (filePath, mimeType) => {
320
346
  // Size-check before reading: an oversized asset must not be pulled into memory
321
347
  // just to be discarded.
@@ -354,7 +380,16 @@ function createAssetInliner(resolveProjectPath, inlineAssets) {
354
380
  warnAssetTooLargeToInline(basePath, asset.tooLarge);
355
381
  return null;
356
382
  }
357
- return asset.dataUrl === null ? null : appendSuffixToUrl(asset.dataUrl, suffix);
383
+ if (asset.dataUrl === null)
384
+ return null;
385
+ if (budgetExhausted || inlinedChars + asset.dataUrl.length > MAX_INLINED_BUNDLE_CHARS) {
386
+ if (!budgetExhausted)
387
+ warnInlineBudgetExhausted(basePath);
388
+ budgetExhausted = true;
389
+ return null;
390
+ }
391
+ inlinedChars += asset.dataUrl.length;
392
+ return appendSuffixToUrl(asset.dataUrl, suffix);
358
393
  };
359
394
  }
360
395
  function isExternalSvgFragmentUse(el, attr, urlValue) {
@@ -414,25 +449,6 @@ function rewriteColorGradingLutWithInlinedAssets(value, inlineAsset) {
414
449
  Reflect.set(lut, "src", inlined);
415
450
  return JSON.stringify(parsed);
416
451
  }
417
- function rewriteSrcsetWithInlinedAssets(srcsetValue, inlineAsset) {
418
- if (!srcsetValue)
419
- return srcsetValue;
420
- return srcsetValue
421
- .split(",")
422
- .map((rawCandidate) => {
423
- const candidate = rawCandidate.trim();
424
- if (!candidate)
425
- return candidate;
426
- const parts = candidate.split(/\s+/);
427
- if (parts.length === 0)
428
- return candidate;
429
- const maybeInlined = inlineAsset(parts[0] ?? "");
430
- if (maybeInlined)
431
- parts[0] = maybeInlined;
432
- return parts.join(" ");
433
- })
434
- .join(", ");
435
- }
436
452
  function rewriteCssUrlsWithInlinedAssets(cssText, inlineAsset) {
437
453
  if (!cssText)
438
454
  return cssText;
@@ -1228,7 +1244,7 @@ async function bundleProject(projectDir, options) {
1228
1244
  for (const el of [...document.querySelectorAll("[srcset]")]) {
1229
1245
  const srcset = el.getAttribute("srcset");
1230
1246
  if (srcset)
1231
- el.setAttribute("srcset", rewriteSrcsetWithInlinedAssets(srcset, inlineAsset));
1247
+ el.setAttribute("srcset", rewriteSrcsetUrls(srcset, (url) => inlineAsset(url) ?? url));
1232
1248
  }
1233
1249
  // Before inlining, so postcss reads paths not font bytes.
1234
1250
  const liveStyles = [...document.querySelectorAll("style")].filter(isAlwaysAppliedStyle);
@@ -1,7 +1,8 @@
1
+ type SpecifierMap = Record<string, string | null>;
1
2
  /** The subset of an import map a composition can declare. */
2
3
  export interface ImportMap {
3
- imports?: Record<string, string>;
4
- scopes?: Record<string, Record<string, string>>;
4
+ imports?: SpecifierMap;
5
+ scopes?: Record<string, SpecifierMap>;
5
6
  }
6
7
  /**
7
8
  * Parses a mounted composition's import map, rebasing each address the way its `src`/`href`
@@ -16,3 +17,4 @@ export declare function parseImportMap(json: string, rebase: (url: string) => st
16
17
  export declare function mergeImportMapsIntoDocument(doc: Document, maps: ImportMap[]): void;
17
18
  /** Emits what inlineSubCompositions collected from mounted files' module and import-map scripts. */
18
19
  export declare function emitMountedModuleScripts(doc: Document, importMaps: ImportMap[], moduleScripts: string[]): void;
20
+ export {};
@@ -1,5 +1,14 @@
1
+ function isRecord(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ function isScopeMap(value) {
5
+ return isRecord(value) && Object.values(value).every(isRecord);
6
+ }
1
7
  const rebaseEntries = (entries, rebase) => entries &&
2
- Object.fromEntries(Object.entries(entries).map(([specifier, url]) => [specifier, rebase(url)]));
8
+ Object.fromEntries(Object.entries(entries).map(([specifier, url]) => [
9
+ specifier,
10
+ typeof url === "string" ? rebase(url) : null,
11
+ ]));
3
12
  /**
4
13
  * Parses a mounted composition's import map, rebasing each address the way its `src`/`href`
5
14
  * attributes are rebased. Returns null for a map the browser would reject too.
@@ -12,9 +21,13 @@ export function parseImportMap(json, rebase) {
12
21
  catch {
13
22
  return null;
14
23
  }
15
- if (!parsed || typeof parsed !== "object")
24
+ if (!isRecord(parsed))
16
25
  return null;
17
26
  const { imports, scopes } = parsed;
27
+ if (imports !== undefined && !isRecord(imports))
28
+ return null;
29
+ if (scopes !== undefined && !isScopeMap(scopes))
30
+ return null;
18
31
  return {
19
32
  imports: rebaseEntries(imports, rebase),
20
33
  scopes: scopes &&
@@ -26,8 +39,14 @@ export function parseImportMap(json, rebase) {
26
39
  }
27
40
  function mergeEntries(into, from, where) {
28
41
  for (const [specifier, url] of Object.entries(from ?? {})) {
29
- if (!(specifier in into))
30
- into[specifier] = url;
42
+ if (!Object.hasOwn(into, specifier)) {
43
+ Object.defineProperty(into, specifier, {
44
+ value: url,
45
+ enumerable: true,
46
+ configurable: true,
47
+ writable: true,
48
+ });
49
+ }
31
50
  else if (into[specifier] !== url) {
32
51
  console.warn(`[HyperFrames] import map conflict for "${specifier}"${where}: keeping ${into[specifier]}, ignoring ${url}.`);
33
52
  }
@@ -54,7 +73,9 @@ export function mergeImportMapsIntoDocument(doc, maps) {
54
73
  for (const map of maps) {
55
74
  mergeEntries(imports, map.imports, "");
56
75
  for (const [prefix, entries] of Object.entries(map.scopes ?? {})) {
57
- mergeEntries((scopes[prefix] ??= {}), entries, ` in scope ${prefix}`);
76
+ const scope = (Object.hasOwn(scopes, prefix) ? scopes[prefix] : undefined) ?? {};
77
+ Object.defineProperty(scopes, prefix, { value: scope, enumerable: true });
78
+ mergeEntries(scope, entries, ` in scope ${prefix}`);
58
79
  }
59
80
  }
60
81
  if (!el) {