Skip to content

fix: better type narrowing escape hatches for custom rel/type#735

Merged
harlan-zw merged 5 commits into
mainfrom
worktree-define-link-script
Apr 10, 2026
Merged

fix: better type narrowing escape hatches for custom rel/type#735
harlan-zw merged 5 commits into
mainfrom
worktree-define-link-script

Conversation

@harlan-zw

@harlan-zw harlan-zw commented Apr 10, 2026

Copy link
Copy Markdown
Collaborator

🔗 Linked issue

❓ Type of change

  • 📖 Documentation
  • 🐞 Bug fix
  • 👌 Enhancement
  • ✨ New feature
  • 🧹 Chore
  • ⚠️ Breaking change

📚 Description

The v3 docstrings and migration guide recommended { rel: '...' } satisfies GenericLink (and the script equivalent) as the pattern for custom rel/type values. That pattern never type-checked against `useHead`, because `GenericLink` and `GenericScript` are intentionally excluded from the `Link` / `Script` unions to prevent silent absorption of known values (e.g. `rel: 'preload'` without `as` staying an error instead of collapsing into the permissive shape).

Two fixes:

  1. `defineLink` / `defineScript` helpers for non-standard values. They use per-element conditional inference (`InferLink` / `InferScript`) so known values stay strict (`rel: 'preload'` still requires `as`, preload-font still requires `crossorigin`, `type: 'module'` still requires `src` or inline content, `type: 'application/ld+json'` still requires `textContent`), and only genuinely unknown values fall through to `GenericLink` / `GenericScript`.
  2. Expanded `KnownLinkRel` with standard link rel keywords that were missing: `me`, `webmention`, `privacy-policy`, `terms-of-service`, `expect`, `compression-dictionary`, and `alternate stylesheet`. These now work directly with `useHead` without needing `defineLink`. `alternate stylesheet` requires `title` per spec; `expect` carries an optional `blocking: 'render'`.

Docstrings on `GenericLink`, `Link`, `GenericScript`, `Script`, plus the migration guide, use-head API reference, and v3 release notes all updated to point at the helpers (and to use genuinely non-standard rels like `openid2.provider` in examples). 17 new type-level tests in `packages/unhead/test/unit/define.test.ts` cover custom-value acceptance, strict-known-value enforcement, the newly-added standard rels, and edge cases like the Favicon rel union and preload image with only `imagesrcset`.

Summary by CodeRabbit

  • New Features

    • Added defineLink and defineScript helpers and exported them for type-safe custom link/script declarations.
    • Expanded recognized standard link/script types and tightened per-rel/type constraints and inline-content rules.
  • Documentation

    • Updated migration, release, and API docs to show defineLink/defineScript patterns and revised examples.
  • Tests

    • Added unit tests validating the new helpers and enforcement of rel/type-specific rules.

…type

The `satisfies GenericLink` / `satisfies GenericScript` pattern the v3
docstrings and migration guide recommended did not actually type-check
against useHead, because `GenericLink` / `GenericScript` are intentionally
excluded from the `Link` / `Script` unions to prevent silent absorption
of known `rel` / `type` values.

Introduce `defineLink` and `defineScript` helpers that keep known-value
strictness (e.g. `rel: 'preload'` still requires `as`, `type: 'module'`
still requires `src` or inline content) while accepting custom values
via `GenericLink` / `GenericScript`. Update docstrings, migration guide,
release notes, and the useHead API reference accordingly.
… etc.)

Several standard `<link>` rel keywords were missing from `KnownLinkRel`
and the `Link` union, making them look like "custom" rels that needed
`defineLink`. Add dedicated interfaces and union members for: `me`,
`webmention`, `privacy-policy`, `terms-of-service`, `expect`,
`compression-dictionary`, and `alternate stylesheet`.

`alternate stylesheet` requires `title` per spec; `expect` carries an
optional `blocking: 'render'`. Update `defineLink` examples and docs to
use genuinely non-standard rels (`openid2.provider`, `EditURI`) instead
of rels that are now directly supported.
@coderabbitai

coderabbitai Bot commented Apr 10, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 235acd6e-93c3-4ab2-b87d-078962022a2d

📥 Commits

Reviewing files that changed from the base of the PR and between b473934 and a08fa46.

📒 Files selected for processing (2)
  • packages/unhead/src/types/schema/script.ts
  • packages/unhead/test/unit/define.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/unhead/test/unit/define.test.ts

📝 Walkthrough

Walkthrough

Adds typed helpers defineLink and defineScript, extends Link/Script discriminated unions with several new specific variants, introduces InferLink/InferScript type utilities, updates exports and docs to use the new helpers, and adds unit tests and export contract updates for these changes.

Changes

Cohort / File(s) Summary
Documentation
docs/6.migration-guide/1.v3.md, docs/7.releases/1.v3.md, docs/head/7.api/composables/0.use-head.md
Replace examples that used satisfies GenericLink/GenericScript with guidance and examples using defineLink/defineScript (or as const) for custom rel/type values; update inline snippets and migration notes.
New helpers & re-exports
packages/unhead/src/define.ts, packages/unhead/src/index.ts
Add defineLink and defineScript typed helper functions (type-only casts) and re-export them from package index.
Link type additions & inference
packages/unhead/src/types/schema/link.ts
Add narrow link interfaces (MeLink, WebmentionLink, PrivacyPolicyLink, TermsOfServiceLink, ExpectLink, CompressionDictionaryLink, AlternateStylesheetLink), extend KnownLinkRel and Link union, and introduce InferLink<T>/internal matching utility.
Script type tightening & inference
packages/unhead/src/types/schema/script.ts
Tighten data script inline-content typing (mutually exclusive textContent/innerHTML), add KnownScriptType, InferScript<T>, and internal match utilities to map type to strict Script members or generic Script.
Public type re-exports
packages/unhead/src/types/schema/head.ts
Re-export newly added narrowed link/script types and the InferLink/InferScript helpers in the public head types surface.
Tests & export contract
packages/unhead/test/unit/define.test.ts, test/exports/unhead.yaml
Add unit tests validating defineLink/defineScript TypeScript behavior and runtime use with useHead; update export contract YAML to include defineLink and defineScript.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I nibble types with careful cheer,
define each link, make scripts sincere.
Strict where needed, open where kind,
A rabbit’s hop for code refined. 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: introducing better type narrowing escape hatches (defineLink/defineScript helpers) for custom rel/type values.
Description check ✅ Passed The description provides comprehensive detail on the problem, the two main fixes (helpers and expanded KnownLinkRel), specific examples, and mentions testing coverage.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-define-link-script

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Bundle Size Gzipped
Client (Minimal) 10.7 kB 4.4 kB
Server (Minimal) 10.6 kB 4.3 kB
Vue Client (Minimal) 11.8 kB 4.8 kB
Vue Server (Minimal) 11.6 kB 4.7 kB

@harlan-zw harlan-zw changed the title feat(unhead): defineLink/defineScript helpers + standard link rels fix(unhead): better type narrowing escape hatches for custom rel/type Apr 10, 2026
@harlan-zw harlan-zw changed the title fix(unhead): better type narrowing escape hatches for custom rel/type fix: better type narrowing escape hatches for custom rel/type Apr 10, 2026

Copilot AI 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.

Pull request overview

This PR fixes the previously recommended (but non-type-checking) pattern for custom <link rel> / <script type> values by introducing typed defineLink / defineScript helpers that preserve strict discriminated-union narrowing for known values while allowing truly unknown values via the generic fallback types. It also expands the built-in KnownLinkRel set to include several missing standard rel keywords so they work directly with useHead.

Changes:

  • Add defineLink / defineScript helpers (exported from unhead) with conditional inference types (InferLink / InferScript).
  • Expand KnownLinkRel and the Link union with additional standard rel interfaces (including alternate stylesheet with required title).
  • Update docs/migration guidance and add new type-level unit tests covering strictness + custom-value acceptance.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/unhead/test/unit/define.test.ts Adds type-level tests covering new helpers and new standard rels.
packages/unhead/src/types/schema/script.ts Updates GenericScript docs and adds KnownScriptType + InferScript inference utilities.
packages/unhead/src/types/schema/link.ts Adds new standard rel interfaces, extends KnownLinkRel/Link, and adds InferLink.
packages/unhead/src/types/schema/head.ts Re-exports the newly added link/script types and inference helpers.
packages/unhead/src/index.ts Exposes defineLink / defineScript from the main package entry.
packages/unhead/src/define.ts Implements the new defineLink / defineScript runtime helpers.
docs/head/7.api/composables/0.use-head.md Updates API docs to recommend helpers instead of satisfies GenericLink/GenericScript.
docs/7.releases/1.v3.md Updates v3 notes to reference the new helpers for custom rel/type values.
docs/6.migration-guide/1.v3.md Updates migration guide examples to use defineLink / defineScript.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/unhead/test/unit/define.test.ts Outdated
Comment thread packages/unhead/test/unit/define.test.ts Outdated

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/unhead/src/types/schema/script.ts`:
- Around line 481-487: The Data script unions allow empty payloads because
DataScriptTextContent uses optional textContent/innerHTML; update the
InferScript-related union branches so that for JSON/LD/speculation scripts the
branch types (e.g., JsonLdScript, SpeculationRulesScript, ApplicationJsonScript
/ the DataScriptTextContent union used by InferScript) require at least one
content field (make textContent or innerHTML non-optional in their respective
union arms) so defineScript({ type: 'application/ld+json' }) without content is
rejected, and add a unit test asserting that creating those scripts without
textContent or innerHTML fails to prevent regression.

In `@packages/unhead/test/unit/define.test.ts`:
- Line 85: Update the two test titles in
packages/unhead/test/unit/define.test.ts that currently read 'rel="..."' to
reference 'type="..."' instead; specifically change the description string in
the it(...) call that begins with 'still enforces `src` or inline content on
rel="module"' (and the similar one at the other occurrence) so they read 'still
enforces `src` or inline content on type="module"' to accurately reflect script
type constraints.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ab74e4c2-7a92-4053-a570-efa96d265b05

📥 Commits

Reviewing files that changed from the base of the PR and between 64b5ac0 and 8a12dca.

📒 Files selected for processing (9)
  • docs/6.migration-guide/1.v3.md
  • docs/7.releases/1.v3.md
  • docs/head/7.api/composables/0.use-head.md
  • packages/unhead/src/define.ts
  • packages/unhead/src/index.ts
  • packages/unhead/src/types/schema/head.ts
  • packages/unhead/src/types/schema/link.ts
  • packages/unhead/src/types/schema/script.ts
  • packages/unhead/test/unit/define.test.ts

Comment thread packages/unhead/src/types/schema/script.ts
Comment thread packages/unhead/test/unit/define.test.ts Outdated
DataScriptTextContent used optional textContent/innerHTML in both union
branches, allowing empty data scripts like `{ type: 'application/ld+json' }`
to type-check with no content at all. Make each branch require its
respective content field so JsonLdScript, SpeculationRulesScript, and
ApplicationJsonScript enforce non-empty payloads at the type level.

Add type tests covering empty-payload rejection for ld+json, speculation
rules, application/json, and importmap. Rename two script test titles
from `rel="..."` to `type="..."` per review feedback.
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.

2 participants