-
-
Notifications
You must be signed in to change notification settings - Fork 195
Open
Description
With current esm.sh/tsx implementation you are not able to import relative .tsx files and preprocess them with esm.sh/tsx
It reduces ways of esm.sh/tsx usage.
Example
index.html
<!DOCTYPE html>
<html>
<head>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/[email protected]",
"react-dom/client": "https://esm.sh/[email protected]/client"
}
}
</script>
<script type="module" src="https://esm.sh/tsx"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
import { createRoot } from "react-dom/client"
import { App } from "./app.tsx" // generates import { App } from "script-0.tsx/app.tsx"
createRoot(root).render(<App />)
</script>
</body>
</html>app.tsx
export function App() {
return <div>App</div>
}
Result
Expected (desired) result: esm.sh/tsx transformed ./app.tsx into valid js content and this code should work fine
Actual result: you get an error
tsx:6 Uncaught TypeError: Failed to resolve module specifier "script-0.tsx/app.tsx". Relative references must start with either "/", "./", or "../".
The same problem with dynamic imports (e.g. const { App } = await import('./app.tsx'))
Solutions
1. Automatically replace all relative imports with await __dynamicImport(...)
async function __dynamicImport(path, options) {
const code = await fetch(path).then(r => r.text())
const hash = getHash(code)
if (localStorage.getItem(hash)) { // should also check for result hash
// local usage, just import() data uri and execute it "inline"
return import(`data:text/javascript,${encodeURIComponent(localStorage.getItem(hash))}`, options)
// or use Blob instead of data uri
}
if (location.hostname == 'localhost') {
const output = await tsx.build(code) // esm.sh tsx build method
localStorage.setItem(hash, output)
// local usage, just import() data uri and execute it "inline"
return import(`data:text/javascript,${encodeURIComponent(output)}`, options)
// or use Blob instead of data uri
}
else {
// check if code was uploaded beforehand
const builtCodeUrl = await fetch(esmRelativeUrl(hash, path)).catch(() => null)
// build result is uploaded to build file, we can import it instead of original file
// e.g. esm.sh/tsx/f4829acaeea20828e385cdb4e3845b64dc6d9def/app.tsx
if (result) return import(builtCodeUrl, options) // import uploaded code by url
// no build result, we should build a new one
const output = await tsx.build(code)
// upload result to server
const uploadedUrl = await fetch(esmRelativeUrl(hash, path), { method: 'POST', body: output })
return import(uploadedUrl, options) // import uploaded code by url
}
}
function esmRelativeUrl(hash, path) {
return new URL(hash + path, import.meta.url) // esm.sh/tsx/f4829acaeea20828e385cdb4e3845b64dc6d9def/app.tsx
}
function getHash(text) {
return '...' // generate hash
}// import { App } from "./app.tsx"
const { App } = await __dynamicImport('./app.tsx')
// now it works!Pros
- it works (mostly)
Cons
- requires top level await support (or we can transpile it like vite-plugin-top-level-await)
-
- most of the browsers (including Safari) support this feature
- cyclic imports will stuck (maybe it's possible to fix it? I'm not sure)
- we should handle every relative imports (even
.jsfiles), because those.jsfiles may import.tsxfiles - we should also replace all
import.meta.url/import.meta.resolveusages fordata uriexecutions
2. Service Workers
If we register a Service Worker that will handle every import ... from '___.tsx' request, we may leave result built code as is, since service worker will resolve every import and transform result into valid js code.
Example
// service-worker.js
self.addEventListener("fetch", fetchHandler)
function fetchHandler(event) {
if (event.request.destination !== 'script') return;
if (!event.request.url.startsWith(self.location.origin)) return;
const match = event.request.url.match(/\.(jsx|ts|tsx|css|vue)/)
if (match) {
event.respondWith((async () => {
const text = fetch(event.request).then(target => target.text())
const hash = getHash(text)
const output = await tsx.build(code) // or fetch uploaded to server // or save to Caches API
const networkResponse = new Response(output, {
headers: { 'Content-Type': 'application/javascript' }
})
return networkResponse
})())
}
}import { App } from "./app.tsx" // processed and transformed by Service Worker
// now it works!I've tried to implement it here: https://github.com/crutch12/esbuild-standalone
And it works great.
Pros
- automatically handles
.tsximports, no__dynamicImportusage - no cyclic imports problems
- handle only .tsx/.ts/.jsx files
Cons
- requires register Service Worker (it's a compelling wall in most cases)
Metadata
Metadata
Assignees
Labels
No labels