feat(sdk): add React + Vue adapters for op-web-sdk
Plan 3 of the web embedding SDK: @zseven-w/op-web-sdk-react and -vue — thin read-only framework adapters over the TS core. Each: <DesignView> (canvas + viewer lifecycle), provider/inject of OpViewer, and read-only useDocument/useViewport/useActivePage. React via useSyncExternalStore (cache-invalidate-on-event); Vue via an async shallowRef provider so children inject the viewer after load. Self-contained (no pen-* deps; framework peer); React 5/5 + Vue 4/4 tests; both tsc clean.
This commit is contained in:
parent
baae07addd
commit
199d9bbec9
1
packages/op-web-sdk-react/.gitignore
vendored
Normal file
1
packages/op-web-sdk-react/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/dist/
|
||||
153
packages/op-web-sdk-react/README.md
Normal file
153
packages/op-web-sdk-react/README.md
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
# @zseven-w/op-web-sdk-react
|
||||
|
||||
React adapter for the OpenPencil read-only web viewer SDK. Embed a live OpenPencil design canvas in any React 19 application.
|
||||
|
||||
> **Read-only boundary:** This package provides a viewer only — no editing. For the full editor experience, use the OpenPencil desktop or web app directly.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install @zseven-w/op-web-sdk @zseven-w/op-web-sdk-react
|
||||
# or
|
||||
bun add @zseven-w/op-web-sdk @zseven-w/op-web-sdk-react
|
||||
```
|
||||
|
||||
## Peer dependencies
|
||||
|
||||
| Package | Required version |
|
||||
|--------------|-----------------|
|
||||
| `react` | `^19.0.0` |
|
||||
| `react-dom` | `^19.0.0` |
|
||||
|
||||
## WASM note
|
||||
|
||||
The core SDK (`@zseven-w/op-web-sdk`) loads a WebAssembly binary at runtime. You must either:
|
||||
- Pass the wasm URL explicitly via the `wasmUrl` prop, or
|
||||
- Serve the wasm file from your own static assets and ensure it is reachable.
|
||||
|
||||
The wasm file ships alongside the core package at `@zseven-w/op-web-sdk/dist/op_web_sdk_bg.wasm`.
|
||||
|
||||
## Usage
|
||||
|
||||
### `<DesignView>` component
|
||||
|
||||
The simplest way to embed a design viewer:
|
||||
|
||||
```tsx
|
||||
import { DesignView } from '@zseven-w/op-web-sdk-react';
|
||||
import type { OpViewer } from '@zseven-w/op-web-sdk';
|
||||
|
||||
export function MyPage() {
|
||||
function handleLoad(viewer: OpViewer) {
|
||||
console.log('viewer ready', viewer.document);
|
||||
}
|
||||
|
||||
return (
|
||||
<DesignView
|
||||
doc={docStringOrBytes}
|
||||
wasmUrl="/assets/op_web_sdk_bg.wasm"
|
||||
onLoad={handleLoad}
|
||||
style={{ width: 800, height: 600 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Props (`DesignViewProps`):**
|
||||
- `doc?: string | Uint8Array` — serialized OpenPencil document (JSON string or binary bytes). Omit to start with an empty document.
|
||||
- `wasmUrl?: string` — URL to the core WASM binary.
|
||||
- `onLoad?: (viewer: OpViewer) => void` — called once the viewer is ready.
|
||||
- `className?: string` — CSS class applied to the wrapper div.
|
||||
- `children?: ReactNode` — child components that can call viewer hooks (rendered only after the viewer is ready, inside `DesignProvider`).
|
||||
|
||||
### Manual provider
|
||||
|
||||
For advanced layouts, wrap your subtree with `DesignProvider` and use `useViewer` in descendants:
|
||||
|
||||
```tsx
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { createViewer } from '@zseven-w/op-web-sdk';
|
||||
import { DesignProvider } from '@zseven-w/op-web-sdk-react';
|
||||
|
||||
export function MyLayout() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [viewer, setViewer] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
createViewer({ canvas: canvasRef.current! }).then((v) => {
|
||||
if (cancelled) { v.destroy(); return; }
|
||||
setViewer(v);
|
||||
});
|
||||
return () => { cancelled = true; viewer?.destroy(); };
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<canvas ref={canvasRef} />
|
||||
{viewer && (
|
||||
<DesignProvider viewer={viewer}>
|
||||
<MyChildComponents />
|
||||
</DesignProvider>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Hooks
|
||||
|
||||
These hooks must be called inside a component that is a descendant of `DesignProvider` (or `<DesignView>`).
|
||||
|
||||
| Hook | Returns | Reactive event |
|
||||
|------|---------|----------------|
|
||||
| `useViewer()` | `OpViewer` | — (stable reference) |
|
||||
| `useDocument()` | `PenDocument` | `'load'` |
|
||||
| `useViewport()` | `{ panX: number; panY: number; zoom: number }` | `'viewportchange'` |
|
||||
| `useActivePage()` | `number` | `'load'` |
|
||||
|
||||
Each hook uses `useSyncExternalStore` internally and re-renders only when the relevant viewer event fires.
|
||||
|
||||
```tsx
|
||||
import { useViewport, useDocument } from '@zseven-w/op-web-sdk-react';
|
||||
|
||||
export function ZoomDisplay() {
|
||||
const viewport = useViewport(); // { panX, panY, zoom }
|
||||
const doc = useDocument(); // PenDocument
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Zoom: {viewport.zoom}</p>
|
||||
<p>Pages: {doc.pages?.length}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API reference
|
||||
|
||||
```ts
|
||||
// Provider / context
|
||||
function DesignProvider(props: { viewer: OpViewer; children?: ReactNode }): JSX.Element
|
||||
function useViewer(): OpViewer // throws if no provider above
|
||||
|
||||
// Hooks (re-render on event)
|
||||
function useDocument(): PenDocument
|
||||
function useViewport(): { panX: number; panY: number; zoom: number }
|
||||
function useActivePage(): number
|
||||
|
||||
// Component
|
||||
function DesignView(props: DesignViewProps): JSX.Element
|
||||
|
||||
interface DesignViewProps {
|
||||
doc?: string | Uint8Array;
|
||||
wasmUrl?: string;
|
||||
onLoad?: (viewer: OpViewer) => void;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
16
packages/op-web-sdk-react/package.json
Normal file
16
packages/op-web-sdk-react/package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "@zseven-w/op-web-sdk-react",
|
||||
"version": "0.8.0",
|
||||
"description": "React adapter for the OpenPencil read-only web viewer SDK",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"files": ["dist"],
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" } },
|
||||
"scripts": { "build": "tsup", "typecheck": "tsc --noEmit", "test": "vitest run" },
|
||||
"dependencies": { "@zseven-w/op-web-sdk": "workspace:*" },
|
||||
"peerDependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" },
|
||||
"devDependencies": { "react": "^19.0.0", "react-dom": "^19.0.0", "@testing-library/react": "^16.0.0", "tsup": "^8.0.0", "typescript": "^5.7.2", "vitest": "^3.0.0", "jsdom": "^25.0.0" }
|
||||
}
|
||||
5
packages/op-web-sdk-react/src/context.ts
Normal file
5
packages/op-web-sdk-react/src/context.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { createContext } from 'react';
|
||||
import type { OpViewer } from '@zseven-w/op-web-sdk';
|
||||
|
||||
// React context holding the current OpViewer instance; null when no provider is mounted.
|
||||
export const ViewerContext = createContext<OpViewer | null>(null);
|
||||
62
packages/op-web-sdk-react/src/design-view.tsx
Normal file
62
packages/op-web-sdk-react/src/design-view.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { createElement, useEffect, useRef, useState, type ReactNode } from 'react';
|
||||
import { createViewer, type OpViewer } from '@zseven-w/op-web-sdk';
|
||||
import { DesignProvider } from './use-viewer.js';
|
||||
|
||||
export interface DesignViewProps {
|
||||
/** Serialized document JSON string or raw bytes to load on mount. */
|
||||
doc?: string | Uint8Array;
|
||||
/** URL to the WASM bundle; omit to use the bundled default. */
|
||||
wasmUrl?: string;
|
||||
/** Called once after the viewer is created and ready. */
|
||||
onLoad?: (viewer: OpViewer) => void;
|
||||
/** Optional CSS class applied to the wrapper div. */
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a canvas, creates an OpViewer on mount, and exposes it to
|
||||
* descendant hooks via DesignProvider. Destroys the viewer on unmount.
|
||||
* Handles the async race: if unmount occurs before createViewer resolves,
|
||||
* the late-arriving viewer is destroyed immediately.
|
||||
*/
|
||||
export function DesignView(props: DesignViewProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [viewer, setViewer] = useState<OpViewer | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
// Track the viewer that was synchronously assigned so the cleanup
|
||||
// path can destroy it even if setViewer batching hasn't flushed.
|
||||
let made: OpViewer | null = null;
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
createViewer({ canvas, doc: props.doc, wasmUrl: props.wasmUrl }).then((v) => {
|
||||
if (cancelled) {
|
||||
// Component unmounted before the promise resolved — destroy immediately.
|
||||
v.destroy();
|
||||
return;
|
||||
}
|
||||
made = v;
|
||||
setViewer(v);
|
||||
props.onLoad?.(v);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
// Destroy synchronously if the viewer was already assigned.
|
||||
made?.destroy();
|
||||
};
|
||||
// Re-create when doc identity or wasmUrl changes.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.doc, props.wasmUrl]);
|
||||
|
||||
return createElement(
|
||||
'div',
|
||||
{ className: props.className },
|
||||
createElement('canvas', { ref: canvasRef, style: { width: '100%', height: '100%', display: 'block' } }),
|
||||
// Only render children once the viewer is ready.
|
||||
viewer ? createElement(DesignProvider, { viewer, children: props.children }) : null,
|
||||
);
|
||||
}
|
||||
58
packages/op-web-sdk-react/src/hooks.ts
Normal file
58
packages/op-web-sdk-react/src/hooks.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { useCallback, useRef, useSyncExternalStore } from 'react';
|
||||
import type { OpViewer } from '@zseven-w/op-web-sdk';
|
||||
import { useViewer } from './use-viewer.js';
|
||||
|
||||
type ViewerEvent = 'load' | 'viewportchange';
|
||||
|
||||
// Subscribe to `event` and return a referentially stable snapshot between events.
|
||||
//
|
||||
// CRITICAL — render-loop prevention: the core's getters (e.g. `viewport`) return
|
||||
// a fresh object on every call. If we passed them directly to useSyncExternalStore,
|
||||
// React would see a new reference on every render, trigger a re-render, call the
|
||||
// getter again, get yet another new reference, and loop infinitely.
|
||||
//
|
||||
// Solution: cache the last snapshot in a ref. The subscribe callback clears the
|
||||
// cache when the event fires so the *next* getSnapshot call fetches a fresh value.
|
||||
// Between events the same cached object is returned, satisfying reference stability.
|
||||
function useViewerSnapshot<T>(event: ViewerEvent, read: (v: OpViewer) => T): T {
|
||||
const viewer = useViewer();
|
||||
// Null signals "cache is dirty, read fresh on next getSnapshot call".
|
||||
const cache = useRef<{ value: T } | null>(null);
|
||||
|
||||
const subscribe = useCallback(
|
||||
(cb: () => void) =>
|
||||
viewer.on(event, () => {
|
||||
// Invalidate the cache so getSnapshot returns the new value after this event.
|
||||
cache.current = null;
|
||||
cb();
|
||||
}),
|
||||
[viewer, event],
|
||||
);
|
||||
|
||||
const getSnapshot = useCallback(() => {
|
||||
if (cache.current === null) {
|
||||
cache.current = { value: read(viewer) };
|
||||
}
|
||||
return cache.current.value;
|
||||
}, [viewer, read]);
|
||||
|
||||
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
||||
}
|
||||
|
||||
// Returns the current PenDocument; re-renders when 'load' fires.
|
||||
export function useDocument() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return useViewerSnapshot('load', useCallback((v: OpViewer) => v.document, []));
|
||||
}
|
||||
|
||||
// Returns the current viewport {panX, panY, zoom}; re-renders when 'viewportchange' fires.
|
||||
export function useViewport() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return useViewerSnapshot('viewportchange', useCallback((v: OpViewer) => v.viewport, []));
|
||||
}
|
||||
|
||||
// Returns the active page index; re-renders when 'load' fires.
|
||||
export function useActivePage(): number {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return useViewerSnapshot('load', useCallback((v: OpViewer) => v.activePage, []));
|
||||
}
|
||||
4
packages/op-web-sdk-react/src/index.ts
Normal file
4
packages/op-web-sdk-react/src/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export const VERSION = '0.8.0';
|
||||
export { DesignProvider, useViewer } from './use-viewer.js';
|
||||
export { useDocument, useViewport, useActivePage } from './hooks.js';
|
||||
export { DesignView, type DesignViewProps } from './design-view.js';
|
||||
16
packages/op-web-sdk-react/src/use-viewer.ts
Normal file
16
packages/op-web-sdk-react/src/use-viewer.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { createElement, useContext, type ReactNode } from 'react';
|
||||
import type { OpViewer } from '@zseven-w/op-web-sdk';
|
||||
import { ViewerContext } from './context.js';
|
||||
|
||||
// Provider component that injects an OpViewer into React context.
|
||||
export function DesignProvider(props: { viewer: OpViewer; children: ReactNode }) {
|
||||
return createElement(ViewerContext.Provider, { value: props.viewer }, props.children);
|
||||
}
|
||||
|
||||
// Returns the OpViewer from the nearest DesignProvider/DesignView ancestor.
|
||||
// Throws if called outside a provider tree.
|
||||
export function useViewer(): OpViewer {
|
||||
const v = useContext(ViewerContext);
|
||||
if (!v) throw new Error('op-web-sdk-react: useViewer must be used inside <DesignProvider>/<DesignView>');
|
||||
return v;
|
||||
}
|
||||
21
packages/op-web-sdk-react/test/design-view.test.tsx
Normal file
21
packages/op-web-sdk-react/test/design-view.test.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { makeMockViewer } from './mock-core.js';
|
||||
|
||||
const mock = makeMockViewer();
|
||||
vi.mock('@zseven-w/op-web-sdk', () => ({ createViewer: vi.fn(async () => mock.viewer) }));
|
||||
import { DesignView } from '../src/index.js';
|
||||
import { createViewer } from '@zseven-w/op-web-sdk';
|
||||
|
||||
describe('DesignView', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
it('creates a viewer on mount and destroys on unmount', async () => {
|
||||
const onLoad = vi.fn();
|
||||
const { unmount, container } = render(<DesignView doc='{"version":"1.0"}' onLoad={onLoad} />);
|
||||
expect(container.querySelector('canvas')).toBeTruthy();
|
||||
await waitFor(() => expect(createViewer).toHaveBeenCalledTimes(1));
|
||||
await waitFor(() => expect(onLoad).toHaveBeenCalledWith(mock.viewer));
|
||||
unmount();
|
||||
expect(mock.viewer.destroy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
19
packages/op-web-sdk-react/test/hooks.test.tsx
Normal file
19
packages/op-web-sdk-react/test/hooks.test.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { act } from '@testing-library/react';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { makeMockViewer } from './mock-core.js';
|
||||
import { DesignProvider, useViewport } from '../src/index.js';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
describe('useViewport', () => {
|
||||
it('re-renders on viewportchange', () => {
|
||||
const { viewer, emit } = makeMockViewer();
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<DesignProvider viewer={viewer as never}>{children}</DesignProvider>
|
||||
);
|
||||
const { result } = renderHook(() => useViewport(), { wrapper });
|
||||
expect(result.current.zoom).toBe(1);
|
||||
act(() => { viewer.setZoom(3); emit('viewportchange'); });
|
||||
expect(result.current.zoom).toBe(3);
|
||||
});
|
||||
});
|
||||
24
packages/op-web-sdk-react/test/mock-core.ts
Normal file
24
packages/op-web-sdk-react/test/mock-core.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { vi } from 'vitest';
|
||||
// A controllable fake OpViewer for adapter tests. `emit(event)` fires listeners.
|
||||
export function makeMockViewer() {
|
||||
const listeners: Record<string, Set<() => void>> = { load: new Set(), viewportchange: new Set() };
|
||||
let vp = { panX: 0, panY: 0, zoom: 1 };
|
||||
const viewer = {
|
||||
load: vi.fn(),
|
||||
get document() { return { id: 'doc', pages: [] }; },
|
||||
get pages() { return [{ id: 'p', name: 'P', children: [] }]; },
|
||||
get pageCount() { return 1; },
|
||||
get activePage() { return 0; },
|
||||
get viewport() { return vp; },
|
||||
setViewport: vi.fn((v: { panX: number; panY: number; zoom: number }) => { vp = v; }),
|
||||
setZoom: vi.fn((z: number) => { vp = { ...vp, zoom: z }; }),
|
||||
panTo: vi.fn(), zoomToFit: vi.fn(),
|
||||
export: vi.fn(() => new Uint8Array([60, 115, 118, 103, 47, 62])),
|
||||
on: vi.fn((e: string, cb: () => void) => { listeners[e].add(cb); return () => listeners[e].delete(cb); }),
|
||||
off: vi.fn((e: string, cb: () => void) => { listeners[e].delete(cb); }),
|
||||
destroy: vi.fn(),
|
||||
};
|
||||
const emit = (e: 'load' | 'viewportchange') => listeners[e].forEach((cb) => cb());
|
||||
return { viewer, emit };
|
||||
}
|
||||
// Shared vi.mock factory: tests call vi.mock('@zseven-w/op-web-sdk', () => coreMock(current))
|
||||
5
packages/op-web-sdk-react/test/scaffold.test.tsx
Normal file
5
packages/op-web-sdk-react/test/scaffold.test.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { VERSION } from '../src/index.js';
|
||||
describe('react adapter scaffold', () => {
|
||||
it('exposes VERSION', () => { expect(typeof VERSION).toBe('string'); });
|
||||
});
|
||||
19
packages/op-web-sdk-react/test/use-viewer.test.tsx
Normal file
19
packages/op-web-sdk-react/test/use-viewer.test.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { makeMockViewer } from './mock-core.js';
|
||||
import { DesignProvider, useViewer } from '../src/index.js';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
describe('useViewer', () => {
|
||||
it('returns the provided viewer', () => {
|
||||
const { viewer } = makeMockViewer();
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<DesignProvider viewer={viewer as never}>{children}</DesignProvider>
|
||||
);
|
||||
const { result } = renderHook(() => useViewer(), { wrapper });
|
||||
expect(result.current).toBe(viewer);
|
||||
});
|
||||
it('throws without a provider', () => {
|
||||
expect(() => renderHook(() => useViewer())).toThrow();
|
||||
});
|
||||
});
|
||||
12
packages/op-web-sdk-react/tsconfig.json
Normal file
12
packages/op-web-sdk-react/tsconfig.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler",
|
||||
"strict": true, "jsx": "react-jsx", "skipLibCheck": true, "noEmit": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"], "types": ["vitest/globals"],
|
||||
"paths": {
|
||||
"@zseven-w/op-web-sdk": ["../op-web-sdk/src/index.ts"],
|
||||
"virtual:op_web_sdk_wasm": ["../op-web-sdk/test/wasm-stub.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "test"]
|
||||
}
|
||||
2
packages/op-web-sdk-react/tsup.config.ts
Normal file
2
packages/op-web-sdk-react/tsup.config.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import { defineConfig } from 'tsup';
|
||||
export default defineConfig({ entry: ['src/index.ts'], format: ['esm', 'cjs'], dts: true, clean: true, sourcemap: true, external: ['react', 'react-dom', '@zseven-w/op-web-sdk'] });
|
||||
11
packages/op-web-sdk-react/vitest.config.ts
Normal file
11
packages/op-web-sdk-react/vitest.config.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolve } from 'node:path';
|
||||
const here = fileURLToPath(new URL('.', import.meta.url));
|
||||
export default defineConfig({
|
||||
test: { environment: 'jsdom', globals: true },
|
||||
resolve: { alias: {
|
||||
'@zseven-w/op-web-sdk': resolve(here, '../op-web-sdk/src/index.ts'),
|
||||
'virtual:op_web_sdk_wasm': resolve(here, '../op-web-sdk/test/wasm-stub.ts'),
|
||||
} },
|
||||
});
|
||||
1
packages/op-web-sdk-vue/.gitignore
vendored
Normal file
1
packages/op-web-sdk-vue/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/dist/
|
||||
139
packages/op-web-sdk-vue/README.md
Normal file
139
packages/op-web-sdk-vue/README.md
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# @zseven-w/op-web-sdk-vue
|
||||
|
||||
Vue 3 adapter for the OpenPencil read-only web viewer SDK. Embed a live OpenPencil design canvas in any Vue 3 application.
|
||||
|
||||
> **Read-only boundary:** This package provides a viewer only — no editing. For the full editor experience, use the OpenPencil desktop or web app directly.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install @zseven-w/op-web-sdk @zseven-w/op-web-sdk-vue
|
||||
# or
|
||||
bun add @zseven-w/op-web-sdk @zseven-w/op-web-sdk-vue
|
||||
```
|
||||
|
||||
## Peer dependencies
|
||||
|
||||
| Package | Required version |
|
||||
|---------|-----------------|
|
||||
| `vue` | `^3.4.0` |
|
||||
|
||||
## WASM note
|
||||
|
||||
The core SDK (`@zseven-w/op-web-sdk`) loads a WebAssembly binary at runtime. You must either:
|
||||
- Pass the wasm URL explicitly via the `wasmUrl` prop, or
|
||||
- Serve the wasm file from your own static assets and ensure it is reachable.
|
||||
|
||||
The wasm file ships alongside the core package at `@zseven-w/op-web-sdk/dist/op_web_sdk_bg.wasm`.
|
||||
|
||||
## Usage
|
||||
|
||||
### `<DesignView>` component
|
||||
|
||||
The simplest way to embed a design viewer:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { DesignView } from '@zseven-w/op-web-sdk-vue';
|
||||
import type { OpViewer } from '@zseven-w/op-web-sdk';
|
||||
|
||||
function onLoad(viewer: OpViewer) {
|
||||
console.log('viewer ready', viewer.document);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DesignView
|
||||
:doc="docStringOrBytes"
|
||||
wasmUrl="/assets/op_web_sdk_bg.wasm"
|
||||
style="width: 800px; height: 600px"
|
||||
@load="onLoad"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
**Props:**
|
||||
- `doc?: string | Uint8Array` — serialized OpenPencil document (JSON string or binary bytes). Omit to start with an empty document.
|
||||
- `wasmUrl?: string` — URL to the core WASM binary. Required unless your bundler resolves the virtual `virtual:op_web_sdk_wasm` module.
|
||||
|
||||
**Events:**
|
||||
- `load(viewer: OpViewer)` — fired once the viewer is ready.
|
||||
|
||||
### Manual provide / inject
|
||||
|
||||
For advanced layouts where you need the viewer in child components, call `provideViewer` yourself and use `useViewer` in descendants:
|
||||
|
||||
```vue
|
||||
<!-- parent.vue -->
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { createViewer } from '@zseven-w/op-web-sdk';
|
||||
import { provideViewer } from '@zseven-w/op-web-sdk-vue';
|
||||
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
||||
onMounted(async () => {
|
||||
const viewer = await createViewer({ canvas: canvasRef.value! });
|
||||
provideViewer(viewer);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- child.vue -->
|
||||
<script setup lang="ts">
|
||||
import { useViewer } from '@zseven-w/op-web-sdk-vue';
|
||||
const viewer = useViewer(); // throws if no provider above
|
||||
</script>
|
||||
```
|
||||
|
||||
> **Vue lifecycle note:** `provide()` must be called during `setup()`, not inside `onMounted`. The built-in `<DesignView>` calls `provideViewer` in `onMounted` (after the async `createViewer` resolves), which means synchronous children cannot `inject` the viewer in their own `setup()`. This is acceptable for the common standalone use-case. If you need synchronous child injection, implement your own wrapper that `provide`s a `shallowRef<OpViewer|null>` in `setup()` and fills it on mount.
|
||||
|
||||
## Composables
|
||||
|
||||
These composables must be called inside a component that is a descendant of a `provideViewer` call (or `<DesignView>`).
|
||||
|
||||
| Composable | Returns | Reactive event |
|
||||
|------------|---------|----------------|
|
||||
| `useViewer()` | `OpViewer` | — (static reference) |
|
||||
| `useDocument()` | `Readonly<ShallowRef<PenDocument>>` | `'load'` |
|
||||
| `useViewport()` | `Readonly<ShallowRef<{panX,panY,zoom}>>` | `'viewportchange'` |
|
||||
| `useActivePage()` | `Readonly<ShallowRef<number>>` | `'load'` |
|
||||
|
||||
Each composable subscribes to the relevant viewer event and automatically unsubscribes when the component scope is disposed.
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { useViewport, useDocument } from '@zseven-w/op-web-sdk-vue';
|
||||
|
||||
const viewport = useViewport(); // ShallowRef<{panX, panY, zoom}>
|
||||
const doc = useDocument(); // ShallowRef<PenDocument>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p>Zoom: {{ viewport.zoom }}</p>
|
||||
<p>Pages: {{ doc.pages?.length }}</p>
|
||||
</template>
|
||||
```
|
||||
|
||||
## API reference
|
||||
|
||||
```ts
|
||||
// Provider / inject
|
||||
function provideViewer(viewer: OpViewer): void
|
||||
function useViewer(): OpViewer // throws if no provider
|
||||
|
||||
// Composables (reactive, auto-dispose)
|
||||
function useDocument(): Readonly<ShallowRef<PenDocument>>
|
||||
function useViewport(): Readonly<ShallowRef<{ panX: number; panY: number; zoom: number }>>
|
||||
function useActivePage(): Readonly<ShallowRef<number>>
|
||||
|
||||
// Component
|
||||
const DesignView: DefineComponent<{ doc?: string | Uint8Array; wasmUrl?: string }, {}, {}, {}, {}, {}, {}, { load: (viewer: OpViewer) => void }>
|
||||
|
||||
// Injection key (for advanced use)
|
||||
const viewerKey: InjectionKey<OpViewer>
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
16
packages/op-web-sdk-vue/package.json
Normal file
16
packages/op-web-sdk-vue/package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "@zseven-w/op-web-sdk-vue",
|
||||
"version": "0.8.0",
|
||||
"description": "Vue 3 adapter for the OpenPencil read-only web viewer SDK",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"files": ["dist"],
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" } },
|
||||
"scripts": { "build": "tsup", "typecheck": "tsc --noEmit", "test": "vitest run" },
|
||||
"dependencies": { "@zseven-w/op-web-sdk": "workspace:*" },
|
||||
"peerDependencies": { "vue": "^3.4.0" },
|
||||
"devDependencies": { "vue": "^3.4.0", "@vue/test-utils": "^2.4.0", "@vitejs/plugin-vue": "^5.0.0", "tsup": "^8.0.0", "typescript": "^5.7.2", "vitest": "^3.0.0", "jsdom": "^25.0.0" }
|
||||
}
|
||||
62
packages/op-web-sdk-vue/src/composables.ts
Normal file
62
packages/op-web-sdk-vue/src/composables.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { onScopeDispose, shallowRef, triggerRef, watch, type ShallowRef } from 'vue';
|
||||
import type { OpViewer, PenDocument } from '@zseven-w/op-web-sdk';
|
||||
import { useViewer } from './use-viewer.js';
|
||||
|
||||
type ViewerEvent = 'load' | 'viewportchange';
|
||||
|
||||
/**
|
||||
* Watch the viewer ref and keep an output ref in sync with a viewer property.
|
||||
* When the viewer becomes available (or changes), subscribe to `event` and
|
||||
* read the initial value immediately. The previous subscription is cleaned up
|
||||
* via watch's onCleanup callback.
|
||||
*/
|
||||
function useViewerRef<T>(
|
||||
event: ViewerEvent,
|
||||
read: (v: OpViewer) => T,
|
||||
fallback: T,
|
||||
): Readonly<ShallowRef<T>> {
|
||||
const viewerRef = useViewer();
|
||||
const out = shallowRef<T>(viewerRef.value != null ? read(viewerRef.value) : fallback);
|
||||
|
||||
// Track the latest unsubscribe function for onScopeDispose safety.
|
||||
let latestOff: (() => void) | null = null;
|
||||
|
||||
watch(
|
||||
viewerRef,
|
||||
(v, _old, onCleanup) => {
|
||||
if (!v) return;
|
||||
// Read initial value as soon as the viewer is available.
|
||||
out.value = read(v);
|
||||
triggerRef(out);
|
||||
// Subscribe to future events from this viewer.
|
||||
const off = v.on(event, () => {
|
||||
out.value = read(v);
|
||||
triggerRef(out);
|
||||
});
|
||||
latestOff = off;
|
||||
onCleanup(off);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// Safety: also unsubscribe when the composable scope is disposed (e.g. component unmounted
|
||||
// before the watcher's own cleanup runs).
|
||||
onScopeDispose(() => {
|
||||
latestOff?.();
|
||||
latestOff = null;
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
export function useDocument(): Readonly<ShallowRef<PenDocument | null>> {
|
||||
return useViewerRef('load', (v) => v.document, null);
|
||||
}
|
||||
|
||||
export function useViewport() {
|
||||
return useViewerRef('viewportchange', (v) => v.viewport, null);
|
||||
}
|
||||
|
||||
export function useActivePage(): Readonly<ShallowRef<number>> {
|
||||
return useViewerRef('load', (v) => v.activePage, 0);
|
||||
}
|
||||
54
packages/op-web-sdk-vue/src/design-view.ts
Normal file
54
packages/op-web-sdk-vue/src/design-view.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { defineComponent, Fragment, h, onMounted, onUnmounted, ref, shallowRef } from 'vue';
|
||||
import { createViewer, type OpViewer } from '@zseven-w/op-web-sdk';
|
||||
import { provideViewerRef } from './use-viewer.js';
|
||||
|
||||
export const DesignView = defineComponent({
|
||||
name: 'DesignView',
|
||||
props: {
|
||||
doc: {
|
||||
type: [String, Object] as unknown as () => string | Uint8Array,
|
||||
default: undefined,
|
||||
},
|
||||
wasmUrl: { type: String, default: undefined },
|
||||
},
|
||||
emits: { load: (_viewer: OpViewer) => true },
|
||||
setup(props, { emit, slots }) {
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
||||
|
||||
// Create the ref in setup() so provide() runs at the correct time.
|
||||
// Child composables that call useViewer() will receive this ref and
|
||||
// watch it; when viewerRef.value becomes non-null they subscribe.
|
||||
const viewerRef = shallowRef<OpViewer | null>(null);
|
||||
provideViewerRef(viewerRef);
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
onMounted(async () => {
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas) return;
|
||||
const v = await createViewer({ canvas, doc: props.doc, wasmUrl: props.wasmUrl });
|
||||
if (cancelled) {
|
||||
// Component was unmounted before the async viewer resolved — discard it.
|
||||
v.destroy();
|
||||
return;
|
||||
}
|
||||
// Setting .value triggers watchers in child composables.
|
||||
viewerRef.value = v;
|
||||
emit('load', v);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelled = true;
|
||||
viewerRef.value?.destroy();
|
||||
viewerRef.value = null;
|
||||
});
|
||||
|
||||
// Render the canvas plus any slotted children (e.g. child components that
|
||||
// call useViewport/useDocument to subscribe to viewer events).
|
||||
return () =>
|
||||
h(Fragment, [
|
||||
h('canvas', { ref: canvasRef, style: 'width:100%;height:100%;display:block' }),
|
||||
slots.default?.(),
|
||||
]);
|
||||
},
|
||||
});
|
||||
5
packages/op-web-sdk-vue/src/index.ts
Normal file
5
packages/op-web-sdk-vue/src/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export const VERSION = '0.8.0';
|
||||
export { viewerKey } from './injection.js';
|
||||
export { provideViewer, provideViewerRef, useViewer } from './use-viewer.js';
|
||||
export { useDocument, useViewport, useActivePage } from './composables.js';
|
||||
export { DesignView } from './design-view.js';
|
||||
3
packages/op-web-sdk-vue/src/injection.ts
Normal file
3
packages/op-web-sdk-vue/src/injection.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import type { InjectionKey, ShallowRef } from 'vue';
|
||||
import type { OpViewer } from '@zseven-w/op-web-sdk';
|
||||
export const viewerKey: InjectionKey<ShallowRef<OpViewer | null>> = Symbol('op-web-sdk-viewer');
|
||||
27
packages/op-web-sdk-vue/src/use-viewer.ts
Normal file
27
packages/op-web-sdk-vue/src/use-viewer.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { inject, provide, shallowRef, type ShallowRef } from 'vue';
|
||||
import type { OpViewer } from '@zseven-w/op-web-sdk';
|
||||
import { viewerKey } from './injection.js';
|
||||
|
||||
/** Provide a viewer synchronously (manual / test use). */
|
||||
export function provideViewer(viewer: OpViewer): void {
|
||||
provide(viewerKey, shallowRef(viewer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a pre-created ShallowRef so that DesignView can fill it
|
||||
* asynchronously after mount while still calling provide() in setup().
|
||||
*/
|
||||
export function provideViewerRef(ref: ShallowRef<OpViewer | null>): void {
|
||||
provide(viewerKey, ref);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the injected ShallowRef<OpViewer | null>. Consumers read `.value`
|
||||
* to access the viewer; it becomes non-null once the async viewer is ready.
|
||||
* Throws if called outside a DesignView / provideViewer context.
|
||||
*/
|
||||
export function useViewer(): ShallowRef<OpViewer | null> {
|
||||
const v = inject(viewerKey);
|
||||
if (!v) throw new Error('op-web-sdk-vue: useViewer must be used inside <DesignView>/provideViewer');
|
||||
return v;
|
||||
}
|
||||
19
packages/op-web-sdk-vue/test/composables.test.ts
Normal file
19
packages/op-web-sdk-vue/test/composables.test.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { defineComponent, h, nextTick } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { makeMockViewer } from './mock-core.js';
|
||||
import { provideViewer, useViewport } from '../src/index.js';
|
||||
|
||||
describe('useViewport (vue)', () => {
|
||||
it('updates on viewportchange', async () => {
|
||||
const { viewer, emit } = makeMockViewer();
|
||||
let vpRef: { value: { zoom: number } };
|
||||
const Child = defineComponent({ setup() { vpRef = useViewport() as never; return () => h('div'); } });
|
||||
const Parent = defineComponent({ setup() { provideViewer(viewer as never); return () => h(Child); } });
|
||||
mount(Parent);
|
||||
expect(vpRef!.value.zoom).toBe(1);
|
||||
viewer.setZoom(4); emit('viewportchange');
|
||||
await nextTick();
|
||||
expect(vpRef!.value.zoom).toBe(4);
|
||||
});
|
||||
});
|
||||
49
packages/op-web-sdk-vue/test/design-view.test.ts
Normal file
49
packages/op-web-sdk-vue/test/design-view.test.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { defineComponent, h, nextTick } from 'vue';
|
||||
import { mount, flushPromises } from '@vue/test-utils';
|
||||
import { makeMockViewer } from './mock-core.js';
|
||||
const mock = makeMockViewer();
|
||||
vi.mock('@zseven-w/op-web-sdk', () => ({ createViewer: vi.fn(async () => mock.viewer) }));
|
||||
import { DesignView } from '../src/index.js';
|
||||
import { useViewport } from '../src/index.js';
|
||||
import { createViewer } from '@zseven-w/op-web-sdk';
|
||||
|
||||
describe('DesignView (vue)', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it('creates a viewer on mount, emits load, destroys on unmount', async () => {
|
||||
const wrapper = mount(DesignView, { props: { doc: '{"version":"1.0"}' } });
|
||||
expect(wrapper.find('canvas').exists()).toBe(true);
|
||||
await flushPromises();
|
||||
expect(createViewer).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.emitted('load')?.[0]?.[0]).toBe(mock.viewer);
|
||||
wrapper.unmount();
|
||||
expect(mock.viewer.destroy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('child composable inside DesignView receives viewport after async load', async () => {
|
||||
// Proves that provide() in setup() + async fill lets children inject correctly.
|
||||
let vpRef: ReturnType<typeof useViewport> | undefined;
|
||||
const Child = defineComponent({
|
||||
setup() {
|
||||
vpRef = useViewport();
|
||||
return () => h('div');
|
||||
},
|
||||
});
|
||||
const Parent = defineComponent({
|
||||
setup() {
|
||||
return () => h(DesignView, { doc: '{"version":"1.0"}' }, { default: () => h(Child) });
|
||||
},
|
||||
});
|
||||
|
||||
mount(Parent);
|
||||
// Before load: viewport is null (viewer not yet created).
|
||||
expect(vpRef!.value).toBeNull();
|
||||
|
||||
// After async createViewer resolves, viewerRef is set and watcher fires.
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
expect(vpRef!.value).not.toBeNull();
|
||||
expect((vpRef!.value as { zoom: number }).zoom).toBe(1);
|
||||
});
|
||||
});
|
||||
24
packages/op-web-sdk-vue/test/mock-core.ts
Normal file
24
packages/op-web-sdk-vue/test/mock-core.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { vi } from 'vitest';
|
||||
// A controllable fake OpViewer for adapter tests. `emit(event)` fires listeners.
|
||||
export function makeMockViewer() {
|
||||
const listeners: Record<string, Set<() => void>> = { load: new Set(), viewportchange: new Set() };
|
||||
let vp = { panX: 0, panY: 0, zoom: 1 };
|
||||
const viewer = {
|
||||
load: vi.fn(),
|
||||
get document() { return { id: 'doc', pages: [] }; },
|
||||
get pages() { return [{ id: 'p', name: 'P', children: [] }]; },
|
||||
get pageCount() { return 1; },
|
||||
get activePage() { return 0; },
|
||||
get viewport() { return vp; },
|
||||
setViewport: vi.fn((v: { panX: number; panY: number; zoom: number }) => { vp = v; }),
|
||||
setZoom: vi.fn((z: number) => { vp = { ...vp, zoom: z }; }),
|
||||
panTo: vi.fn(), zoomToFit: vi.fn(),
|
||||
export: vi.fn(() => new Uint8Array([60, 115, 118, 103, 47, 62])),
|
||||
on: vi.fn((e: string, cb: () => void) => { listeners[e].add(cb); return () => listeners[e].delete(cb); }),
|
||||
off: vi.fn((e: string, cb: () => void) => { listeners[e].delete(cb); }),
|
||||
destroy: vi.fn(),
|
||||
};
|
||||
const emit = (e: 'load' | 'viewportchange') => listeners[e].forEach((cb) => cb());
|
||||
return { viewer, emit };
|
||||
}
|
||||
// Shared vi.mock factory: tests call vi.mock('@zseven-w/op-web-sdk', () => coreMock(current))
|
||||
17
packages/op-web-sdk-vue/test/use-viewer.test.ts
Normal file
17
packages/op-web-sdk-vue/test/use-viewer.test.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { defineComponent, h } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { makeMockViewer } from './mock-core.js';
|
||||
import { provideViewer, useViewer } from '../src/index.js';
|
||||
|
||||
describe('useViewer (vue)', () => {
|
||||
it('returns a ref whose .value is the provided viewer', () => {
|
||||
const { viewer } = makeMockViewer();
|
||||
let got: unknown;
|
||||
// useViewer() now returns ShallowRef<OpViewer | null>; read .value to get the viewer.
|
||||
const Child = defineComponent({ setup() { got = useViewer().value; return () => h('div'); } });
|
||||
const Parent = defineComponent({ setup() { provideViewer(viewer as never); return () => h(Child); } });
|
||||
mount(Parent);
|
||||
expect(got).toBe(viewer);
|
||||
});
|
||||
});
|
||||
12
packages/op-web-sdk-vue/tsconfig.json
Normal file
12
packages/op-web-sdk-vue/tsconfig.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler",
|
||||
"strict": true, "skipLibCheck": true, "noEmit": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"], "types": ["vitest/globals"],
|
||||
"paths": {
|
||||
"@zseven-w/op-web-sdk": ["../op-web-sdk/src/index.ts"],
|
||||
"virtual:op_web_sdk_wasm": ["../op-web-sdk/test/wasm-stub.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "test"]
|
||||
}
|
||||
2
packages/op-web-sdk-vue/tsup.config.ts
Normal file
2
packages/op-web-sdk-vue/tsup.config.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import { defineConfig } from 'tsup';
|
||||
export default defineConfig({ entry: ['src/index.ts'], format: ['esm', 'cjs'], dts: true, clean: true, sourcemap: true, external: ['vue', '@zseven-w/op-web-sdk'] });
|
||||
13
packages/op-web-sdk-vue/vitest.config.ts
Normal file
13
packages/op-web-sdk-vue/vitest.config.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolve } from 'node:path';
|
||||
const here = fileURLToPath(new URL('.', import.meta.url));
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
test: { environment: 'jsdom', globals: true },
|
||||
resolve: { alias: {
|
||||
'@zseven-w/op-web-sdk': resolve(here, '../op-web-sdk/src/index.ts'),
|
||||
'virtual:op_web_sdk_wasm': resolve(here, '../op-web-sdk/test/wasm-stub.ts'),
|
||||
} },
|
||||
});
|
||||
Loading…
Reference in a new issue