From 66945cb0009fa76a745b4c19e70a2b092b8ec0f4 Mon Sep 17 00:00:00 2001 From: hand-dot Date: Thu, 7 May 2026 17:58:04 +0900 Subject: [PATCH] fix(playground): harden jsx worker evaluator --- playground/e2e/jsxPlaygroundRuntime.test.ts | 14 +++ playground/src/routes/JsxPlayground.tsx | 111 +++++++++++++----- playground/src/routes/jsxPlaygroundRuntime.ts | 19 ++- playground/src/routes/jsxPlaygroundWorker.ts | 9 +- 4 files changed, 115 insertions(+), 38 deletions(-) diff --git a/playground/e2e/jsxPlaygroundRuntime.test.ts b/playground/e2e/jsxPlaygroundRuntime.test.ts index c3ec4e16..fcc6a5ff 100644 --- a/playground/e2e/jsxPlaygroundRuntime.test.ts +++ b/playground/e2e/jsxPlaygroundRuntime.test.ts @@ -21,6 +21,20 @@ describe('JSX playground runtime', () => { expect(() => compileJsxFunctionBody('return window.localStorage;')).toThrow( 'does not allow window', ); + expect(() => + compileJsxFunctionBody('return importScripts("https://example.com/a.js");'), + ).toThrow('does not allow importScripts'); + expect(() => + compileJsxFunctionBody('return ({fetch("/")});'), + ).toThrow('does not allow fetch'); + }); + + it('allows restricted words in rendered text content', () => { + expect(() => + compileJsxFunctionBody( + 'return (window document importScripts);', + ), + ).not.toThrow(); }); it('renders JSX into a normal pdfme template result', async () => { diff --git a/playground/src/routes/JsxPlayground.tsx b/playground/src/routes/JsxPlayground.tsx index 06e15236..6bfdd658 100644 --- a/playground/src/routes/JsxPlayground.tsx +++ b/playground/src/routes/JsxPlayground.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import type { Template } from '@pdfme/common'; import type { RenderResult } from '@pdfme/jsx'; import { Viewer } from '@pdfme/ui'; @@ -16,43 +16,22 @@ const RENDER_TIMEOUT_MS = 15_000; type WorkerResponse = | { + id: number; ok: true; result: RenderResult; } | { error: string; + id: number; ok: false; }; -const renderJsxSourceInWorker = (source: string) => - new Promise((resolve, reject) => { - const worker = new JsxPlaygroundWorker(); - const timeoutId = window.setTimeout(() => { - worker.terminate(); - reject(new Error('JSX render timed out.')); - }, RENDER_TIMEOUT_MS); - - const cleanup = () => { - window.clearTimeout(timeoutId); - worker.terminate(); - }; - - worker.onmessage = (event: MessageEvent) => { - cleanup(); - if (event.data.ok) { - resolve(event.data.result); - } else { - reject(new Error(event.data.error)); - } - }; - - worker.onerror = (event) => { - cleanup(); - reject(new Error(event.message || 'JSX render worker failed.')); - }; - - worker.postMessage({ font: getFontsData(), source }); - }); +type PendingRender = { + id: number; + reject: (error: Error) => void; + resolve: (result: RenderResult) => void; + timeoutId: number; +}; const configureJsxEditor: Parameters[0]['beforeMount'] = (monaco) => { const typeScriptLanguage = monaco.languages.typescript; @@ -96,6 +75,9 @@ declare function PageBreak(props?: Record): unknown; export default function JsxPlayground() { const viewerRootRef = useRef(null); const viewerRef = useRef(null); + const renderWorkerRef = useRef(null); + const pendingRenderRef = useRef(null); + const nextRenderRequestIdRef = useRef(0); const [source, setSource] = useState(initialJsx); const [template, setTemplate] = useState