Skip to content

fix: resolve local Fern preview component imports#772

Merged
andreatnvidia merged 4 commits into
mainfrom
andreatgretel/fix/fern-local-preview-components
Jun 25, 2026
Merged

fix: resolve local Fern preview component imports#772
andreatnvidia merged 4 commits into
mainfrom
andreatgretel/fix/fern-local-preview-components

Conversation

@andreatnvidia

@andreatnvidia andreatnvidia commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fix local Fern fallback previews for custom component imports like @/components/Authors.
  • Build a real temporary components/ tree and add extensionless symlinks for .js, .jsx, .ts, and .tsx files.
  • Preserve the existing symlink behavior for the rest of the temporary preview root.

Context

The local fallback preview path was added so docs can still be served when the hosted nvidia Fern theme is unavailable. That path builds a temporary Fern root, removes global-theme: nvidia, and points Fern at the temporary root.

The temporary root previously symlinked components/ as a whole. That preserved the source files, but Fern's local renderer can resolve imports like @/components/Authors as an extensionless file path. In the real tree, the file is components/Authors.tsx, so local preview could render pages with Failed to render this content even though the component file existed.

This keeps the fallback preview behavior local to the temp root: component files are still symlinked from the source tree, and extensionless aliases are added only in the generated preview directory.

Testing

  • .venv/bin/ruff check --fix fern/scripts/serve-local-docs-preview.py
  • .venv/bin/ruff format fern/scripts/serve-local-docs-preview.py
  • .venv/bin/python fern/scripts/serve-local-docs-preview.py --root fern -- test -L components/Authors
  • local temp fixture covering components/Foo/ plus components/Foo.tsx
  • make check-fern-docs-locally - 0 errors, 1 existing warning

@github-actions

Copy link
Copy Markdown
Contributor

Fern preview: https://nvidia-preview-pr-772.docs.buildwithfern.com/nemo/datadesigner

Fern previews include the docs-website version archive with PR changes synced into latest. Notebook tutorials are rendered without execution outputs in previews.

@andreatnvidia
andreatnvidia marked this pull request as ready for review June 25, 2026 21:12
@andreatnvidia
andreatnvidia requested a review from a team as a code owner June 25, 2026 21:12
@github-actions

Copy link
Copy Markdown
Contributor

Code Review — PR #772: fix: resolve local Fern preview component imports

Summary

This PR fixes the local Fern docs fallback preview so that custom component imports like @/components/Authors resolve correctly. Previously the temp preview root symlinked the whole components/ directory as a single link. Fern's local renderer resolves @/components/Authors as an extensionless path, but the real file is components/Authors.tsx, so pages using such components rendered Failed to render this content.

The fix replaces the wholesale components/ symlink with link_components(), which:

  • Builds a real components/ directory tree in the preview root (mkdir per directory).
  • Symlinks each file back to the source tree.
  • For files with .js/.jsx/.ts/.tsx suffixes, adds an additional extensionless symlink alias (Authors.tsxAuthors) so import resolution succeeds.

Scope: 1 file, +21/-0. Structural risk: LOW (dev-tooling script, outside the three data_designer packages; layering invariants don't apply).

Findings

Correctness

  • Directory-before-file ordering is correct (good). sorted(components_root.rglob("*")) guarantees a parent directory is created before any file inside it, because a Path compares its parts tuple and the parent path is a strict prefix. The mkdir()-then-symlink flow is therefore safe for the common nested case.
  • Symlink targets are absolute (good). root is .resolve()d in main(), so both the real symlink and the extensionless alias point at absolute source paths — they remain valid regardless of the preview root's location.
  • (Minor) Extensionless alias can collide → FileExistsError. If two component files share a basename across allowed suffixes (e.g. Foo.ts and Foo.tsx, or Foo.js and Foo.tsx), both map to the same alias Foo; the second target.with_suffix("").symlink_to(source) raises FileExistsError and aborts the whole preview. Similarly, a file Authors.tsx alongside a directory Authors/ would collide (the dir is mkdir'd first, then the alias tries to create Authors). Both are unlikely in a Fern components/ tree (typically one file per component), but a guarding if not target.with_suffix("").exists(): (or try/except FileExistsError) would make the script robust rather than crashing on an edge case. Worth a one-line guard given the cost is trivial.
  • with_suffix("") only strips the last suffix. Foo.module.css-style names aren't in COMPONENT_EXTENSIONS, so not an issue here, but note the alias for Foo.test.tsx would be Foo.test (probably the desired behavior). No action needed.

Code Quality & Style

  • Module-level constant COMPONENT_EXTENSIONS is a clean, readable choice and matches the file's existing style (cf. LOCAL_THEME_CONFIG).
  • Type annotations on link_components(root, preview_root) -> None and the from __future__ import annotations header are present — consistent with project conventions.
  • The early-continue integration into build_preview_root is minimal and keeps the non-component path untouched, exactly as the PR description claims.
  • Naming (components_root, preview_components_root, source, target) is clear and consistent with the surrounding code.

Performance

  • Switching from one symlink to a per-file walk + mkdir/symlink is more syscalls, but this is a local dev-preview script run interactively; the cost is negligible.

Test Coverage

  • No automated test accompanies the change. This is consistent with the rest of this script (no existing tests for serve-local-docs-preview.py), and the PR documents manual verification: the test -L components/Authors symlink check and make check-fern-docs-locally (0 errors). Acceptable for a dev-tooling script, though the project's "no untested code paths" invariant formally applies only to the shipped packages, not fern/scripts/.

Security

  • No new external input, network, or secret handling. Symlinks point only within the resolved docs root into a process-scoped TemporaryDirectory. No concerns.

Structural Impact

(graphify, 2.2s)

Risk: LOW (localized change)

  • 1 Python files, 0 AST entities, 0/82 clusters

Verdict

Approve with a minor optional suggestion. The fix is correct for the common case, well-scoped, and matches project conventions. The only substantive item is the unguarded extensionless-alias symlink_to, which can raise FileExistsError on a basename collision (e.g. Foo.ts + Foo.tsx, or a file/dir name clash) and abort the preview. Adding an existence check or try/except FileExistsError around the alias creation would harden the script; it is not a blocker given how unlikely such a layout is in a Fern components/ tree.

@greptile-apps

greptile-apps Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Replaces the coarse components/ directory symlink in the local Fern preview with a recursive per-file walk that builds an identical directory tree and adds extensionless aliases (e.g. AuthorsAuthors.tsx) for every .js/.jsx/.ts/.tsx file, fixing @/components/Authors-style imports in the local renderer.

  • link_components() iterates the sorted rglob output so parent directories are always mkdir-ed before their children, then symlinks each file and conditionally adds the extensionless alias only when no path of that name already exists.
  • build_preview_root() now routes the components child through link_components() instead of the generic symlink_to fallback.

Confidence Score: 5/5

The change is scoped entirely to the local preview script and writes only into a tempfile.TemporaryDirectory, so it cannot affect production docs or the source tree.

The recursive walk guarantees directories are created before their contents because sorted lexicographic order places any directory path before paths nested inside it. The extensionless alias is guarded by alias.exists(), preventing a crash when a directory and a same-stem file coexist. No data-mutating or irreversible operations are involved.

No files require special attention.

Important Files Changed

Filename Overview
fern/scripts/serve-local-docs-preview.py Adds link_components() to replace the single-directory symlink with a recursive per-file walk that also creates extensionless aliases for .js/.jsx/.ts/.tsx files; collision guard (if not alias.exists()) prevents FileExistsError when a dir and stem-matched file coexist.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[build_preview_root] --> B{child.name == 'components'?}
    B -- Yes --> C[link_components root, preview_root]
    B -- No --> D[target.symlink_to child]
    C --> E[preview_components_root.mkdir]
    E --> F[sorted rglob over source components/]
    F --> G{source.is_dir?}
    G -- Yes --> H[target.mkdir]
    G -- No --> I[target.symlink_to source]
    I --> J{suffix in COMPONENT_EXTENSIONS?}
    J -- Yes --> K{alias.exists?}
    K -- No --> L[alias.symlink_to source]
    K -- Yes --> M[skip extensionless alias]
    J -- No --> N[done]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[build_preview_root] --> B{child.name == 'components'?}
    B -- Yes --> C[link_components root, preview_root]
    B -- No --> D[target.symlink_to child]
    C --> E[preview_components_root.mkdir]
    E --> F[sorted rglob over source components/]
    F --> G{source.is_dir?}
    G -- Yes --> H[target.mkdir]
    G -- No --> I[target.symlink_to source]
    I --> J{suffix in COMPONENT_EXTENSIONS?}
    J -- Yes --> K{alias.exists?}
    K -- No --> L[alias.symlink_to source]
    K -- Yes --> M[skip extensionless alias]
    J -- No --> N[done]
Loading

Reviews (2): Last reviewed commit: "fix: guard local preview component alias..." | Re-trigger Greptile

Comment thread fern/scripts/serve-local-docs-preview.py Outdated

@johnnygreco johnnygreco left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice work on this one, @andreatgretel — this is a tidy fix for a pretty awkward local-preview edge case.

Summary

This PR updates the local Fern fallback preview so components/ is materialized as a temporary tree with symlinked source files and extensionless aliases for JS/TS component imports. The implementation matches the PR description and keeps the change scoped to the fallback preview path.

Findings

No findings.

What Looks Good

  • The change preserves the existing temp-root symlink behavior for non-components/ entries, so the fix stays nicely localized.
  • The extensionless aliases cover both top-level components like Authors.tsx and nested data modules like devnotes/deep-research-trajectories/example-marcia.ts.
  • The same-stem directory guard from the follow-up commit avoids the collision raised in the earlier review thread without complicating the common path.

Verdict

Ship it — ready to merge as-is.


This review was generated by an AI assistant.

@andreatnvidia
andreatnvidia merged commit ea69663 into main Jun 25, 2026
66 checks passed
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