Skip to content

Commit 14fa75d

Browse files
authored
Merge branch 'main' into main
2 parents 235ebac + 3e7f83a commit 14fa75d

19 files changed

Lines changed: 2370 additions & 294 deletions

File tree

examples/issue-44-react-dev-server/index.html

Lines changed: 484 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>nodepod - issue #44 followup - react router basename</title>
7+
<style>
8+
* { margin: 0; padding: 0; box-sizing: border-box; }
9+
body { font-family: monospace; background: #1a1a2e; color: #e0e0e0; padding: 20px; line-height: 1.5; }
10+
h1 { font-size: 18px; margin-bottom: 4px; color: #fff; }
11+
p { font-size: 13px; color: #888; margin-bottom: 12px; max-width: 900px; }
12+
a { color: #8ac4e6; }
13+
code { background: #0d0d1a; padding: 2px 6px; border-radius: 3px; color: #a8e6a3; font-size: 12px; }
14+
.layout { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
15+
.pane {
16+
background: #0d0d1a; border: 1px solid #333; border-radius: 6px;
17+
padding: 12px; display: flex; flex-direction: column; min-height: 560px;
18+
}
19+
.pane h2 { font-size: 13px; margin-bottom: 8px; color: #8ac4e6; }
20+
#output {
21+
flex: 1; white-space: pre-wrap; font-size: 12px; line-height: 1.55;
22+
overflow-y: auto; max-height: 540px;
23+
}
24+
#frame { flex: 1; border: 1px solid #333; border-radius: 4px; background: #fff; min-height: 400px; }
25+
#status {
26+
font-size: 12px; padding: 6px 10px; border-radius: 3px;
27+
background: #181830; color: #aaa; border: 1px solid #222; margin-bottom: 8px;
28+
}
29+
.stdout { color: #a8e6a3; }
30+
.stderr { color: #e68a8a; }
31+
.info { color: #8ac4e6; }
32+
.dim { color: #666; }
33+
.pass { color: #a8e6a3; font-weight: bold; }
34+
.fail { color: #e68a8a; font-weight: bold; }
35+
.warn { color: #e6c38a; }
36+
.title { color: #c38ae6; font-weight: bold; }
37+
.controls { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; flex-wrap: wrap; }
38+
button {
39+
background: #2a2a4a; color: #e0e0e0; border: 1px solid #444;
40+
padding: 6px 12px; border-radius: 4px; font-family: monospace; cursor: pointer;
41+
}
42+
button:hover { background: #3a3a5a; }
43+
button:disabled { opacity: 0.5; cursor: not-allowed; }
44+
</style>
45+
</head>
46+
<body>
47+
<h1>nodepod - issue <a href="https://github.com/ScelarOrg/Nodepod/issues/44">#44</a> followup - react router basename</h1>
48+
<p>
49+
repro of the followup report: a vite + react app using
50+
react-router-dom BrowserRouter mounted inside a nodepod iframe never
51+
matches any app routes. the iframe's location.pathname is
52+
<code>/__virtual__/podXXXX/5173/</code>, which doesn't line up with
53+
routes declared as <code>/</code>, <code>/about</code> etc. the user
54+
gets the catch-all (NoMatch) branch and the iframe devtools logs
55+
<code>No routes matched location "/__virtual__/pod.../5173/"</code>.
56+
</p>
57+
58+
<div class="controls">
59+
<button id="run">boot + install + dev</button>
60+
<button id="clear">clear log</button>
61+
</div>
62+
63+
<div class="layout">
64+
<div class="pane">
65+
<h2>log</h2>
66+
<div id="output"></div>
67+
</div>
68+
<div class="pane">
69+
<h2>preview iframe</h2>
70+
<div id="status">idle</div>
71+
<iframe id="frame" sandbox="allow-scripts allow-same-origin allow-forms allow-popups"></iframe>
72+
</div>
73+
</div>
74+
75+
<script type="module">
76+
import { Nodepod } from "/dist/index.mjs";
77+
78+
// project files as JS template literals. the escape <\/ inside any
79+
// script-tag strings gets unescaped when the template literal runs
80+
const FILES = {
81+
'/app/package.json': `{
82+
"name": "router-repro",
83+
"private": true,
84+
"version": "0.0.0",
85+
"type": "module",
86+
"scripts": {
87+
"dev": "vite",
88+
"build": "vite build",
89+
"preview": "vite preview"
90+
},
91+
"dependencies": {
92+
"react": "^19.1.0",
93+
"react-dom": "^19.1.0",
94+
"react-router-dom": "^7.0.0"
95+
},
96+
"devDependencies": {
97+
"@vitejs/plugin-react": "^5.0.0",
98+
"vite": "8.0.10"
99+
}
100+
}
101+
`,
102+
103+
'/app/vite.config.js': `import { defineConfig } from 'vite';
104+
import react from '@vitejs/plugin-react';
105+
106+
export default defineConfig({
107+
plugins: [react()],
108+
server: { host: '0.0.0.0', port: 5173, strictPort: true },
109+
});
110+
`,
111+
112+
'/app/index.html': `<!DOCTYPE html>
113+
<html lang="en">
114+
<head>
115+
<meta charset="UTF-8" />
116+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
117+
<title>router repro</title>
118+
</head>
119+
<body>
120+
<div id="root"></div>
121+
<script type="module" src="/src/main.jsx"><\/script>
122+
</body>
123+
</html>
124+
`,
125+
126+
'/app/src/main.jsx': `import React from 'react';
127+
import ReactDOM from 'react-dom/client';
128+
import { BrowserRouter } from 'react-router-dom';
129+
import App from './App.jsx';
130+
131+
ReactDOM.createRoot(document.getElementById('root')).render(
132+
<React.StrictMode>
133+
<BrowserRouter>
134+
<App />
135+
</BrowserRouter>
136+
</React.StrictMode>
137+
);
138+
`,
139+
140+
'/app/src/App.jsx': `import { Routes, Route, Link, useLocation } from 'react-router-dom';
141+
142+
function DebugBar() {
143+
const loc = useLocation();
144+
return (
145+
<div style={{ background: '#eef', padding: 8, fontFamily: 'monospace', fontSize: 12 }}>
146+
<div>router location.pathname = <b>{loc.pathname}</b></div>
147+
<div>window.location.pathname = <b>{window.location.pathname}</b></div>
148+
</div>
149+
);
150+
}
151+
152+
function Home() {
153+
return (
154+
<div style={{ padding: 24 }}>
155+
<h1>home</h1>
156+
<p>if you see this, react-router matched the / route correctly.</p>
157+
<Link to="/about">go to /about</Link>
158+
</div>
159+
);
160+
}
161+
162+
function About() {
163+
return (
164+
<div style={{ padding: 24 }}>
165+
<h1>about</h1>
166+
<Link to="/">back home</Link>
167+
</div>
168+
);
169+
}
170+
171+
function NoMatch() {
172+
return (
173+
<div style={{ padding: 24, background: '#fee', color: '#900' }}>
174+
<h1>no route matched</h1>
175+
<p>
176+
BrowserRouter saw pathname <code>{window.location.pathname}</code> and
177+
none of the app routes match. this is the bug.
178+
</p>
179+
</div>
180+
);
181+
}
182+
183+
export default function App() {
184+
return (
185+
<>
186+
<DebugBar />
187+
<Routes>
188+
<Route path="/" element={<Home />} />
189+
<Route path="/about" element={<About />} />
190+
<Route path="*" element={<NoMatch />} />
191+
</Routes>
192+
</>
193+
);
194+
}
195+
`,
196+
};
197+
198+
const out = document.getElementById('output');
199+
const frame = document.getElementById('frame');
200+
const statusEl = document.getElementById('status');
201+
const runBtn = document.getElementById('run');
202+
const clearBtn = document.getElementById('clear');
203+
204+
function log(t, cls = 'stdout') {
205+
const span = document.createElement('span');
206+
span.className = cls;
207+
span.textContent = t + '\n';
208+
out.appendChild(span);
209+
out.scrollTop = out.scrollHeight;
210+
}
211+
function setStatus(t, cls = '') { statusEl.textContent = t; statusEl.className = cls; }
212+
clearBtn.addEventListener('click', () => { out.textContent = ''; });
213+
214+
runBtn.addEventListener('click', async () => {
215+
runBtn.disabled = true;
216+
frame.removeAttribute('src');
217+
try {
218+
log('booting nodepod (workdir=/app) ...', 'info');
219+
setStatus('booting ...', 'warn');
220+
221+
let serverUrl = null;
222+
const ready = new Promise((resolve) => {
223+
window.__onReady = (url) => { serverUrl = url; resolve(url); };
224+
});
225+
226+
const pod = await Nodepod.boot({
227+
workdir: '/app',
228+
watermark: false,
229+
onServerReady: (port, url) => {
230+
log(`[onServerReady] port=${port} url=${url}`, 'info');
231+
window.__onReady(url);
232+
},
233+
});
234+
log('booted.\n', 'info');
235+
236+
log('writing project files ...', 'info');
237+
await Promise.all(
238+
Object.entries(FILES).map(([p, c]) => pod.fs.writeFile(p, c))
239+
);
240+
log('done.\n', 'info');
241+
242+
log('--- npm install ---', 'title');
243+
const install = await pod.spawn('npm', ['install']);
244+
install.on('output', (t) => log(t.trimEnd(), 'stdout'));
245+
install.on('error', (t) => log(t.trimEnd(), 'stderr'));
246+
const installRes = await install.completion;
247+
if (installRes.exitCode !== 0) {
248+
log(`install failed (exit=${installRes.exitCode})`, 'fail');
249+
setStatus('install failed', 'fail');
250+
return;
251+
}
252+
log('install done.\n', 'pass');
253+
254+
log('--- npm run dev ---', 'title');
255+
setStatus('starting dev ...', 'warn');
256+
const dev = await pod.spawn('npm', ['run', 'dev']);
257+
dev.on('output', (t) => log(t.trimEnd(), 'stdout'));
258+
dev.on('error', (t) => log(t.trimEnd(), 'stderr'));
259+
260+
const url = await Promise.race([
261+
ready,
262+
new Promise((r) => setTimeout(() => r(null), 60000)),
263+
]);
264+
if (!url) {
265+
log('timeout waiting for dev server', 'fail');
266+
setStatus('timeout', 'fail');
267+
dev.kill();
268+
return;
269+
}
270+
271+
log(`\npointing iframe at ${url}`, 'info');
272+
log(`BrowserRouter will see pathname = ${new URL(url).pathname}`, 'warn');
273+
log(`expected: the red "no route matched" box renders`, 'warn');
274+
setStatus('check the iframe', 'warn');
275+
frame.src = url;
276+
} catch (e) {
277+
log(`\nuncaught: ${e?.stack || e}`, 'fail');
278+
setStatus('uncaught', 'fail');
279+
} finally {
280+
runBtn.disabled = false;
281+
}
282+
});
283+
</script>
284+
</body>
285+
</html>

0 commit comments

Comments
 (0)