Skip to content

Commit d3fe5e4

Browse files
fix(studio,server): unify mount path to /_studio/ for CLI, Vercel, and self-host
Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/6df1882e-39c5-45ae-ab1d-e15e4912d3aa Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
1 parent 854046a commit d3fe5e4

File tree

8 files changed

+56
-92
lines changed

8 files changed

+56
-92
lines changed

apps/server/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# @objectstack/example-host
22

3+
## Unreleased
4+
5+
### Patch Changes
6+
7+
- **Unified Studio mount path to `/_studio/` for all deployments** (CLI embedded, Vercel, self-host).
8+
- `vercel.json`: studio SPA now serves under `/_studio/:path*` with a dedicated rewrite to `/_studio/index.html`. Root `/` and bare `/_studio` redirect to `/_studio/`. Asset caching headers scoped to `/_studio/assets/*`. `VITE_BASE=/_studio/` is set in `build.env`.
9+
- `scripts/build-vercel.sh`: studio dist is copied to `public/_studio/` (previously `public/`), so Vercel serves it under the same sub-path the CLI uses. This resolves the deep-link / sidebar-click routing failures that occurred when the Studio was mounted at the public root.
10+
311
## 4.0.4
412

513
### Patch Changes

apps/server/scripts/build-vercel.sh

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,16 @@ cd apps/server
2727
# 2. Bundle API serverless function
2828
node scripts/bundle-api.mjs
2929

30-
# 3. Copy studio dist files to public/ for UI serving
31-
echo "[build-vercel] Copying studio dist to public/..."
30+
# 3. Copy studio dist files to public/_studio/ for UI serving.
31+
# Studio is always mounted under /_studio/ (same convention as the CLI
32+
# static plugin). Vite builds with base: '/_studio/' so its asset URLs
33+
# and router basepath are already correct for this mount point.
34+
echo "[build-vercel] Copying studio dist to public/_studio/..."
3235
rm -rf public
33-
mkdir -p public
36+
mkdir -p public/_studio
3437
if [ -d "../studio/dist" ]; then
35-
cp -r ../studio/dist/* public/
36-
echo "[build-vercel] ✓ Copied studio dist to public/"
38+
cp -r ../studio/dist/. public/_studio/
39+
echo "[build-vercel] ✓ Copied studio dist to public/_studio/"
3740
else
3841
echo "[build-vercel] ⚠ Studio dist not found (skipped)"
3942
fi

apps/server/vercel.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"build": {
77
"env": {
88
"VITE_RUNTIME_MODE": "server",
9-
"VITE_SERVER_URL": ""
9+
"VITE_SERVER_URL": "",
10+
"VITE_BASE": "/_studio/"
1011
}
1112
},
1213
"functions": {
@@ -17,14 +18,18 @@
1718
},
1819
"headers": [
1920
{
20-
"source": "/assets/(.*)",
21+
"source": "/_studio/assets/(.*)",
2122
"headers": [
2223
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
2324
]
2425
}
2526
],
27+
"redirects": [
28+
{ "source": "/", "destination": "/_studio/", "permanent": false },
29+
{ "source": "/_studio", "destination": "/_studio/", "permanent": false }
30+
],
2631
"rewrites": [
2732
{ "source": "/api/:path*", "destination": "/api/[[...route]]" },
28-
{ "source": "/((?!api/).*)", "destination": "/index.html" }
33+
{ "source": "/_studio/:path*", "destination": "/_studio/index.html" }
2934
]
3035
}

apps/studio/CHANGELOG.md

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,16 @@
44

55
### Patch Changes
66

7-
- Fix TanStack Router basepath resolution when Studio is mounted under a sub-path
8-
(e.g. `/_studio/` via the CLI `--ui` flag). Previously, routes such as
9-
`/_studio/packages` or `/_studio/:package/objects/:name` failed to match —
10-
the router treated the mount prefix as a `$package` route parameter, producing
11-
"Not Found" errors on hard refresh and dropping the prefix on sidebar clicks
12-
(`navigate({ to: '/packages' })``/packages`).
13-
14-
The router now resolves its `basepath` from (1) a runtime global
15-
`window.__OBJECTSTACK_STUDIO_BASEPATH__` injected by the host server, falling
16-
back to (2) Vite's `import.meta.env.BASE_URL`, then (3) `'/'`. The runtime
17-
override is required because `import.meta.env.BASE_URL` is a build-time
18-
constant — a pre-built dist (shipped via npm and re-hosted by the CLI static
19-
plugin under `/_studio/`) would otherwise hard-code basepath as `'/'`
20-
regardless of its actual mount path. The CLI's `createStudioStaticPlugin` now
21-
injects the appropriate global so the same dist works at any mount point.
7+
- **Unified Studio mount path to `/_studio/` for all deployments.** The Vite
8+
build default is now `base: '/_studio/'` (was `'./'`), baking the correct
9+
absolute asset URLs and router basepath into every bundle. This removes the
10+
previous build-time/runtime ambiguity that required the host server to
11+
rewrite `href="/..."` URLs or inject a `window.__OBJECTSTACK_STUDIO_BASEPATH__`
12+
marker.
13+
- `resolveBasepath()` in `src/router.ts` simplified to rely solely on Vite's
14+
`import.meta.env.BASE_URL`, which now always yields `/_studio/` for
15+
production bundles and CLI dev (the CLI dev server sets
16+
`VITE_BASE=/_studio/`). Runtime `window` injection workaround removed.
2217

2318
## 4.0.4
2419

apps/studio/src/router.ts

Lines changed: 10 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,49 +11,20 @@ import { createRouter } from '@tanstack/react-router';
1111
import { routeTree } from './routeTree.gen';
1212

1313
/**
14-
* Compute the router basepath for TanStack Router.
14+
* Compute the router basepath from Vite's `BASE_URL`.
1515
*
16-
* When Studio is mounted under a sub-path (e.g. `/_studio/`), TanStack Router
17-
* must strip that prefix before matching route patterns. Otherwise URLs such
18-
* as `/_studio/packages` are mis-interpreted as `/$package="_studio"/packages`.
16+
* Studio is always mounted under `/_studio/` — the Vite build sets
17+
* `base: '/_studio/'` by default (see `vite.config.ts`), which makes
18+
* `import.meta.env.BASE_URL === '/_studio/'` for every production bundle
19+
* and `/_studio/` for the CLI dev server (which also sets `VITE_BASE`).
1920
*
20-
* Resolution order (first match wins):
21-
*
22-
* 1. **Runtime global `window.__OBJECTSTACK_STUDIO_BASEPATH__`** — injected
23-
* into `index.html` by the host server (see `createStudioStaticPlugin`
24-
* in `@objectstack/cli`) when the pre-built dist is served under a
25-
* sub-path. This is the authoritative signal for *any* deployment where
26-
* the same pre-built bundle is re-hosted at a different mount point —
27-
* `import.meta.env.BASE_URL` is a build-time constant and cannot
28-
* capture this.
29-
*
30-
* 2. **Vite `import.meta.env.BASE_URL`** — works at dev-server time when
31-
* `VITE_BASE` is set (e.g. the CLI dev proxy), and for bundles that
32-
* were explicitly built with a non-default `base` config.
33-
*
34-
* 3. Fallback: `'/'` (root deployment).
35-
*
36-
* TanStack Router expects the basepath WITHOUT a trailing slash (except for
37-
* the root `'/'`), so we normalise accordingly.
21+
* TanStack Router expects the basepath WITHOUT a trailing slash (except
22+
* for the root `'/'`), so we normalise accordingly.
3823
*/
39-
function normalise(base: string): string {
40-
const trimmed = base.trim();
41-
if (!trimmed || trimmed === '/' || trimmed === './') return '/';
42-
return trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed;
43-
}
44-
4524
function resolveBasepath(): string {
46-
// 1. Runtime injection from host server (works for any pre-built dist)
47-
if (typeof window !== 'undefined') {
48-
const injected = (window as unknown as { __OBJECTSTACK_STUDIO_BASEPATH__?: string })
49-
.__OBJECTSTACK_STUDIO_BASEPATH__;
50-
if (typeof injected === 'string' && injected.length > 0) {
51-
return normalise(injected);
52-
}
53-
}
54-
55-
// 2. Vite build-time / dev-server base
56-
return normalise(import.meta.env.BASE_URL ?? '/');
25+
const base = (import.meta.env.BASE_URL ?? '/').trim();
26+
if (!base || base === '/' || base === './') return '/';
27+
return base.endsWith('/') ? base.slice(0, -1) : base;
5728
}
5829

5930
export const router = createRouter({

apps/studio/vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const hmrConfig = process.env.VITE_HMR_PORT
1010

1111
// https://vitejs.dev/config/
1212
export default defineConfig({
13-
base: process.env.VITE_BASE || './', // Relative base for sub-path mounting (e.g. /_studio/)
13+
base: process.env.VITE_BASE || '/_studio/', // Studio is always mounted under /_studio/ (CLI, Vercel, self-host)
1414
resolve: {
1515
dedupe: ['react', 'react-dom'],
1616
alias: {

packages/cli/CHANGELOG.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@
44

55
### Patch Changes
66

7-
- `createStudioStaticPlugin` now injects
8-
`<script>window.__OBJECTSTACK_STUDIO_BASEPATH__="/_studio";</script>` into the
9-
rewritten `index.html`. This allows the Studio's TanStack Router to discover
10-
its mount path at runtime — a pre-built Studio `dist/` (whose
11-
`import.meta.env.BASE_URL` is baked in as `'./'` at build time) previously
12-
hard-coded its router basepath to `'/'`, breaking deep-link refresh and
13-
causing sidebar navigation to drop the `/_studio` prefix.
7+
- `createStudioStaticPlugin` simplified now that the Studio is always built with
8+
`base: '/_studio/'`: asset URLs in `index.html` are already absolute and
9+
correct, so the HTML is served verbatim (no `href="/..."` rewriting, no
10+
runtime basepath script injection). Single source of truth for the mount
11+
path: Vite `base`.
1412

1513
## 4.0.4
1614

packages/cli/src/utils/studio.ts

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -281,26 +281,10 @@ export function createStudioStaticPlugin(distPath: string, options?: { isDev?: b
281281
return;
282282
}
283283

284-
// Read and rewrite index.html so asset paths respect the mount path.
285-
// The dist may have been built with base '/' (absolute paths like
286-
// /assets/...) which won't resolve when mounted under /_studio/.
287-
//
288-
// We also inject a tiny inline script that publishes the runtime
289-
// basepath as `window.__OBJECTSTACK_STUDIO_BASEPATH__`. The Studio's
290-
// TanStack Router reads this at boot to configure its `basepath` —
291-
// without it, a pre-built bundle (whose `import.meta.env.BASE_URL`
292-
// is baked in at build time) would treat the mount prefix as a
293-
// route parameter and fail to resolve sub-routes.
294-
const rawHtml = fs.readFileSync(indexPath, 'utf-8');
295-
const withRewrittenUrls = rawHtml.replace(
296-
/(\s(?:href|src))="\/(?!\/)/g,
297-
`$1="${STUDIO_PATH}/`,
298-
);
299-
const basepathScript =
300-
`<script>window.__OBJECTSTACK_STUDIO_BASEPATH__=${JSON.stringify(STUDIO_PATH)};</script>`;
301-
const rewrittenHtml = withRewrittenUrls.includes('</head>')
302-
? withRewrittenUrls.replace('</head>', `${basepathScript}</head>`)
303-
: `${basepathScript}${withRewrittenUrls}`;
284+
// Studio is always built with `base: '/_studio/'`, so its asset URLs
285+
// (and runtime router basepath) are already absolute and correct. We
286+
// can serve the pre-built dist verbatim.
287+
const indexHtml = fs.readFileSync(indexPath, 'utf-8');
304288

305289
// In dev mode, redirect root to Studio for convenience
306290
if (options?.isDev) {
@@ -327,8 +311,8 @@ export function createStudioStaticPlugin(distPath: string, options?: { isDev?: b
327311
});
328312
}
329313

330-
// SPA fallback: serve rewritten index.html for non-file routes
331-
return new Response(rewrittenHtml, {
314+
// SPA fallback: serve index.html for non-file routes
315+
return new Response(indexHtml, {
332316
headers: { 'content-type': 'text/html; charset=utf-8' },
333317
});
334318
});

0 commit comments

Comments
 (0)