fix(render): scope strict lint to composition (#2261)

* fix(render): scope strict lint to composition

* fix(lint): validate explicit composition scope
This commit is contained in:
Miguel Ángel
2026-07-12 22:18:11 -04:00
committed by GitHub
parent 2e34a2d5a0
commit 38badff820
3 changed files with 54 additions and 8 deletions
+2 -1
View File
@@ -807,7 +807,8 @@ export default defineCommand({
// ── Pre-render lint ────────────────────────────────────────────────── // ── Pre-render lint ──────────────────────────────────────────────────
{ {
const lintResult = await lintProject(project.dir); const explicitEntry = args.composition && args.composition !== "." ? entryFile : undefined;
const lintResult = await lintProject(project.dir, explicitEntry);
if (!quiet && (lintResult.totalErrors > 0 || lintResult.totalWarnings > 0)) { if (!quiet && (lintResult.totalErrors > 0 || lintResult.totalWarnings > 0)) {
console.log(""); console.log("");
for (const line of formatLintFindings(lintResult, { errorsFirst: true })) console.log(line); for (const line of formatLintFindings(lintResult, { errorsFirst: true })) console.log(line);
@@ -876,6 +876,43 @@ describe("texture_mask_asset_not_found", () => {
}); });
describe("multiple_root_compositions", () => { describe("multiple_root_compositions", () => {
it("scopes lint to an explicit render composition entry", async () => {
const project = makeProject(validHtml());
const standalone = join(project, "standalone.html");
writeFileSync(standalone, validHtml("standalone"));
const { totalErrors, results } = await lintProject(project, standalone);
expect(totalErrors).toBe(0);
expect(results.map((result) => result.file)).toEqual(["standalone.html"]);
expect(
results[0]?.result.findings.find((finding) => finding.code === "multiple_root_compositions"),
).toBeUndefined();
});
it("reports findings from an explicit render composition entry", async () => {
const project = makeProject(validHtml());
const standalone = join(project, "standalone.html");
writeFileSync(standalone, htmlWithMissingMediaId());
const { totalErrors, results } = await lintProject(project, standalone);
expect(totalErrors).toBeGreaterThan(0);
expect(results[0]?.result.findings.some((finding) => finding.code === "media_missing_id")).toBe(
true,
);
});
it("rejects an explicit render composition entry outside the project", async () => {
const project = makeProject(validHtml());
const outsideDir = tmpProject("outside-entry");
dirs.push(outsideDir);
const outsideEntry = join(outsideDir, "standalone.html");
writeFileSync(outsideEntry, validHtml("standalone"));
await expect(lintProject(project, outsideEntry)).rejects.toThrow(/outside.*project/i);
});
it("fires when two HTML files have data-composition-id", async () => { it("fires when two HTML files have data-composition-id", async () => {
const project = makeProject(validHtml()); const project = makeProject(validHtml());
writeFileSync( writeFileSync(
+15 -7
View File
@@ -180,8 +180,16 @@ function resolveCssAssetCandidates(
return resolveLocalAssetCandidates(projectDir, url); return resolveLocalAssetCandidates(projectDir, url);
} }
export async function lintProject(projectDir: string): Promise<ProjectLintResult> { export async function lintProject(
const indexPath = resolve(projectDir, "index.html"); projectDir: string,
entryFile?: string,
): Promise<ProjectLintResult> {
const indexPath = entryFile ? resolve(entryFile) : resolve(projectDir, "index.html");
if (entryFile && !isWithinProjectRoot(projectDir, indexPath)) {
throw new Error(`Explicit lint entry is outside the project directory: ${entryFile}`);
}
const rootFile = relative(resolve(projectDir), indexPath).replace(/\\/g, "/") || "index.html";
const rootCompSrcPath = rootFile === "index.html" ? undefined : rootFile;
const results: Array<{ file: string; result: HyperframeLintResult }> = []; const results: Array<{ file: string; result: HyperframeLintResult }> = [];
let totalErrors = 0; let totalErrors = 0;
let totalWarnings = 0; let totalWarnings = 0;
@@ -190,16 +198,16 @@ export async function lintProject(projectDir: string): Promise<ProjectLintResult
const rootHtml = readFileSync(indexPath, "utf-8"); const rootHtml = readFileSync(indexPath, "utf-8");
const rootResult = await lintHyperframeHtml(rootHtml, { const rootResult = await lintHyperframeHtml(rootHtml, {
filePath: indexPath, filePath: indexPath,
externalStyles: collectExternalStyles(projectDir, rootHtml), externalStyles: collectExternalStyles(projectDir, rootHtml, rootCompSrcPath),
}); });
results.push({ file: "index.html", result: rootResult }); results.push({ file: rootFile, result: rootResult });
totalErrors += rootResult.errorCount; totalErrors += rootResult.errorCount;
totalWarnings += rootResult.warningCount; totalWarnings += rootResult.warningCount;
totalInfos += rootResult.infoCount; totalInfos += rootResult.infoCount;
const allHtmlSources: HtmlSource[] = [{ html: rootHtml }]; const allHtmlSources: HtmlSource[] = [{ html: rootHtml, compSrcPath: rootCompSrcPath }];
const compositionsDir = resolve(projectDir, "compositions"); const compositionsDir = resolve(projectDir, "compositions");
if (existsSync(compositionsDir)) { if (!entryFile && existsSync(compositionsDir)) {
const collectHtmlFiles = (dir: string, rel: string): string[] => { const collectHtmlFiles = (dir: string, rel: string): string[] => {
const out: string[] = []; const out: string[] = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) { for (const entry of readdirSync(dir, { withFileTypes: true })) {
@@ -239,7 +247,7 @@ export async function lintProject(projectDir: string): Promise<ProjectLintResult
...lintAudioSrcNotFound(projectDir, allHtmlSources), ...lintAudioSrcNotFound(projectDir, allHtmlSources),
...lintMissingLocalAsset(projectDir, allHtmlSources), ...lintMissingLocalAsset(projectDir, allHtmlSources),
...lintTextureMaskAssetNotFound(projectDir, allHtmlSources), ...lintTextureMaskAssetNotFound(projectDir, allHtmlSources),
...lintMultipleRootCompositions(projectDir), ...(!entryFile ? lintMultipleRootCompositions(projectDir) : []),
...lintDuplicateAudioTracks(allHtmlSources), ...lintDuplicateAudioTracks(allHtmlSources),
...lintMissingOrEmptySubComposition(projectDir, rootHtml), ...lintMissingOrEmptySubComposition(projectDir, rootHtml),
]; ];