Skip to content

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

Merged
miguel-heygen merged 2 commits into
mainfrom
fix/strict-composition-lint-scope
Jul 13, 2026
Merged

fix(render): scope strict lint to composition#2261
miguel-heygen merged 2 commits into
mainfrom
fix/strict-composition-lint-scope

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What

Scope pre-render lint to the file selected by render --composition.

Why

On CLI 0.7.53, render --composition standalone.html --strict still linted sibling root HTML files, emitted multiple_root_compositions, and aborted. Users had to copy the same entry and assets into an isolated temporary project even though the command explicitly selected one composition.

How

  • Allow lintProject to accept an explicit entry file.
  • Treat that file as the root lint source and resolve its local assets relative to its location.
  • Skip unrelated compositions/ discovery and the project-wide multiple-root rule only for explicit-entry lint.
  • Pass the resolved entry from the render command when --composition is set.
  • Preserve existing whole-project behavior when rendering index.html.

Test plan

  • Reproduced published 0.7.53 strict abort with two valid root compositions
  • bunx vitest run packages/cli/src/utils/lintProject.test.ts packages/cli/src/commands/render.test.ts (125 passed)
  • bun run --filter @hyperframes/cli typecheck
  • npx oxfmt --check packages/lint/src/project.ts packages/cli/src/commands/render.ts packages/cli/src/utils/lintProject.test.ts
  • npx oxlint packages/lint/src/project.ts packages/cli/src/commands/render.ts packages/cli/src/utils/lintProject.test.ts

@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Field feedback exposed a related no-index case: render -c standalone.dc.html is rejected by resolveProject before the explicit entry is resolved. I opened stacked PR #2307. It also passes lintProject the absolute render target (the current render call passes project-relative entryFile, which resolves against process cwd and fails for explicit files). Verified end-to-end without root index.html.

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at ec4f456c.

Right scope for the fix. The trio of changes composes coherently: (1) target the explicit entry as the root HTML source, (2) skip compositions/ discovery so sibling entries aren't linted, (3) skip lintMultipleRootCompositions entirely since "multiple roots" is not a real problem when the caller explicitly named which root to use.

Traced the pieces that keep working correctly under the entry-scope path:

  • lintMissingOrEmptySubComposition(projectDir, rootHtml) at project.ts:249 walks data-composition-src refs from the root HTML — and those refs are always root-relative (per the comment mirroring assertSubCompositionsUsable), so scoping the root to standalone.html still resolves its sub-compositions correctly against projectDir. No regression.
  • collectExternalStyles(projectDir, rootHtml, rootCompSrcPath) — the newly-added rootCompSrcPath correctly threads the entry's rel path into collectLocalStylesheets, matching how nested sub-compositions already pass compSrcPath at project.ts:233. Stylesheet resolution stays consistent between the two.
  • The explicitEntry = args.composition && args.composition !== "." ? entryFile : undefined guard in render.ts is correctly derived — entryFile is set from resolveCompositionEntryArg(args.composition, ...) (statSync-validated) at line 396, so by the time we reach the lint gate the path is known-good.
  • Callers lint.ts:47, preview.ts:215, publish.ts:44 all still call lintProject(dir) positionally — the new entryFile? optional parameter is a pure additive extension, so those keep the old behavior.

Nits

  • 🟡 rootFile = relative(resolve(projectDir), indexPath).replace(/\\/g, "/") || "index.html" at project.ts:186 — if the caller passes an entry file outside projectDir, relative() produces "../other/entry.html", which flows into results.push({ file: rootFile }) and every downstream lint reporter as a path with parent traversal. No security issue (the file is still read from indexPath, not the rel path), but the display + asset resolution would misbehave. The caller in render.ts won't produce that shape today, but the function is now exported with a wider contract than that. A if (!indexPath.startsWith(resolve(projectDir))) throw new Error(...) guard would fail fast for future callers.

  • 🟡 Test covers the passing case (clean project, entry with 0 findings). Would be tighter to also assert that when the entry file itself contains real findings (e.g. a broken asset reference in standalone.html), those DO surface — proves the explicit-entry path is reporting entries, not just silently skipping the whole lint. One extra test case.

LGTM. Base of the stack — moving on to #2307 which builds on this.

Review by Rames D Jusso

@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

add a fail-fast guard for explicit entries outside projectDir, and prove findings in the explicit entry still surface

Addressed in 893072717: lintProject now rejects an explicit entry outside the project before any read/reporting, and the regression suite covers both the rejection and a real media_missing_id finding from the selected entry. Evidence: lintProject.test.ts 75/75; lint + CLI typecheck; targeted format check.

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R2 reviewed at 89307271, delta from R1 head ec4f456c.

Both R1 nits cleanly addressed:

  • Outside-project guardisWithinProjectRoot(projectDir, indexPath) gate at packages/lint/src/project.ts:188 reuses the same helper already used by resolveLocalAssetCandidates (line 142), so behavior is symmetric with the rest of the module. resolve(entryFile) without a base is safe here because the guard's relative(projectRoot, candidate) + !startsWith("..") && !isAbsolute(relativePath) catches both absolute-outside and CWD-relative escapes.
  • Non-clean explicit-entry regression — new it("reports findings from an explicit render composition entry", ...) asserts totalErrors > 0 and matches media_missing_id on the explicit entry. Companion it("rejects an explicit render composition entry outside the project", ...) locks the guard message shape.

LGTM from my side.

Review by Rames D Jusso

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving on Rames-D's go (R2 delta clean, nit round addressed) + Magi's CI-green confirmation. Verified at final head 89307271: CI green, no changes-requested, mergeable.

@miguel-heygen miguel-heygen merged commit 38badff into main Jul 13, 2026
54 checks passed
@miguel-heygen miguel-heygen deleted the fix/strict-composition-lint-scope branch July 13, 2026 02:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants