Repository navigation
fix(portal): allow the preview to serve the installation its dependencies link to - #418
Conversation
…cies link to The prepared project's node_modules are links into the CLI's installation, which the dev server's allow list does not name. A file from there is served only once the module importing it has been transformed, an order a browser cache skips: Safari keeps the entry cached across previews on the fixed port, executes it without asking the fresh server, and is refused the TanStack dev entry with a 403, leaving the page blank (apimatic-io#2287). Naming the real directories the links lead to keeps every dependency served whatever the browser asks for first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses the review of the allow-list fix: - Keep the workspace root Vite trusts by default, which naming the allow list by hand had dropped, and add the installations to it. - Trust only the installation the dependencies are linked from: the node_modules holding them, or the pnpm store around them. The template's walk to the outermost node_modules trusted every global npm package. - Resolve the installations in the CLI, where the links are made, and pass them in portal.config.json instead of rediscovering them in the template on every config load, build and prerender included. - Spell them with the native real path, so a Windows 8.3 short name never leaves the entry and the request spelt apart. - Keep the new comments to one line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…may serve" This reverts commit 09bcb54. The build with it left the preview blank in Safari on macOS, where the build before it loaded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A page imports its images from the static directory, which sits outside the project and was missing from the allow list. Like the dependencies, an image was served only once the page importing it had been transformed, so a browser running that page from its cache was refused it. Vite already serves the directory at the site root as publicDir, so naming it exposes nothing new. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
mrafnadeem-apimatic
left a comment
There was a problem hiding this comment.
Approving on the condition that a follow-up PR addresses the comments in this review, the inline one included.
Reviewed at 1ded182f on 2026-10-05 and rechecked at 6b249331.
The PR's effective change is 0d15e2da and 6b249331. The first scans the prepared project's node_modules and adds the outermost node_modules above each symlinked package to server.fs.allow; the second adds paths.staticDir. The CLI-computed list in 09bcb54b was reverted because Safari on macOS still showed a blank page with it. projectDirectory in vite.config.ts is the temporary copy of portal-template/ that the CLI runs Vite in, never the CLI repo root.
Findings
| # | Finding | Status | Evidence |
|---|---|---|---|
| 1 | The outermost node_modules is the wrong boundary: too wide for npm global installs, too narrow for pnpm global installs |
Confirmed | Table below |
| 2 | Images a page imports from the static directory get a 403 when asked for before the page | Fixed in 6b249331 |
The page imports /@fs/<source>/static/images/logo.png?import. Image first: 403. Page first: 200. With paths.staticDir in the allow list: 200. |
| 3 | Starting the CLI through a Windows 8.3 short path blanks the page in any browser, in normal request order | Confirmed with an npm global install | Vite 8.2.2 refuses short-name paths before it reads the allow list (isFileLoadingAllowed, node.js:20501). TanStack's entry imports client.tsx by a short /@fs/ URL. Pointing the symlinks and viteBinary at canonical() paths loads all 120 /@fs/ files. Not tried on dev. |
| 4 | The allow list doesn't explain why 09bcb54b failed in Safari |
Confirmed | In every layout 09bcb54b allows at least as much as the PR, so the Safari result points to cache state or another difference between the test runs |
| 5 | Setting server.fs.allow drops Vite's default, searchForWorkspaceRoot(root), and vite.config.ts:60-61 wrongly calls that default "the project alone" |
Confirmed in Vite's docs | It matters when the Windows cross-drive fallback puts the project inside a user's workspace |
| 6 | The PR's premise needs Safari to run a cached no-cache module without revalidating it |
Partly | The page (/) is sent with no Cache-Control; modules with no-cache and an ETag; pre-bundled dependencies immutable. A stale ?v= gets 504. A browser that revalidates gets 304 then 200 and never hits the 403. |
| 7 | The e2e tests' bare 200 checks pass for a TanStack file or an image that has moved | Confirmed | A missing /@fs/ file returns 200 text/html, the portal's fallback page (portal-serve.test.ts:74 and :86) |
| 8 | The Windows CI job likely passes the e2e tests without the fix | Inferred | test.yml runs them on windows-latest. The fallback puts the project inside the repo, whose pnpm-workspace.yaml already lets Vite's default serve node_modules and the fixture's static directory. |
| 9 | entries and realPath ignore every file-system error, not just "file not found" |
Read in the code | dependency-directories.ts:29-43 |
| 10 | after calls fs.rmSync(root) without checking that before set root |
Read in the diff | portal-serve.test.ts:58 |
| 11 | Rescanning on every config load | Negligible | One scan per serve; four per build, about 60 ms of a 21 s build |
Allow-list coverage, counted as the page's 120 /@fs/ files that fall outside each version's list:
| Layout | PR (0d15e2da) allows |
Missing | 09bcb54b allows |
Missing |
|---|---|---|---|---|
| pnpm dev checkout | <cli>/node_modules |
0 | <cli>/node_modules/.pnpm |
0 |
| npm global | <prefix>/node_modules, which includes every global package |
0 | <prefix>/node_modules/@apimatic/cli/node_modules |
0 |
| pnpm 10 global | 20 per-package global/5/.pnpm/<pkg>/node_modules |
69 | global/5/.pnpm |
0 |
| pnpm 11 global | per-package links/…/<hash>/node_modules |
69 | the same | 69 |
The 69 include @tanstack/react-start-client and router-core. These counts measure coverage only: a file is still served once its importer has been transformed.
Over npm global, the extra width exposes every non-denied file of every global package to any local process while the preview runs. Vite still refuses .env*, key and certificate files, .npmrc, .yarnrc.yml and .git/**, and its CORS and host checks keep websites out.
.ai/instructions.md
The PR targets dev, and the tests follow the mkdtemp/afterEach and mirrored-location rules. The comments break the one-line-of-why rule:
| Where | Replace with |
|---|---|
dependency-directories.ts:4-10 |
// Safari runs a cached entry before its importer is transformed (apimatic-io#2287). |
vite.config.ts:60-61 |
No comment, with allow: [searchForWorkspaceRoot(projectDirectory), ...] |
portal-serve.test.ts:62-67 |
The test name 'serves a dependency file asked for before its importer (apimatic-io#2287)' |
portal-serve.test.ts:17-18 |
// Opt-in: a real dev server needs a minute and every runtime dependency. |
dependency-directories.test.ts:7-10 |
Delete; the test names say it |
Recommendations
- Change the boundary so it covers the pnpm 10 and pnpm 11 global stores, where 69 of the page's files fall outside it, without taking in every npm global package (finding 1).
- Return
canonical()paths frompackageDirectory, whichlinkDependenciesandviteBinaryboth take their paths from (finding 3). The template'srealPaththen follows the symlinks to long paths, so the allow list needs no change of its own. - Have
entriesandrealPathcatch onlyENOENT, so a permission or symlink-loop error stops the config load with its message instead of silently dropping packages from the allow list and bringing the 403 back (finding 9). - Make the e2e tests check that a JavaScript module and an image came back, not just a 200 (finding 7).
- Try
Cache-Control: no-storein Safari on macOS, a fix that would need no allow-list change (finding 6).no-cache, which Vite sends on modules, lets a browser keep a copy as long as it asks the server before reusing it;no-storeforbids keeping one at all. With no stored entry to run, Safari has to request it, the fresh server transforms it, andclient.tsxis then served under the allow listdevalready has.- The page (
/) is sent with noCache-Control, so a middleware can setno-storeon it. Whether that alone stops Safari reusing the entry is unknown. It is the cheaper change: modules keep their 304s and pre-bundled dependencies stayimmutable. - A middleware can't reach modules, because Vite's transform middleware sets their
Cache-Controlafter it.server.headerscan, because Vite applies it last, but it covers every module and pre-bundled dependency, so each preview load downloads all of them again. - Test both in the scenario that blanked the page, two previews in a row on the same port, with Web Inspector's Network tab open. The entry should no longer show as "(memory cache)".
- If either works, it replaces the scan in
dependency-directories.ts, and findings 1, 5, 9 and 11 go with it. The e2e test would then check the header, sinceclient.tsxasked for first gets a 403 again.
- The page (
Findings 2, 3, 4, 6, 7 and 11 come from local runs on Windows, replaying requests over HTTP. Nothing was run in Safari.
| const [paths, identity] = await Promise.all([readBuildPaths(), readPortalIdentity()]); | ||
| const pages = await prerenderPages(paths, identity.siteUrl); | ||
| const publicDir: string | false = paths.staticDir ?? false; | ||
| const projectDirectory = fileURLToPath(new URL('.', import.meta.url)); |
There was a problem hiding this comment.
Rename variable to portalProjectDirectory to match CONTEXT.md.
|
🎉 This PR is included in version 2.0.0-beta.4 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Follow-up to #418, which was approved on the condition that its review comments are addressed in a separate PR. Every comment is addressed here. ## Changes - **Real paths for the dependencies and the Vite binary** (finding 3). The package lookup answers canonical paths, so a Windows 8.3 short name or a symlinked install can no longer leave the entry's `/@fs/` import refused in every browser. The new unit test fails without the change on a pnpm checkout. - **The narrowest directory holding the dependencies** (finding 1). The allow list took the outermost `node_modules` above each linked package: every global package under an npm global install, and only each package's own directory under a pnpm global install, where 69 of the page's 120 `/@fs/` files fell outside it. It now takes the directory all the linked installations share: the CLI's own `node_modules` under npm, the `.pnpm` or `links` store under pnpm. Where the installations share nothing short of the disk's root, as with an `npm link`ed dependency on another drive, each is listed on its own rather than widening to the root. Unit tests cover the four layouts in the review's table and the divergent case. - **Vite's default workspace root is kept** (finding 5), and the project directory is `portalProjectDirectory` after the glossary (inline comment). - **Nothing is skipped** (finding 9, tightened in review). A missing `node_modules` or a link leading nowhere stops the config load with its own error, naming the path, rather than surfacing later as a blank page. - **Serve tests check what came back** (finding 7): a JavaScript content type for the dependency file and `image/png` for the static image, since a missing file answers 200 with the fallback page. Cleanup only removes what set-up made (finding 10). - **One-line comments**, as the review's table asks. ## Recommendation 5: `Cache-Control: no-store`, tested in Safari Tested on macOS, Safari, from the `dev` commit before #418 so the bug could show. Control: the first preview loaded and the next preview on the same port went blank. With `no-store` on the page: every run blank. With `no-store` on every module too: first run loaded, every later run blank, Safari restarted between. In Web Inspector the entry module, `virtual:tanstack-start-dev-client-entry`, showed `(memory)` as its transfer size in every case, on reload too. Safari serves the module from its memory cache whatever the header says, which confirms finding 6's premise and rules out the header as a fix. The allow list stays. ## Verification - Full suite, both end-to-end suites, lint and type check pass. - Replaying the review's measurement: the page's 120 `/@fs/` files, asked of a fresh server before anything else, are all served from a real `npm install -g` and a real `pnpm add -g` (pnpm 11, `links` store). The old boundary refused 69 under pnpm. - A build of this branch at 7754fb5 on macOS, Safari, with the stale cache from earlier previews in place: quickstart, two repeat previews, a page with a static image over two previews, and a second quickstart all loaded. The commits since restructure the fold, add the root guard and let the lookup throw; the unit and serve tests cover them, and on a project linked like the CLI links it the old and new lookup answer the same allow list. ## Note on Windows CI As finding 8 says, on `windows-latest` the test project lands inside the repository, which Vite's default already trusts, so the serve tests pass there with or without the allow list. They guard it on macOS and Linux. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
|
🎉 This PR is included in version 2.0.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |



Problem
On macOS,
apimatic quickstartopens the portal preview on a blank page in Safari withUnhandled Promise Rejection: TypeError: Importing a module script failed.Pasting the same address into a new tab loads the portal. Reported in apimatic/apimatic-io#2287.Root cause
The prepared project's
node_modulesare links into the CLI's installation, which the dev server'sserver.fs.allowlist does not name. Vite serves a file from outside that list only after the module importing it has been transformed, which marks the file safe. Browsers normally fetch the importer first, so the side effect holds.Safari breaks the order. The preview always runs on port 23513, so Safari keeps the entry module cached across runs, executes the cached copy without asking the fresh server, and its first request is the TanStack dev entry inside the CLI's installation. Nothing has marked it safe, so the server answers 403, the entry's top-level
await importrejects, and the page stays blank. Captured on a failing run:This also explains why the issue's module crawls never reproduced it: a crawler has no cache, always fetches the importer first, and so always sees 200.
Fix
The template now resolves the real directories its dependencies link to, npm and pnpm layouts alike, and names them in
server.fs.allow, so every dependency is servable whatever the browser asks for first.Testing
test/e2e/portal-serve.test.tsreplays Safari: cold dev server, the/@fs/URL of the TanStack entry requested before anything else. Against the old config it fails with the same 403 and message as the Mac; with the fix it gets 200.test/portal-template/dependency-directories.test.tscovers the directory resolution, including the pnpm store layout, a copied dependency, and a dangling link.🤖 Generated with Claude Code