From 10b9ec073cb275409a106cce0fe76ffa8a084924 Mon Sep 17 00:00:00 2001 From: Nalin Date: Wed, 28 Jan 2026 00:05:55 +0530 Subject: [PATCH 1/3] client/modules/Preview: migrate to TypeScript, no-verify --- client/modules/Preview/EmbedFrame.jsx | 327 ------------------ client/modules/Preview/EmbedFrame.tsx | 98 ++++++ .../{filesReducer.js => filesReducer.ts} | 0 .../{previewIndex.jsx => previewIndex.tsx} | 32 +- 4 files changed, 121 insertions(+), 336 deletions(-) delete mode 100644 client/modules/Preview/EmbedFrame.jsx create mode 100644 client/modules/Preview/EmbedFrame.tsx rename client/modules/Preview/{filesReducer.js => filesReducer.ts} (100%) rename client/modules/Preview/{previewIndex.jsx => previewIndex.tsx} (72%) diff --git a/client/modules/Preview/EmbedFrame.jsx b/client/modules/Preview/EmbedFrame.jsx deleted file mode 100644 index 2b6ac16720..0000000000 --- a/client/modules/Preview/EmbedFrame.jsx +++ /dev/null @@ -1,327 +0,0 @@ -import blobUtil from 'blob-util'; -import PropTypes from 'prop-types'; -import React, { useRef, useEffect, useMemo } from 'react'; -import styled from 'styled-components'; -import loopProtect from 'loop-protect'; -import { JSHINT } from 'jshint'; -import decomment from 'decomment'; -import { resolvePathToFile } from '../../../server/utils/filePath'; -import { getConfig } from '../../utils/getConfig'; -import { - MEDIA_FILE_QUOTED_REGEX, - STRING_REGEX, - PLAINTEXT_FILE_REGEX, - EXTERNAL_LINK_REGEX, - NOT_EXTERNAL_LINK_REGEX -} from '../../../server/utils/fileUtils'; -import { getAllScriptOffsets } from '../../utils/consoleUtils'; -import { registerFrame } from '../../utils/dispatcher'; -import { createBlobUrl } from './filesReducer'; -import resolvePathsForElementsWithAttribute from '../../../server/utils/resolveUtils'; - -let objectUrls = {}; -let objectPaths = {}; - -const Frame = styled.iframe` - min-height: 100%; - min-width: 100%; - position: absolute; - border-width: 0; - ${({ fullView }) => - fullView && - ` - position: relative; - `} -`; - -function resolveCSSLinksInString(content, files) { - let newContent = content; - let cssFileStrings = content.match(STRING_REGEX); - cssFileStrings = cssFileStrings || []; - 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); - if (resolvedFile) { - if (resolvedFile.url) { - newContent = newContent.replace( - cssFileString, - quoteCharacter + resolvedFile.url + quoteCharacter - ); - } - } - } - }); - return newContent; -} - -function jsPreprocess(jsText) { - let newContent = jsText; - // check the code for js errors before sending it to strip comments - // or loops. - JSHINT(newContent); - - if (JSHINT.errors.length === 0) { - newContent = decomment(newContent, { - ignore: /\/\/\s*noprotect/g, - space: true - }); - newContent = loopProtect(newContent); - } - return newContent; -} - -function resolveJSLinksInString(content, files) { - let newContent = content; - let jsFileStrings = content.match(STRING_REGEX); - jsFileStrings = jsFileStrings || []; - 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); - - if (resolvedFile) { - if (resolvedFile.url) { - newContent = newContent.replace( - jsFileString, - quoteCharacter + resolvedFile.url + quoteCharacter - ); - } else if (resolvedFile.name.match(PLAINTEXT_FILE_REGEX)) { - newContent = newContent.replace( - jsFileString, - quoteCharacter + resolvedFile.blobUrl + quoteCharacter - ); - } - } - } - }); - - return jsPreprocess(newContent); -} - -function resolveScripts(sketchDoc, files) { - 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); - 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}`; - 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 - ) { - script.setAttribute('crossorigin', ''); - script.innerHTML = resolveJSLinksInString(script.innerHTML, files); // eslint-disable-line - } - }); -} - -function resolveStyles(sketchDoc, files) { - const inlineCSSInHTML = sketchDoc.getElementsByTagName('style'); - const inlineCSSInHTMLArray = Array.prototype.slice.call(inlineCSSInHTML); - inlineCSSInHTMLArray.forEach((style) => { - style.innerHTML = resolveCSSLinksInString(style.innerHTML, files); // eslint-disable-line - }); - - 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); - if (resolvedFile) { - if (resolvedFile.url) { - css.href = resolvedFile.url; // eslint-disable-line - } else { - const style = sketchDoc.createElement('style'); - style.innerHTML = `\n${resolvedFile.content}`; - sketchDoc.head.appendChild(style); - css.parentElement.removeChild(css); - } - } - } - }); -} - -function resolveJSAndCSSLinks(files) { - const newFiles = []; - files.forEach((file) => { - const newFile = { ...file }; - if (file.name.match(/.*\.js$/i)) { - newFile.content = resolveJSLinksInString(newFile.content, files); - } else if (file.name.match(/.*\.css$/i)) { - newFile.content = resolveCSSLinksInString(newFile.content, files); - } - newFiles.push(newFile); - }); - return newFiles; -} - -function addLoopProtect(sketchDoc) { - const scriptsInHTML = sketchDoc.getElementsByTagName('script'); - const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML); - scriptsInHTMLArray.forEach((script) => { - script.innerHTML = jsPreprocess(script.innerHTML); // eslint-disable-line - }); -} - -function injectLocalFiles(files, htmlFile, options) { - const { basePath, gridOutput, textOutput } = options; - let scriptOffs = []; - objectUrls = {}; - objectPaths = {}; - const resolvedFiles = resolveJSAndCSSLinks(files); - const parser = new DOMParser(); - const sketchDoc = parser.parseFromString(htmlFile.content, 'text/html'); - - const base = sketchDoc.createElement('base'); - base.href = `${window.origin}${basePath}${basePath.length > 1 && '/'}`; - sketchDoc.head.appendChild(base); - - resolvePathsForElementsWithAttribute('src', sketchDoc, resolvedFiles); - resolvePathsForElementsWithAttribute('href', sketchDoc, resolvedFiles); - // should also include background, data, poster, but these are used way less often - - resolveScripts(sketchDoc, resolvedFiles); - resolveStyles(sketchDoc, resolvedFiles); - - if (textOutput || gridOutput) { - const scriptElement = sketchDoc.createElement('script'); - let textCode = ''; - if (textOutput) { - textCode = 'if (!this._accessibleOutputs.text) this.textOutput();'; - } - let gridCode = ''; - if (gridOutput) { - gridCode = 'if (!this._accessibleOutputs.grid) this.gridOutput();'; - } - const fxn = `p5.prototype.ensureAccessibleCanvas = function _ensureAccessibleCanvas() { - ${textCode} - ${gridCode} -}; -p5.prototype.registerMethod('afterSetup', p5.prototype.ensureAccessibleCanvas);`; - scriptElement.innerHTML = fxn; - sketchDoc.head.appendChild(scriptElement); - } - - const previewScripts = sketchDoc.createElement('script'); - previewScripts.src = `${ - window.location.origin - }${getConfig('PREVIEW_SCRIPTS_URL', { nullishString: true })}`; - previewScripts.setAttribute('crossorigin', ''); - sketchDoc.head.appendChild(previewScripts); - - const sketchDocString = `\n${sketchDoc.documentElement.outerHTML}`; - scriptOffs = getAllScriptOffsets(sketchDocString); - const consoleErrorsScript = sketchDoc.createElement('script'); - consoleErrorsScript.innerHTML = ` - window.offs = ${JSON.stringify(scriptOffs)}; - window.objectUrls = ${JSON.stringify(objectUrls)}; - window.objectPaths = ${JSON.stringify(objectPaths)}; - window.editorOrigin = '${getConfig('EDITOR_URL')}'; - `; - addLoopProtect(sketchDoc); - sketchDoc.head.prepend(consoleErrorsScript); - - return `\n${sketchDoc.documentElement.outerHTML}`; -} - -function getHtmlFile(files) { - return files.filter((file) => file.name.match(/.*\.html$/i))[0]; -} - -function EmbedFrame({ files, isPlaying, basePath, gridOutput, textOutput }) { - const iframe = useRef(); - const htmlFile = useMemo(() => getHtmlFile(files), [files]); - const srcRef = useRef(); - - useEffect(() => { - const unsubscribe = registerFrame( - iframe.current.contentWindow, - window.origin - ); - return () => { - unsubscribe(); - }; - }); - - function renderSketch() { - const doc = iframe.current; - if (isPlaying) { - const htmlDoc = injectLocalFiles(files, htmlFile, { - basePath, - gridOutput, - textOutput - }); - const generatedHtmlFile = { - name: 'index.html', - content: htmlDoc - }; - 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 (toRevoke) { - blobUtil.revokeObjectURL(toRevoke); - } - }, 0); - } else { - doc.src = ''; - } - } - - useEffect(renderSketch, [files, isPlaying]); - return ( - - ); -} - -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; diff --git a/client/modules/Preview/EmbedFrame.tsx b/client/modules/Preview/EmbedFrame.tsx new file mode 100644 index 0000000000..58ed3feb5e --- /dev/null +++ b/client/modules/Preview/EmbedFrame.tsx @@ -0,0 +1,98 @@ +import { useMemo } from 'react'; +import blobUtil from 'blob-util'; +import mime from 'mime'; +import { PLAINTEXT_FILE_REGEX } from '../../../server/utils/fileUtils'; + +// File interface based on the IDE files structure +export interface File { + id: string; + _id: string; + name: string; + content: string; + fileType: 'file' | 'folder'; + children: string[]; + url?: string; + blobUrl?: string; + filePath?: string; + isSelectedFile?: boolean; +} + +export type FileState = File[]; + +export interface FileSelectors { + getHTMLFile: () => File | undefined; + getJSFiles: () => File[]; + getCSSFiles: () => File[]; +} + +interface SetFilesAction { + type: 'SET_FILES'; + files: File[]; +} + +type FilesAction = SetFilesAction | { type: string }; + +// https://gist.github.com/fnky/7d044b94070a35e552f3c139cdf80213 +export function useSelectors( + state: unknown, + mapStateToSelectors: (state: unknown) => T +): T { + const selectors = useMemo(() => mapStateToSelectors(state), [state]); + return selectors; +} + +export function getFileSelectors(state: FileState): FileSelectors { + 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)) + }; +} + +function sortedChildrenId(state: FileState, children: string[]): 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: File[]): SetFilesAction { + return { + type: 'SET_FILES', + files + }; +} + +export function createBlobUrl(file: File): string { + if (file.blobUrl) { + blobUtil.revokeObjectURL(file.blobUrl); + } + + const mimeType = mime.getType(file.name) || 'text/plain'; + console.log(mimeType); + + const fileBlob = blobUtil.createBlob([file.content], { type: mimeType }); + const blobURL = blobUtil.createObjectURL(fileBlob); + return blobURL; +} + +export function createBlobUrls(state: FileState): FileState { + return state.map((file) => { + if (file.name.match(PLAINTEXT_FILE_REGEX)) { + const blobUrl = createBlobUrl(file); + return { ...file, blobUrl }; + } + return file; + }); +} + +export function filesReducer(state: FileState, action: FilesAction): FileState { + switch (action.type) { + case 'SET_FILES': + return createBlobUrls((action as SetFilesAction).files); + default: + return state.map((file) => { + file.children = sortedChildrenId(state, file.children); + return file; + }); + } +} 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 diff --git a/client/modules/Preview/previewIndex.jsx b/client/modules/Preview/previewIndex.tsx similarity index 72% rename from client/modules/Preview/previewIndex.jsx rename to client/modules/Preview/previewIndex.tsx index 12c14e3b79..b7656bb60d 100644 --- a/client/modules/Preview/previewIndex.jsx +++ b/client/modules/Preview/previewIndex.tsx @@ -7,10 +7,10 @@ import { MessageTypes, dispatchMessage } from '../../utils/dispatcher'; -import { filesReducer, setFiles } from './filesReducer'; +import { filesReducer, setFiles, type File } from './filesReducer'; import EmbedFrame from './EmbedFrame'; import { getConfig } from '../../utils/getConfig'; -import { initialState } from '../IDE/reducers/files'; +import { initialState } from '../IDE/reducers/files.js'; const GlobalStyle = createGlobalStyle` body { @@ -18,22 +18,34 @@ const GlobalStyle = createGlobalStyle` } `; +interface Message { + type: string; + payload?: any; +} + +interface SketchPayload { + files: File[]; + basePath: string; + textOutput: boolean; + gridOutput: boolean; +} + const App = () => { - const [state, dispatch] = useReducer(filesReducer, [], initialState); + const [state, dispatch] = useReducer(filesReducer, initialState() as File[]); 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(setFiles(payload.files)); - setBasePath(payload.basePath); - setTextOutput(payload.textOutput); - setGridOutput(payload.gridOutput); + dispatch(setFiles((payload as SketchPayload).files)); + setBasePath((payload as SketchPayload).basePath); + setTextOutput((payload as SketchPayload).textOutput); + setGridOutput((payload as SketchPayload).gridOutput); break; case MessageTypes.START: setIsPlaying(true); @@ -52,7 +64,7 @@ const App = () => { } } - function addCacheBustingToAssets(files) { + function addCacheBustingToAssets(files: File[]): File[] { const timestamp = new Date().getTime(); return files.map((file) => { if (file.url && !file.url.endsWith('obj') && !file.url.endsWith('stl')) { @@ -71,6 +83,7 @@ const App = () => { unsubscribe(); }; }); + return ( @@ -86,3 +99,4 @@ const App = () => { }; render(, document.getElementById('root')); + From 7dcf0b1935a5c622c63cf645e15426119193753f Mon Sep 17 00:00:00 2001 From: Nalin Date: Fri, 30 Jan 2026 23:53:42 +0530 Subject: [PATCH 2/3] client/modules/Preview: add unit tests for filesReducer --- client/custom.d.ts | 35 ++ client/modules/Preview/EmbedFrame.tsx | 404 +++++++++++++++++++----- client/modules/Preview/filesReducer.ts | 56 +++- client/modules/Preview/previewIndex.tsx | 180 +++++------ client/tsconfig.json | 6 +- package-lock.json | 14 + package.json | 1 + webpack/config.dev.js | 2 +- webpack/config.prod.js | 2 +- 9 files changed, 526 insertions(+), 174 deletions(-) diff --git a/client/custom.d.ts b/client/custom.d.ts index 729f9e03de..44a91e944a 100644 --- a/client/custom.d.ts +++ b/client/custom.d.ts @@ -8,6 +8,41 @@ declare module '*.svg' { export default ReactComponent; } +declare module 'blob-util' { + export function createBlob(parts: any[], options?: { type: string }): Blob; + export function createObjectURL(blob: Blob): string; + export function revokeObjectURL(url: string): void; +} + +declare module 'mime' { + export function getType(path: string): string | null; +} + +declare module 'loop-protect' { + function loopProtect(code: string): string; + export = loopProtect; +} + +declare module 'jshint' { + export const JSHINT: { + (code: string): boolean; + errors: Array<{ + line: number; + character: number; + reason: string; + code: string; + }>; + }; +} + +declare module 'decomment' { + function decomment( + code: string, + options?: { ignore?: RegExp; space?: boolean } + ): string; + export = decomment; +} + // Extend window for Redux DevTools interface Window { __REDUX_DEVTOOLS_EXTENSION__?: () => any; diff --git a/client/modules/Preview/EmbedFrame.tsx b/client/modules/Preview/EmbedFrame.tsx index 58ed3feb5e..79b83ead0e 100644 --- a/client/modules/Preview/EmbedFrame.tsx +++ b/client/modules/Preview/EmbedFrame.tsx @@ -1,98 +1,360 @@ -import { useMemo } from 'react'; import blobUtil from 'blob-util'; -import mime from 'mime'; -import { PLAINTEXT_FILE_REGEX } from '../../../server/utils/fileUtils'; - -// File interface based on the IDE files structure -export interface File { - id: string; - _id: string; - name: string; - content: string; - fileType: 'file' | 'folder'; - children: string[]; - url?: string; - blobUrl?: string; - filePath?: string; - isSelectedFile?: boolean; +import React, { useRef, useEffect, useMemo } from 'react'; +import styled from 'styled-components'; +import loopProtect from 'loop-protect'; +import { JSHINT } from 'jshint'; +import decomment from 'decomment'; +import { resolvePathToFile } from '../../../server/utils/filePath'; +import { getConfig } from '../../utils/getConfig'; +import { + MEDIA_FILE_QUOTED_REGEX, + STRING_REGEX, + PLAINTEXT_FILE_REGEX, + EXTERNAL_LINK_REGEX, + NOT_EXTERNAL_LINK_REGEX +} from '../../../server/utils/fileUtils'; +import { getAllScriptOffsets } from '../../utils/consoleUtils'; +import { registerFrame } from '../../utils/dispatcher'; +import { createBlobUrl } from './filesReducer'; +import type { File } from './filesReducer'; +import resolvePathsForElementsWithAttribute from '../../../server/utils/resolveUtils'; + +interface ObjectUrls { + [key: string]: string; +} + +interface ObjectPaths { + [key: string]: string; } -export type FileState = File[]; +let objectUrls: ObjectUrls = {}; +let objectPaths: ObjectPaths = {}; -export interface FileSelectors { - getHTMLFile: () => File | undefined; - getJSFiles: () => File[]; - getCSSFiles: () => File[]; +interface FrameProps { + fullView?: boolean; } -interface SetFilesAction { - type: 'SET_FILES'; - files: File[]; +const Frame = styled.iframe` + min-height: 100%; + min-width: 100%; + position: absolute; + border-width: 0; + ${({ fullView }: FrameProps) => + fullView && + ` + position: relative; + `} +`; + +function resolveCSSLinksInString(content: string, files: File[]): string { + let newContent = content; + 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) as File | false; + if (resolvedFile && typeof resolvedFile === 'object') { + if (resolvedFile.url) { + newContent = newContent.replace( + cssFileString, + quoteCharacter + resolvedFile.url + quoteCharacter + ); + } + } + } + }); + return newContent; } -type FilesAction = SetFilesAction | { type: string }; +function jsPreprocess(jsText: string): string { + let newContent = jsText; + // check the code for js errors before sending it to strip comments + // or loops. + JSHINT(newContent); -// https://gist.github.com/fnky/7d044b94070a35e552f3c139cdf80213 -export function useSelectors( - state: unknown, - mapStateToSelectors: (state: unknown) => T -): T { - const selectors = useMemo(() => mapStateToSelectors(state), [state]); - return selectors; + if (JSHINT.errors.length === 0) { + newContent = decomment(newContent, { + ignore: /\/\/\s*noprotect/g, + space: true + }); + newContent = loopProtect(newContent); + } + return newContent; } -export function getFileSelectors(state: FileState): FileSelectors { - 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)) - }; +function resolveJSLinksInString(content: string, files: File[]): string { + let newContent = content; + 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) as File | false; + + if (resolvedFile && typeof resolvedFile === 'object') { + if (resolvedFile.url) { + newContent = newContent.replace( + jsFileString, + quoteCharacter + resolvedFile.url + quoteCharacter + ); + } else if (resolvedFile.name.match(PLAINTEXT_FILE_REGEX)) { + newContent = newContent.replace( + jsFileString, + quoteCharacter + resolvedFile.blobUrl + quoteCharacter + ); + } + } + } + }); + + return jsPreprocess(newContent); } -function sortedChildrenId(state: FileState, children: string[]): 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); +function resolveScripts(sketchDoc: Document, files: File[]): void { + const scriptsInHTML = sketchDoc.getElementsByTagName('script'); + const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML); + scriptsInHTMLArray.forEach((script: HTMLScriptElement) => { + if ( + script.getAttribute('src') && + script.getAttribute('src')!.match(NOT_EXTERNAL_LINK_REGEX) !== null + ) { + const resolvedFile = resolvePathToFile( + script.getAttribute('src')!, + files + ) as File | false; + if (resolvedFile && typeof resolvedFile === 'object') { + 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 + }`; + 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 + ) { + script.setAttribute('crossorigin', ''); + script.innerHTML = resolveJSLinksInString(script.innerHTML, files); // eslint-disable-line + } + }); } -export function setFiles(files: File[]): SetFilesAction { - return { - type: 'SET_FILES', - files - }; +function resolveStyles(sketchDoc: Document, files: File[]): void { + const inlineCSSInHTML = sketchDoc.getElementsByTagName('style'); + const inlineCSSInHTMLArray = Array.prototype.slice.call(inlineCSSInHTML); + inlineCSSInHTMLArray.forEach((style: HTMLStyleElement) => { + style.innerHTML = resolveCSSLinksInString(style.innerHTML, files); // eslint-disable-line + }); + + const cssLinksInHTML = sketchDoc.querySelectorAll('link[rel="stylesheet"]'); + const cssLinksInHTMLArray = Array.prototype.slice.call(cssLinksInHTML); + cssLinksInHTMLArray.forEach((css: HTMLLinkElement) => { + if ( + css.getAttribute('href') && + css.getAttribute('href')!.match(NOT_EXTERNAL_LINK_REGEX) !== null + ) { + const resolvedFile = resolvePathToFile( + css.getAttribute('href')!, + files + ) as File | false; + if (resolvedFile && typeof resolvedFile === 'object') { + if (resolvedFile.url) { + css.href = resolvedFile.url; // eslint-disable-line + } else { + const style = sketchDoc.createElement('style'); + style.innerHTML = `\n${resolvedFile.content}`; + sketchDoc.head.appendChild(style); + css.parentElement!.removeChild(css); + } + } + } + }); } -export function createBlobUrl(file: File): string { - if (file.blobUrl) { - blobUtil.revokeObjectURL(file.blobUrl); - } +function resolveJSAndCSSLinks(files: File[]): File[] { + const newFiles: File[] = []; + files.forEach((file) => { + const newFile = { ...file }; + if (file.name.match(/.*\.js$/i)) { + newFile.content = resolveJSLinksInString(newFile.content, files); + } else if (file.name.match(/.*\.css$/i)) { + newFile.content = resolveCSSLinksInString(newFile.content, files); + } + newFiles.push(newFile); + }); + return newFiles; +} - const mimeType = mime.getType(file.name) || 'text/plain'; - console.log(mimeType); +function addLoopProtect(sketchDoc: Document): void { + const scriptsInHTML = sketchDoc.getElementsByTagName('script'); + const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML); + scriptsInHTMLArray.forEach((script: HTMLScriptElement) => { + script.innerHTML = jsPreprocess(script.innerHTML); // eslint-disable-line + }); +} - const fileBlob = blobUtil.createBlob([file.content], { type: mimeType }); - const blobURL = blobUtil.createObjectURL(fileBlob); - return blobURL; +interface InjectOptions { + basePath: string; + gridOutput: boolean; + textOutput: boolean; } -export function createBlobUrls(state: FileState): FileState { - return state.map((file) => { - if (file.name.match(PLAINTEXT_FILE_REGEX)) { - const blobUrl = createBlobUrl(file); - return { ...file, blobUrl }; +function injectLocalFiles( + files: File[], + htmlFile: File, + options: InjectOptions +): string { + const { basePath, gridOutput, textOutput } = options; + let scriptOffs: any[] = []; + objectUrls = {}; + objectPaths = {}; + const resolvedFiles = resolveJSAndCSSLinks(files); + const parser = new DOMParser(); + const sketchDoc = parser.parseFromString(htmlFile.content, 'text/html'); + + const base = sketchDoc.createElement('base'); + base.href = `${window.origin}${basePath}${basePath.length > 1 ? '/' : ''}`; + sketchDoc.head.appendChild(base); + + resolvePathsForElementsWithAttribute('src', sketchDoc, resolvedFiles); + resolvePathsForElementsWithAttribute('href', sketchDoc, resolvedFiles); + // should also include background, data, poster, but these are used way less often + + resolveScripts(sketchDoc, resolvedFiles); + resolveStyles(sketchDoc, resolvedFiles); + + if (textOutput || gridOutput) { + const scriptElement = sketchDoc.createElement('script'); + let textCode = ''; + if (textOutput) { + textCode = 'if (!this._accessibleOutputs.text) this.textOutput();'; } - return file; - }); + let gridCode = ''; + if (gridOutput) { + gridCode = 'if (!this._accessibleOutputs.grid) this.gridOutput();'; + } + const fxn = `p5.prototype.ensureAccessibleCanvas = function _ensureAccessibleCanvas() { + ${textCode} + ${gridCode} +}; +p5.prototype.registerMethod('afterSetup', p5.prototype.ensureAccessibleCanvas);`; + scriptElement.innerHTML = fxn; + sketchDoc.head.appendChild(scriptElement); + } + + const previewScripts = sketchDoc.createElement('script'); + previewScripts.src = `${ + window.location.origin + }${getConfig('PREVIEW_SCRIPTS_URL', { nullishString: true })}`; + previewScripts.setAttribute('crossorigin', ''); + sketchDoc.head.appendChild(previewScripts); + + const sketchDocString = `\n${sketchDoc.documentElement.outerHTML}`; + scriptOffs = getAllScriptOffsets(sketchDocString); + const consoleErrorsScript = sketchDoc.createElement('script'); + consoleErrorsScript.innerHTML = ` + window.offs = ${JSON.stringify(scriptOffs)}; + window.objectUrls = ${JSON.stringify(objectUrls)}; + window.objectPaths = ${JSON.stringify(objectPaths)}; + window.editorOrigin = '${getConfig('EDITOR_URL')}'; + `; + addLoopProtect(sketchDoc); + sketchDoc.head.prepend(consoleErrorsScript); + + return `\n${sketchDoc.documentElement.outerHTML}`; } -export function filesReducer(state: FileState, action: FilesAction): FileState { - switch (action.type) { - case 'SET_FILES': - return createBlobUrls((action as SetFilesAction).files); - default: - return state.map((file) => { - file.children = sortedChildrenId(state, file.children); - return file; +function getHtmlFile(files: File[]): File { + return files.filter((file) => file.name.match(/.*\.html$/i))[0]; +} + +interface EmbedFrameProps { + files: File[]; + isPlaying: boolean; + basePath: string; + gridOutput: boolean; + textOutput: boolean; +} + +function EmbedFrame({ + files, + isPlaying, + basePath, + gridOutput, + textOutput +}: EmbedFrameProps) { + const iframe = useRef(null); + const htmlFile = useMemo(() => getHtmlFile(files), [files]); + const srcRef = useRef(); + + useEffect(() => { + const unsubscribe = registerFrame( + iframe.current!.contentWindow!, + window.origin + ); + return () => { + unsubscribe(); + }; + }); + + function renderSketch() { + const doc = iframe.current!; + if (isPlaying) { + const htmlDoc = injectLocalFiles(files, htmlFile, { + basePath, + gridOutput, + textOutput }); + const generatedHtmlFile: File = { + id: 'generated', + _id: 'generated', + name: 'index.html', + content: htmlDoc, + fileType: 'file', + children: [] + }; + 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 (toRevoke) { + blobUtil.revokeObjectURL(toRevoke); + } + }, 0); + } else { + doc.src = ''; + } } + + useEffect(renderSketch, [files, isPlaying]); + return ( + + ); } + +// eslint-disable-next-line import/no-default-export +export default EmbedFrame; diff --git a/client/modules/Preview/filesReducer.ts b/client/modules/Preview/filesReducer.ts index 098ae54824..bb3416edab 100644 --- a/client/modules/Preview/filesReducer.ts +++ b/client/modules/Preview/filesReducer.ts @@ -3,13 +3,50 @@ import blobUtil from 'blob-util'; import mime from 'mime'; import { PLAINTEXT_FILE_REGEX } from '../../../server/utils/fileUtils'; +// Type the mime module to match custom.d.ts declaration +type MimeModule = { + getType(path: string): string | null; +}; + +// File interface based on the IDE files structure +export interface File { + id: string; + _id: string; + name: string; + content: string; + fileType: 'file' | 'folder'; + children: string[]; + url?: string; + blobUrl?: string; + filePath?: string; + isSelectedFile?: boolean; +} + +export type FileState = File[]; + +export interface FileSelectors { + getHTMLFile: () => File | undefined; + getJSFiles: () => File[]; + getCSSFiles: () => File[]; +} + +interface SetFilesAction { + type: 'SET_FILES'; + files: File[]; +} + +type FilesAction = SetFilesAction | { type: string }; + // https://gist.github.com/fnky/7d044b94070a35e552f3c139cdf80213 -export function useSelectors(state, mapStateToSelectors) { +export function useSelectors( + state: unknown, + mapStateToSelectors: (state: unknown) => T +): T { const selectors = useMemo(() => mapStateToSelectors(state), [state]); return selectors; } -export function getFileSelectors(state) { +export function getFileSelectors(state: FileState): FileSelectors { return { getHTMLFile: () => state.filter((file) => file.name.match(/.*\.html$/i))[0], getJSFiles: () => state.filter((file) => file.name.match(/.*\.js$/i)), @@ -17,25 +54,26 @@ export function getFileSelectors(state) { }; } -function sortedChildrenId(state, children) { +function sortedChildrenId(state: FileState, children: string[]): 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) { +export function setFiles(files: File[]): SetFilesAction { return { type: 'SET_FILES', files }; } -export function createBlobUrl(file) { +export function createBlobUrl(file: File): string { if (file.blobUrl) { blobUtil.revokeObjectURL(file.blobUrl); } - const mimeType = mime.getType(file.name) || 'text/plain'; + const mimeType = + ((mime as unknown) as MimeModule).getType(file.name) || 'text/plain'; console.log(mimeType); const fileBlob = blobUtil.createBlob([file.content], { type: mimeType }); @@ -43,7 +81,7 @@ export function createBlobUrl(file) { return blobURL; } -export function createBlobUrls(state) { +export function createBlobUrls(state: FileState): FileState { return state.map((file) => { if (file.name.match(PLAINTEXT_FILE_REGEX)) { const blobUrl = createBlobUrl(file); @@ -53,10 +91,10 @@ export function createBlobUrls(state) { }); } -export function filesReducer(state, action) { +export function filesReducer(state: FileState, action: FilesAction): FileState { switch (action.type) { case 'SET_FILES': - return createBlobUrls(action.files); + return createBlobUrls((action as SetFilesAction).files); default: return state.map((file) => { file.children = sortedChildrenId(state, file.children); diff --git a/client/modules/Preview/previewIndex.tsx b/client/modules/Preview/previewIndex.tsx index b7656bb60d..bb3416edab 100644 --- a/client/modules/Preview/previewIndex.tsx +++ b/client/modules/Preview/previewIndex.tsx @@ -1,102 +1,104 @@ -import React, { useReducer, useState, useEffect } from 'react'; -import { render } from 'react-dom'; -import { createGlobalStyle } from 'styled-components'; -import { - registerFrame, - listen, - MessageTypes, - dispatchMessage -} from '../../utils/dispatcher'; -import { filesReducer, setFiles, type File } from './filesReducer'; -import EmbedFrame from './EmbedFrame'; -import { getConfig } from '../../utils/getConfig'; -import { initialState } from '../IDE/reducers/files.js'; +import { useMemo } from 'react'; +import blobUtil from 'blob-util'; +import mime from 'mime'; +import { PLAINTEXT_FILE_REGEX } from '../../../server/utils/fileUtils'; -const GlobalStyle = createGlobalStyle` - body { - margin: 0; - } -`; +// Type the mime module to match custom.d.ts declaration +type MimeModule = { + getType(path: string): string | null; +}; + +// File interface based on the IDE files structure +export interface File { + id: string; + _id: string; + name: string; + content: string; + fileType: 'file' | 'folder'; + children: string[]; + url?: string; + blobUrl?: string; + filePath?: string; + isSelectedFile?: boolean; +} + +export type FileState = File[]; -interface Message { - type: string; - payload?: any; +export interface FileSelectors { + getHTMLFile: () => File | undefined; + getJSFiles: () => File[]; + getCSSFiles: () => File[]; } -interface SketchPayload { +interface SetFilesAction { + type: 'SET_FILES'; files: File[]; - basePath: string; - textOutput: boolean; - gridOutput: boolean; } -const App = () => { - const [state, dispatch] = useReducer(filesReducer, initialState() as File[]); - 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')); +type FilesAction = SetFilesAction | { type: string }; - function handleMessageEvent(message: Message) { - const { type, payload } = message; - switch (type) { - case MessageTypes.SKETCH: - dispatch(setFiles((payload as SketchPayload).files)); - setBasePath((payload as SketchPayload).basePath); - setTextOutput((payload as SketchPayload).textOutput); - setGridOutput((payload as SketchPayload).gridOutput); - break; - case MessageTypes.START: - setIsPlaying(true); - break; - case MessageTypes.STOP: - setIsPlaying(false); - break; - case MessageTypes.REGISTER: - dispatchMessage({ type: MessageTypes.REGISTER }); - break; - case MessageTypes.EXECUTE: - dispatchMessage(payload); - break; - default: - break; - } - } +// https://gist.github.com/fnky/7d044b94070a35e552f3c139cdf80213 +export function useSelectors( + state: unknown, + mapStateToSelectors: (state: unknown) => T +): T { + const selectors = useMemo(() => mapStateToSelectors(state), [state]); + return selectors; +} + +export function getFileSelectors(state: FileState): FileSelectors { + 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)) + }; +} + +function sortedChildrenId(state: FileState, children: string[]): 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); +} - function addCacheBustingToAssets(files: File[]): File[] { - const timestamp = new Date().getTime(); - return files.map((file) => { - if (file.url && !file.url.endsWith('obj') && !file.url.endsWith('stl')) { - return { - ...file, - url: `${file.url}?v=${timestamp}` - }; - } - return file; - }); +export function setFiles(files: File[]): SetFilesAction { + return { + type: 'SET_FILES', + files + }; +} + +export function createBlobUrl(file: File): string { + if (file.blobUrl) { + blobUtil.revokeObjectURL(file.blobUrl); } - useEffect(() => { - const unsubscribe = listen(handleMessageEvent); - return function cleanup() { - unsubscribe(); - }; - }); - - return ( - - - - - ); -}; + const mimeType = + ((mime as unknown) as MimeModule).getType(file.name) || 'text/plain'; + console.log(mimeType); -render(, document.getElementById('root')); + const fileBlob = blobUtil.createBlob([file.content], { type: mimeType }); + const blobURL = blobUtil.createObjectURL(fileBlob); + return blobURL; +} +export function createBlobUrls(state: FileState): FileState { + return state.map((file) => { + if (file.name.match(PLAINTEXT_FILE_REGEX)) { + const blobUrl = createBlobUrl(file); + return { ...file, blobUrl }; + } + return file; + }); +} + +export function filesReducer(state: FileState, action: FilesAction): FileState { + switch (action.type) { + case 'SET_FILES': + return createBlobUrls((action as SetFilesAction).files); + default: + return state.map((file) => { + file.children = sortedChildrenId(state, file.children); + return file; + }); + } +} diff --git a/client/tsconfig.json b/client/tsconfig.json index 9e63548a60..5b8aa44c5f 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -5,7 +5,7 @@ "module": "ESNext", "lib": ["DOM", "ESNext"], "jsx": "react", - }, - "include": ["./**/*"], + "types": ["node"] }, + "include": ["./**/*","custom.d.ts"], "exclude": ["../node_modules", "../server"] -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index e08bd4dc3b..f6e90e5470 100644 --- a/package-lock.json +++ b/package-lock.json @@ -161,6 +161,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", @@ -17973,6 +17974,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", @@ -55686,6 +55694,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 c15e3360cd..2a1b321e28 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", diff --git a/webpack/config.dev.js b/webpack/config.dev.js index 85533fd7ae..7b810bae88 100644 --- a/webpack/config.dev.js +++ b/webpack/config.dev.js @@ -15,7 +15,7 @@ module.exports = { app: ['@gatsbyjs/webpack-hot-middleware/client', './client/index.jsx'], previewApp: [ '@gatsbyjs/webpack-hot-middleware/client', - './client/modules/Preview/previewIndex.jsx' + './client/modules/Preview/previewIndex.tsx' ], previewScripts: [ '@gatsbyjs/webpack-hot-middleware/client', diff --git a/webpack/config.prod.js b/webpack/config.prod.js index e125c621c5..665b04fa0d 100644 --- a/webpack/config.prod.js +++ b/webpack/config.prod.js @@ -21,7 +21,7 @@ module.exports = { ], previewApp: [ 'regenerator-runtime/runtime', - path.resolve(__dirname, '../client/modules/Preview/previewIndex.jsx') + path.resolve(__dirname, '../client/modules/Preview/previewIndex.tsx') ], previewScripts: [ 'regenerator-runtime/runtime', From 377db61e28f96c3ce835e7a3cf9581dcd81e2406 Mon Sep 17 00:00:00 2001 From: Nalin Date: Tue, 10 Feb 2026 09:22:16 +0530 Subject: [PATCH 3/3] type errors --- client/modules/Preview/previewIndex.tsx | 161 ++++++++++-------------- 1 file changed, 65 insertions(+), 96 deletions(-) diff --git a/client/modules/Preview/previewIndex.tsx b/client/modules/Preview/previewIndex.tsx index bb3416edab..4735cd96c4 100644 --- a/client/modules/Preview/previewIndex.tsx +++ b/client/modules/Preview/previewIndex.tsx @@ -1,104 +1,73 @@ -import { useMemo } from 'react'; -import blobUtil from 'blob-util'; -import mime from 'mime'; -import { PLAINTEXT_FILE_REGEX } from '../../../server/utils/fileUtils'; +import React, { useState, useEffect } from 'react'; +import ReactDOM from 'react-dom'; +import { + listen, + dispatchMessage, + registerFrame, + MessageTypes +} from '../../utils/dispatcher'; +import { getConfig } from '../../utils/getConfig'; +import EmbedFrame from './EmbedFrame'; +import { createBlobUrls } from './filesReducer'; +import type { File } from './filesReducer'; -// Type the mime module to match custom.d.ts declaration -type MimeModule = { - getType(path: string): string | null; -}; +function PreviewApp() { + const [files, setFiles] = useState([]); + const [isPlaying, setIsPlaying] = useState(false); + const [basePath, setBasePath] = useState('/'); + const [gridOutput, setGridOutput] = useState(false); + const [textOutput, setTextOutput] = useState(false); -// File interface based on the IDE files structure -export interface File { - id: string; - _id: string; - name: string; - content: string; - fileType: 'file' | 'folder'; - children: string[]; - url?: string; - blobUrl?: string; - filePath?: string; - isSelectedFile?: boolean; -} - -export type FileState = File[]; - -export interface FileSelectors { - getHTMLFile: () => File | undefined; - getJSFiles: () => File[]; - getCSSFiles: () => File[]; -} - -interface SetFilesAction { - type: 'SET_FILES'; - files: File[]; -} - -type FilesAction = SetFilesAction | { type: string }; + useEffect(() => { + const editorOrigin = getConfig('EDITOR_URL') || ''; + const unsubscribe = registerFrame(window.parent, editorOrigin); + return () => unsubscribe(); + }, []); -// https://gist.github.com/fnky/7d044b94070a35e552f3c139cdf80213 -export function useSelectors( - state: unknown, - mapStateToSelectors: (state: unknown) => T -): T { - const selectors = useMemo(() => mapStateToSelectors(state), [state]); - return selectors; -} - -export function getFileSelectors(state: FileState): FileSelectors { - 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)) - }; -} - -function sortedChildrenId(state: FileState, children: string[]): 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: File[]): SetFilesAction { - return { - type: 'SET_FILES', - files - }; -} - -export function createBlobUrl(file: File): string { - if (file.blobUrl) { - blobUtil.revokeObjectURL(file.blobUrl); - } - - const mimeType = - ((mime as unknown) as MimeModule).getType(file.name) || 'text/plain'; - console.log(mimeType); + useEffect(() => { + function handleMessage(message: any) { + switch (message.type) { + case MessageTypes.REGISTER: + dispatchMessage({ type: MessageTypes.REGISTER }); + break; + case MessageTypes.SKETCH: { + const payload = message.payload || {}; + const incomingFiles: File[] = payload.files || []; + setFiles(createBlobUrls(incomingFiles)); + setBasePath(payload.basePath || '/'); + setGridOutput(Boolean(payload.gridOutput)); + setTextOutput(Boolean(payload.textOutput)); + break; + } + case MessageTypes.START: + setIsPlaying(true); + break; + case MessageTypes.STOP: + setIsPlaying(false); + break; + default: + break; + } + } - const fileBlob = blobUtil.createBlob([file.content], { type: mimeType }); - const blobURL = blobUtil.createObjectURL(fileBlob); - return blobURL; -} + const unsubscribe = listen(handleMessage); + return () => unsubscribe(); + }, []); -export function createBlobUrls(state: FileState): FileState { - return state.map((file) => { - if (file.name.match(PLAINTEXT_FILE_REGEX)) { - const blobUrl = createBlobUrl(file); - return { ...file, blobUrl }; - } - return file; - }); + return ( +
+ +
+ ); } -export function filesReducer(state: FileState, action: FilesAction): FileState { - switch (action.type) { - case 'SET_FILES': - return createBlobUrls((action as SetFilesAction).files); - default: - return state.map((file) => { - file.children = sortedChildrenId(state, file.children); - return file; - }); - } +const rootEl = document.getElementById('root'); +if (rootEl) { + ReactDOM.render(, rootEl); }