Open curran opened 1 week ago
Some ideas from ChatGPT (o1-preview):
Certainly! Let's update the old implementation to leverage modern browser APIs, focusing on using ES6 modules, fetch
, and Blob URLs instead of the older XMLHttpRequest
and overriding methods. We'll also use Blob URLs to handle scripts and styles, and we'll intercept fetch
requests to serve in-memory files.
Note: While service workers are a powerful tool for intercepting network requests, they cannot be registered from Blob URLs or data URLs due to security restrictions. Since we want to avoid calling out to a server and keep everything client-side, we'll use Blob URLs and override the fetch
API in a controlled manner.
Here's the updated code:
export default function (template, files) {
// Collect the files, excluding 'index.html' and 'thumbnail.png'
const fileMap = {};
Object.keys(files).forEach((fileName) => {
if (fileName === 'index.html' || fileName === 'thumbnail.png') return;
fileMap[fileName] = files[fileName].content;
});
// Create Blob URLs for each file
const blobUrlMap = {};
Object.keys(fileMap).forEach((fileName) => {
const content = fileMap[fileName];
const blob = new Blob([content], { type: getContentType(fileName) });
const blobUrl = URL.createObjectURL(blob);
blobUrlMap[fileName] = blobUrl;
});
// Replace references in the template
template = replaceReferences(template, blobUrlMap);
// Inject code to override 'window.fetch' for fetch requests to the original filenames
const fetchOverrideScript = `
<script type="module">
(function() {
const originalFetch = window.fetch;
const blobUrlMap = ${JSON.stringify(blobUrlMap)};
window.fetch = function(input, init) {
const url = typeof input === 'string' ? input : input.url;
const pathname = new URL(url, location.href).pathname.slice(1);
if (blobUrlMap[pathname]) {
return originalFetch(blobUrlMap[pathname], init);
}
return originalFetch(input, init);
};
})();
</script>
`;
// Prepend the fetch override script to the template
template = fetchOverrideScript + template;
return template;
function getContentType(fileName) {
if (fileName.endsWith('.js')) {
return 'application/javascript';
} else if (fileName.endsWith('.css')) {
return 'text/css';
} else if (fileName.endsWith('.html')) {
return 'text/html';
} else if (fileName.endsWith('.json')) {
return 'application/json';
} else if (fileName.endsWith('.svg')) {
return 'image/svg+xml';
} else if (fileName.endsWith('.png')) {
return 'image/png';
} else if (fileName.endsWith('.jpg') || fileName.endsWith('.jpeg')) {
return 'image/jpeg';
} else if (fileName.endsWith('.gif')) {
return 'image/gif';
} else {
return 'text/plain';
}
}
function replaceReferences(html, blobUrlMap) {
// Replace <script src="file.js" type="module"> with Blob URL
html = html.replace(/<script\s+([^>]*?)src=["']([^"']+)["']([^>]*)><\/script>/g, (match, beforeSrc, src, afterSrc) => {
const fileName = src.startsWith('./') ? src.slice(2) : src;
if (blobUrlMap[fileName]) {
const attrs = `${beforeSrc.trim()} ${afterSrc.trim()}`.trim();
return `<script type="module" src="${blobUrlMap[fileName]}" ${attrs}></script>`;
}
return match;
});
// Replace <link href="file.css" rel="stylesheet"> with Blob URL
html = html.replace(/<link\s+([^>]*?)href=["']([^"']+)["']([^>]*)>/g, (match, beforeHref, href, afterHref) => {
const fileName = href.startsWith('./') ? href.slice(2) : href;
if (blobUrlMap[fileName]) {
const attrs = `${beforeHref.trim()} ${afterHref.trim()}`.trim();
return `<link href="${blobUrlMap[fileName]}" ${attrs}>`;
}
return match;
});
return html;
}
}
Explanation:
Collecting Files:
files
object and collect all files except index.html
and thumbnail.png
.fileMap
for easy access.Creating Blob URLs:
fileMap
, we create a Blob with the appropriate MIME type.URL.createObjectURL(blob)
and store it in blobUrlMap
.Replacing References in the Template:
template
HTML string for <script>
and <link>
tags that reference our files.src
or href
attributes with the corresponding Blob URLs.type="module"
as per the requirement.Overriding the Fetch API:
window.fetch
.fetch
checks if the requested URL matches one of our files.fetch
calls in the user code to access the in-memory files without network requests.Returning the Modified Template:
Leveraging Modern Browser APIs:
type="module"
and ensure that module scripts are properly loaded from Blob URLs.XMLHttpRequest
, we intercept fetch
requests, which is the modern way of handling HTTP requests in the browser.Usage Example:
Suppose you have the following files
object:
const files = {
'index.html': {
content: `
<html>
<head>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Hello World</h1>
<script type="module" src="script.js"></script>
</body>
</html>
`,
},
'script.js': {
content: `
import { message } from './module.js';
console.log(message);
`,
},
'module.js': {
content: `
export const message = 'This is a module!';
`,
},
'styles.css': {
content: `
body { background-color: lightblue; }
`,
},
};
You can use the updated function as follows:
const parseCode = /* the updated function */;
const modifiedTemplate = parseCode(files['index.html'].content, files);
// Now you can set the `modifiedTemplate` as the `srcdoc` of an iframe
const iframe = document.createElement('iframe');
iframe.srcdoc = modifiedTemplate;
document.body.appendChild(iframe);
Considerations:
fetch
requests client-side allows us to work within these constraints.window.fetch
should be done cautiously, as it can affect other network requests. In this controlled environment, it's acceptable, but it's important to be aware of potential side effects.Benefits:
type="module"
, users can write modular JavaScript code using the latest syntax.This updated implementation modernizes the original code by leveraging current web technologies, providing a seamless, in-browser coding experience that aligns with modern development practices.
I think there is an opportunity to re-build the VizHub runtime environment to be based on purely HTML standards with ES Modules. In this variant of the runtime, there would be no need to run the source code through any various parsing and processing, meaning this version of the runtime would have zero dependency on Rollup.
Would indirectly solve by use of import maps and native ES modules the following issue:
Here's a start at it:
index.html
viz.js
index.js
This remains the same contract as the current V3 runtime environment - that
index.js
exports a function calledmain
that accepts a container, and the{ state, setState }
state management thing.This works great when locally hosted. I suppose this would have the downside of not supporting libraries that don't publish an ESM build.
In order to implement something like this as the next-gen VizHub runtime, I'd want to solve the following before launching it:
main
, which the current runtime v3 supports and is a really nice (dare I say "killer") feature to have