mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
feat(cli): auto-detect aspect_ratio from composition dims when --aspect-ratio is omitted (#1145)
When the user runs `hyperframes cloud render` without `--aspect-ratio` and the project source is a local directory, parse the entry HTML's root `<div data-composition-id ...>` for `data-width` / `data-height` and pick the supported aspect ratio that matches within ±0.05 tolerance: - 16:9 (≈1.778) ← landscape 1920×1080, 4K 3840×2160, etc. - 9:16 (≈0.563) ← portrait 1080×1920 - 1:1 (=1.0) ← square 1080×1080 If the composition's ratio matches one of these, the CLI sets `aspect_ratio` in the submit body and prints a one-line note (`Detected aspect ratio: 9:16 (from index.html dims 1080×1920)`). If the composition has no root div, no dims, or a ratio outside all three tolerance bands (e.g. 4:5, 5:4, 21:9), the CLI logs a one-line warning explaining the fallback and leaves `aspect_ratio` out of the submit body — the server defaults to 16:9, and the user can pass `--aspect-ratio` explicitly to override. Explicit `--aspect-ratio` always wins. Detection is skipped for `--asset-id` / `--url` project sources since the composition isn't on disk; user gets a brief note in that case too. New helper: `packages/cli/src/cloud/detectAspectRatio.ts` (pure regex parse, no DOM library dep). 23 tests cover canonical matches, in-band tolerance, all three non-match patterns (no root div, no dims, ratio out of bands), and authoring edge cases (unquoted attrs, attribute order, self-closing tags, multi-composition files). Closes the `auto` carve-out flagged in ef#38182's deferred-scope note — the CLI gets auto-detect without requiring a server-side zip-parse capability (no API change).
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
ASPECT_RATIO_MATCH_TOLERANCE,
|
||||
detectAspectRatioFromHtmlString,
|
||||
} from "./detectAspectRatio.js";
|
||||
|
||||
const ROOT_DIV = (width: string | number, height: string | number, extra = "") =>
|
||||
`<div data-composition-id="root" data-width="${width}" data-height="${height}"${extra ? " " + extra : ""}></div>`;
|
||||
|
||||
describe("detectAspectRatioFromHtmlString — matches", () => {
|
||||
it("matches 16:9 on canonical 1920x1080", () => {
|
||||
const result = detectAspectRatioFromHtmlString(ROOT_DIV(1920, 1080));
|
||||
expect(result.kind).toBe("matched");
|
||||
if (result.kind === "matched") {
|
||||
expect(result.aspectRatio).toBe("16:9");
|
||||
expect(result.width).toBe(1920);
|
||||
expect(result.height).toBe(1080);
|
||||
}
|
||||
});
|
||||
|
||||
it("matches 9:16 on canonical 1080x1920", () => {
|
||||
const result = detectAspectRatioFromHtmlString(ROOT_DIV(1080, 1920));
|
||||
expect(result.kind).toBe("matched");
|
||||
if (result.kind === "matched") {
|
||||
expect(result.aspectRatio).toBe("9:16");
|
||||
}
|
||||
});
|
||||
|
||||
it("matches 1:1 on canonical 1080x1080", () => {
|
||||
const result = detectAspectRatioFromHtmlString(ROOT_DIV(1080, 1080));
|
||||
expect(result.kind).toBe("matched");
|
||||
if (result.kind === "matched") {
|
||||
expect(result.aspectRatio).toBe("1:1");
|
||||
}
|
||||
});
|
||||
|
||||
it("matches 16:9 on 1280x720 (smaller resolution, same ratio)", () => {
|
||||
const result = detectAspectRatioFromHtmlString(ROOT_DIV(1280, 720));
|
||||
expect(result.kind).toBe("matched");
|
||||
if (result.kind === "matched") expect(result.aspectRatio).toBe("16:9");
|
||||
});
|
||||
|
||||
it("matches 16:9 on 3840x2160 (4K)", () => {
|
||||
const result = detectAspectRatioFromHtmlString(ROOT_DIV(3840, 2160));
|
||||
expect(result.kind).toBe("matched");
|
||||
if (result.kind === "matched") expect(result.aspectRatio).toBe("16:9");
|
||||
});
|
||||
|
||||
it("matches within tolerance for 1916x1080 (16:9 with slight pixel slop)", () => {
|
||||
// 1916/1080 = 1.774 — within ±0.05 of 16/9 = 1.778
|
||||
const result = detectAspectRatioFromHtmlString(ROOT_DIV(1916, 1080));
|
||||
expect(result.kind).toBe("matched");
|
||||
if (result.kind === "matched") expect(result.aspectRatio).toBe("16:9");
|
||||
});
|
||||
});
|
||||
|
||||
describe("detectAspectRatioFromHtmlString — non-matches surface as warnings", () => {
|
||||
it("returns no-match for 4:5 portrait social (864x1080)", () => {
|
||||
const result = detectAspectRatioFromHtmlString(ROOT_DIV(864, 1080));
|
||||
expect(result.kind).toBe("no-match");
|
||||
if (result.kind === "no-match") {
|
||||
expect(result.width).toBe(864);
|
||||
expect(result.height).toBe(1080);
|
||||
expect(result.ratio).toBeCloseTo(0.8, 2);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns no-match for 5:4 (1350x1080) — outside 1:1 tolerance band", () => {
|
||||
const result = detectAspectRatioFromHtmlString(ROOT_DIV(1350, 1080));
|
||||
expect(result.kind).toBe("no-match");
|
||||
if (result.kind === "no-match") {
|
||||
expect(result.ratio).toBeCloseTo(1.25, 2);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns no-match for 3:2 ratio (1500x1000) — outside all three bands", () => {
|
||||
// 1500/1000 = 1.5. 16:9 = 1.778 (Δ ≈ 0.28), 1:1 = 1.0 (Δ = 0.5),
|
||||
// 9:16 = 0.5625 (Δ ≈ 0.94). All outside ±0.05. Common ratio for
|
||||
// older photo-style compositions; needs the no-match warning so
|
||||
// the user opts into a supported ratio explicitly.
|
||||
const result = detectAspectRatioFromHtmlString(ROOT_DIV(1500, 1000));
|
||||
expect(result.kind).toBe("no-match");
|
||||
if (result.kind === "no-match") {
|
||||
expect(result.ratio).toBeCloseTo(1.5, 2);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns no-match for 21:9 ultrawide (2560x1080)", () => {
|
||||
// 2560/1080 = 2.37. Outside ±0.05 of 16/9 = 1.778.
|
||||
const result = detectAspectRatioFromHtmlString(ROOT_DIV(2560, 1080));
|
||||
expect(result.kind).toBe("no-match");
|
||||
});
|
||||
|
||||
it("4:5 is genuinely outside the 1:1 tolerance band — sanity-check the cutoff", () => {
|
||||
// 4:5 = 0.8; 1:1 = 1.0; difference = 0.2 — well outside 0.05 tolerance.
|
||||
// This pins the tolerance choice: if someone tightens or loosens it,
|
||||
// 4:5 must still NOT match 1:1, otherwise we silently mis-classify
|
||||
// portrait social as square.
|
||||
const ratio = 0.8;
|
||||
const diffFromOne = Math.abs(ratio - 1.0);
|
||||
expect(diffFromOne).toBeGreaterThan(ASPECT_RATIO_MATCH_TOLERANCE);
|
||||
});
|
||||
});
|
||||
|
||||
describe("detectAspectRatioFromHtmlString — structural edge cases", () => {
|
||||
it("returns no-root-div when HTML has no composition root", () => {
|
||||
const html = "<html><body><h1>no composition here</h1></body></html>";
|
||||
expect(detectAspectRatioFromHtmlString(html).kind).toBe("no-root-div");
|
||||
});
|
||||
|
||||
it("returns no-dims when root div is missing data-width", () => {
|
||||
const html = `<div data-composition-id="root" data-height="1080"></div>`;
|
||||
expect(detectAspectRatioFromHtmlString(html).kind).toBe("no-dims");
|
||||
});
|
||||
|
||||
it("returns no-dims when root div is missing data-height", () => {
|
||||
const html = `<div data-composition-id="root" data-width="1920"></div>`;
|
||||
expect(detectAspectRatioFromHtmlString(html).kind).toBe("no-dims");
|
||||
});
|
||||
|
||||
it("returns invalid-dims when width/height are zero", () => {
|
||||
const result = detectAspectRatioFromHtmlString(ROOT_DIV(0, 1080));
|
||||
expect(result.kind).toBe("invalid-dims");
|
||||
if (result.kind === "invalid-dims") expect(result.width).toBe(0);
|
||||
});
|
||||
|
||||
it("returns no-dims when data-width includes a sign character", () => {
|
||||
// The attribute extractor only captures unsigned numbers — a negative
|
||||
// value (`-1920`) doesn't match the regex at all and the field reads
|
||||
// as "missing". That's the desired outcome; we don't want to surface
|
||||
// negative dims as a separate failure mode the user has to interpret.
|
||||
const result = detectAspectRatioFromHtmlString(ROOT_DIV(-1920, 1080));
|
||||
expect(result.kind).toBe("no-dims");
|
||||
});
|
||||
|
||||
it("handles unquoted attribute values", () => {
|
||||
const html = `<div data-composition-id=root data-width=1920 data-height=1080></div>`;
|
||||
const result = detectAspectRatioFromHtmlString(html);
|
||||
expect(result.kind).toBe("matched");
|
||||
if (result.kind === "matched") expect(result.aspectRatio).toBe("16:9");
|
||||
});
|
||||
|
||||
it("handles single-quoted attribute values", () => {
|
||||
const html = `<div data-composition-id='root' data-width='1920' data-height='1080'></div>`;
|
||||
expect(detectAspectRatioFromHtmlString(html).kind).toBe("matched");
|
||||
});
|
||||
|
||||
it("ignores attributes on the wrong tag", () => {
|
||||
// A non-composition div has the dims; the actual root has neither. The
|
||||
// extractor must NOT swap attributes across tags.
|
||||
const html = `
|
||||
<div class="header" data-width="100" data-height="100"></div>
|
||||
<div data-composition-id="root"></div>
|
||||
`;
|
||||
expect(detectAspectRatioFromHtmlString(html).kind).toBe("no-dims");
|
||||
});
|
||||
|
||||
it("uses the FIRST composition root encountered (the page root)", () => {
|
||||
// Sub-compositions also have data-composition-id; the regex finds the
|
||||
// first match, which by HF convention is the page-level root.
|
||||
const html = `
|
||||
<div data-composition-id="root" data-width="1920" data-height="1080">
|
||||
<div data-composition-id="sub" data-width="1080" data-height="1080"></div>
|
||||
</div>
|
||||
`;
|
||||
const result = detectAspectRatioFromHtmlString(html);
|
||||
expect(result.kind).toBe("matched");
|
||||
if (result.kind === "matched") {
|
||||
expect(result.aspectRatio).toBe("16:9");
|
||||
expect(result.width).toBe(1920);
|
||||
expect(result.height).toBe(1080);
|
||||
}
|
||||
});
|
||||
|
||||
it("handles attributes in arbitrary order", () => {
|
||||
const html = `<div data-width="1080" id="root" data-height="1920" data-composition-id="root" class="bg"></div>`;
|
||||
const result = detectAspectRatioFromHtmlString(html);
|
||||
expect(result.kind).toBe("matched");
|
||||
if (result.kind === "matched") expect(result.aspectRatio).toBe("9:16");
|
||||
});
|
||||
|
||||
it("handles whitespace + newlines inside the opening tag", () => {
|
||||
const html = `<div
|
||||
data-composition-id="root"
|
||||
data-width="1080"
|
||||
data-height="1080"
|
||||
></div>`;
|
||||
expect(detectAspectRatioFromHtmlString(html).kind).toBe("matched");
|
||||
});
|
||||
|
||||
it("handles a self-closing-style tag (rare but valid in MDX/JSX-flavored input)", () => {
|
||||
const html = `<div data-composition-id="root" data-width="1920" data-height="1080"/>`;
|
||||
expect(detectAspectRatioFromHtmlString(html).kind).toBe("matched");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Auto-detect a HyperFrames composition's aspect ratio from the entry HTML's
|
||||
* root `<div data-composition-id ...>` `data-width` / `data-height` attributes.
|
||||
*
|
||||
* The cloud-render CLI uses this when the user hasn't passed `--aspect-ratio`
|
||||
* explicitly AND the project source is a local directory (asset-id and url
|
||||
* paths can't be inspected client-side). The result is passed through to the
|
||||
* `/v3/hyperframes/renders` request body, so the rendered output preserves
|
||||
* the composition's intended ratio without the user having to remember the
|
||||
* flag.
|
||||
*
|
||||
* Detection only matches the three values the API currently supports:
|
||||
* `16:9`, `9:16`, `1:1`. Anything else (e.g. 4:5, 5:4, or an unusual custom
|
||||
* ratio) returns a `"no-match"` result with the computed ratio for the
|
||||
* caller to surface to the user. `auto`-style fallbacks (server-side, etc.)
|
||||
* are out of scope here.
|
||||
*
|
||||
* Parsing approach: a narrow regex over the HTML. The root composition div is
|
||||
* a well-defined pattern (`data-composition-id` always appears on it) and we
|
||||
* only need two attributes off the same tag. Pulling in `jsdom` or a full DOM
|
||||
* parser is heavier than the problem warrants.
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export type SupportedAspectRatio = "16:9" | "9:16" | "1:1";
|
||||
|
||||
export type AspectRatioDetection =
|
||||
| {
|
||||
kind: "matched";
|
||||
aspectRatio: SupportedAspectRatio;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
| { kind: "no-root-div" }
|
||||
| { kind: "no-dims" }
|
||||
| { kind: "invalid-dims"; width: number; height: number }
|
||||
| { kind: "no-match"; width: number; height: number; ratio: number }
|
||||
| { kind: "read-error"; error: string };
|
||||
|
||||
// Absolute tolerance on the computed ratio. Wide enough to absorb
|
||||
// floating-point sloppiness on canonical ratios (e.g. 1920×1080 = 1.7778);
|
||||
// tight enough to keep 4:5 (0.8) and 5:4 (1.25) outside the bands so they
|
||||
// fall through to the "no-match" warning instead of getting silently
|
||||
// mis-classified as 1:1 or 16:9.
|
||||
const RATIO_TOLERANCE = 0.05;
|
||||
|
||||
const SUPPORTED_RATIOS: Array<{ value: SupportedAspectRatio; ratio: number }> = [
|
||||
{ value: "16:9", ratio: 16 / 9 },
|
||||
{ value: "9:16", ratio: 9 / 16 },
|
||||
{ value: "1:1", ratio: 1 },
|
||||
];
|
||||
|
||||
// First `<div ... data-composition-id="..." ...>` opening tag in the file.
|
||||
// Quote style is intentionally permissive — single, double, or unquoted all
|
||||
// match. Case-insensitive to handle `<DIV>` or `Data-Composition-Id` mid-edit.
|
||||
const ROOT_COMPOSITION_DIV_RE =
|
||||
/<div\b[^>]*?\bdata-composition-id\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)[^>]*>/i;
|
||||
|
||||
// `data-width` / `data-height` attribute extractors. Accept integer or float
|
||||
// values, quoted or unquoted. The `\d` class restricts to ASCII digits — no
|
||||
// locale comma surprises.
|
||||
const DATA_WIDTH_RE =
|
||||
/\bdata-width\s*=\s*(?:"(\d+(?:\.\d+)?)"|'(\d+(?:\.\d+)?)'|(\d+(?:\.\d+)?))(?=\s|>|\/)/i;
|
||||
const DATA_HEIGHT_RE =
|
||||
/\bdata-height\s*=\s*(?:"(\d+(?:\.\d+)?)"|'(\d+(?:\.\d+)?)'|(\d+(?:\.\d+)?))(?=\s|>|\/)/i;
|
||||
|
||||
function extractAttributeNumber(tag: string, re: RegExp): number | null {
|
||||
const match = tag.match(re);
|
||||
if (!match) return null;
|
||||
// First capture group that matched (quoted-double | quoted-single | unquoted).
|
||||
const raw = match[1] ?? match[2] ?? match[3];
|
||||
if (raw === undefined) return null;
|
||||
const value = Number(raw);
|
||||
return Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the HTML at `entryHtmlPath` and detect which supported aspect ratio
|
||||
* the composition's root div is authored at.
|
||||
*
|
||||
* Pure function except for `readFileSync` — no logging, no `process.exit`.
|
||||
* The caller decides how to surface each result kind to the user.
|
||||
*/
|
||||
export function detectAspectRatioFromHtml(entryHtmlPath: string): AspectRatioDetection {
|
||||
let html: string;
|
||||
try {
|
||||
html = readFileSync(entryHtmlPath, "utf-8");
|
||||
} catch (err) {
|
||||
return { kind: "read-error", error: err instanceof Error ? err.message : String(err) };
|
||||
}
|
||||
return detectAspectRatioFromHtmlString(html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as `detectAspectRatioFromHtml`, but takes the HTML as a string instead
|
||||
* of a file path. Exposed for tests + composition-string callers.
|
||||
*/
|
||||
export function detectAspectRatioFromHtmlString(html: string): AspectRatioDetection {
|
||||
const tagMatch = html.match(ROOT_COMPOSITION_DIV_RE);
|
||||
if (!tagMatch) return { kind: "no-root-div" };
|
||||
|
||||
const openTag = tagMatch[0];
|
||||
const width = extractAttributeNumber(openTag, DATA_WIDTH_RE);
|
||||
const height = extractAttributeNumber(openTag, DATA_HEIGHT_RE);
|
||||
|
||||
if (width === null || height === null) return { kind: "no-dims" };
|
||||
if (width <= 0 || height <= 0) return { kind: "invalid-dims", width, height };
|
||||
|
||||
const ratio = width / height;
|
||||
for (const candidate of SUPPORTED_RATIOS) {
|
||||
if (Math.abs(ratio - candidate.ratio) <= RATIO_TOLERANCE) {
|
||||
return { kind: "matched", aspectRatio: candidate.value, width, height };
|
||||
}
|
||||
}
|
||||
return { kind: "no-match", width, height, ratio };
|
||||
}
|
||||
|
||||
/**
|
||||
* The tolerance used when matching the computed ratio to a supported value.
|
||||
* Exposed for tests + caller introspection (e.g. warning messages that want
|
||||
* to mention the bounds).
|
||||
*/
|
||||
export const ASPECT_RATIO_MATCH_TOLERANCE = RATIO_TOLERANCE;
|
||||
@@ -26,6 +26,10 @@
|
||||
|
||||
import { defineCommand } from "citty";
|
||||
|
||||
import {
|
||||
detectAspectRatioFromHtml,
|
||||
type AspectRatioDetection,
|
||||
} from "../../cloud/detectAspectRatio.js";
|
||||
import { c } from "../../ui/colors.js";
|
||||
import { errorBox, formatBytes, formatDuration } from "../../ui/format.js";
|
||||
import { resolveProject } from "../../utils/project.js";
|
||||
@@ -182,7 +186,7 @@ export default defineCommand({
|
||||
const resolution = parseEnumFlag(args.resolution, VALID_RESOLUTION, {
|
||||
flag: "--resolution",
|
||||
});
|
||||
const aspectRatio = parseEnumFlag(args["aspect-ratio"], VALID_ASPECT_RATIO, {
|
||||
const explicitAspectRatio = parseEnumFlag(args["aspect-ratio"], VALID_ASPECT_RATIO, {
|
||||
flag: "--aspect-ratio",
|
||||
});
|
||||
const pollIntervalMs = parsePollIntervalMs(args["poll-interval"]);
|
||||
@@ -198,6 +202,14 @@ export default defineCommand({
|
||||
url: args.url,
|
||||
});
|
||||
|
||||
// When the user didn't pass --aspect-ratio explicitly AND the project is
|
||||
// a local dir, parse the entry HTML's root composition div for
|
||||
// data-width/data-height and pick the matching supported ratio. Saves
|
||||
// the user from having to specify a value that's already implicit in
|
||||
// the composition they authored. Explicit flag always wins.
|
||||
const aspectRatio =
|
||||
explicitAspectRatio ?? maybeAutoDetectAspectRatio(project, args.composition, asJson);
|
||||
|
||||
const variables = resolveVariablesAndValidateIfLocal(
|
||||
args.variables,
|
||||
args["variables-file"],
|
||||
@@ -341,6 +353,70 @@ function resolveProjectInput(opts: {
|
||||
return { kind: "dir", dir: opts.dir ?? "." };
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort aspect-ratio detection for the cloud-render submit body when
|
||||
* the user hasn't passed `--aspect-ratio`. Returns the detected value (one
|
||||
* of `"16:9" | "9:16" | "1:1"`) or `undefined` to let the server's default
|
||||
* (16:9) apply.
|
||||
*
|
||||
* Detection only fires when the project source is a local directory — for
|
||||
* `--asset-id` and `--url` the composition zip isn't on disk and parsing
|
||||
* it client-side isn't worth the extra fetch. The user gets a one-line
|
||||
* note explaining the fallback.
|
||||
*
|
||||
* Logs to stdout in human-readable mode; suppressed in `--json` mode so the
|
||||
* machine-readable output stays clean.
|
||||
*/
|
||||
// fallow-ignore-next-line complexity
|
||||
function maybeAutoDetectAspectRatio(
|
||||
project: ProjectInputSource,
|
||||
compositionArg: string | undefined,
|
||||
asJson: boolean,
|
||||
): "16:9" | "9:16" | "1:1" | undefined {
|
||||
if (project.kind !== "dir") {
|
||||
const reason = project.kind === "asset_id" ? "--asset-id" : "--url";
|
||||
logDetection(asJson, `Auto-detect skipped (project is ${reason})`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const dir = project.dir ?? ".";
|
||||
const entryRelative = compositionArg ?? "index.html";
|
||||
const entryPath = resolvePath(dir, entryRelative);
|
||||
|
||||
const detection = detectAspectRatioFromHtml(entryPath);
|
||||
logDetection(asJson, summarizeDetection(detection, entryRelative));
|
||||
return detection.kind === "matched" ? detection.aspectRatio : undefined;
|
||||
}
|
||||
|
||||
const ASPECT_FALLBACK_HINT =
|
||||
"server will default aspect_ratio to 16:9. Pass --aspect-ratio to override.";
|
||||
|
||||
function logDetection(asJson: boolean, message: string): void {
|
||||
if (asJson) return;
|
||||
// `matched` is the only branch with its own affirmative phrasing; the
|
||||
// rest share the fallback hint to keep the user oriented after a miss.
|
||||
const suffix = message.startsWith("Detected aspect ratio") ? "" : `; ${ASPECT_FALLBACK_HINT}`;
|
||||
console.log(c.dim(` ${message}${suffix}`));
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
function summarizeDetection(detection: AspectRatioDetection, entryRelative: string): string {
|
||||
switch (detection.kind) {
|
||||
case "matched":
|
||||
return `Detected aspect ratio: ${detection.aspectRatio} (from ${entryRelative} dims ${detection.width}×${detection.height})`;
|
||||
case "no-root-div":
|
||||
return `No <div data-composition-id> found in ${entryRelative}`;
|
||||
case "no-dims":
|
||||
return `${entryRelative} root composition has no data-width / data-height`;
|
||||
case "invalid-dims":
|
||||
return `${entryRelative} root has invalid dims (${detection.width}×${detection.height})`;
|
||||
case "no-match":
|
||||
return `${entryRelative} dims ${detection.width}×${detection.height} (ratio ${detection.ratio.toFixed(2)}) don't match 16:9, 9:16, or 1:1`;
|
||||
case "read-error":
|
||||
return `Couldn't read ${entryRelative} for aspect-ratio detection (${detection.error})`;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveVariablesAndValidateIfLocal(
|
||||
inline: string | undefined,
|
||||
filePath: string | undefined,
|
||||
|
||||
Reference in New Issue
Block a user