From b3e1039e6dfc0c160885c0ffba4cb68b4622e0c0 Mon Sep 17 00:00:00 2001 From: Nalin Dalal Date: Fri, 15 May 2026 14:10:34 +0530 Subject: [PATCH 01/11] jsPreprocess.js: migrate to TS, no-verify --- client/modules/Preview/{jsPreprocess.js => jsPreprocess.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/Preview/{jsPreprocess.js => jsPreprocess.ts} (100%) diff --git a/client/modules/Preview/jsPreprocess.js b/client/modules/Preview/jsPreprocess.ts similarity index 100% rename from client/modules/Preview/jsPreprocess.js rename to client/modules/Preview/jsPreprocess.ts From 284dd2dd8ca2212121fce56f531f26c8b5f191ea Mon Sep 17 00:00:00 2001 From: Nalin Dalal Date: Fri, 15 May 2026 14:21:58 +0530 Subject: [PATCH 02/11] client/modules/Preview/jsPreprocess.ts: add types & named exports --- client/modules/Preview/jsPreprocess.ts | 230 +++++++++++++++---------- 1 file changed, 142 insertions(+), 88 deletions(-) diff --git a/client/modules/Preview/jsPreprocess.ts b/client/modules/Preview/jsPreprocess.ts index 6acd46e6e8..dd7dd068fd 100644 --- a/client/modules/Preview/jsPreprocess.ts +++ b/client/modules/Preview/jsPreprocess.ts @@ -1,22 +1,32 @@ import * as acorn from 'acorn'; import * as walk from 'acorn-walk'; +import type { SimpleVisitors, AncestorVisitors } from 'acorn-walk'; import escodegen from 'escodegen'; const LOOP_TIMEOUT_MS = 100; -function isShaderCall(node) { +interface LoopInfo { + loop: acorn.ForStatement | acorn.WhileStatement | acorn.DoWhileStatement; + parentBlock: acorn.BlockStatement | acorn.Program | null; +} + +function isShaderCall(node: acorn.CallExpression): boolean { const { callee } = node; - const isBuildShader = - callee.type === 'Identifier' && /^build\w*Shader$/.test(callee.name); - const isModifyCall = - callee.type === 'MemberExpression' && callee.property.name === 'modify'; - return isBuildShader || isModifyCall; + if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') { + return false; + } + if (callee.type === 'Identifier') { + return /^build\w*Shader$/.test(callee.name); + } + return ( + callee.property.type === 'Identifier' && callee.property.name === 'modify' + ); } -function collectShaderFunctionNames(ast) { - const names = new Set(); - walk.simple(ast, { - CallExpression(node) { +function collectShaderFunctionNames(ast: acorn.Program): Set { + const names = new Set(); + const visitors: SimpleVisitors = { + CallExpression(node: acorn.CallExpression) { if (isShaderCall(node)) { node.arguments.forEach((arg) => { if (arg.type === 'Identifier') { @@ -25,37 +35,49 @@ function collectShaderFunctionNames(ast) { }); } } - }); + }; + walk.simple(ast, visitors); return names; } -function makeVarDecl(varName) { +function makeVarDecl(varName: string): acorn.VariableDeclaration { return { type: 'VariableDeclaration', kind: 'var', declarations: [ - { + ({ type: 'VariableDeclarator', - id: { type: 'Identifier', name: varName }, - init: { + id: ({ + type: 'Identifier', + name: varName + } as unknown) as acorn.Identifier, + init: ({ type: 'CallExpression', callee: { type: 'MemberExpression', - object: { type: 'Identifier', name: 'Date' }, - property: { type: 'Identifier', name: 'now' }, + object: ({ + type: 'Identifier', + name: 'Date' + } as unknown) as acorn.Expression, + property: ({ + type: 'Identifier', + name: 'now' + } as unknown) as acorn.Identifier, computed: false }, arguments: [] - } - } - ] + } as unknown) as acorn.Expression + } as unknown) as acorn.VariableDeclarator + ], + start: 0, + end: 0 }; } -function makeCheckStatement(varName, line) { +function makeCheckStatement(varName: string, line: number): acorn.IfStatement { return { type: 'IfStatement', - test: { + test: ({ type: 'BinaryExpression', operator: '>', left: { @@ -78,8 +100,8 @@ function makeCheckStatement(varName, line) { value: LOOP_TIMEOUT_MS, raw: String(LOOP_TIMEOUT_MS) } - }, - consequent: { + } as unknown) as acorn.BinaryExpression, + consequent: ({ type: 'BlockStatement', body: [ { @@ -100,108 +122,140 @@ function makeCheckStatement(varName, line) { arguments: [{ type: 'Literal', value: line, raw: String(line) }] } }, - { type: 'BreakStatement', label: null } + { type: 'BreakStatement' } ] - }, - alternate: null + } as unknown) as acorn.BlockStatement, + alternate: null, + start: 0, + end: 0 }; } -function collectLoopsToProtect(ast, shaderNames) { - const loops = []; +function collectLoopsToProtect( + ast: acorn.Program, + shaderNames: Set +): LoopInfo[] { + const loops: LoopInfo[] = []; - function visitNode(node, ancestors) { - const isInsideShader = ancestors.some((ancestor, idx) => { - if ( - ancestor.type === 'FunctionDeclaration' && - shaderNames.has(ancestor.id?.name) - ) { + const visitors: AncestorVisitors = { + ForStatement( + node: acorn.ForStatement, + _state: undefined, + ancestors: acorn.Node[] + ) { + collectLoop(node, ancestors, shaderNames, loops); + }, + WhileStatement( + node: acorn.WhileStatement, + _state: undefined, + ancestors: acorn.Node[] + ) { + collectLoop(node, ancestors, shaderNames, loops); + }, + DoWhileStatement( + node: acorn.DoWhileStatement, + _state: undefined, + ancestors: acorn.Node[] + ) { + collectLoop(node, ancestors, shaderNames, loops); + } + }; + + walk.ancestor(ast, visitors); + return loops; +} + +function collectLoop( + node: acorn.ForStatement | acorn.WhileStatement | acorn.DoWhileStatement, + ancestors: acorn.Node[], + shaderNames: Set, + loops: LoopInfo[] +): void { + const isInsideShader = ancestors.some((ancestor, idx) => { + if (ancestor.type === 'FunctionDeclaration') { + const fn = ancestor as acorn.FunctionDeclaration; + if (fn.id && shaderNames.has(fn.id.name)) { return true; } - if ( - ancestor.type === 'FunctionExpression' || - ancestor.type === 'ArrowFunctionExpression' - ) { - const parent = ancestors[idx - 1]; - if ( - parent?.type === 'CallExpression' && - isShaderCall(parent) && - parent.arguments.includes(ancestor) - ) { + } + if ( + ancestor.type === 'FunctionExpression' || + ancestor.type === 'ArrowFunctionExpression' + ) { + const parent = ancestors[idx - 1]; + if (parent?.type === 'CallExpression') { + if (isShaderCall(parent as acorn.CallExpression)) { return true; } - if ( - parent?.type === 'VariableDeclarator' && - shaderNames.has(parent.id?.name) - ) { + } + if (parent?.type === 'VariableDeclarator') { + const varId = (parent as acorn.VariableDeclarator).id; + if (varId.type === 'Identifier' && shaderNames.has(varId.name)) { return true; } } - return false; - }); - - if (isInsideShader) return; - - let parentBlock = null; - for (let i = ancestors.length - 1; i >= 0; i--) { - const ancestor = ancestors[i]; - if ( - ancestor !== node && - (ancestor.type === 'BlockStatement' || ancestor.type === 'Program') - ) { - parentBlock = ancestor; - break; - } } + return false; + }); - loops.push({ loop: node, parentBlock }); - } + if (isInsideShader) return; - walk.ancestor(ast, { - ForStatement: visitNode, - WhileStatement: visitNode, - DoWhileStatement: visitNode - }); + let parentBlock: acorn.BlockStatement | acorn.Program | null = null; + for (let i = ancestors.length - 1; i >= 0; i--) { + const ancestor = ancestors[i]; + if ( + ancestor !== node && + (ancestor.type === 'BlockStatement' || ancestor.type === 'Program') + ) { + parentBlock = ancestor as acorn.BlockStatement | acorn.Program; + break; + } + } - return loops; + loops.push({ loop: node, parentBlock }); } -function injectProtection(loops) { - loops.forEach(({ loop, parentBlock }, idx) => { +function injectProtection(loops: LoopInfo[]): void { + loops.forEach((info, idx) => { const varName = `_LP${idx}`; - const { line } = loop.loc.start; + const line = info.loop.loc?.start.line ?? 0; const check = makeCheckStatement(varName, line); - if (loop.body.type === 'BlockStatement') { - loop.body.body.unshift(check); + if (info.loop.body.type === 'BlockStatement') { + info.loop.body.body.unshift(check); } else { - loop.body = { type: 'BlockStatement', body: [check, loop.body] }; + info.loop.body = ({ + type: 'BlockStatement', + body: [check, info.loop.body] + } as unknown) as acorn.Statement; } - if (parentBlock) { + if (info.parentBlock) { const varDecl = makeVarDecl(varName); - const nodeIdx = parentBlock.body.indexOf(loop); + const nodeIdx = info.parentBlock.body.indexOf( + (info.loop as unknown) as acorn.Statement + ); if (nodeIdx !== -1) { - parentBlock.body.splice(nodeIdx, 0, varDecl); + info.parentBlock.body.splice(nodeIdx, 0, varDecl); } } }); } -function parseJs(jsText) { - const options = { ecmaVersion: 'latest', locations: true }; +function parseJs(jsText: string): acorn.Program | null { + const options = { ecmaVersion: 'latest' as const, locations: true }; try { - return acorn.parse(jsText, { ...options, sourceType: 'script' }); - } catch (e) { + return acorn.parse(jsText, { ...options, sourceType: 'script' as const }); + } catch { try { - return acorn.parse(jsText, { ...options, sourceType: 'module' }); - } catch (e2) { + return acorn.parse(jsText, { ...options, sourceType: 'module' as const }); + } catch { return null; } } } -export function jsPreprocess(jsText) { +export function jsPreprocess(jsText: string): string { if (/\/\/\s*noprotect/.test(jsText)) { return jsText; } From 1801e573dfd67b1b687fa635914b8a02d42e919d Mon Sep 17 00:00:00 2001 From: Nalin Dalal Date: Fri, 15 May 2026 14:22:34 +0530 Subject: [PATCH 03/11] client/modules/Preview/jsPreprocess.unit.test.ts: migrate to TS, no-verify --- .../{jsPreprocess.unit.test.js => jsPreprocess.unit.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/Preview/{jsPreprocess.unit.test.js => jsPreprocess.unit.test.ts} (100%) diff --git a/client/modules/Preview/jsPreprocess.unit.test.js b/client/modules/Preview/jsPreprocess.unit.test.ts similarity index 100% rename from client/modules/Preview/jsPreprocess.unit.test.js rename to client/modules/Preview/jsPreprocess.unit.test.ts From 5696012e8de0660db7f4576d59b1a7426c655c55 Mon Sep 17 00:00:00 2001 From: Nalin Dalal Date: Fri, 15 May 2026 14:25:27 +0530 Subject: [PATCH 04/11] client/modules/Preview/filesReducer.ts: migrate to TS, no-verify --- client/modules/Preview/{filesReducer.js => filesReducer.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/Preview/{filesReducer.js => filesReducer.ts} (100%) diff --git a/client/modules/Preview/filesReducer.js b/client/modules/Preview/filesReducer.ts similarity index 100% rename from client/modules/Preview/filesReducer.js rename to client/modules/Preview/filesReducer.ts From bd83b2bbc111f5786b625a73ad2ce0ea14b50e10 Mon Sep 17 00:00:00 2001 From: Nalin Dalal Date: Fri, 15 May 2026 14:30:34 +0530 Subject: [PATCH 05/11] client/modules/Preview/filesReducer.ts: add types & refactor to named exports --- client/modules/Preview/filesReducer.ts | 49 +++++++++++++------------ client/modules/Preview/previewIndex.jsx | 4 +- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/client/modules/Preview/filesReducer.ts b/client/modules/Preview/filesReducer.ts index 15534d6d7e..a0a1e92f1b 100644 --- a/client/modules/Preview/filesReducer.ts +++ b/client/modules/Preview/filesReducer.ts @@ -1,48 +1,51 @@ -import { useMemo } from 'react'; import blobUtil from 'blob-util'; import mime from 'mime'; import { PLAINTEXT_FILE_REGEX } from '../../../server/utils/fileUtils'; -// https://gist.github.com/fnky/7d044b94070a35e552f3c139cdf80213 -export function useSelectors(state, mapStateToSelectors) { - const selectors = useMemo(() => mapStateToSelectors(state), [state]); - return selectors; +export interface PreviewFile { + id: string; + name: string; + content?: string; + blobUrl?: string; + url?: string; + children: string[]; + fileType: 'file' | 'folder'; } -export function getFileSelectors(state) { +export interface SetFilesAction { + type: 'SET_FILES'; + files: PreviewFile[]; +} + +export type FilesReducerAction = SetFilesAction; + +export function setFilesAction(files: PreviewFile[]) { return { - getHTMLFile: () => state.filter((file) => file.name.match(/.*\.html$/i))[0], - getJSFiles: () => state.filter((file) => file.name.match(/.*\.js$/i)), - getCSSFiles: () => state.filter((file) => file.name.match(/.*\.css$/i)) + type: 'SET_FILES' as const, + files }; } -function sortedChildrenId(state, children) { +function sortedChildrenId(state: PreviewFile[], children: string[]) { const childrenArray = state.filter((file) => children.includes(file.id)); childrenArray.sort((a, b) => (a.name > b.name ? 1 : -1)); return childrenArray.map((child) => child.id); } -export function setFiles(files) { - return { - type: 'SET_FILES', - files - }; -} - -export function createBlobUrl(file) { +export function createBlobUrl(file: PreviewFile) { if (file.blobUrl) { blobUtil.revokeObjectURL(file.blobUrl); } - const mimeType = mime.getType(file.name) || 'text/plain'; - - const fileBlob = blobUtil.createBlob([file.content], { type: mimeType }); + const mimeType = mime.lookup(file.name) || 'text/plain'; + const fileBlob = blobUtil.createBlob([file.content ?? ''], { + type: mimeType + }); const blobURL = blobUtil.createObjectURL(fileBlob); return blobURL; } -export function createBlobUrls(state) { +export function createBlobUrls(state: PreviewFile[]) { return state.map((file) => { if (file.name.match(PLAINTEXT_FILE_REGEX)) { const blobUrl = createBlobUrl(file); @@ -52,7 +55,7 @@ export function createBlobUrls(state) { }); } -export function filesReducer(state, action) { +export function filesReducer(state: PreviewFile[], action: FilesReducerAction) { switch (action.type) { case 'SET_FILES': return createBlobUrls(action.files); diff --git a/client/modules/Preview/previewIndex.jsx b/client/modules/Preview/previewIndex.jsx index 12c14e3b79..b7a09d2d40 100644 --- a/client/modules/Preview/previewIndex.jsx +++ b/client/modules/Preview/previewIndex.jsx @@ -7,7 +7,7 @@ import { MessageTypes, dispatchMessage } from '../../utils/dispatcher'; -import { filesReducer, setFiles } from './filesReducer'; +import { filesReducer, setFilesAction } from './filesReducer'; import EmbedFrame from './EmbedFrame'; import { getConfig } from '../../utils/getConfig'; import { initialState } from '../IDE/reducers/files'; @@ -30,7 +30,7 @@ const App = () => { const { type, payload } = message; switch (type) { case MessageTypes.SKETCH: - dispatch(setFiles(payload.files)); + dispatch(setFilesAction(payload.files)); setBasePath(payload.basePath); setTextOutput(payload.textOutput); setGridOutput(payload.gridOutput); From 3b23cbba44f1d9b4b914923700c1fe2ef97dac2e Mon Sep 17 00:00:00 2001 From: Nalin Dalal Date: Fri, 15 May 2026 14:31:00 +0530 Subject: [PATCH 06/11] client/modules/Preview/EmbedFrame.tsx: migrate to TS, no-verify --- client/modules/Preview/{EmbedFrame.jsx => EmbedFrame.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/Preview/{EmbedFrame.jsx => EmbedFrame.tsx} (100%) diff --git a/client/modules/Preview/EmbedFrame.jsx b/client/modules/Preview/EmbedFrame.tsx similarity index 100% rename from client/modules/Preview/EmbedFrame.jsx rename to client/modules/Preview/EmbedFrame.tsx From d543ea8d704869e144d53b81c81da9d7f344afcd Mon Sep 17 00:00:00 2001 From: Nalin Dalal Date: Fri, 15 May 2026 15:19:52 +0530 Subject: [PATCH 07/11] client/modules/Preview: convert EmbedFrame to named exports & fix lint --- client/modules/Preview/EmbedFrame.tsx | 163 ++++++++++++------------ client/modules/Preview/previewIndex.jsx | 2 +- 2 files changed, 80 insertions(+), 85 deletions(-) diff --git a/client/modules/Preview/EmbedFrame.tsx b/client/modules/Preview/EmbedFrame.tsx index 02c766bd47..b4e924a700 100644 --- a/client/modules/Preview/EmbedFrame.tsx +++ b/client/modules/Preview/EmbedFrame.tsx @@ -1,5 +1,4 @@ import blobUtil from 'blob-util'; -import PropTypes from 'prop-types'; import React, { useRef, useEffect, useMemo } from 'react'; import styled from 'styled-components'; import { jsPreprocess } from './jsPreprocess'; @@ -13,14 +12,20 @@ import { NOT_EXTERNAL_LINK_REGEX } from '../../../server/utils/fileUtils'; import { getAllScriptOffsets } from '../../utils/consoleUtils'; +import type { ScriptOffset } from '../../utils/consoleUtils'; import { registerFrame } from '../../utils/dispatcher'; import { createBlobUrl } from './filesReducer'; +import type { PreviewFile } from './filesReducer'; import resolvePathsForElementsWithAttribute from '../../../server/utils/resolveUtils'; -let objectUrls = {}; -let objectPaths = {}; +let objectUrls: Record = {}; +let objectPaths: Record = {}; -const Frame = styled.iframe` +interface FrameProps { + fullView?: boolean; +} + +const Frame = styled.iframe` min-height: 100%; min-width: 100%; position: absolute; @@ -32,15 +37,14 @@ const Frame = styled.iframe` `} `; -function resolveCSSLinksInString(content, files) { +function resolveCSSLinksInString(content: string, files: PreviewFile[]) { let newContent = content; - let cssFileStrings = content.match(STRING_REGEX); - cssFileStrings = cssFileStrings || []; + const cssFileStrings = content.match(STRING_REGEX) || []; cssFileStrings.forEach((cssFileString) => { if (cssFileString.match(MEDIA_FILE_QUOTED_REGEX)) { - const filePath = cssFileString.substr(1, cssFileString.length - 2); - const quoteCharacter = cssFileString.substr(0, 1); - const resolvedFile = resolvePathToFile(filePath, files); + const filePath = cssFileString.slice(1, -1); + const quoteCharacter = cssFileString[0]; + const resolvedFile = resolvePathToFile(filePath, files) as PreviewFile | false | undefined; if (resolvedFile) { if (resolvedFile.url) { newContent = newContent.replace( @@ -54,15 +58,14 @@ function resolveCSSLinksInString(content, files) { return newContent; } -function resolveJSLinksInString(content, files) { +function resolveJSLinksInString(content: string, files: PreviewFile[]) { let newContent = content; - let jsFileStrings = content.match(STRING_REGEX); - jsFileStrings = jsFileStrings || []; + const jsFileStrings = content.match(STRING_REGEX) || []; jsFileStrings.forEach((jsFileString) => { if (jsFileString.match(MEDIA_FILE_QUOTED_REGEX)) { - const filePath = jsFileString.substr(1, jsFileString.length - 2); - const quoteCharacter = jsFileString.substr(0, 1); - const resolvedFile = resolvePathToFile(filePath, files); + const filePath = jsFileString.slice(1, -1); + const quoteCharacter = jsFileString[0]; + const resolvedFile = resolvePathToFile(filePath, files) as PreviewFile | false | undefined; if (resolvedFile) { if (resolvedFile.url) { @@ -83,105 +86,94 @@ function resolveJSLinksInString(content, files) { return jsPreprocess(newContent); } -function resolveScripts(sketchDoc, files) { +function resolveScripts(sketchDoc: Document, files: PreviewFile[]) { const scriptsInHTML = sketchDoc.getElementsByTagName('script'); const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML); scriptsInHTMLArray.forEach((script) => { - if ( - script.getAttribute('src') && - script.getAttribute('src').match(NOT_EXTERNAL_LINK_REGEX) !== null - ) { - const resolvedFile = resolvePathToFile(script.getAttribute('src'), files); + const src = script.getAttribute('src'); + if (src && src.match(NOT_EXTERNAL_LINK_REGEX) !== null) { + const resolvedFile = resolvePathToFile(src, files) as PreviewFile | false | undefined; if (resolvedFile) { if (resolvedFile.url) { script.setAttribute('src', resolvedFile.url); } else { - // in the future, when using y.js, could remake the blob for only the file(s) - // that changed const blobUrl = createBlobUrl(resolvedFile); script.setAttribute('src', blobUrl); - const blobPath = blobUrl.split('/').pop(); - // objectUrls[blobUrl] = `${resolvedFile.filePath}${ - // resolvedFile.filePath.length > 0 ? '/' : '' - // }${resolvedFile.name}`; - objectUrls[blobUrl] = `${resolvedFile.filePath}/${resolvedFile.name}`; + const blobPath = blobUrl.split('/').pop() || ''; + objectUrls[blobUrl] = `${(resolvedFile as unknown as { filePath: string }).filePath}/${resolvedFile.name}`; objectPaths[blobPath] = resolvedFile.name; - // script.setAttribute('data-tag', `${startTag}${resolvedFile.name}`); - // script.removeAttribute('src'); - // script.innerHTML = resolvedFile.content; // eslint-disable-line } } - } else if ( - !( - script.getAttribute('src') && - script.getAttribute('src').match(EXTERNAL_LINK_REGEX) - ) !== null - ) { + } else if (!(src && src.match(EXTERNAL_LINK_REGEX)) !== null) { script.setAttribute('crossorigin', ''); - script.innerHTML = resolveJSLinksInString(script.innerHTML, files); // eslint-disable-line + script.innerHTML = resolveJSLinksInString(script.innerHTML, files); } }); } -function resolveStyles(sketchDoc, files) { +function resolveStyles(sketchDoc: Document, files: PreviewFile[]) { const inlineCSSInHTML = sketchDoc.getElementsByTagName('style'); const inlineCSSInHTMLArray = Array.prototype.slice.call(inlineCSSInHTML); inlineCSSInHTMLArray.forEach((style) => { - style.innerHTML = resolveCSSLinksInString(style.innerHTML, files); // eslint-disable-line + style.innerHTML = resolveCSSLinksInString(style.innerHTML, files); }); const cssLinksInHTML = sketchDoc.querySelectorAll('link[rel="stylesheet"]'); const cssLinksInHTMLArray = Array.prototype.slice.call(cssLinksInHTML); cssLinksInHTMLArray.forEach((css) => { - if ( - css.getAttribute('href') && - css.getAttribute('href').match(NOT_EXTERNAL_LINK_REGEX) !== null - ) { - const resolvedFile = resolvePathToFile(css.getAttribute('href'), files); + const href = css.getAttribute('href'); + if (href && href.match(NOT_EXTERNAL_LINK_REGEX) !== null) { + const resolvedFile = resolvePathToFile(href, files) as PreviewFile | false | undefined; if (resolvedFile) { if (resolvedFile.url) { - css.href = resolvedFile.url; // eslint-disable-line + css.setAttribute('href', resolvedFile.url); } else { const style = sketchDoc.createElement('style'); - style.innerHTML = `\n${resolvedFile.content}`; + style.innerHTML = `\n${resolvedFile.content || ''}`; sketchDoc.head.appendChild(style); - css.parentElement.removeChild(css); + css.parentElement?.removeChild(css); } } } }); } -function resolveJSAndCSSLinks(files) { - const newFiles = []; +function resolveJSAndCSSLinks(files: PreviewFile[]) { + const newFiles: PreviewFile[] = []; files.forEach((file) => { const newFile = { ...file }; if (file.name.match(/.*\.js$/i)) { - newFile.content = resolveJSLinksInString(newFile.content, files); + newFile.content = resolveJSLinksInString(newFile.content || '', files); } else if (file.name.match(/.*\.css$/i)) { - newFile.content = resolveCSSLinksInString(newFile.content, files); + newFile.content = resolveCSSLinksInString(newFile.content || '', files); } newFiles.push(newFile); }); return newFiles; } -function addLoopProtect(sketchDoc) { +function addLoopProtect(sketchDoc: Document) { const scriptsInHTML = sketchDoc.getElementsByTagName('script'); const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML); scriptsInHTMLArray.forEach((script) => { - script.innerHTML = jsPreprocess(script.innerHTML); // eslint-disable-line + script.innerHTML = jsPreprocess(script.innerHTML); }); } -function injectLocalFiles(files, htmlFile, options) { +interface InjectLocalFilesOptions { + basePath: string; + gridOutput: boolean; + textOutput: boolean; +} + +function injectLocalFiles(files: PreviewFile[], htmlFile: PreviewFile, options: InjectLocalFilesOptions) { const { basePath, gridOutput, textOutput } = options; - let scriptOffs = []; + let scriptOffs: ScriptOffset[] = []; objectUrls = {}; objectPaths = {}; const resolvedFiles = resolveJSAndCSSLinks(files); const parser = new DOMParser(); - const sketchDoc = parser.parseFromString(htmlFile.content, 'text/html'); + const sketchDoc = parser.parseFromString(htmlFile.content || '', 'text/html'); const base = sketchDoc.createElement('base'); base.href = `${window.origin}${basePath}${basePath.length > 1 && '/'}`; @@ -235,16 +227,27 @@ p5.prototype.registerMethod('afterSetup', p5.prototype.ensureAccessibleCanvas);` return `\n${sketchDoc.documentElement.outerHTML}`; } -function getHtmlFile(files) { - return files.filter((file) => file.name.match(/.*\.html$/i))[0]; +function getHtmlFile(files: PreviewFile[]) { + return files.find((file) => file.name.match(/.*\.html$/i)); +} + +interface EmbedFrameProps { + files: PreviewFile[]; + isPlaying: boolean; + basePath: string; + gridOutput: boolean; + textOutput: boolean; } -function EmbedFrame({ files, isPlaying, basePath, gridOutput, textOutput }) { - const iframe = useRef(); +function EmbedFrame({ files, isPlaying, basePath, gridOutput, textOutput }: EmbedFrameProps) { + const iframe = useRef(null); const htmlFile = useMemo(() => getHtmlFile(files), [files]); - const srcRef = useRef(); + const srcRef = useRef(null); useEffect(() => { + if (!iframe.current) { + return; + } const unsubscribe = registerFrame( iframe.current.contentWindow, window.origin @@ -252,31 +255,37 @@ function EmbedFrame({ files, isPlaying, basePath, gridOutput, textOutput }) { return () => { unsubscribe(); }; + // eslint-disable-next-line consistent-return }); function renderSketch() { const doc = iframe.current; - if (isPlaying) { + if (isPlaying && doc && htmlFile) { const htmlDoc = injectLocalFiles(files, htmlFile, { basePath, gridOutput, textOutput }); - const generatedHtmlFile = { + const generatedHtmlFile: PreviewFile = { + id: 'generated', name: 'index.html', - content: htmlDoc + content: htmlDoc, + children: [], + fileType: 'file' }; const htmlUrl = createBlobUrl(generatedHtmlFile); const toRevoke = srcRef.current; srcRef.current = htmlUrl; - // BRO FOR SOME REASON YOU HAVE TO DO THIS TO GET IT TO WORK ON SAFARI setTimeout(() => { - doc.src = htmlUrl; + if (doc) { + doc.src = htmlUrl; + } if (toRevoke) { blobUtil.revokeObjectURL(toRevoke); } }, 0); - } else { + // eslint-disable-next-line consistent-return + } else if (doc) { doc.src = ''; } } @@ -292,18 +301,4 @@ function EmbedFrame({ files, isPlaying, basePath, gridOutput, textOutput }) { ); } -EmbedFrame.propTypes = { - files: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - content: PropTypes.string.isRequired - }) - ).isRequired, - isPlaying: PropTypes.bool.isRequired, - basePath: PropTypes.string.isRequired, - gridOutput: PropTypes.bool.isRequired, - textOutput: PropTypes.bool.isRequired -}; - -export default EmbedFrame; +export { EmbedFrame }; diff --git a/client/modules/Preview/previewIndex.jsx b/client/modules/Preview/previewIndex.jsx index b7a09d2d40..297e84601d 100644 --- a/client/modules/Preview/previewIndex.jsx +++ b/client/modules/Preview/previewIndex.jsx @@ -8,7 +8,7 @@ import { dispatchMessage } from '../../utils/dispatcher'; import { filesReducer, setFilesAction } from './filesReducer'; -import EmbedFrame from './EmbedFrame'; +import { EmbedFrame } from './EmbedFrame'; import { getConfig } from '../../utils/getConfig'; import { initialState } from '../IDE/reducers/files'; From 9245a8164b242e9b18cdf809f9d504345e1f6c5a Mon Sep 17 00:00:00 2001 From: Nalin Dalal Date: Fri, 15 May 2026 15:20:10 +0530 Subject: [PATCH 08/11] client/modules/Preview/previewIndex.tsx: migrate to TSX, no-verify --- client/modules/Preview/{previewIndex.jsx => previewIndex.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/Preview/{previewIndex.jsx => previewIndex.tsx} (100%) diff --git a/client/modules/Preview/previewIndex.jsx b/client/modules/Preview/previewIndex.tsx similarity index 100% rename from client/modules/Preview/previewIndex.jsx rename to client/modules/Preview/previewIndex.tsx From 8f5a9b34e2658527b6269cd4c752eb95d49deb52 Mon Sep 17 00:00:00 2001 From: Nalin Dalal Date: Fri, 15 May 2026 15:23:55 +0530 Subject: [PATCH 09/11] client/modules/Preview: fix lint errors --- client/modules/Preview/EmbedFrame.tsx | 3 +-- client/modules/Preview/previewIndex.tsx | 32 +++++++++++++++---------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/client/modules/Preview/EmbedFrame.tsx b/client/modules/Preview/EmbedFrame.tsx index b4e924a700..3eb3e9c583 100644 --- a/client/modules/Preview/EmbedFrame.tsx +++ b/client/modules/Preview/EmbedFrame.tsx @@ -255,7 +255,6 @@ function EmbedFrame({ files, isPlaying, basePath, gridOutput, textOutput }: Embe return () => { unsubscribe(); }; - // eslint-disable-next-line consistent-return }); function renderSketch() { @@ -284,10 +283,10 @@ function EmbedFrame({ files, isPlaying, basePath, gridOutput, textOutput }: Embe blobUtil.revokeObjectURL(toRevoke); } }, 0); - // eslint-disable-next-line consistent-return } else if (doc) { doc.src = ''; } + return; } useEffect(renderSketch, [files, isPlaying]); diff --git a/client/modules/Preview/previewIndex.tsx b/client/modules/Preview/previewIndex.tsx index 297e84601d..bee55e6420 100644 --- a/client/modules/Preview/previewIndex.tsx +++ b/client/modules/Preview/previewIndex.tsx @@ -7,10 +7,14 @@ import { MessageTypes, dispatchMessage } from '../../utils/dispatcher'; -import { filesReducer, setFilesAction } from './filesReducer'; +import type { Message } from '../../utils/dispatcher'; +import { + filesReducer, + setFilesAction, + type PreviewFile +} from './filesReducer'; import { EmbedFrame } from './EmbedFrame'; import { getConfig } from '../../utils/getConfig'; -import { initialState } from '../IDE/reducers/files'; const GlobalStyle = createGlobalStyle` body { @@ -19,22 +23,25 @@ const GlobalStyle = createGlobalStyle` `; const App = () => { - const [state, dispatch] = useReducer(filesReducer, [], initialState); + const [state, dispatch] = useReducer(filesReducer, [] as PreviewFile[]); const [isPlaying, setIsPlaying] = useState(false); const [basePath, setBasePath] = useState(''); const [textOutput, setTextOutput] = useState(false); const [gridOutput, setGridOutput] = useState(false); registerFrame(window.parent, getConfig('EDITOR_URL')); - function handleMessageEvent(message) { + function handleMessageEvent(message: Message) { const { type, payload } = message; switch (type) { - case MessageTypes.SKETCH: - dispatch(setFilesAction(payload.files)); - setBasePath(payload.basePath); - setTextOutput(payload.textOutput); - setGridOutput(payload.gridOutput); + // eslint-disable-next-line max-len + case MessageTypes.SKETCH: { + const sketchPayload = payload as { files: PreviewFile[]; basePath: string; textOutput: boolean; gridOutput: boolean }; + dispatch(setFilesAction(sketchPayload.files)); + setBasePath(sketchPayload.basePath); + setTextOutput(sketchPayload.textOutput); + setGridOutput(sketchPayload.gridOutput); break; + } case MessageTypes.START: setIsPlaying(true); break; @@ -45,14 +52,14 @@ const App = () => { dispatchMessage({ type: MessageTypes.REGISTER }); break; case MessageTypes.EXECUTE: - dispatchMessage(payload); + dispatchMessage(payload as Parameters[0]); break; default: break; } } - function addCacheBustingToAssets(files) { + function addCacheBustingToAssets(files: PreviewFile[]) { const timestamp = new Date().getTime(); return files.map((file) => { if (file.url && !file.url.endsWith('obj') && !file.url.endsWith('stl')) { @@ -71,6 +78,7 @@ const App = () => { unsubscribe(); }; }); + return ( @@ -85,4 +93,4 @@ const App = () => { ); }; -render(, document.getElementById('root')); +render(, document.getElementById('root')); \ No newline at end of file From 9b69c216e486b29fa1723a3418c9fe5b4536227c Mon Sep 17 00:00:00 2001 From: Nalin Dalal Date: Wed, 20 May 2026 16:22:38 +0530 Subject: [PATCH 10/11] fix:linting error --- client/modules/Preview/EmbedFrame.tsx | 41 +++++++++++++++++++------ client/modules/Preview/previewIndex.tsx | 16 +++++----- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/client/modules/Preview/EmbedFrame.tsx b/client/modules/Preview/EmbedFrame.tsx index 3eb3e9c583..c6b49c0723 100644 --- a/client/modules/Preview/EmbedFrame.tsx +++ b/client/modules/Preview/EmbedFrame.tsx @@ -44,7 +44,10 @@ function resolveCSSLinksInString(content: string, files: PreviewFile[]) { if (cssFileString.match(MEDIA_FILE_QUOTED_REGEX)) { const filePath = cssFileString.slice(1, -1); const quoteCharacter = cssFileString[0]; - const resolvedFile = resolvePathToFile(filePath, files) as PreviewFile | false | undefined; + const resolvedFile = resolvePathToFile(filePath, files) as + | PreviewFile + | false + | undefined; if (resolvedFile) { if (resolvedFile.url) { newContent = newContent.replace( @@ -65,7 +68,10 @@ function resolveJSLinksInString(content: string, files: PreviewFile[]) { if (jsFileString.match(MEDIA_FILE_QUOTED_REGEX)) { const filePath = jsFileString.slice(1, -1); const quoteCharacter = jsFileString[0]; - const resolvedFile = resolvePathToFile(filePath, files) as PreviewFile | false | undefined; + const resolvedFile = resolvePathToFile(filePath, files) as + | PreviewFile + | false + | undefined; if (resolvedFile) { if (resolvedFile.url) { @@ -92,7 +98,10 @@ function resolveScripts(sketchDoc: Document, files: PreviewFile[]) { scriptsInHTMLArray.forEach((script) => { const src = script.getAttribute('src'); if (src && src.match(NOT_EXTERNAL_LINK_REGEX) !== null) { - const resolvedFile = resolvePathToFile(src, files) as PreviewFile | false | undefined; + const resolvedFile = resolvePathToFile(src, files) as + | PreviewFile + | false + | undefined; if (resolvedFile) { if (resolvedFile.url) { script.setAttribute('src', resolvedFile.url); @@ -100,7 +109,9 @@ function resolveScripts(sketchDoc: Document, files: PreviewFile[]) { const blobUrl = createBlobUrl(resolvedFile); script.setAttribute('src', blobUrl); const blobPath = blobUrl.split('/').pop() || ''; - objectUrls[blobUrl] = `${(resolvedFile as unknown as { filePath: string }).filePath}/${resolvedFile.name}`; + objectUrls[blobUrl] = `${ + ((resolvedFile as unknown) as { filePath: string }).filePath + }/${resolvedFile.name}`; objectPaths[blobPath] = resolvedFile.name; } } @@ -123,7 +134,10 @@ function resolveStyles(sketchDoc: Document, files: PreviewFile[]) { cssLinksInHTMLArray.forEach((css) => { const href = css.getAttribute('href'); if (href && href.match(NOT_EXTERNAL_LINK_REGEX) !== null) { - const resolvedFile = resolvePathToFile(href, files) as PreviewFile | false | undefined; + const resolvedFile = resolvePathToFile(href, files) as + | PreviewFile + | false + | undefined; if (resolvedFile) { if (resolvedFile.url) { css.setAttribute('href', resolvedFile.url); @@ -166,7 +180,11 @@ interface InjectLocalFilesOptions { textOutput: boolean; } -function injectLocalFiles(files: PreviewFile[], htmlFile: PreviewFile, options: InjectLocalFilesOptions) { +function injectLocalFiles( + files: PreviewFile[], + htmlFile: PreviewFile, + options: InjectLocalFilesOptions +) { const { basePath, gridOutput, textOutput } = options; let scriptOffs: ScriptOffset[] = []; objectUrls = {}; @@ -239,14 +257,20 @@ interface EmbedFrameProps { textOutput: boolean; } -function EmbedFrame({ files, isPlaying, basePath, gridOutput, textOutput }: EmbedFrameProps) { +function EmbedFrame({ + files, + isPlaying, + basePath, + gridOutput, + textOutput +}: EmbedFrameProps) { const iframe = useRef(null); const htmlFile = useMemo(() => getHtmlFile(files), [files]); const srcRef = useRef(null); useEffect(() => { if (!iframe.current) { - return; + return () => {}; } const unsubscribe = registerFrame( iframe.current.contentWindow, @@ -286,7 +310,6 @@ function EmbedFrame({ files, isPlaying, basePath, gridOutput, textOutput }: Embe } else if (doc) { doc.src = ''; } - return; } useEffect(renderSketch, [files, isPlaying]); diff --git a/client/modules/Preview/previewIndex.tsx b/client/modules/Preview/previewIndex.tsx index bee55e6420..c6f29b9a4c 100644 --- a/client/modules/Preview/previewIndex.tsx +++ b/client/modules/Preview/previewIndex.tsx @@ -8,11 +8,8 @@ import { dispatchMessage } from '../../utils/dispatcher'; import type { Message } from '../../utils/dispatcher'; -import { - filesReducer, - setFilesAction, - type PreviewFile -} from './filesReducer'; +import { filesReducer, setFilesAction } from './filesReducer'; +import type { PreviewFile } from './filesReducer'; import { EmbedFrame } from './EmbedFrame'; import { getConfig } from '../../utils/getConfig'; @@ -35,7 +32,12 @@ const App = () => { switch (type) { // eslint-disable-next-line max-len case MessageTypes.SKETCH: { - const sketchPayload = payload as { files: PreviewFile[]; basePath: string; textOutput: boolean; gridOutput: boolean }; + const sketchPayload = payload as { + files: PreviewFile[]; + basePath: string; + textOutput: boolean; + gridOutput: boolean; + }; dispatch(setFilesAction(sketchPayload.files)); setBasePath(sketchPayload.basePath); setTextOutput(sketchPayload.textOutput); @@ -93,4 +95,4 @@ const App = () => { ); }; -render(, document.getElementById('root')); \ No newline at end of file +render(, document.getElementById('root')); From 406e5ff35d8137a1878810aaa634eea2264d3906 Mon Sep 17 00:00:00 2001 From: Nalin Dalal Date: Sun, 24 May 2026 23:03:29 +0530 Subject: [PATCH 11/11] fix: typecheck errors --- client/custom.d.ts | 2 ++ package-lock.json | 14 ++++++++++++++ package.json | 1 + 3 files changed, 17 insertions(+) diff --git a/client/custom.d.ts b/client/custom.d.ts index 729f9e03de..cc0982df7a 100644 --- a/client/custom.d.ts +++ b/client/custom.d.ts @@ -8,6 +8,8 @@ declare module '*.svg' { export default ReactComponent; } +declare module 'blob-util'; + // Extend window for Redux DevTools interface Window { __REDUX_DEVTOOLS_EXTENSION__?: () => any; diff --git a/package-lock.json b/package-lock.json index 217b261f43..b72d94c773 100644 --- a/package-lock.json +++ b/package-lock.json @@ -162,6 +162,7 @@ "@testing-library/jest-dom": "^5.15.0", "@testing-library/react": "^12.1.2", "@types/bcryptjs": "^2.4.6", + "@types/blob-util": "^1.3.3", "@types/classnames": "^2.3.0", "@types/friendly-words": "^1.2.2", "@types/jest": "^29.5.14", @@ -13356,6 +13357,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/blob-util": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/blob-util/-/blob-util-1.3.3.tgz", + "integrity": "sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -46015,6 +46023,12 @@ "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", "dev": true }, + "@types/blob-util": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/blob-util/-/blob-util-1.3.3.tgz", + "integrity": "sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w==", + "dev": true + }, "@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", diff --git a/package.json b/package.json index 5519bafec0..da342968f1 100644 --- a/package.json +++ b/package.json @@ -136,6 +136,7 @@ "@testing-library/jest-dom": "^5.15.0", "@testing-library/react": "^12.1.2", "@types/bcryptjs": "^2.4.6", + "@types/blob-util": "^1.3.3", "@types/classnames": "^2.3.0", "@types/friendly-words": "^1.2.2", "@types/jest": "^29.5.14",