@hyperframes/lint 0.8.221 → 0.8.223

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/browser.d.ts CHANGED
@@ -43,6 +43,13 @@ type HyperframeLinterOptions = {
43
43
  file?: string;
44
44
  rootRelativePath?: string;
45
45
  }>;
46
+ externalScripts?: Array<{
47
+ src: string;
48
+ attrs: string;
49
+ content: string;
50
+ file: string;
51
+ rootRelativePath: string;
52
+ }>;
46
53
  /**
47
54
  * Set to `true` when linting compositions destined for distributed / Lambda
48
55
  * rendering, where system-font capture (`allowSystemFontCapture`) is
@@ -51,6 +58,7 @@ type HyperframeLinterOptions = {
51
58
  * render time — the font will silently fall back to whatever the OS provides.
52
59
  */
53
60
  distributed?: boolean;
61
+ projectFontFamilies?: string[];
54
62
  /** Who is running the lint: Studio raises the structure rules to errors, the CLI keeps them warnings. */
55
63
  host?: "studio" | "cli";
56
64
  };
package/dist/browser.js CHANGED
@@ -786,7 +786,17 @@ function buildLintContext(html, options = {}) {
786
786
  rootRelativePath: style.rootRelativePath
787
787
  }))
788
788
  ];
789
- const scripts = structure.scripts;
789
+ const scripts = [
790
+ ...structure.scripts,
791
+ ...(options.externalScripts ?? []).map((script) => ({
792
+ attrs: script.attrs,
793
+ content: script.content,
794
+ raw: script.content,
795
+ index: -1,
796
+ file: script.file,
797
+ rootRelativePath: script.rootRelativePath
798
+ }))
799
+ ];
790
800
  const compositionIds = collectCompositionIds(tags);
791
801
  const rootTag = findRootTag(source, tags);
792
802
  const rootCompositionId = readDecodedAttr(rootTag?.raw || "", "data-composition-id");
@@ -1389,7 +1399,10 @@ var coreRules = [
1389
1399
  }
1390
1400
  if (/(?:^|\s)data-no-timeline(?=[\s=/]|$)/i.test(rootTag?.attrs || "")) return [];
1391
1401
  const findings = [];
1392
- if (!TIMELINE_REGISTRY_INIT_PATTERN.test(source) && !TIMELINE_REGISTRY_ASSIGN_PATTERN.test(source) && !TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN.test(source)) {
1402
+ const registered = [source, ...(options.externalScripts ?? []).map((script) => script.content)];
1403
+ if (!registered.some(
1404
+ (text) => TIMELINE_REGISTRY_INIT_PATTERN.test(text) || TIMELINE_REGISTRY_ASSIGN_PATTERN.test(text) || TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN.test(text)
1405
+ )) {
1393
1406
  findings.push({
1394
1407
  code: "missing_timeline_registry",
1395
1408
  severity: "error",
@@ -1560,9 +1573,9 @@ var coreRules = [
1560
1573
  ({ scripts, locate }) => {
1561
1574
  const findings = [];
1562
1575
  for (const script of scripts) {
1563
- const type = readDecodedAttr(script.raw, "type")?.trim().toLowerCase() ?? "";
1564
- if (readDecodedAttr(script.raw, "src") !== null || SCRIPT_SYNTAX_EXEMPT_TYPES.has(type))
1565
- continue;
1576
+ const tag = `<script ${script.attrs}>`;
1577
+ const type = readDecodedAttr(tag, "type")?.trim().toLowerCase() ?? "";
1578
+ if (readDecodedAttr(tag, "src") !== null || SCRIPT_SYNTAX_EXEMPT_TYPES.has(type)) continue;
1566
1579
  const syntaxError = getInlineScriptSyntaxError(script.content);
1567
1580
  if (!syntaxError) continue;
1568
1581
  findings.push({
@@ -6439,6 +6452,9 @@ function extractFontFaceFamilies(styles) {
6439
6452
  }
6440
6453
  return families;
6441
6454
  }
6455
+ function declaredFontFamilies(styles, options) {
6456
+ return /* @__PURE__ */ new Set([...extractFontFaceFamilies(styles), ...options.projectFontFamilies ?? []]);
6457
+ }
6442
6458
  function normalizeUsedFontName(part) {
6443
6459
  const name = part.trim().replace(/\s*!important\s*$/i, "").replace(/^['"]|['"]$/g, "").trim().toLowerCase();
6444
6460
  if (!name || name.includes("(") || name.includes(")")) return null;
@@ -6548,7 +6564,7 @@ var fontRules = [
6548
6564
  // so there is nothing for the author to act on.
6549
6565
  ({ styles, options }) => {
6550
6566
  if (!options.distributed) return [];
6551
- const declared = extractFontFaceFamilies(styles);
6567
+ const declared = declaredFontFamilies(styles, options);
6552
6568
  const used = extractUsedFontFamilies(styles);
6553
6569
  const aliased = collectAliasedFonts(used, declared);
6554
6570
  if (aliased.length === 0) return [];
@@ -6564,7 +6580,7 @@ var fontRules = [
6564
6580
  ({ styles, source, rawSource, options, locate }) => {
6565
6581
  if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return [];
6566
6582
  const findings = [];
6567
- const declared = extractFontFaceFamilies(styles);
6583
+ const declared = declaredFontFamilies(styles, options);
6568
6584
  const used = extractUsedFontFamilies(styles);
6569
6585
  const googleFonts = collectGoogleFontFamilies(source, styles);
6570
6586
  const undeclared = used.filter(
package/dist/index.d.ts CHANGED
@@ -43,6 +43,13 @@ type HyperframeLinterOptions = {
43
43
  file?: string;
44
44
  rootRelativePath?: string;
45
45
  }>;
46
+ externalScripts?: Array<{
47
+ src: string;
48
+ attrs: string;
49
+ content: string;
50
+ file: string;
51
+ rootRelativePath: string;
52
+ }>;
46
53
  /**
47
54
  * Set to `true` when linting compositions destined for distributed / Lambda
48
55
  * rendering, where system-font capture (`allowSystemFontCapture`) is
@@ -51,6 +58,7 @@ type HyperframeLinterOptions = {
51
58
  * render time — the font will silently fall back to whatever the OS provides.
52
59
  */
53
60
  distributed?: boolean;
61
+ projectFontFamilies?: string[];
54
62
  /** Who is running the lint: Studio raises the structure rules to errors, the CLI keeps them warnings. */
55
63
  host?: "studio" | "cli";
56
64
  };
package/dist/index.js CHANGED
@@ -792,7 +792,17 @@ function buildLintContext(html, options = {}) {
792
792
  rootRelativePath: style.rootRelativePath
793
793
  }))
794
794
  ];
795
- const scripts = structure.scripts;
795
+ const scripts = [
796
+ ...structure.scripts,
797
+ ...(options.externalScripts ?? []).map((script) => ({
798
+ attrs: script.attrs,
799
+ content: script.content,
800
+ raw: script.content,
801
+ index: -1,
802
+ file: script.file,
803
+ rootRelativePath: script.rootRelativePath
804
+ }))
805
+ ];
796
806
  const compositionIds = collectCompositionIds(tags);
797
807
  const rootTag = findRootTag(source, tags);
798
808
  const rootCompositionId = readDecodedAttr(rootTag?.raw || "", "data-composition-id");
@@ -1395,7 +1405,10 @@ var coreRules = [
1395
1405
  }
1396
1406
  if (/(?:^|\s)data-no-timeline(?=[\s=/]|$)/i.test(rootTag?.attrs || "")) return [];
1397
1407
  const findings = [];
1398
- if (!TIMELINE_REGISTRY_INIT_PATTERN.test(source) && !TIMELINE_REGISTRY_ASSIGN_PATTERN.test(source) && !TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN.test(source)) {
1408
+ const registered = [source, ...(options.externalScripts ?? []).map((script) => script.content)];
1409
+ if (!registered.some(
1410
+ (text) => TIMELINE_REGISTRY_INIT_PATTERN.test(text) || TIMELINE_REGISTRY_ASSIGN_PATTERN.test(text) || TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN.test(text)
1411
+ )) {
1399
1412
  findings.push({
1400
1413
  code: "missing_timeline_registry",
1401
1414
  severity: "error",
@@ -1566,9 +1579,9 @@ var coreRules = [
1566
1579
  ({ scripts, locate }) => {
1567
1580
  const findings = [];
1568
1581
  for (const script of scripts) {
1569
- const type = readDecodedAttr(script.raw, "type")?.trim().toLowerCase() ?? "";
1570
- if (readDecodedAttr(script.raw, "src") !== null || SCRIPT_SYNTAX_EXEMPT_TYPES.has(type))
1571
- continue;
1582
+ const tag = `<script ${script.attrs}>`;
1583
+ const type = readDecodedAttr(tag, "type")?.trim().toLowerCase() ?? "";
1584
+ if (readDecodedAttr(tag, "src") !== null || SCRIPT_SYNTAX_EXEMPT_TYPES.has(type)) continue;
1572
1585
  const syntaxError = getInlineScriptSyntaxError(script.content);
1573
1586
  if (!syntaxError) continue;
1574
1587
  findings.push({
@@ -6445,6 +6458,9 @@ function extractFontFaceFamilies(styles) {
6445
6458
  }
6446
6459
  return families;
6447
6460
  }
6461
+ function declaredFontFamilies(styles, options) {
6462
+ return /* @__PURE__ */ new Set([...extractFontFaceFamilies(styles), ...options.projectFontFamilies ?? []]);
6463
+ }
6448
6464
  function normalizeUsedFontName(part) {
6449
6465
  const name = part.trim().replace(/\s*!important\s*$/i, "").replace(/^['"]|['"]$/g, "").trim().toLowerCase();
6450
6466
  if (!name || name.includes("(") || name.includes(")")) return null;
@@ -6554,7 +6570,7 @@ var fontRules = [
6554
6570
  // so there is nothing for the author to act on.
6555
6571
  ({ styles, options }) => {
6556
6572
  if (!options.distributed) return [];
6557
- const declared = extractFontFaceFamilies(styles);
6573
+ const declared = declaredFontFamilies(styles, options);
6558
6574
  const used = extractUsedFontFamilies(styles);
6559
6575
  const aliased = collectAliasedFonts(used, declared);
6560
6576
  if (aliased.length === 0) return [];
@@ -6570,7 +6586,7 @@ var fontRules = [
6570
6586
  ({ styles, source, rawSource, options, locate }) => {
6571
6587
  if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return [];
6572
6588
  const findings = [];
6573
- const declared = extractFontFaceFamilies(styles);
6589
+ const declared = declaredFontFamilies(styles, options);
6574
6590
  const used = extractUsedFontFamilies(styles);
6575
6591
  const googleFonts = collectGoogleFontFamilies(source, styles);
6576
6592
  const undeclared = used.filter(
@@ -7489,6 +7505,30 @@ function collectExternalStyles(projectDir, html, compSrcPath) {
7489
7505
  }
7490
7506
  return styles;
7491
7507
  }
7508
+ var MAX_EXTERNAL_SCRIPT_BYTES = 1e6;
7509
+ function collectExternalScripts(projectDir, html, compSrcPath) {
7510
+ const scripts = [];
7511
+ const { document } = parseHTML3(html);
7512
+ for (const element of querySelectorAllIncludingTemplates(document, "script[src]")) {
7513
+ const src = element.getAttribute("src") ?? "";
7514
+ if (!isLocalStylesheetHref(src) || /\.min\.m?js(?:[?#]|$)/i.test(src)) continue;
7515
+ const path = src.split(/[?#]/)[0] ?? src;
7516
+ const besideFile = compSrcPath ? join3(dirname2(compSrcPath), path) : path;
7517
+ const script = resolveExistingLocalAsset3(projectDir, besideFile) ?? (compSrcPath && staysInsideProjectRoot(path) ? resolveExistingLocalAsset3(projectDir, path) : null);
7518
+ if (!script) continue;
7519
+ const read = readProjectFile2(script.resolved);
7520
+ if (read.kind !== "file" || read.text.length > MAX_EXTERNAL_SCRIPT_BYTES) continue;
7521
+ const attrs = Array.from(element.attributes).map(({ name, value }) => `${name}="${value.replace(/&/g, "&amp;").replace(/"/g, "&quot;")}"`).join(" ");
7522
+ scripts.push({
7523
+ src,
7524
+ attrs,
7525
+ content: read.text,
7526
+ file: join3(projectDir, script.rootRelativePath),
7527
+ rootRelativePath: script.rootRelativePath
7528
+ });
7529
+ }
7530
+ return scripts;
7531
+ }
7492
7532
  function collectCssSources(projectDir, html, compSrcPath) {
7493
7533
  const sources = [];
7494
7534
  const { document } = parseHTML3(html);
@@ -7522,6 +7562,42 @@ function resolveCssAssetCandidates(projectDir, url, htmlCompSrcPath, cssRootRela
7522
7562
  }
7523
7563
  return resolveLocalAssetCandidates(projectDir, url);
7524
7564
  }
7565
+ function mountedSources(projectDir, rootHtml, rootCompSrcPath) {
7566
+ const sources = [{ html: rootHtml, compSrcPath: rootCompSrcPath }];
7567
+ const seen = /* @__PURE__ */ new Set();
7568
+ for (const source of sources) {
7569
+ const { document } = parseHTML3(source.html);
7570
+ for (const host of querySelectorAllIncludingTemplates(document, "[data-composition-src]")) {
7571
+ const src = host.getAttribute("data-composition-src")?.trim();
7572
+ if (!src || isUnresolvedAssetPlaceholder2(src) || isRemoteOrInlineUrl2(src)) continue;
7573
+ const path = resolve(projectDir, src);
7574
+ if (seen.has(path) || !isWithinProjectRoot2(projectDir, path)) continue;
7575
+ seen.add(path);
7576
+ const read = readProjectFile2(path);
7577
+ if (read.kind !== "file") continue;
7578
+ sources.push({
7579
+ html: read.text,
7580
+ compSrcPath: relative2(resolve(projectDir), path).replace(/\\/g, "/")
7581
+ });
7582
+ }
7583
+ }
7584
+ return sources;
7585
+ }
7586
+ function declaredProjectFontFamilies(projectDir, sources) {
7587
+ const families = /* @__PURE__ */ new Set();
7588
+ for (const { html, compSrcPath } of sources) {
7589
+ const { document } = parseHTML3(html);
7590
+ const styles = [
7591
+ ...querySelectorAllIncludingTemplates(document, "style").map((style) => ({
7592
+ content: style.textContent ?? ""
7593
+ })),
7594
+ ...collectLocalStylesheets(projectDir, document, compSrcPath)
7595
+ ];
7596
+ for (const family of extractFontFaceFamilies(styles)) families.add(family);
7597
+ for (const family of collectGoogleFontFamilies(html, styles)) families.add(family);
7598
+ }
7599
+ return [...families];
7600
+ }
7525
7601
  async function lintProject(projectDir, entryFile, hostOptions = {}) {
7526
7602
  const indexPath = entryFile ? resolve(entryFile) : resolve(projectDir, "index.html");
7527
7603
  if (entryFile && !isWithinProjectRoot2(projectDir, indexPath)) {
@@ -7534,11 +7610,17 @@ async function lintProject(projectDir, entryFile, hostOptions = {}) {
7534
7610
  let totalWarnings = 0;
7535
7611
  let totalInfos = 0;
7536
7612
  const rootHtml = readFileSync(indexPath, "utf-8");
7613
+ const projectFontFamilies = declaredProjectFontFamilies(
7614
+ projectDir,
7615
+ mountedSources(projectDir, rootHtml, rootCompSrcPath)
7616
+ );
7537
7617
  const rootResult = await lintHyperframeHtml(rootHtml, {
7538
7618
  ...hostOptions,
7539
7619
  filePath: indexPath,
7540
7620
  compSrcPath: rootFile,
7541
- externalStyles: collectExternalStyles(projectDir, rootHtml, rootCompSrcPath)
7621
+ externalStyles: collectExternalStyles(projectDir, rootHtml, rootCompSrcPath),
7622
+ projectFontFamilies,
7623
+ externalScripts: collectExternalScripts(projectDir, rootHtml, rootCompSrcPath)
7542
7624
  });
7543
7625
  results.push({ file: rootFile, result: rootResult, contentHash: contentDigest(rootHtml) });
7544
7626
  totalErrors += rootResult.errorCount;
@@ -7572,7 +7654,9 @@ async function lintProject(projectDir, entryFile, hostOptions = {}) {
7572
7654
  filePath,
7573
7655
  isSubComposition: true,
7574
7656
  compSrcPath,
7575
- externalStyles: collectExternalStyles(projectDir, html, compSrcPath)
7657
+ externalStyles: collectExternalStyles(projectDir, html, compSrcPath),
7658
+ projectFontFamilies,
7659
+ externalScripts: collectExternalScripts(projectDir, html, compSrcPath)
7576
7660
  });
7577
7661
  results.push({
7578
7662
  file: `compositions/${file}`,
@@ -7590,6 +7674,7 @@ async function lintProject(projectDir, entryFile, hostOptions = {}) {
7590
7674
  ...lintAudioSrcNotFound(projectDir, allHtmlSources),
7591
7675
  ...lintMissingLocalAsset(projectDir, allHtmlSources),
7592
7676
  ...lintTextureMaskAssetNotFound(projectDir, allHtmlSources),
7677
+ ...lintLinkedStylesheetAssets(projectDir, allHtmlSources),
7593
7678
  ...!entryFile ? lintMultipleRootCompositions(projectDir) : [],
7594
7679
  ...!entryFile ? lintBlankRootWithStandaloneComposition(rootHtml, allHtmlSources) : [],
7595
7680
  ...lintDuplicateAudioTracks(projectDir, rootHtml),
@@ -7799,6 +7884,62 @@ function lintTextureMaskAssetNotFound(projectDir, htmlSources) {
7799
7884
  };
7800
7885
  });
7801
7886
  }
7887
+ function nearestAncestorMatch(projectDir, cssRootRelativePath, url) {
7888
+ const root = resolve(projectDir);
7889
+ for (let dir = resolve(root, dirname2(cssRootRelativePath), ".."); ; dir = dirname2(dir)) {
7890
+ if (!isWithinProjectRoot2(projectDir, dir)) return null;
7891
+ if (resolveLocalAssetCandidates(projectDir, relative2(root, join3(dir, url))).some(existsSync2)) {
7892
+ return relative2(dirname2(resolve(root, cssRootRelativePath)), join3(dir, url)).replace(
7893
+ /\\/g,
7894
+ "/"
7895
+ );
7896
+ }
7897
+ if (dir === root) return null;
7898
+ }
7899
+ }
7900
+ function lintLinkedStylesheetAssets(projectDir, htmlSources) {
7901
+ const seen = /* @__PURE__ */ new Set();
7902
+ const findings = [];
7903
+ for (const { html, compSrcPath } of htmlSources) {
7904
+ for (const { content, rootRelativePath } of collectLocalStylesheets(
7905
+ projectDir,
7906
+ parseHTML3(html).document,
7907
+ compSrcPath
7908
+ )) {
7909
+ let root;
7910
+ try {
7911
+ root = postcss4.parse(content);
7912
+ } catch (error) {
7913
+ if (!(error instanceof postcss4.CssSyntaxError)) throw error;
7914
+ continue;
7915
+ }
7916
+ root.walkDecls((declaration) => {
7917
+ if (/^(?:-webkit-)?mask-image$/i.test(declaration.prop)) return;
7918
+ for (const match of declaration.value.matchAll(CSS_URL_RE)) {
7919
+ const rawUrl = match[1] ?? match[2] ?? match[3] ?? "";
7920
+ if (isUnresolvedAssetPlaceholder2(rawUrl)) continue;
7921
+ const url = cleanAssetUrl2(rawUrl);
7922
+ if (!url || isRemoteOrInlineUrl2(url) || url.startsWith("/")) continue;
7923
+ if (resolveCssAssetCandidates(projectDir, url, void 0, rootRelativePath).some(existsSync2))
7924
+ continue;
7925
+ const key = `${rootRelativePath}\0${url}`;
7926
+ if (seen.has(key)) continue;
7927
+ seen.add(key);
7928
+ const found = nearestAncestorMatch(projectDir, rootRelativePath, url);
7929
+ findings.push({
7930
+ code: "missing_stylesheet_asset",
7931
+ severity: "warning",
7932
+ file: join3(projectDir, rootRelativePath),
7933
+ message: `${rootRelativePath} references url(${url}), which is not found beside the stylesheet (looked in ${dirname2(rootRelativePath)}/). The browser resolves url() from the stylesheet, so the file will 404.`,
7934
+ fixHint: found ? `A file exists at "${found}" relative to the stylesheet. Change the url() to that path.` : `Add "${url}" beside the stylesheet, or point the url() at an existing file.`
7935
+ });
7936
+ }
7937
+ });
7938
+ }
7939
+ }
7940
+ return findings;
7941
+ }
7942
+ var PAGES_OPENED_BESIDE_INDEX = /* @__PURE__ */ new Set(["caption-skin.html", "podcast-lanes.html"]);
7802
7943
  function lintMultipleRootCompositions(projectDir) {
7803
7944
  const findings = [];
7804
7945
  try {
@@ -7807,7 +7948,7 @@ function lintMultipleRootCompositions(projectDir) {
7807
7948
  );
7808
7949
  const rootCompositions = [];
7809
7950
  for (const file of rootHtmlFiles) {
7810
- if (file === "caption-skin.html") continue;
7951
+ if (PAGES_OPENED_BESIDE_INDEX.has(file)) continue;
7811
7952
  const content = readFileSync(join3(projectDir, file), "utf-8");
7812
7953
  if (/data-composition-id/i.test(content)) {
7813
7954
  rootCompositions.push(file);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/lint",
3
- "version": "0.8.221",
3
+ "version": "0.8.223",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/heygen-com/hyperframes",
@@ -35,7 +35,7 @@
35
35
  "linkedom": "^0.18.12",
36
36
  "postcss": "^8.5.8",
37
37
  "postcss-selector-parser": "^7.1.4",
38
- "@hyperframes/parsers": "0.8.221"
38
+ "@hyperframes/parsers": "0.8.223"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^25.0.10",