@hyperframes/lint 0.8.159 → 0.8.160

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.
Files changed (3) hide show
  1. package/dist/browser.js +124 -12
  2. package/dist/index.js +198 -31
  3. package/package.json +2 -2
package/dist/browser.js CHANGED
@@ -1495,10 +1495,10 @@ var coreRules = [
1495
1495
  }
1496
1496
  ];
1497
1497
  for (const script of scripts) {
1498
- const withoutComments = stripJsComments(script.content);
1499
- const executable = stripStringLiterals(withoutComments);
1498
+ const withoutComments2 = stripJsComments(script.content);
1499
+ const executable = stripStringLiterals(withoutComments2);
1500
1500
  for (const { pattern, label, hint, scansStrings } of patterns) {
1501
- const match = pattern.exec(scansStrings ? withoutComments : executable);
1501
+ const match = pattern.exec(scansStrings ? withoutComments2 : executable);
1502
1502
  if (match) {
1503
1503
  findings.push({
1504
1504
  ...locate(script, match.index),
@@ -3878,8 +3878,8 @@ function collectStaticCssVariableDefinitions(tags, styles) {
3878
3878
  ];
3879
3879
  const definitionPattern = /(?:^|[;{])\s*(--[A-Za-z0-9_-]+)\s*:/gm;
3880
3880
  for (const source of sources) {
3881
- const withoutComments = source.replace(/\/\*[\s\S]*?\*\//g, " ");
3882
- for (const match of withoutComments.matchAll(definitionPattern)) {
3881
+ const withoutComments2 = source.replace(/\/\*[\s\S]*?\*\//g, " ");
3882
+ for (const match of withoutComments2.matchAll(definitionPattern)) {
3883
3883
  if (match[1]) definitions.add(match[1]);
3884
3884
  }
3885
3885
  }
@@ -4073,8 +4073,8 @@ function targetedSelectorTokens(selector) {
4073
4073
  const compounds = group.trim().split(/[\s>+~]+/).filter(Boolean);
4074
4074
  const last = compounds[compounds.length - 1];
4075
4075
  if (!last) continue;
4076
- const simple2 = last.match(/[#.][A-Za-z0-9_-]+/g);
4077
- if (simple2) for (const token of simple2) tokens.add(token);
4076
+ const simple3 = last.match(/[#.][A-Za-z0-9_-]+/g);
4077
+ if (simple3) for (const token of simple3) tokens.add(token);
4078
4078
  }
4079
4079
  return tokens;
4080
4080
  }
@@ -4276,8 +4276,8 @@ function firstParamMemberAccessHazard(fn) {
4276
4276
  while ((match = pattern.exec(fn.body)) !== null) {
4277
4277
  const member = match[1] ?? "";
4278
4278
  const after = fn.body.slice(match.index + match[0].length);
4279
- const isCall = /^\s*\(/.test(after);
4280
- if (isCall && NUMBER_METHODS.has(member)) continue;
4279
+ const isCall2 = /^\s*\(/.test(after);
4280
+ if (isCall2 && NUMBER_METHODS.has(member)) continue;
4281
4281
  return member;
4282
4282
  }
4283
4283
  return null;
@@ -6367,6 +6367,116 @@ var structureRules = [
6367
6367
  }
6368
6368
  ];
6369
6369
 
6370
+ // src/shaderDomains.ts
6371
+ import { parse as parse3, parseExpressionAt } from "acorn";
6372
+ import { simple as simple2 } from "acorn-walk";
6373
+ function withoutComments(source) {
6374
+ return source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\r\n]*/g, (text) => text.replace(/[^\r\n]/g, " "));
6375
+ }
6376
+ function staticShaders(source) {
6377
+ const shaders = [];
6378
+ try {
6379
+ const ast = parse3(source, {
6380
+ ecmaVersion: "latest",
6381
+ sourceType: "module",
6382
+ allowReturnOutsideFunction: true
6383
+ });
6384
+ simple2(ast, {
6385
+ Literal(node) {
6386
+ if (typeof node.value !== "string") return;
6387
+ shaders.push({
6388
+ text: node.value,
6389
+ start: node.start + 1,
6390
+ exactOffsets: source.slice(node.start + 1, node.end - 1) === node.value
6391
+ });
6392
+ },
6393
+ TemplateLiteral(node) {
6394
+ if (node.expressions.length !== 0) return;
6395
+ const value = node.quasis[0]?.value;
6396
+ if (value?.cooked == null) return;
6397
+ shaders.push({
6398
+ text: value.cooked,
6399
+ start: node.start + 1,
6400
+ exactOffsets: source.slice(node.start + 1, node.end - 1) === value.cooked
6401
+ });
6402
+ }
6403
+ });
6404
+ } catch {
6405
+ }
6406
+ return shaders.filter(({ text }) => /\bvoid\s+main\s*\(/.test(withoutComments(text)));
6407
+ }
6408
+ function isNumber(node, value) {
6409
+ return node?.type === "Literal" && node.value === value;
6410
+ }
6411
+ function isCall(node, name) {
6412
+ return node?.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === name;
6413
+ }
6414
+ function unboundedCosine(node) {
6415
+ if (node.type !== "CallExpression") return false;
6416
+ if (isCall(node, "dot")) return node.arguments.length === 2;
6417
+ if (!isCall(node, "max") || node.arguments.length !== 2) return false;
6418
+ const a = argumentAt(node, 0);
6419
+ const b = argumentAt(node, 1);
6420
+ return zeroFlooredDot(a, b) || zeroFlooredDot(b, a);
6421
+ }
6422
+ function argumentAt(call, index) {
6423
+ const node = call.arguments[index];
6424
+ return node?.type === "SpreadElement" ? void 0 : node;
6425
+ }
6426
+ function zeroFlooredDot(a, b) {
6427
+ return isCall(a, "dot") && isNumber(b, 0);
6428
+ }
6429
+ function unsafeCosinePower(call) {
6430
+ if (call.arguments.length !== 2) return false;
6431
+ const [base] = call.arguments;
6432
+ return base?.type === "BinaryExpression" && base.operator === "-" && base.left.type !== "PrivateIdentifier" && isNumber(base.left, 1) && unboundedCosine(base.right);
6433
+ }
6434
+ function unsafePowOffsets(source) {
6435
+ const offsets = [];
6436
+ const masked = withoutComments(source);
6437
+ for (const match of masked.matchAll(/\bpow\s*\(/g)) {
6438
+ try {
6439
+ const expression = parseExpressionAt(masked, match.index, { ecmaVersion: "latest" });
6440
+ simple2(expression, {
6441
+ CallExpression(node) {
6442
+ if (node.start === match.index && unsafeCosinePower(node)) offsets.push(node.start);
6443
+ }
6444
+ });
6445
+ } catch {
6446
+ }
6447
+ }
6448
+ return offsets;
6449
+ }
6450
+ function shaderDomainOffsets(source, glsl = false) {
6451
+ const shaders = glsl ? [{ text: source, start: 0, exactOffsets: true }] : staticShaders(source);
6452
+ return shaders.flatMap(
6453
+ ({ text, start, exactOffsets }) => unsafePowOffsets(text).map((offset) => exactOffsets ? start + offset : void 0)
6454
+ );
6455
+ }
6456
+
6457
+ // src/rules/shaders.ts
6458
+ function shaderDomainWarning() {
6459
+ return {
6460
+ code: "shader_unbounded_cosine_pow",
6461
+ severity: "warning",
6462
+ message: "Shader pow(1 - dot(...), ...) can receive a negative base when the cosine exceeds 1, including through GPU rounding. Undefined GLSL results can spread through glow or transmission textures as black artifacts.",
6463
+ fixHint: "Bound the cosine on both sides: pow(1.0 - clamp(dot(normal, view), 0.0, 1.0), exponent). A lower-only max(dot(...), 0.0) does not bound the upper end. Inspect the affected frames on the target GPU."
6464
+ };
6465
+ }
6466
+ var shaderRules = [
6467
+ ({ scripts, locate }) => scripts.flatMap((script) => {
6468
+ const tag = `<script ${script.attrs}>`;
6469
+ if (readAttr(tag, "src") !== null) return [];
6470
+ const type = readAttr(tag, "type") ?? "";
6471
+ const glsl = /^(?:x-shader\/x-(?:fragment|vertex)|text\/glsl)$/i.test(type);
6472
+ if (type && !glsl && !/^(?:module|(?:text|application)\/javascript)$/i.test(type)) return [];
6473
+ return shaderDomainOffsets(script.content, glsl).map((offset) => ({
6474
+ ...shaderDomainWarning(),
6475
+ ...locate(script, offset)
6476
+ }));
6477
+ })
6478
+ ];
6479
+
6370
6480
  // src/hyperframeLinter.ts
6371
6481
  var RULE_GROUPS = [
6372
6482
  { group: "core", rules: coreRules },
@@ -6378,7 +6488,8 @@ var RULE_GROUPS = [
6378
6488
  { group: "textures", rules: textureRules },
6379
6489
  { group: "fonts", rules: fontRules },
6380
6490
  { group: "slideshow", rules: slideshowRules },
6381
- { group: "structure", rules: structureRules }
6491
+ { group: "structure", rules: structureRules },
6492
+ { group: "shaders", rules: shaderRules }
6382
6493
  ];
6383
6494
  var LINT_RULE_COUNT = RULE_GROUPS.reduce((n, g) => n + g.rules.length, 0);
6384
6495
  var LINT_RULE_GROUP_COUNTS = Object.fromEntries(
@@ -6390,7 +6501,8 @@ function dedupeKeyFor(finding) {
6390
6501
  finding.severity,
6391
6502
  finding.selector || "",
6392
6503
  finding.elementId || "",
6393
- finding.message
6504
+ finding.message,
6505
+ ...finding.code === "shader_unbounded_cosine_pow" ? [finding.line, finding.column] : []
6394
6506
  ].join("|");
6395
6507
  }
6396
6508
  async function runRules(ctx, filePath) {
@@ -6428,7 +6540,7 @@ async function lintHyperframeHtml(html, options = {}) {
6428
6540
  const { findings: rawFindings, timings } = await runRules(ctx, options.filePath);
6429
6541
  const locate = createSourceLocator(html);
6430
6542
  const findings = rawFindings.map((finding) => {
6431
- if (finding.line !== void 0 || finding.code === "invalid_inline_script_syntax" || finding.code === "non_deterministic_code" || [
6543
+ if (finding.line !== void 0 || finding.code === "shader_unbounded_cosine_pow" || finding.code === "invalid_inline_script_syntax" || finding.code === "non_deterministic_code" || [
6432
6544
  "gsap_infinite_repeat",
6433
6545
  "gsap_repeat_ceil_overshoot",
6434
6546
  "gsap_repeat_floor_unclamped"
package/dist/index.js CHANGED
@@ -1501,10 +1501,10 @@ var coreRules = [
1501
1501
  }
1502
1502
  ];
1503
1503
  for (const script of scripts) {
1504
- const withoutComments = stripJsComments(script.content);
1505
- const executable = stripStringLiterals(withoutComments);
1504
+ const withoutComments2 = stripJsComments(script.content);
1505
+ const executable = stripStringLiterals(withoutComments2);
1506
1506
  for (const { pattern, label, hint, scansStrings } of patterns) {
1507
- const match = pattern.exec(scansStrings ? withoutComments : executable);
1507
+ const match = pattern.exec(scansStrings ? withoutComments2 : executable);
1508
1508
  if (match) {
1509
1509
  findings.push({
1510
1510
  ...locate(script, match.index),
@@ -3884,8 +3884,8 @@ function collectStaticCssVariableDefinitions(tags, styles) {
3884
3884
  ];
3885
3885
  const definitionPattern = /(?:^|[;{])\s*(--[A-Za-z0-9_-]+)\s*:/gm;
3886
3886
  for (const source of sources) {
3887
- const withoutComments = source.replace(/\/\*[\s\S]*?\*\//g, " ");
3888
- for (const match of withoutComments.matchAll(definitionPattern)) {
3887
+ const withoutComments2 = source.replace(/\/\*[\s\S]*?\*\//g, " ");
3888
+ for (const match of withoutComments2.matchAll(definitionPattern)) {
3889
3889
  if (match[1]) definitions.add(match[1]);
3890
3890
  }
3891
3891
  }
@@ -4079,8 +4079,8 @@ function targetedSelectorTokens(selector) {
4079
4079
  const compounds = group.trim().split(/[\s>+~]+/).filter(Boolean);
4080
4080
  const last = compounds[compounds.length - 1];
4081
4081
  if (!last) continue;
4082
- const simple2 = last.match(/[#.][A-Za-z0-9_-]+/g);
4083
- if (simple2) for (const token of simple2) tokens.add(token);
4082
+ const simple3 = last.match(/[#.][A-Za-z0-9_-]+/g);
4083
+ if (simple3) for (const token of simple3) tokens.add(token);
4084
4084
  }
4085
4085
  return tokens;
4086
4086
  }
@@ -4282,8 +4282,8 @@ function firstParamMemberAccessHazard(fn) {
4282
4282
  while ((match = pattern.exec(fn.body)) !== null) {
4283
4283
  const member = match[1] ?? "";
4284
4284
  const after = fn.body.slice(match.index + match[0].length);
4285
- const isCall = /^\s*\(/.test(after);
4286
- if (isCall && NUMBER_METHODS.has(member)) continue;
4285
+ const isCall2 = /^\s*\(/.test(after);
4286
+ if (isCall2 && NUMBER_METHODS.has(member)) continue;
4287
4287
  return member;
4288
4288
  }
4289
4289
  return null;
@@ -6373,6 +6373,116 @@ var structureRules = [
6373
6373
  }
6374
6374
  ];
6375
6375
 
6376
+ // src/shaderDomains.ts
6377
+ import { parse as parse3, parseExpressionAt } from "acorn";
6378
+ import { simple as simple2 } from "acorn-walk";
6379
+ function withoutComments(source) {
6380
+ return source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\r\n]*/g, (text) => text.replace(/[^\r\n]/g, " "));
6381
+ }
6382
+ function staticShaders(source) {
6383
+ const shaders = [];
6384
+ try {
6385
+ const ast = parse3(source, {
6386
+ ecmaVersion: "latest",
6387
+ sourceType: "module",
6388
+ allowReturnOutsideFunction: true
6389
+ });
6390
+ simple2(ast, {
6391
+ Literal(node) {
6392
+ if (typeof node.value !== "string") return;
6393
+ shaders.push({
6394
+ text: node.value,
6395
+ start: node.start + 1,
6396
+ exactOffsets: source.slice(node.start + 1, node.end - 1) === node.value
6397
+ });
6398
+ },
6399
+ TemplateLiteral(node) {
6400
+ if (node.expressions.length !== 0) return;
6401
+ const value = node.quasis[0]?.value;
6402
+ if (value?.cooked == null) return;
6403
+ shaders.push({
6404
+ text: value.cooked,
6405
+ start: node.start + 1,
6406
+ exactOffsets: source.slice(node.start + 1, node.end - 1) === value.cooked
6407
+ });
6408
+ }
6409
+ });
6410
+ } catch {
6411
+ }
6412
+ return shaders.filter(({ text }) => /\bvoid\s+main\s*\(/.test(withoutComments(text)));
6413
+ }
6414
+ function isNumber(node, value) {
6415
+ return node?.type === "Literal" && node.value === value;
6416
+ }
6417
+ function isCall(node, name) {
6418
+ return node?.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === name;
6419
+ }
6420
+ function unboundedCosine(node) {
6421
+ if (node.type !== "CallExpression") return false;
6422
+ if (isCall(node, "dot")) return node.arguments.length === 2;
6423
+ if (!isCall(node, "max") || node.arguments.length !== 2) return false;
6424
+ const a = argumentAt(node, 0);
6425
+ const b = argumentAt(node, 1);
6426
+ return zeroFlooredDot(a, b) || zeroFlooredDot(b, a);
6427
+ }
6428
+ function argumentAt(call, index) {
6429
+ const node = call.arguments[index];
6430
+ return node?.type === "SpreadElement" ? void 0 : node;
6431
+ }
6432
+ function zeroFlooredDot(a, b) {
6433
+ return isCall(a, "dot") && isNumber(b, 0);
6434
+ }
6435
+ function unsafeCosinePower(call) {
6436
+ if (call.arguments.length !== 2) return false;
6437
+ const [base] = call.arguments;
6438
+ return base?.type === "BinaryExpression" && base.operator === "-" && base.left.type !== "PrivateIdentifier" && isNumber(base.left, 1) && unboundedCosine(base.right);
6439
+ }
6440
+ function unsafePowOffsets(source) {
6441
+ const offsets = [];
6442
+ const masked = withoutComments(source);
6443
+ for (const match of masked.matchAll(/\bpow\s*\(/g)) {
6444
+ try {
6445
+ const expression = parseExpressionAt(masked, match.index, { ecmaVersion: "latest" });
6446
+ simple2(expression, {
6447
+ CallExpression(node) {
6448
+ if (node.start === match.index && unsafeCosinePower(node)) offsets.push(node.start);
6449
+ }
6450
+ });
6451
+ } catch {
6452
+ }
6453
+ }
6454
+ return offsets;
6455
+ }
6456
+ function shaderDomainOffsets(source, glsl = false) {
6457
+ const shaders = glsl ? [{ text: source, start: 0, exactOffsets: true }] : staticShaders(source);
6458
+ return shaders.flatMap(
6459
+ ({ text, start, exactOffsets }) => unsafePowOffsets(text).map((offset) => exactOffsets ? start + offset : void 0)
6460
+ );
6461
+ }
6462
+
6463
+ // src/rules/shaders.ts
6464
+ function shaderDomainWarning() {
6465
+ return {
6466
+ code: "shader_unbounded_cosine_pow",
6467
+ severity: "warning",
6468
+ message: "Shader pow(1 - dot(...), ...) can receive a negative base when the cosine exceeds 1, including through GPU rounding. Undefined GLSL results can spread through glow or transmission textures as black artifacts.",
6469
+ fixHint: "Bound the cosine on both sides: pow(1.0 - clamp(dot(normal, view), 0.0, 1.0), exponent). A lower-only max(dot(...), 0.0) does not bound the upper end. Inspect the affected frames on the target GPU."
6470
+ };
6471
+ }
6472
+ var shaderRules = [
6473
+ ({ scripts, locate }) => scripts.flatMap((script) => {
6474
+ const tag = `<script ${script.attrs}>`;
6475
+ if (readAttr(tag, "src") !== null) return [];
6476
+ const type = readAttr(tag, "type") ?? "";
6477
+ const glsl = /^(?:x-shader\/x-(?:fragment|vertex)|text\/glsl)$/i.test(type);
6478
+ if (type && !glsl && !/^(?:module|(?:text|application)\/javascript)$/i.test(type)) return [];
6479
+ return shaderDomainOffsets(script.content, glsl).map((offset) => ({
6480
+ ...shaderDomainWarning(),
6481
+ ...locate(script, offset)
6482
+ }));
6483
+ })
6484
+ ];
6485
+
6376
6486
  // src/hyperframeLinter.ts
6377
6487
  var RULE_GROUPS = [
6378
6488
  { group: "core", rules: coreRules },
@@ -6384,7 +6494,8 @@ var RULE_GROUPS = [
6384
6494
  { group: "textures", rules: textureRules },
6385
6495
  { group: "fonts", rules: fontRules },
6386
6496
  { group: "slideshow", rules: slideshowRules },
6387
- { group: "structure", rules: structureRules }
6497
+ { group: "structure", rules: structureRules },
6498
+ { group: "shaders", rules: shaderRules }
6388
6499
  ];
6389
6500
  var LINT_RULE_COUNT = RULE_GROUPS.reduce((n, g) => n + g.rules.length, 0);
6390
6501
  var LINT_RULE_GROUP_COUNTS = Object.fromEntries(
@@ -6396,7 +6507,8 @@ function dedupeKeyFor(finding) {
6396
6507
  finding.severity,
6397
6508
  finding.selector || "",
6398
6509
  finding.elementId || "",
6399
- finding.message
6510
+ finding.message,
6511
+ ...finding.code === "shader_unbounded_cosine_pow" ? [finding.line, finding.column] : []
6400
6512
  ].join("|");
6401
6513
  }
6402
6514
  async function runRules(ctx, filePath) {
@@ -6434,7 +6546,7 @@ async function lintHyperframeHtml(html, options = {}) {
6434
6546
  const { findings: rawFindings, timings } = await runRules(ctx, options.filePath);
6435
6547
  const locate = createSourceLocator(html);
6436
6548
  const findings = rawFindings.map((finding) => {
6437
- if (finding.line !== void 0 || finding.code === "invalid_inline_script_syntax" || finding.code === "non_deterministic_code" || [
6549
+ if (finding.line !== void 0 || finding.code === "shader_unbounded_cosine_pow" || finding.code === "invalid_inline_script_syntax" || finding.code === "non_deterministic_code" || [
6438
6550
  "gsap_infinite_repeat",
6439
6551
  "gsap_repeat_ceil_overshoot",
6440
6552
  "gsap_repeat_floor_unclamped"
@@ -6530,7 +6642,7 @@ function shouldBlockRender(strictErrors, strictAll, totalErrors, totalWarnings)
6530
6642
  // src/project.ts
6531
6643
  import { createHash } from "crypto";
6532
6644
  import { existsSync as existsSync2, readFileSync, readdirSync } from "fs";
6533
- import { dirname, extname, join as join2, relative, resolve } from "path";
6645
+ import { dirname as dirname2, extname, join as join3, relative as relative2, resolve } from "path";
6534
6646
  import { rewriteAssetPath as rewriteAssetPath2 } from "@hyperframes/parsers/asset-paths";
6535
6647
  import { checkSubCompositionUsability } from "@hyperframes/parsers/sub-composition-validity";
6536
6648
  import { parseHTML as parseHTML3 } from "linkedom";
@@ -6539,10 +6651,10 @@ import {
6539
6651
  collectSubCompositionSrcs,
6540
6652
  isRemoteOrInlineUrl as isRemoteOrInlineUrl2,
6541
6653
  isUnresolvedAssetPlaceholder as isUnresolvedAssetPlaceholder2,
6542
- isWithinProjectRoot,
6654
+ isWithinProjectRoot as isWithinProjectRoot2,
6543
6655
  maskNonScannableRanges as maskNonScannableRanges2,
6544
- readProjectFile,
6545
- resolveExistingLocalAsset as resolveExistingLocalAsset2,
6656
+ readProjectFile as readProjectFile2,
6657
+ resolveExistingLocalAsset as resolveExistingLocalAsset3,
6546
6658
  resolveLocalAssetCandidates,
6547
6659
  resolveProjectRelativeSrc
6548
6660
  } from "@hyperframes/parsers/asset-resolution";
@@ -6783,6 +6895,60 @@ async function lintHevcPreviewCodec(candidates) {
6783
6895
  ];
6784
6896
  }
6785
6897
 
6898
+ // src/projectShaders.ts
6899
+ import { realpathSync } from "fs";
6900
+ import { dirname, join as join2, relative } from "path";
6901
+ import {
6902
+ isWithinProjectRoot,
6903
+ readProjectFile,
6904
+ resolveExistingLocalAsset as resolveExistingLocalAsset2
6905
+ } from "@hyperframes/parsers/asset-resolution";
6906
+ function isAuthoredScript(path) {
6907
+ return /\.(?:js|mjs|cjs)$/i.test(path) && !/(?:\.min\.js$|(?:^|[/\\])(?:vendor|node_modules)[/\\])/i.test(path);
6908
+ }
6909
+ function localScript(projectDir, src, compSrcPath) {
6910
+ if (/^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i.test(src)) return;
6911
+ const besideFile = src.startsWith("/") ? src : join2(dirname(compSrcPath ?? "index.html"), src);
6912
+ const asset = resolveExistingLocalAsset2(projectDir, besideFile);
6913
+ if (!asset) return;
6914
+ try {
6915
+ const file = realpathSync(asset.resolved);
6916
+ if (!isWithinProjectRoot(realpathSync(projectDir), file)) return;
6917
+ if (!isAuthoredScript(relative(projectDir, file))) return;
6918
+ return file;
6919
+ } catch {
6920
+ return;
6921
+ }
6922
+ }
6923
+ function linkedScripts(projectDir, { html, compSrcPath }) {
6924
+ const files = [];
6925
+ for (const script of parseHtmlStructure(html).scripts) {
6926
+ const tag = `<script ${script.attrs}>`;
6927
+ const type = readDecodedAttr(tag, "type") ?? "";
6928
+ if (type && !/^(?:module|(?:text|application)\/javascript)$/i.test(type)) continue;
6929
+ const src = readDecodedAttr(tag, "src");
6930
+ if (!src) continue;
6931
+ const file = localScript(projectDir, src, compSrcPath);
6932
+ if (file) files.push(file);
6933
+ }
6934
+ return files;
6935
+ }
6936
+ function lintProjectShaderScripts(projectDir, sources) {
6937
+ const files = new Set(sources.flatMap((source) => linkedScripts(projectDir, source)));
6938
+ const findings = [];
6939
+ for (const file of files) {
6940
+ const read = readProjectFile(file);
6941
+ if (read.kind !== "file") continue;
6942
+ for (const offset of shaderDomainOffsets(read.text)) {
6943
+ findings.push({
6944
+ ...shaderDomainWarning(),
6945
+ ...offset === void 0 ? { file } : sourcePosition(read.text, offset, file)
6946
+ });
6947
+ }
6948
+ }
6949
+ return findings;
6950
+ }
6951
+
6786
6952
  // src/project.ts
6787
6953
  function parseSubCompHtml(html) {
6788
6954
  return parseHTML3(html).document;
@@ -6814,10 +6980,10 @@ function collectLocalStylesheets(projectDir, document, compSrcPath) {
6814
6980
  if (!rel.split(/\s+/).some((part) => part.toLowerCase() === "stylesheet")) continue;
6815
6981
  const href = link.getAttribute("href") ?? "";
6816
6982
  if (!isLocalStylesheetHref(href)) continue;
6817
- const besideFile = compSrcPath ? join2(dirname(compSrcPath), href) : href;
6818
- const stylesheet = resolveExistingLocalAsset2(projectDir, besideFile) ?? (compSrcPath && staysInsideProjectRoot(href) ? resolveExistingLocalAsset2(projectDir, href) : null);
6983
+ const besideFile = compSrcPath ? join3(dirname2(compSrcPath), href) : href;
6984
+ const stylesheet = resolveExistingLocalAsset3(projectDir, besideFile) ?? (compSrcPath && staysInsideProjectRoot(href) ? resolveExistingLocalAsset3(projectDir, href) : null);
6819
6985
  if (!stylesheet) continue;
6820
- const read = readProjectFile(stylesheet.resolved);
6986
+ const read = readProjectFile2(stylesheet.resolved);
6821
6987
  if (read.kind !== "file") continue;
6822
6988
  styles.push({ href, content: read.text, rootRelativePath: stylesheet.rootRelativePath });
6823
6989
  }
@@ -6831,7 +6997,7 @@ function collectExternalStyles(projectDir, html, compSrcPath) {
6831
6997
  document,
6832
6998
  compSrcPath
6833
6999
  )) {
6834
- styles.push({ href, content, file: join2(projectDir, rootRelativePath), rootRelativePath });
7000
+ styles.push({ href, content, file: join3(projectDir, rootRelativePath), rootRelativePath });
6835
7001
  }
6836
7002
  return styles;
6837
7003
  }
@@ -6858,22 +7024,22 @@ function collectCssSources(projectDir, html, compSrcPath) {
6858
7024
  function resolveCssAssetCandidates(projectDir, url, htmlCompSrcPath, cssRootRelativePath) {
6859
7025
  if (url.startsWith("/")) return resolveLocalAssetCandidates(projectDir, url);
6860
7026
  if (cssRootRelativePath) {
6861
- return resolveLocalAssetCandidates(projectDir, join2(dirname(cssRootRelativePath), url));
7027
+ return resolveLocalAssetCandidates(projectDir, join3(dirname2(cssRootRelativePath), url));
6862
7028
  }
6863
7029
  if (htmlCompSrcPath) {
6864
7030
  return resolveLocalAssetCandidates(
6865
7031
  projectDir,
6866
- rewriteAssetPath2(htmlCompSrcPath, url, (path) => existsSync2(join2(projectDir, path)))
7032
+ rewriteAssetPath2(htmlCompSrcPath, url, (path) => existsSync2(join3(projectDir, path)))
6867
7033
  );
6868
7034
  }
6869
7035
  return resolveLocalAssetCandidates(projectDir, url);
6870
7036
  }
6871
7037
  async function lintProject(projectDir, entryFile, hostOptions = {}) {
6872
7038
  const indexPath = entryFile ? resolve(entryFile) : resolve(projectDir, "index.html");
6873
- if (entryFile && !isWithinProjectRoot(projectDir, indexPath)) {
7039
+ if (entryFile && !isWithinProjectRoot2(projectDir, indexPath)) {
6874
7040
  throw new Error(`Explicit lint entry is outside the project directory: ${entryFile}`);
6875
7041
  }
6876
- const rootFile = relative(resolve(projectDir), indexPath).replace(/\\/g, "/") || "index.html";
7042
+ const rootFile = relative2(resolve(projectDir), indexPath).replace(/\\/g, "/") || "index.html";
6877
7043
  const rootCompSrcPath = rootFile === "index.html" ? void 0 : rootFile;
6878
7044
  const results = [];
6879
7045
  let totalErrors = 0;
@@ -6899,7 +7065,7 @@ async function lintProject(projectDir, entryFile, hostOptions = {}) {
6899
7065
  const relPath = rel ? `${rel}/${entry.name}` : entry.name;
6900
7066
  if (entry.isDirectory()) {
6901
7067
  if (!rel && entry.name === "components") continue;
6902
- out.push(...collectHtmlFiles(join2(dir, entry.name), relPath));
7068
+ out.push(...collectHtmlFiles(join3(dir, entry.name), relPath));
6903
7069
  } else if (entry.isFile() && entry.name.endsWith(".html") && !entry.name.startsWith("._")) {
6904
7070
  out.push(relPath);
6905
7071
  }
@@ -6908,7 +7074,7 @@ async function lintProject(projectDir, entryFile, hostOptions = {}) {
6908
7074
  };
6909
7075
  const files = collectHtmlFiles(compositionsDir, "").sort();
6910
7076
  for (const file of files) {
6911
- const filePath = join2(compositionsDir, file);
7077
+ const filePath = join3(compositionsDir, file);
6912
7078
  const html = readFileSync(filePath, "utf-8");
6913
7079
  const compSrcPath = `compositions/${file}`;
6914
7080
  allHtmlSources.push({ html, compSrcPath });
@@ -6931,6 +7097,7 @@ async function lintProject(projectDir, entryFile, hostOptions = {}) {
6931
7097
  }
6932
7098
  }
6933
7099
  const projectFindings = [
7100
+ ...lintProjectShaderScripts(projectDir, allHtmlSources),
6934
7101
  ...lintProjectAudioFiles(projectDir, allHtmlSources),
6935
7102
  ...lintAudioSrcNotFound(projectDir, allHtmlSources),
6936
7103
  ...lintMissingLocalAsset(projectDir, allHtmlSources),
@@ -7026,7 +7193,7 @@ function lintAudioSrcNotFound(projectDir, htmlSources) {
7026
7193
  const src = mediaSrcOf(match);
7027
7194
  if (/^(https?:|data:|blob:)/i.test(src)) continue;
7028
7195
  if (isUnresolvedAssetPlaceholder2(src)) continue;
7029
- const rootRelative = compSrcPath ? rewriteAssetPath2(compSrcPath, src, (path) => existsSync2(join2(projectDir, path))) : src;
7196
+ const rootRelative = compSrcPath ? rewriteAssetPath2(compSrcPath, src, (path) => existsSync2(join3(projectDir, path))) : src;
7030
7197
  if (!existsSync2(resolveProjectRelativeSrc(rootRelative, projectDir))) {
7031
7198
  const file = sourceFile(projectDir, compSrcPath);
7032
7199
  missingByFile.set(file, (missingByFile.get(file) ?? /* @__PURE__ */ new Set()).add(src));
@@ -7060,8 +7227,8 @@ function lintMissingLocalAsset(projectDir, htmlSources) {
7060
7227
  const src = cleanAssetUrl2(rawSrc);
7061
7228
  if (!src) continue;
7062
7229
  if (isRemoteOrInlineUrl2(src)) continue;
7063
- const rootRelative = compSrcPath ? rewriteAssetPath2(compSrcPath, src, (path) => existsSync2(join2(projectDir, path))) : src;
7064
- const resolvedAsset = resolveExistingLocalAsset2(projectDir, rootRelative);
7230
+ const rootRelative = compSrcPath ? rewriteAssetPath2(compSrcPath, src, (path) => existsSync2(join3(projectDir, path))) : src;
7231
+ const resolvedAsset = resolveExistingLocalAsset3(projectDir, rootRelative);
7065
7232
  if (resolvedAsset) continue;
7066
7233
  const resolvedKey = resolve(projectDir, rootRelative);
7067
7234
  const file = sourceFile(projectDir, compSrcPath);
@@ -7129,7 +7296,7 @@ function lintMultipleRootCompositions(projectDir) {
7129
7296
  const rootCompositions = [];
7130
7297
  for (const file of rootHtmlFiles) {
7131
7298
  if (file === "caption-skin.html") continue;
7132
- const content = readFileSync(join2(projectDir, file), "utf-8");
7299
+ const content = readFileSync(join3(projectDir, file), "utf-8");
7133
7300
  if (/data-composition-id/i.test(content)) {
7134
7301
  rootCompositions.push(file);
7135
7302
  }
@@ -7195,7 +7362,7 @@ function lintMissingOrEmptySubComposition(projectDir, rootHtml) {
7195
7362
  const filePath = resolve(projectDir, srcPath);
7196
7363
  if (visited.has(filePath)) continue;
7197
7364
  visited.add(filePath);
7198
- const read = readProjectFile(filePath);
7365
+ const read = readProjectFile2(filePath);
7199
7366
  if (read.kind === "missing") {
7200
7367
  if (!checked.has(srcPath)) {
7201
7368
  checked.set(srcPath, { srcPath, problem: "the file does not exist" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/lint",
3
- "version": "0.8.159",
3
+ "version": "0.8.160",
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.159"
38
+ "@hyperframes/parsers": "0.8.160"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^25.0.10",