Skip to content

Commit e3a3350

Browse files
committed
fix: bump @ioai/wasm-zstd to 1.1.2 and document Next.js integration
Based on release/1.3.4 (no main plot/chart changes). - Upgrade @ioai/wasm-zstd to ^1.1.2 (static glue import; fixes Turbopack "Failed to resolve module specifier './wasm-zstd-*.js'" in inline workers) - Regenerate package-lock.json from registry.npmjs.org - Add Next.js (App Router / Turbopack) section and troubleshooting to EMBEDDING.md / EMBEDDING.zh.md Bump to 1.3.5.
1 parent 9bb9b97 commit e3a3350

4 files changed

Lines changed: 222 additions & 8 deletions

File tree

docs/EMBEDDING.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,109 @@ module.exports = {
249249
};
250250
```
251251

252+
### Next.js (App Router / Turbopack)
253+
254+
ROSView is a browser-only component (it relies on Web Workers, WASM, `window`, etc.) and **cannot be server-rendered**. Integrating it into Next.js takes three steps:
255+
256+
**1. Transpile the package in `next.config.js`**
257+
258+
```js
259+
// next.config.js
260+
const nextConfig = {
261+
transpilePackages: ['@ioai/rosview'],
262+
};
263+
264+
module.exports = nextConfig;
265+
```
266+
267+
**2. Wrap it in a Client Component loaded with `ssr: false`**
268+
269+
`ssr: false` is only allowed inside a **Client Component** — you cannot use it with `next/dynamic` directly in a Server Component (e.g. `page.tsx`). So create a `'use client'` wrapper:
270+
271+
```tsx
272+
// components/RosViewerClient.tsx
273+
'use client';
274+
275+
import '@ioai/rosview/style.css';
276+
import dynamic from 'next/dynamic';
277+
278+
// Client-only load to avoid touching browser APIs during SSR
279+
const RosViewer = dynamic(
280+
() => import('@ioai/rosview').then((m) => ({ default: m.RosViewer })),
281+
{ ssr: false },
282+
);
283+
284+
export function RosViewerClient({ url }: { url: string }) {
285+
return (
286+
<div style={{ width: '100%', height: '100vh' }}>
287+
<RosViewer url={url} theme="dark" />
288+
</div>
289+
);
290+
}
291+
```
292+
293+
```tsx
294+
// app/visualize/page.tsx — a Server Component rendering the client wrapper
295+
import { RosViewerClient } from '@/components/RosViewerClient';
296+
297+
export default async function VisualizePage({
298+
searchParams,
299+
}: {
300+
searchParams: Promise<{ url?: string }>;
301+
}) {
302+
const { url } = await searchParams;
303+
return <RosViewerClient url={url ?? ''} />;
304+
}
305+
```
306+
307+
**3. (Optional) Serve files through a Range-capable route**
308+
309+
To stream local/private mcap/bag recordings, expose a GET route that supports `Accept-Ranges` and point `url` at it:
310+
311+
```ts
312+
// app/api/recording/route.ts
313+
import fs from 'node:fs';
314+
import { NextRequest, NextResponse } from 'next/server';
315+
316+
export async function GET(req: NextRequest) {
317+
const filePath = '/data/recording.mcap'; // validated, safe path
318+
const { size } = await fs.promises.stat(filePath);
319+
const range = req.headers.get('range');
320+
321+
// No Range: return the whole file but still advertise Range support
322+
if (!range) {
323+
const stream = fs.createReadStream(filePath);
324+
return new NextResponse(stream as unknown as ReadableStream, {
325+
status: 200,
326+
headers: {
327+
'Content-Type': 'application/octet-stream',
328+
'Content-Length': String(size),
329+
'Accept-Ranges': 'bytes',
330+
},
331+
});
332+
}
333+
334+
const [startStr, endStr] = range.replace('bytes=', '').split('-');
335+
const start = Number(startStr);
336+
const end = endStr ? Number(endStr) : size - 1;
337+
const stream = fs.createReadStream(filePath, { start, end });
338+
339+
return new NextResponse(stream as unknown as ReadableStream, {
340+
status: 206,
341+
headers: {
342+
'Content-Type': 'application/octet-stream',
343+
'Content-Range': `bytes ${start}-${end}/${size}`,
344+
'Content-Length': String(end - start + 1),
345+
'Accept-Ranges': 'bytes',
346+
},
347+
});
348+
}
349+
```
350+
351+
> **About `db3`:** ROSView does **not** support remote streaming of db3 (SQLite needs random access to the whole database). Download the db3 file in full as a `File` on the client and pass it via the `file` / `files` prop. mcap / bag can be streamed directly through the Range route above.
352+
353+
> **Version requirement:** Use **`@ioai/rosview` ≥ 1.3.5** (which depends on `@ioai/wasm-zstd` ≥ 1.1.2). 1.3.5 fixes the Turbopack inline-worker regression from 1.3.4 (`Failed to resolve module specifier './wasm-zstd-*.js'`, caused by the inline worker running from a `blob:` URL and unable to resolve the zstd glue's relative dynamic import). `@ioai/wasm-zstd` 1.1.2 statically inlines the glue into the worker, fixing this for good.
354+
252355
---
253356

254357
## Troubleshooting
@@ -281,6 +384,10 @@ Access-Control-Allow-Origin: https://your-app.example.com
281384

282385
Firefox has stricter CSP enforcement for workers. If you use a `Content-Security-Policy` header, add `worker-src 'self' blob:`.
283386

387+
### "Failed to resolve module specifier './wasm-zstd-*.js'"
388+
389+
This occurs with `@ioai/rosview` 1.3.4 (paired with `@ioai/wasm-zstd` 1.1.1) under bundlers that run inline workers from `blob:` URLs, such as Next.js Turbopack. Upgrade to **`@ioai/rosview` ≥ 1.3.5** (`@ioai/wasm-zstd` ≥ 1.1.2) — newer versions statically inline the zstd glue into the worker, removing the runtime relative dynamic import.
390+
284391
---
285392

286393
## TypeScript support

docs/EMBEDDING.zh.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,109 @@ module.exports = {
249249
};
250250
```
251251

252+
### Next.js(App Router / Turbopack)
253+
254+
ROSView 是纯浏览器端组件(依赖 Web Worker、WASM、`window` 等),**不能在服务端渲染**。在 Next.js 中集成时遵循以下三点即可:
255+
256+
**1. 在 `next.config.js` 中转译本包**
257+
258+
```js
259+
// next.config.js
260+
const nextConfig = {
261+
transpilePackages: ['@ioai/rosview'],
262+
};
263+
264+
module.exports = nextConfig;
265+
```
266+
267+
**2. 用客户端组件包裹,并以 `ssr: false` 动态加载**
268+
269+
`ssr: false` 只能在**客户端组件**里使用,不能直接在服务端组件(如 `page.tsx`)里对 `next/dynamic` 使用。因此请新建一个 `'use client'` 包裹组件:
270+
271+
```tsx
272+
// components/RosViewerClient.tsx
273+
'use client';
274+
275+
import '@ioai/rosview/style.css';
276+
import dynamic from 'next/dynamic';
277+
278+
// 仅在客户端加载,避免 SSR 阶段触碰浏览器 API
279+
const RosViewer = dynamic(
280+
() => import('@ioai/rosview').then((m) => ({ default: m.RosViewer })),
281+
{ ssr: false },
282+
);
283+
284+
export function RosViewerClient({ url }: { url: string }) {
285+
return (
286+
<div style={{ width: '100%', height: '100vh' }}>
287+
<RosViewer url={url} theme="dark" />
288+
</div>
289+
);
290+
}
291+
```
292+
293+
```tsx
294+
// app/visualize/page.tsx —— 服务端组件直接渲染上面的客户端组件
295+
import { RosViewerClient } from '@/components/RosViewerClient';
296+
297+
export default async function VisualizePage({
298+
searchParams,
299+
}: {
300+
searchParams: Promise<{ url?: string }>;
301+
}) {
302+
const { url } = await searchParams;
303+
return <RosViewerClient url={url ?? ''} />;
304+
}
305+
```
306+
307+
**3.(可选)提供支持 HTTP Range 的文件接口**
308+
309+
要流式可视化本地/私有的 mcap/bag,可在 Next.js 中提供一个支持 `Accept-Ranges` 的 GET 路由,把 `url` 指向它即可:
310+
311+
```ts
312+
// app/api/recording/route.ts
313+
import fs from 'node:fs';
314+
import { NextRequest, NextResponse } from 'next/server';
315+
316+
export async function GET(req: NextRequest) {
317+
const filePath = '/data/recording.mcap'; // 经过校验的安全路径
318+
const { size } = await fs.promises.stat(filePath);
319+
const range = req.headers.get('range');
320+
321+
// 无 Range:返回整文件,但仍声明支持 Range
322+
if (!range) {
323+
const stream = fs.createReadStream(filePath);
324+
return new NextResponse(stream as unknown as ReadableStream, {
325+
status: 200,
326+
headers: {
327+
'Content-Type': 'application/octet-stream',
328+
'Content-Length': String(size),
329+
'Accept-Ranges': 'bytes',
330+
},
331+
});
332+
}
333+
334+
const [startStr, endStr] = range.replace('bytes=', '').split('-');
335+
const start = Number(startStr);
336+
const end = endStr ? Number(endStr) : size - 1;
337+
const stream = fs.createReadStream(filePath, { start, end });
338+
339+
return new NextResponse(stream as unknown as ReadableStream, {
340+
status: 206,
341+
headers: {
342+
'Content-Type': 'application/octet-stream',
343+
'Content-Range': `bytes ${start}-${end}/${size}`,
344+
'Content-Length': String(end - start + 1),
345+
'Accept-Ranges': 'bytes',
346+
},
347+
});
348+
}
349+
```
350+
351+
> **关于 `db3`**:ROSView **不支持远程流式** db3(SQLite 需随机读取整库)。请在前端先把 db3 整文件下载为 `File`,再通过 `file` / `files` prop 传入。mcap / bag 则可直接用上面的 Range 接口流式加载。
352+
353+
> **版本要求**:请使用 **`@ioai/rosview` ≥ 1.3.5**(依赖 `@ioai/wasm-zstd` ≥ 1.1.2)。1.3.5 修复了 1.3.4 在 Turbopack 下因 inline worker 以 `blob:` URL 运行、无法解析 zstd glue 相对动态 import 而抛出的 `Failed to resolve module specifier './wasm-zstd-*.js'``@ioai/wasm-zstd` 1.1.2 将 glue 静态内联进 worker,彻底解决该问题。
354+
252355
---
253356

254357
## 故障排查
@@ -281,6 +384,10 @@ Access-Control-Allow-Origin: https://your-app.example.com
281384

282385
Firefox 对 Worker 的 CSP 更严格。若使用 `Content-Security-Policy` 响应头,请添加 `worker-src 'self' blob:`
283386

387+
### "Failed to resolve module specifier './wasm-zstd-*.js'"
388+
389+
该错误出现在 `@ioai/rosview` 1.3.4(搭配 `@ioai/wasm-zstd` 1.1.1)在以 `blob:` URL 运行 inline worker 的打包器(如 Next.js Turbopack)下。升级到 **`@ioai/rosview` ≥ 1.3.5**`@ioai/wasm-zstd` ≥ 1.1.2)即可修复——新版本会将 zstd glue 静态内联进 worker,不再有运行时相对动态 import。
390+
284391
---
285392

286393
## TypeScript 支持

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ioai/rosview",
3-
"version": "1.3.4",
3+
"version": "1.3.5",
44
"description": "High-performance robotics data visualization for MCAP, ROS bag, ROS2 db3, HDF5 and BVH — embeddable React component and standalone SPA",
55
"keywords": [
66
"ros",
@@ -97,7 +97,7 @@
9797
"@foxglove/rosmsg-serialization": "^2.0.4",
9898
"@foxglove/rosmsg2-serialization": "^3.0.3",
9999
"@ioai/hdf5": "^1.0.0",
100-
"@ioai/wasm-zstd": "^1.1.1",
100+
"@ioai/wasm-zstd": "^1.1.2",
101101
"@mcap/browser": "^1.1.0",
102102
"@mcap/core": "^2.0.2",
103103
"@playwright/test": "^1.59.1",

0 commit comments

Comments
 (0)