Skip to content

Fix bundled dashboard symlinks in stack-cli#1485

Merged
N2D4 merged 5 commits into
devfrom
devin/1779746952-fix-mintlify-dev-next-import
May 26, 2026
Merged

Fix bundled dashboard symlinks in stack-cli#1485
N2D4 merged 5 commits into
devfrom
devin/1779746952-fix-mintlify-dev-next-import

Conversation

@N2D4
Copy link
Copy Markdown
Contributor

@N2D4 N2D4 commented May 25, 2026

Fixes the packaged development-environment dashboard failing to resolve next after Mintlify setup.

Changes:

  • Copy the dashboard standalone runtime with symlink dereferencing so npm pack ships real files instead of broken pnpm symlinks.
  • Hoist dereferenced dependencies used by Next.js and instrumentation into the packaged dashboard runtime.
  • Keep runtime startup simpler by removing symlink manifest restoration.

Validation:

  • Reproduced the packed dashboard module-resolution failure before the fix.
  • pnpm --dir /home/ubuntu/repos/stack-auth --filter=@stackframe/stack-cli build
  • Packed and extracted @stackframe/stack-cli; verified dist/dashboard contains 0 symlinks.
  • Started extracted dist/dashboard/apps/dashboard/server.js; verified Next.js 16.1.7 and Ready.
  • pnpm --dir /home/ubuntu/repos/stack-auth --filter=@stackframe/stack-cli lint
  • pnpm --dir /home/ubuntu/repos/stack-auth --filter=@stackframe/stack-cli typecheck

Link to Devin session: https://app.devin.ai/sessions/13728b5ccea44f23b2d300c79ffc2440
Requested by: @N2D4

Summary by CodeRabbit

  • New Features

    • Dashboard packaging now preserves symlinks during bundling and runtime; hoisted dependency targets are detected and included so linked dependencies remain resolvable.
  • Bug Fixes

    • Resolved issues where symbolic links could be lost or broken during asset copying by validating and dereferencing hoisted links for a more reliable dev experience.

Review Change Stack

@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@vercel
Copy link
Copy Markdown

vercel Bot commented May 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-auth-hosted-components Ready Ready Preview, Comment May 26, 2026 2:10am
stack-auth-mcp Ready Ready Preview, Comment May 26, 2026 2:10am
stack-auth-skills Ready Ready Preview, Comment May 26, 2026 2:10am
stack-backend Ready Ready Preview, Comment May 26, 2026 2:10am
stack-dashboard Ready Ready Preview, Comment May 26, 2026 2:10am
stack-demo Ready Ready Preview, Comment May 26, 2026 2:10am
stack-docs Ready Ready Preview, Comment May 26, 2026 2:10am
stack-preview-backend Ready Ready Preview, Comment May 26, 2026 2:10am
stack-preview-dashboard Ready Ready Preview, Comment May 26, 2026 2:10am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f00239d9-5e8b-4c26-bf7f-6b10024b8b0e

📥 Commits

Reviewing files that changed from the base of the PR and between 8be79f0 and 8629399.

📒 Files selected for processing (1)
  • packages/stack-cli/scripts/copy-runtime-assets.mjs

📝 Walkthrough

Walkthrough

Adds symlink-aware dashboard asset copying: script helpers detect and copy hoisted pnpm symlink targets into the dashboard dist, and the dev runtime copy now preserves symlinks by using verbatimSymlinks when preparing the runtime.

Changes

Symlink-aware dashboard copy

Layer / File(s) Summary
Symlink helpers and hoisted-dep copier
packages/stack-cli/scripts/copy-runtime-assets.mjs
Imports readlinkSync/readdirSync. Adds helpers to filter existing files, dereference and copy symlink targets into the dashboard dist, extract dependency names from pnpm node_modules/.pnpm/.../node_modules/... paths, and recursively walk a pnpm node_modules directory to copy hoisted symlink targets while skipping broken/non-applicable symlinks.
Dashboard copy invocation and runtime preservation
packages/stack-cli/scripts/copy-runtime-assets.mjs, packages/stack-cli/src/commands/dev.ts
Changes the dashboard standalone copy to dereference/copy only existing files and runs the hoisted-dependency copier against the dashboard .pnpm layout. prepareDashboardRuntime now copies the bundled dashboard into the runtime using cpSync(..., { verbatimSymlinks: true }) to preserve symlinks.

Sequence Diagram

sequenceDiagram
  participant CopyScript as copy-runtime-assets.mjs
  participant BundledDist as bundled/dashboard/dist
  participant PnpmLayout as bundled/dashboard/node_modules/.pnpm
  participant RuntimePrep as prepareDashboardRuntime

  CopyScript->>BundledDist: copy standalone dashboard (dereference existing files / filter)
  CopyScript->>PnpmLayout: walk `.pnpm` node_modules and resolve hoisted symlinks
  PnpmLayout->>BundledDist: copy resolved hoisted dependency targets into dist/node_modules/<dep>
  RuntimePrep->>BundledDist: cpSync bundled dashboard into runtime with verbatimSymlinks:true
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • mantrakp04

Poem

🐰 I sniffed through pnpm nests and found each thread,
I chased the links where hoisted packages led,
Copied resolved targets into dist with care,
Kept runtime copies exact and fair,
A little rabbit hops to keep builds fed.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change—fixing symlink handling for the bundled dashboard in stack-cli.
Description check ✅ Passed The description provides clear context, lists specific changes, and includes comprehensive validation steps with build commands and verification results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 devin/1779746952-fix-mintlify-dev-next-import

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.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 25, 2026

Greptile Summary

This PR fixes the packed stack-cli dashboard failing to start with Cannot find module 'next' by preserving symlink information across the npm packaging step. A build-time script now records all symlinks in the Next.js standalone output into a symlinks.json manifest; at runtime, those symlinks are recreated inside the per-run dashboard directory before server.js is launched.

  • copy-runtime-assets.mjs gains collectSymlinks which recursively walks the dashboard dist, records each symlink's relative path and raw target, and writes the manifest after the cpSync.
  • dev.ts gains restoreDashboardRuntimeSymlinks which reads the manifest, removes the npm-expanded copies of each former symlink, and calls symlinkSync to recreate them before sentinel replacement and server startup.

Confidence Score: 4/5

The core logic is sound and the author validated the end-to-end fix. The one scenario to watch is a build environment that produces absolute symlink targets, which would silently create broken symlinks on end-user machines.

The approach — manifest at pack time, recreate at runtime — is the right way to survive npm's symlink-flattening. The replaceDashboardRuntimeSentinels traversal correctly skips symlink entries, so sentinel injection is unaffected. The only practical gap is that readlinkSync stores targets verbatim: pnpm workspaces consistently produce relative targets so this works today, but there's no guard if a future build configuration introduces absolute targets.

Both changed files are straightforward. copy-runtime-assets.mjs is worth a second look around the readlinkSync storage of raw symlink targets.

Important Files Changed

Filename Overview
packages/stack-cli/scripts/copy-runtime-assets.mjs Adds collectSymlinks helper to record all symlinks in the dashboard standalone build, writes them to a symlinks.json manifest; cpSync now uses verbatimSymlinks:true to preserve symlinks in the local dist folder.
packages/stack-cli/src/commands/dev.ts Adds parseDashboardSymlinksManifest / restoreDashboardRuntimeSymlinks to read symlinks.json and recreate the symlinks inside the per-run runtime directory before starting server.js; also switches the runtime cpSync to verbatimSymlinks:true.

Sequence Diagram

sequenceDiagram
    participant B as Build (copy-runtime-assets.mjs)
    participant D as dist/dashboard
    participant P as npm pack
    participant R as prepareDashboardRuntime (dev.ts)
    participant RT as runtimeRoot

    B->>D: cpSync standalone (verbatimSymlinks:true)
    B->>D: collectSymlinks → write symlinks.json
    D->>P: npm pack (symlinks expanded to real dirs)
    P-->>R: installed package (no symlinks)
    R->>RT: cpSync bundled dashboard (verbatimSymlinks:true, no-op for symlinks)
    R->>RT: restoreDashboardRuntimeSymlinks (reads symlinks.json, rmSync+symlinkSync each entry)
    R->>RT: replaceDashboardRuntimeSentinels (skips symlinks, rewrites .js sentinels)
    R->>RT: spawn node server.js
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
packages/stack-cli/scripts/copy-runtime-assets.mjs:51-54
Absolute symlink targets would silently break on end-user machines. `readlinkSync` returns the raw target, which could be an absolute path (e.g. `/home/ubuntu/.pnpm/next@16/…`) valid on the build machine but not at install time. Filtering those out at build time — rather than recreating them and finding them broken at runtime — keeps the manifest portable. The dangling-symlink guard (`existsSync`) already filters out targets that are broken at build time, but it won't catch targets whose absolute path simply doesn't exist on the end-user's machine.

```suggestion
      const target = readlinkSync(path);
      if (require("path").isAbsolute(target)) {
        // Absolute symlink targets are not portable across machines; skip them.
        continue;
      }
      symlinks.push({
        path: path.slice(root.length + 1),
        target,
      });
```

Reviews (1): Last reviewed commit: "Fix bundled dashboard module symlinks" | Re-trigger Greptile

Comment on lines +51 to +54
symlinks.push({
path: path.slice(root.length + 1),
target: readlinkSync(path),
});
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.

P2 Absolute symlink targets would silently break on end-user machines. readlinkSync returns the raw target, which could be an absolute path (e.g. /home/ubuntu/.pnpm/next@16/…) valid on the build machine but not at install time. Filtering those out at build time — rather than recreating them and finding them broken at runtime — keeps the manifest portable. The dangling-symlink guard (existsSync) already filters out targets that are broken at build time, but it won't catch targets whose absolute path simply doesn't exist on the end-user's machine.

Suggested change
symlinks.push({
path: path.slice(root.length + 1),
target: readlinkSync(path),
});
const target = readlinkSync(path);
if (require("path").isAbsolute(target)) {
// Absolute symlink targets are not portable across machines; skip them.
continue;
}
symlinks.push({
path: path.slice(root.length + 1),
target,
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/stack-cli/scripts/copy-runtime-assets.mjs
Line: 51-54

Comment:
Absolute symlink targets would silently break on end-user machines. `readlinkSync` returns the raw target, which could be an absolute path (e.g. `/home/ubuntu/.pnpm/next@16/…`) valid on the build machine but not at install time. Filtering those out at build time — rather than recreating them and finding them broken at runtime — keeps the manifest portable. The dangling-symlink guard (`existsSync`) already filters out targets that are broken at build time, but it won't catch targets whose absolute path simply doesn't exist on the end-user's machine.

```suggestion
      const target = readlinkSync(path);
      if (require("path").isAbsolute(target)) {
        // Absolute symlink targets are not portable across machines; skip them.
        continue;
      }
      symlinks.push({
        path: path.slice(root.length + 1),
        target,
      });
```

How can I resolve this? If you propose a fix, please make it concise.

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.

Fixed in 40524cc by failing the build if the dashboard standalone output contains an absolute symlink target. That keeps the generated manifest portable instead of recreating machine-local paths at runtime.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 2 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread packages/stack-cli/src/commands/dev.ts Outdated
Comment thread packages/stack-cli/scripts/copy-runtime-assets.mjs Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
packages/stack-cli/scripts/copy-runtime-assets.mjs (2)

56-56: 💤 Low value

Consider using path.relative for clearer intent.

The current slice-based approach works correctly, but using path.relative(root, path) is more idiomatic and self-documenting.

♻️ Optional refactor
+import { dirname, isAbsolute, join, relative, resolve } from "path";
       symlinks.push({
-        path: path.slice(root.length + 1),
+        path: relative(root, path),
         target,
       });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stack-cli/scripts/copy-runtime-assets.mjs` at line 56, Replace the
manual slice expression path.slice(root.length + 1) with the clearer idiom
path.relative(root, path); locate the code that builds the object containing the
path property (the line using path.slice(...)) and update the value to use
path.relative(root, path) so intent is explicit and edge cases with different
separators are handled by the path module.

48-50: 💤 Low value

Consider logging warnings for skipped broken symlinks.

Broken symlinks are silently skipped without any indication. While this may be intentional, logging a warning would help identify unexpected broken symlinks during the build process and aid in debugging.

📝 Optional logging enhancement
     if (entry.isSymbolicLink()) {
       if (!existsSync(path)) {
+        console.warn(`Skipping broken symlink: ${path}`);
         continue;
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stack-cli/scripts/copy-runtime-assets.mjs` around lines 48 - 50, At
the existsSync(path) check where broken symlinks are silently skipped, add a
warning log before the continue so skipped paths are visible; update the branch
handling the failed existsSync(path) to emit a clear message (e.g., with
console.warn or the repo logger) that includes the path variable and a short
note that the symlink/target was missing, then continue as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/stack-cli/scripts/copy-runtime-assets.mjs`:
- Line 56: Replace the manual slice expression path.slice(root.length + 1) with
the clearer idiom path.relative(root, path); locate the code that builds the
object containing the path property (the line using path.slice(...)) and update
the value to use path.relative(root, path) so intent is explicit and edge cases
with different separators are handled by the path module.
- Around line 48-50: At the existsSync(path) check where broken symlinks are
silently skipped, add a warning log before the continue so skipped paths are
visible; update the branch handling the failed existsSync(path) to emit a clear
message (e.g., with console.warn or the repo logger) that includes the path
variable and a short note that the symlink/target was missing, then continue as
before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2bed0301-f82d-4916-9413-1e1c6725175a

📥 Commits

Reviewing files that changed from the base of the PR and between da837fc and 40524cc.

📒 Files selected for processing (1)
  • packages/stack-cli/scripts/copy-runtime-assets.mjs

@devin-ai-integration
Copy link
Copy Markdown
Contributor

CI investigation notes:

  • build (22.x) failed in apps/backend/src/lib/redirect-urls.test.tsx (should implicitly validate hosted handler domains for the project, expected false to be true). I reproduced the same failure locally by running the test with NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX=67, and confirmed the same failure exists on the current base branch dev in run 26413460309 / job 77752734743.
  • E2E Tests (Node 22.x, Freestyle prod) failed in emails/email-queue.test.ts with expected 'sending' to be 'sent'. I also confirmed this same failure exists on the current base branch dev in run 26413460319 / job 77752734791.
  • The PR diff only touches packages/stack-cli/scripts/copy-runtime-assets.mjs and packages/stack-cli/src/commands/dev.ts; no lockfile or email/redirect code changed.

These CI failures appear preexisting/unrelated to the bundled dashboard symlink fix.

return undefined;
}

function copyDashboardHoistedDependencies(pnpmRoot, current = pnpmRoot) {
Copy link
Copy Markdown

@vercel vercel Bot May 26, 2026

Choose a reason for hiding this comment

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

The copyDashboardHoistedDependencies function now properly checks if the directory exists before calling readdirSync() ## Bug Explanation (FIXED)

The copyDashboardHoistedDependencies function at line 72 was calling readdirSync(current) without first checking if the directory exists. The function is invoked on line 110 with the path join(dashboardStandaloneSrc, "node_modules/.pnpm"), which may not exist in all build scenarios.

When readdirSync() is called on a non-existent directory, it throws an ENOENT (Error: no such file or directory) error, causing the entire build process to fail. This can happen when there are no hoisted dependencies in the dashboard standalone build or the .pnpm directory wasn't created during the Next.js build process.

Fix Explanation

An early return check has been added at the beginning of the copyDashboardHoistedDependencies function:

if (!existsSync(current)) {
  return;
}

This check gracefully handles the case where the directory doesn't exist by simply returning without attempting to read its contents. The existsSync function is already imported from the fs module. This allows the build to continue successfully even when the .pnpm directory is absent, which is a valid and expected scenario in some build configurations.

Fix on Vercel

Comment thread packages/stack-cli/scripts/copy-runtime-assets.mjs
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/stack-cli/scripts/copy-runtime-assets.mjs`:
- Around line 52-53: The current path parsing (parts =
path.slice(pnpmRoot.length + 1).split("/") and
parts.lastIndexOf("node_modules")) is POSIX-only and the copy logic copies
non-hoisted symlinks instead of preferring hoisted ones, causing conflicts;
change parsing to normalize and split using the platform separator (use
path.normalize and split by path.sep or use path.relative(pnpmRoot,
p).split(path.sep)), locate indices by searching for the literal ".pnpm" and the
following "node_modules" segment (rather than a blind
lastIndexOf("node_modules")), and update the symlink selection logic (the block
around the current hoisted/non-hoisted selection at lines ~82-84) to prefer
entries whose source path includes ".pnpm/node_modules" (i.e., hoisted targets)
when multiple targets map to the same destination; ensure Windows paths are
handled consistently by using path utilities (path.normalize, path.relative,
path.sep) throughout.
🪄 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: 350d230e-1149-434c-b9a1-fcd319f01c71

📥 Commits

Reviewing files that changed from the base of the PR and between 40524cc and 8be79f0.

📒 Files selected for processing (2)
  • packages/stack-cli/scripts/copy-runtime-assets.mjs
  • packages/stack-cli/src/commands/dev.ts

Comment thread packages/stack-cli/scripts/copy-runtime-assets.mjs Outdated
Copilot AI review requested due to automatic review settings May 26, 2026 02:01
@N2D4 N2D4 review requested due to automatic review settings May 26, 2026 02:01
@N2D4 N2D4 merged commit c8954ad into dev May 26, 2026
32 of 36 checks passed
@N2D4 N2D4 deleted the devin/1779746952-fix-mintlify-dev-next-import branch May 26, 2026 02:08
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.

1 participant