diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 0987b5f..d75743c 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -39,6 +39,11 @@ jobs:
mkdir -p site/app
cp -r ./docs/.vitepress/dist/* ./site
cp -r ./dist/* ./site/app
+ - name: Make the app cross-origin isolated
+ # GitHub Pages can't send the COOP/COEP headers that libOpenCOR needs,
+ # so load the docs' service worker registration script
+ # (see docs/public/coi-register.js), whose scope also covers the app.
+ run: sed -i 's#
#\n #' site/app/index.html
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js
index 0db4827..cae913f 100644
--- a/docs/.vitepress/config.js
+++ b/docs/.vitepress/config.js
@@ -1,14 +1,22 @@
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vitepress';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
+import libopencor from '../../vite-plugin-libopencor.js';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
const versionNumber = process.env.npm_package_version;
+const base = '/simulationvuer/';
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: 'SimulationVuer',
description: 'API documentation for SimulationVuer',
- base: '/simulationvuer/',
+ base,
+ // Make the pages cross-origin isolated on hosts that can't send the COOP/COEP headers (e.g., GitHub Pages).
+ head: [['script', { src: `${base}coi-register.js` }]],
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
nav: [
@@ -55,6 +63,26 @@ export default defineConfig({
},
markdown: { attrs: { disable: true } },
vite: {
+ // Load the .env files (e.g., VITE_API_LOCATION for the demos) from the project root, like the app does, rather than
+ // from VitePress' root (i.e., docs).
+
+ envDir: path.resolve(__dirname, '../..'),
+
+ // Some dependencies (e.g., has-hover, used by Plotly.js through PlotVuer) reference Node's `global`,
+ // which doesn't exist in browsers.
+ // Note: VitePress runs on Vite 5, whose `define` doesn't reach pre-bundled dependencies in dev,
+ // so it is also passed to esbuild through `optimizeDeps`.
+
+ define: {
+ global: 'globalThis',
+ },
+ optimizeDeps: {
+ esbuildOptions: {
+ define: {
+ global: 'globalThis',
+ },
+ },
+ },
css: {
preprocessorOptions: {
scss: {
@@ -74,6 +102,17 @@ export default defineConfig({
}),
],
}),
+ libopencor(),
+ {
+ name: 'cross-origin-isolation',
+ configureServer(server) {
+ server.middlewares.use((_req, res, next) => {
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
+ res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
+ next();
+ });
+ },
+ },
],
},
});
diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css
index a39de35..67e6992 100644
--- a/docs/.vitepress/theme/custom.css
+++ b/docs/.vitepress/theme/custom.css
@@ -61,3 +61,7 @@
}
}
}
+
+.Layout * {
+ box-sizing: border-box;
+}
diff --git a/docs/public/coi-register.js b/docs/public/coi-register.js
new file mode 100644
index 0000000..7bd2846
--- /dev/null
+++ b/docs/public/coi-register.js
@@ -0,0 +1,47 @@
+// Register the cross-origin isolation service worker (see coi-serviceworker.js)
+// when the host doesn't send the COOP/COEP headers (e.g., GitHub Pages),
+// and reload once so that the page is served through it.
+// Note: this is loaded as a classic (i.e., non-module) script from the docs' head and, on GitHub Pages, from the app's
+// head (see .github/workflows/deploy.yml). The service worker is located relative to this script, so its scope
+// (i.e., the docs' base) also covers the app deployed under it.
+// Note: the reload is only done once per session to avoid a reload loop if isolation still can't be achieved.
+
+(() => {
+ if (window.crossOriginIsolated || !window.isSecureContext || !('serviceWorker' in navigator)) {
+ try {
+ sessionStorage.removeItem('coiReloaded');
+ } catch {
+ // Ignore storage errors (e.g., in a private window).
+ }
+
+ return;
+ }
+
+ const reload = () => {
+ try {
+ if (sessionStorage.getItem('coiReloaded')) {
+ return;
+ }
+
+ sessionStorage.setItem('coiReloaded', '1');
+ } catch {
+ // Ignore storage errors (e.g., in a private window).
+ }
+
+ location.reload();
+ };
+
+ const serviceWorkerUrl = new URL('coi-serviceworker.js', document.currentScript.src);
+
+ navigator.serviceWorker.register(serviceWorkerUrl).then((registration) => {
+ if (navigator.serviceWorker.controller) {
+ return;
+ }
+
+ if (registration.active) {
+ reload();
+ } else {
+ navigator.serviceWorker.addEventListener('controllerchange', reload);
+ }
+ });
+})();
diff --git a/docs/public/coi-serviceworker.js b/docs/public/coi-serviceworker.js
new file mode 100644
index 0000000..b78cedf
--- /dev/null
+++ b/docs/public/coi-serviceworker.js
@@ -0,0 +1,46 @@
+// Cross-origin isolation service worker for the docs.
+//
+// libOpenCOR is built with Emscripten pthreads, which need SharedArrayBuffer (i.e., a shared WebAssembly.Memory),
+// which browsers only allow on cross-origin isolated pages. That requires the Cross-Origin-Opener-Policy and
+// Cross-Origin-Embedder-Policy response headers, which the dev server sends, but static hosts like GitHub Pages can't.
+// So this service worker adds them to every response in its scope (see the registration script in
+// docs/.vitepress/config.js).
+
+self.addEventListener('install', () => {
+ self.skipWaiting();
+});
+
+self.addEventListener('activate', (event) => {
+ event.waitUntil(self.clients.claim());
+});
+
+self.addEventListener('fetch', (event) => {
+ const request = event.request;
+
+ // Work around a Chrome bug where "only-if-cached" requests fail unless they are same-origin.
+
+ if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
+ return;
+ }
+
+ event.respondWith(
+ fetch(request).then((response) => {
+ // Opaque responses (from no-cors cross-origin requests) can't be modified, so return them as is.
+
+ if (response.status === 0) {
+ return response;
+ }
+
+ const headers = new Headers(response.headers);
+
+ headers.set('Cross-Origin-Opener-Policy', 'same-origin');
+ headers.set('Cross-Origin-Embedder-Policy', 'require-corp');
+
+ return new Response(response.body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers,
+ });
+ }),
+ );
+});
diff --git a/vite-plugin-libopencor.js b/vite-plugin-libopencor.js
new file mode 100644
index 0000000..3d32858
--- /dev/null
+++ b/vite-plugin-libopencor.js
@@ -0,0 +1,148 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const libopencorDir = path.resolve(__dirname, 'node_modules/@opencor/opencor/dist/libopencor');
+
+// Serve libOpenCOR for OpenCOR's runtime probe.
+//
+// In @opencor/opencor, opencor.es.js locates its glue code at runtime:
+// new URL("libopencor/downloads/wasm//libopencor.js", document.baseURI).href
+// and fetches that same-origin path (HEAD) before falling back to opencor.ws. The WASM is always loaded from a
+// hardcoded opencor.ws base via locateFile. The probe is evaluated at runtime and the glue is imported
+// dynamically (with @vite-ignore), so there is no static URL for Vite to rewrite.
+//
+// The probe is satisfied by:
+// - Dev (serve): serving "libopencor/*" from the installed package via the middleware below, so the probe
+// resolves same-origin (required for pthread Workers).
+// - Build: copying the package's libOpenCOR dir to /libopencor via closeBundle, so the document-relative
+// probe hits when the page is served from the output root (otherwise OpenCOR falls back to opencor.ws).
+//
+// The base and outDir come from the resolved config, so the same plugin works for both the app/library (base "/",
+// outDir "dist") and the VitePress docs (base "/simulationvuer/", outDir "docs/.vitepress/dist").
+
+export default function libopencor() {
+ let resolvedConfig;
+
+ return {
+ name: 'opencor-libopencor',
+ configResolved(config) {
+ resolvedConfig = config;
+ },
+ configureServer(server) {
+ // Serve libopencor files from @opencor/opencor during development so the dynamic import in opencor.es.js
+ // resolves same-origin (required for pthread Workers).
+ // Note: a relative base (e.g., "./" in app mode) is served from "/" in dev.
+
+ const base = resolvedConfig.base.startsWith('/') ? resolvedConfig.base : '/';
+ const prefix = `${base.endsWith('/') ? base : `${base}/`}libopencor/`;
+
+ // Resolve symlinks in libopencorDir once so the per-request guard compares real paths on both sides.
+
+ let realLibopencorDir;
+
+ try {
+ realLibopencorDir = fs.realpathSync(libopencorDir);
+ } catch {
+ realLibopencorDir = libopencorDir;
+ }
+
+ server.middlewares.use((req, res, next) => {
+ // Parse the URL to strip any query string, and decode it safely (malformed sequences are rejected).
+
+ let pathname;
+
+ try {
+ pathname = decodeURIComponent(new URL(req.url, 'http://localhost').pathname);
+ } catch {
+ next();
+
+ return;
+ }
+
+ if (!pathname.startsWith(prefix)) {
+ next();
+
+ return;
+ }
+
+ // Resolve the requested subpath and verify the result stays within libopencorDir, guarding against
+ // directory traversal (e.g., "/libopencor/../../..." or percent-encoded ".." segments) and symlink
+ // escapes (a symlink inside libopencorDir pointing outside would pass a purely lexical check).
+ // Note: realpathSync() resolves symlinks, so a symlink pointing outside libopencorDir resolves to a path
+ // outside it, which the relative check below rejects. It also throws if the path doesn't exist.
+
+ let filePath;
+
+ try {
+ filePath = fs.realpathSync(path.resolve(libopencorDir, pathname.slice(prefix.length)));
+ } catch {
+ next();
+
+ return;
+ }
+
+ // Reject paths escaping the directory: exactly "..", starting with "../", or absolute (e.g., a
+ // different drive on Windows).
+ // Note: a plain startsWith("..") check would wrongly reject legitimate files like "..foo.js".
+
+ const relative = path.relative(realLibopencorDir, filePath);
+
+ if (
+ relative === '' ||
+ relative === '..' ||
+ relative.startsWith(`..${path.sep}`) ||
+ path.isAbsolute(relative)
+ ) {
+ next();
+
+ return;
+ }
+
+ // Read the file and serve it with the correct headers.
+
+ let content;
+
+ try {
+ if (!fs.statSync(filePath).isFile()) {
+ next();
+
+ return;
+ }
+
+ content = fs.readFileSync(filePath);
+ } catch {
+ next();
+
+ return;
+ }
+
+ res.writeHead(200, {
+ 'Content-Type': 'application/javascript',
+ 'Cross-Origin-Embedder-Policy': 'require-corp',
+ 'Cross-Origin-Resource-Policy': 'same-origin',
+ });
+ res.end(content);
+ });
+ },
+ closeBundle() {
+ // Copy libOpenCOR to the output root (/libopencor) so that OpenCOR's document-relative probe
+ // (`/libopencor/downloads/wasm//libopencor.js`) resolves: in app mode and for the docs, the
+ // output dir is the deployment root, and in lib mode the probed files ship in the package for consumers that
+ // serve them at the page root (everyone else falls back to opencor.ws).
+ // Note: SSR builds (e.g., VitePress' server-side render pass) don't produce a deployable output, so skip them.
+
+ if (resolvedConfig.build.ssr || !fs.existsSync(libopencorDir)) {
+ return;
+ }
+
+ const targetDir = path.join(
+ path.resolve(resolvedConfig.root, resolvedConfig.build.outDir),
+ 'libopencor',
+ );
+
+ fs.cpSync(libopencorDir, targetDir, { recursive: true, force: true });
+ },
+ };
+}
diff --git a/vite.config.js b/vite.config.js
index fde0d9c..6fd46c9 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,6 +1,5 @@
import vue from '@vitejs/plugin-vue';
-import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import AutoImport from 'unplugin-auto-import/vite';
@@ -8,9 +7,10 @@ import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
import Components from 'unplugin-vue-components/vite';
import { defineConfig } from 'vite';
+import libopencor from './vite-plugin-libopencor.js';
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const pathSrc = path.resolve(__dirname, './src');
-const libopencorDir = path.resolve(__dirname, 'node_modules/@opencor/opencor/dist/libopencor');
export default defineConfig(({ command, mode }) => {
const config = {
@@ -40,132 +40,7 @@ export default defineConfig(({ command, mode }) => {
dts: 'src/components.d.ts',
}),
- // Serve libOpenCOR for OpenCOR's runtime probe.
- //
- // In @opencor/opencor, opencor.es.js locates its glue code at runtime:
- // new URL("libopencor/downloads/wasm//libopencor.js", document.baseURI).href
- // and fetches that same-origin path (HEAD) before falling back to opencor.ws. The WASM is always loaded from a
- // hardcoded opencor.ws base via locateFile. The probe is evaluated at runtime and the glue is imported
- // dynamically (with @vite-ignore), so there is no static URL for Vite to rewrite.
- //
- // The probe is satisfied by:
- // - Dev (serve): serving "/libopencor/*" from the installed package via the middleware below, so the probe
- // resolves same-origin (required for pthread Workers).
- // - Build (lib): copying the package's libOpenCOR dir to dist/libopencor via closeBundle, so the
- // document-relative probe hits when the page is served from the dist root (otherwise OpenCOR
- // falls back to opencor.ws).
-
- {
- name: 'opencor-libopencor',
- configureServer(server) {
- // Serve libopencor files from @opencor/opencor during development so the dynamic import in opencor.es.js
- // resolves same-origin (required for pthread Workers).
-
- // Resolve symlinks in libopencorDir once so the per-request guard compares real paths on both sides.
-
- let realLibopencorDir;
-
- try {
- realLibopencorDir = fs.realpathSync(libopencorDir);
- } catch {
- realLibopencorDir = libopencorDir;
- }
-
- server.middlewares.use((req, res, next) => {
- // Parse the URL to strip any query string, and decode it safely (malformed sequences are rejected).
-
- let pathname;
-
- try {
- pathname = decodeURIComponent(new URL(req.url, 'http://localhost').pathname);
- } catch {
- next();
-
- return;
- }
-
- if (!pathname.startsWith('/libopencor/')) {
- next();
-
- return;
- }
-
- // Resolve the requested subpath and verify the result stays within libopencorDir, guarding against
- // directory traversal (e.g., "/libopencor/../../..." or percent-encoded ".." segments) and symlink
- // escapes (a symlink inside libopencorDir pointing outside would pass a purely lexical check).
- // Note: realpathSync() resolves symlinks, so a symlink pointing outside libopencorDir resolves to a path
- // outside it, which the relative check below rejects. It also throws if the path doesn't exist.
-
- let filePath;
-
- try {
- filePath = fs.realpathSync(
- path.resolve(libopencorDir, pathname.slice('/libopencor/'.length)),
- );
- } catch {
- next();
-
- return;
- }
-
- // Reject paths escaping the directory: exactly "..", starting with "../", or absolute (e.g., a
- // different drive on Windows).
- // Note: a plain startsWith("..") check would wrongly reject legitimate files like "..foo.js".
-
- const relative = path.relative(realLibopencorDir, filePath);
-
- if (
- relative === '' ||
- relative === '..' ||
- relative.startsWith(`..${path.sep}`) ||
- path.isAbsolute(relative)
- ) {
- next();
-
- return;
- }
-
- // Read the file and serve it with the correct headers.
-
- let content;
-
- try {
- if (!fs.statSync(filePath).isFile()) {
- next();
-
- return;
- }
-
- content = fs.readFileSync(filePath);
- } catch {
- next();
-
- return;
- }
-
- res.writeHead(200, {
- 'Content-Type': 'application/javascript',
- 'Cross-Origin-Embedder-Policy': 'require-corp',
- 'Cross-Origin-Resource-Policy': 'same-origin',
- });
- res.end(content);
- });
- },
- closeBundle() {
- // Copy liboOpenCOR to the dist root (dist/libopencor) so that OpenCOR's document-relative probe
- // (`/libopencor/downloads/wasm//libopencor.js`) resolves: in app mode, dist is the
- // deployment root, and in lib mode the probed files ship in the package for consumers that serve them at the
- // page root (everyone else falls back to opencor.ws).
-
- if (!fs.existsSync(libopencorDir)) {
- return;
- }
-
- const targetDir = path.join(path.resolve(__dirname, 'dist'), 'libopencor');
-
- fs.cpSync(libopencorDir, targetDir, { recursive: true, force: true });
- },
- },
+ libopencor(),
],
base: mode === 'app' ? './' : '/',
build:
@@ -213,6 +88,12 @@ export default defineConfig(({ command, mode }) => {
'process.env.HTTP_PROXY': 8081,
global: 'globalThis',
};
+ } else if (mode === 'app') {
+ // Some dependencies (e.g., has-hover, used by Plotly.js through PlotVuer) reference Node's `global`,
+ // which doesn't exist in browsers. (In lib mode, it's up to the consumer's bundler.)
+ config.define = {
+ global: 'globalThis',
+ };
}
return config;
diff --git a/vuese-generator.js b/vuese-generator.js
index 649624f..1ad5a8e 100644
--- a/vuese-generator.js
+++ b/vuese-generator.js
@@ -65,7 +65,7 @@ function transformData(data = []) {
prop.describe = [prop.description.replaceAll('\n', ' ')];
}
- if (prop.type) {
+ if (prop.type?.name) {
// Handle multiple types separated by '|'
// Convert to array to avoid markdown table issues
if (prop.type.name.indexOf('|') !== -1) {