Last active
October 27, 2023 07:49
-
-
Save nestarz/b5c4b6c08f93e9dc7e14cfb85554174c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <reference lib="deno.unstable" /> | |
import { denoPlugins } from "https://deno.land/x/[email protected]/mod.ts"; | |
import * as esbuild from "https://deno.land/x/[email protected]/wasm.js"; | |
import { | |
collectAndCleanScripts, | |
getHashSync, | |
scripted, | |
storeFunctionExecution, | |
} from "https://deno.land/x/[email protected]/mod.ts"; | |
import { walk } from "https://deno.land/[email protected]/fs/walk.ts"; | |
import { | |
dirname, | |
join, | |
relative, | |
toFileUrl, | |
} from "https://deno.land/[email protected]/path/mod.ts"; | |
import { getIslands, IslandDef } from "https://deno.land/x/[email protected]/client.ts"; | |
interface Snapshot { | |
build_id: string; | |
files: Record<string, string[]>; | |
} | |
const files = []; | |
for await ( | |
const { path } of walk(Deno.cwd(), { | |
maxDepth: 10, | |
exts: [".js", ".jsx", ".tsx", ".ts", ".json"], | |
}) | |
) { | |
if (!path.includes("_islet")) { | |
files.push({ url: path, size: (await Deno.stat(path)).size }); | |
} | |
} | |
let buildId = getHashSync( | |
JSON.stringify(files.toSorted((a, b) => a.url.localeCompare(b.url))), | |
); | |
console.log(buildId, files, Deno.cwd()); | |
const setBuildId = (id: string) => (buildId = id); | |
const createIslandId = (key: string) => | |
getHashSync( | |
[buildId, relative(import.meta.resolve("./"), key)] | |
.filter((v) => v) | |
.join("_"), | |
); | |
export const getIslandUrl = <T>(fn: T, key = "default") => | |
`/islands/${createIslandId(getIslands(key).get(fn)?.url!)}.js`; | |
export const config = { | |
routeOverride: "/islands/:id*", | |
}; | |
function deepApply<T>(data: T, applyFn): T { | |
function isObject(object: unknown): object is Record<keyof never, unknown> { | |
return object instanceof Object && object.constructor === Object; | |
} | |
if (Array.isArray(data)) { | |
return (data as unknown[]).map((value) => | |
isObject(value) ? deepApply(value, applyFn) : value | |
) as unknown as T; | |
} | |
const entries = Object.entries(data as Record<string, unknown>).reduce( | |
(p, [key, value]) => { | |
const r = applyFn(key, value, p); | |
return r; | |
}, | |
data, | |
); | |
const clean = Object.entries(entries).map(([key, v]) => { | |
const value = isObject(v) ? deepApply(v, applyFn) : v; | |
return [key, value]; | |
}); | |
return Object.fromEntries(clean) as T; | |
} | |
const createCounter = (startAt = 0) => ((i) => () => i++)(startAt); // prettier-ignore | |
const initCounter = createCounter(0); | |
const buildCounter = createCounter(0); | |
const transformCounter = createCounter(0); | |
class SuffixTransformStream extends TransformStream<Uint8Array, Uint8Array> { | |
constructor(suffix: string) { | |
super({ | |
flush(controller) { | |
controller.enqueue(new TextEncoder().encode(suffix)); | |
controller.terminate(); | |
}, | |
}); | |
} | |
} | |
export interface Manifest { | |
key?: string; | |
baseUrl: URL; | |
// islands: URL | URL[]; | |
prefix: string; | |
jsxImportSource: string; | |
buildDir?: string; | |
importMapFileName?: string; | |
esbuildOptions?: Partial<Parameters<typeof esbuild.build>[0]>; | |
openKv: () => ReturnType<typeof Deno.openKv>; | |
dev?: boolean; | |
} | |
const esbuildState = (( | |
done = false, | |
ongoingPromise: null | Promise<null | void> = null, | |
) => ({ | |
isInitialized: () => done, | |
init: () => { | |
if (ongoingPromise) return ongoingPromise; | |
const id = initCounter(); | |
console.time(`[init-${id}] ${esbuild.version}`); | |
const wasmURL = | |
`https://raw.githubusercontent.com/esbuild/deno-esbuild/v${esbuild.version}/esbuild.wasm`; | |
ongoingPromise = esbuild | |
.initialize( | |
!globalThis.Worker || Deno.Command === undefined | |
? { wasmURL, worker: false } | |
: {}, | |
) | |
.then(() => { | |
done = true; | |
console.timeEnd(`[init-${id}] ${esbuild.version}`); | |
}) | |
.catch((err) => | |
err.toString().includes("more than once") ? null : console.error(err) | |
); | |
return ongoingPromise!; | |
}, | |
}))(); | |
type EsBuild = Awaited<ReturnType<typeof esbuild.build>>; | |
interface SnapshotReader { | |
getPaths: () => string[]; | |
read: (path: string) => Promise<ReadableStream<Uint8Array> | null>; | |
dependencies: (path: string) => string[]; | |
json: () => { | |
[k: string]: string[] | undefined; | |
}; | |
} | |
const buildSnapshot = ( | |
buildOptions: esbuild.BuildOptions, | |
bundle: EsBuild, | |
): SnapshotReader => { | |
const absWorkingDirLen = toFileUrl(buildOptions.absWorkingDir!).href.length + | |
1; | |
const files = new Map<string, Uint8Array>(); | |
const dependencies = new Map<string, string[]>(); | |
for (const file of bundle.outputFiles!) { | |
const path = toFileUrl(file.path).href.slice(absWorkingDirLen); | |
files.set(path, file.contents); | |
} | |
const metaOutputs = new Map(Object.entries(bundle.metafile!.outputs)); | |
for (const [path, entry] of metaOutputs.entries()) { | |
const imports = entry.imports | |
.filter(({ kind }) => kind === "import-statement") | |
.map(({ path }) => path); | |
dependencies.set(path, imports); | |
} | |
return { | |
getPaths: () => Array.from(files.keys()), | |
read: (path: string) => | |
Promise.resolve( | |
files.get(path) | |
? new ReadableStream({ | |
start(controller) { | |
controller.enqueue(files.get(path)!); | |
controller.close(); | |
}, | |
}) | |
: null, | |
), | |
dependencies: (path: string) => dependencies.get(path) ?? [], | |
json: () => | |
Object.fromEntries( | |
Array.from(files.keys()).map((key) => [key, dependencies.get(key)]), | |
), | |
}; | |
}; | |
const snapshotFromJson = ( | |
json: Snapshot, | |
snapshotDirPath: string, | |
): SnapshotReader => { | |
const dependencies = new Map<string, string[]>(Object.entries(json.files)); | |
const files = new Map<string, string>(); | |
Object.keys(json.files).forEach((name) => { | |
const filePath = join(snapshotDirPath, name); | |
files.set(name, filePath); | |
}); | |
return { | |
getPaths: () => Array.from(files.keys()), | |
read: async (path: string) => { | |
const filePath = files.get(path); | |
if (filePath !== undefined) { | |
try { | |
const file = await Deno.open(filePath, { read: true }); | |
return file.readable; | |
} catch (_err) { | |
return null; | |
} | |
} | |
// Handler will turn this into a 404 | |
return null; | |
}, | |
dependencies: (path: string) => dependencies.get(path) ?? [], | |
json: () => | |
Object.fromEntries( | |
Array.from(files.keys()).map((key) => [key, dependencies.get(key)]), | |
), | |
}; | |
}; | |
const transformScript = async (script: string) => { | |
esbuildState.init().catch(console.error); | |
if (!esbuildState.isInitialized()) return script; | |
const id = `[esbuild-${transformCounter()}] transform`; | |
console.time(id); | |
const scripts = await esbuild.transform(script, { minify: true }); | |
console.timeEnd(id); | |
return scripts.code; | |
}; | |
export const addScripts = async ( | |
html: string | ReadableStream, | |
minify = true, | |
): Promise<typeof html> => { | |
const scripts = collectAndCleanScripts(); | |
const code = minify ? await transformScript(scripts) : scripts; | |
const script = `<script type="module" defer>${code}</script>`; | |
if (html instanceof ReadableStream) { | |
return html.pipeThrough(new SuffixTransformStream(script)); | |
} | |
return `${ | |
html.replace( | |
html.includes("</body>") ? /(<\/body>)/ : /(.*)/, | |
(_, $1) => `${script}${$1}`, | |
) | |
}`; | |
}; | |
interface IslandHandlerGetter { | |
get: (id: string) => ReturnType<SnapshotReader["read"]>; | |
} | |
const createIslands = async ( | |
manifest: Manifest, | |
initSnapshot: SnapshotReader | null, | |
snapshotPath: string, | |
): Promise<IslandHandlerGetter> => { | |
if (initSnapshot) { | |
return { get: (id: string) => initSnapshot.read("islands/" + id) }; | |
} | |
const absWorkingDir = Deno.cwd(); | |
const buildConfig: Parameters<typeof esbuild.build>[0] = { | |
plugins: [ | |
...denoPlugins({ | |
loader: "native", | |
configPath: new URL( | |
manifest.importMapFileName ?? "deno.json", | |
manifest.baseUrl, | |
).href.slice(7), | |
}), | |
], | |
entryPoints: [ | |
...Array.from(getIslands(manifest.key ?? "default")).map( | |
([, island]) => ({ | |
out: createIslandId(island.url), | |
in: island.url, | |
}), | |
), | |
], | |
platform: "browser", | |
target: ["chrome99", "firefox99", "safari15"], | |
format: "esm", | |
jsx: manifest.jsxImportSource ? "automatic" : "transform", | |
jsxFactory: "h", | |
jsxFragment: "Fragment", | |
jsxImportSource: manifest.jsxImportSource, | |
bundle: true, | |
splitting: true, | |
metafile: true, | |
treeShaking: true, | |
outdir: manifest.prefix, | |
absWorkingDir, | |
write: false, | |
sourcemap: manifest.dev ? "linked" : false, | |
minify: true, | |
...(manifest.esbuildOptions ?? {}), | |
}; | |
const bundle = await esbuildState | |
.init() | |
.then(() => esbuild.build(buildConfig)); | |
const buildDir = dirname(snapshotPath); | |
const id = `[esbuild-${buildCounter()}] build`; | |
console.time(id); | |
const snapshotReader = buildSnapshot(buildConfig, bundle); | |
console.timeEnd(id); | |
await Deno.remove(buildDir, { recursive: true }).catch(() => null); | |
await Deno.mkdir(buildDir, { recursive: true }).catch(() => null); | |
await Promise.all( | |
snapshotReader.getPaths().map(async (fileName) => { | |
const data = await snapshotReader.read(fileName); | |
if (data === null) return; | |
const path = join(buildDir, fileName); | |
await Deno.mkdir(dirname(path), { recursive: true }).catch(() => null); | |
return Deno.writeFile(path, data); | |
}), | |
); | |
await Deno.writeTextFile( | |
snapshotPath, | |
JSON.stringify( | |
{ build_id: buildId, files: snapshotReader.json() }, | |
null, | |
2, | |
), | |
); | |
return { | |
get: (id: string) => snapshotReader.read("islands/" + id), | |
}; | |
}; | |
export const createHandler = async (manifest: Manifest) => { | |
const promiseCache: Map<string, Promise<IslandHandlerGetter>> = new Map(); | |
const buildDir = manifest.buildDir ?? "_islet"; | |
const snapshotPath = join(buildDir, "snapshot.json"); | |
const json: Snapshot | null = JSON.parse( | |
await Deno.readTextFile(snapshotPath).catch(() => "null"), | |
); | |
const snapshot = json?.build_id === buildId | |
? snapshotFromJson(json, buildDir) | |
: null; | |
if (json?.build_id === buildId) setBuildId(json.build_id); | |
console.log(json, snapshot); | |
return async (_req: Request, _ctx: any, match: Record<string, string>) => { | |
if (!promiseCache.has(manifest.baseUrl.href)) { | |
console.log(manifest.baseUrl.href); | |
promiseCache.set( | |
manifest.baseUrl.href, | |
createIslands(manifest, snapshot, snapshotPath), | |
); | |
} | |
const islands = await promiseCache.get(manifest.baseUrl.href)!; | |
const contents = await islands.get(match.id); | |
return contents | |
? new Response(contents, { | |
headers: { | |
"content-type": "text/javascript", | |
"Cache-Control": "public, max-age=31536000, immutable", | |
}, | |
}) | |
: new Response(null, { status: 404 }); | |
}; | |
}; | |
type HydrateFn = ( | |
container: Element | Document, | |
initialChildren: VNode, | |
) => Root; | |
const hydrate = ( | |
node: HTMLElement, | |
specifier: string, | |
exportName: string, | |
): void => { | |
const closest = node.parentElement?.closest("[data-islet-type=island]"); | |
if (closest) return; | |
const parseStyleStr = (styleStr: string): { [key: string]: string } => | |
styleStr | |
.split(";") | |
.map((style) => style.split(":").map((d) => d.trim())) | |
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); | |
const processAttributes = ( | |
attributes: NamedNodeMap, | |
): Record<string, unknown> => | |
Array.from(attributes).reduce( | |
(acc, { name, value }) => { | |
acc[name === "class" ? "className" : name] = name === "style" | |
? parseStyleStr(value) | |
: value; | |
return acc; | |
}, | |
{ key: Math.random() }, | |
); | |
const getType = async (node: HTMLElement) => | |
node.dataset?.isletType === "island" | |
? await import(window._ISLET[node.dataset.isletId].url).then( | |
(module) => | |
module[window._ISLET[node.dataset.isletId].exportName ?? "default"], | |
) | |
: null; | |
const toVirtual = async (h, node: Element | null): Promise<any> => { | |
if (node?.nodeType !== 1) return node?.textContent; | |
const tagName = node.tagName?.toLowerCase(); | |
const attributes = processAttributes(node.attributes ?? {}); | |
const children = node.childNodes.length > 0 | |
? await Promise.all( | |
[...node.childNodes].map((child) => toVirtual(h, child)), | |
) | |
: null; | |
const type = await getType(node); | |
if (!type) return h(tagName, attributes, children); | |
const islandProps = JSON.parse(window._ISLET[node.dataset.isletId].props); | |
islandProps.children = await toVirtual( | |
h, | |
node.querySelector("[data-islet-type]"), | |
); | |
return h(tagName, attributes, h(type, islandProps)); | |
}; | |
const mapAsync = (arr, fn) => Promise.all(arr.map(async (x) => await fn(x))); | |
const transformStaticNodeToVirtual = async (h, props) => | |
Object.fromEntries( | |
await mapAsync(Object.entries(props), async ([k, v]) => { | |
if (v?.specifier) { | |
const [importedV, propsV] = await Promise.all([ | |
import(v.specifier), | |
transformStaticNodeToVirtual(h, v.props), | |
]); | |
return [k, h(importedV[v.exportName], propsV)]; | |
} else if (v?.type) { | |
return [k, h(v.type, await transformStaticNodeToVirtual(h, v.props))]; | |
} else return [k, v]; | |
}), | |
); | |
const renderTask = () => | |
import(specifier).then(async (o: { h: typeof h; hydrate: HydrateFn }) => { | |
const { h, hydrate: rawHydrate, withFragment } = o; | |
const type = o[exportName]; | |
const hydrate = (a: unknown, b: unknown) => | |
rawHydrate.length === 2 ? rawHydrate(a, b) : rawHydrate(b, a); | |
const container = withFragment ? document.createDocumentFragment() : node; | |
const children = await toVirtual( | |
h, | |
node.querySelector("[data-islet-type]"), | |
); | |
const props = JSON.parse(window._ISLET[node.dataset.isletId].props); | |
props.children = children; | |
const resolvedProps = await transformStaticNodeToVirtual(h, props); | |
hydrate(h(type, resolvedProps), container); | |
}); | |
"scheduler" in globalThis | |
? globalThis.scheduler!.postTask(renderTask) | |
: setTimeout(renderTask, 0); | |
}; | |
const createIslandScript = (prefix: string, { url, exportName }: IslandDef) => { | |
const id = createIslandId(url); | |
return scripted(hydrate, `${prefix}/islands/${id}.js`, exportName); | |
}; | |
const transformVirtualNodeToStatic = (params, islands) => { | |
const newParams = deepApply( | |
params, | |
(key: string, value: unknown, obj: Record<string, unknown>) => { | |
const component = key === "type" && | |
islands.get(value?.type ?? value) && | |
typeof value === "function" | |
? islands.get(value?.type ?? value) | |
: null; | |
if (component) { | |
const id = createIslandId(component.url); | |
return { | |
...obj, | |
[key]: value, | |
specifier: `/islands/${id}.js`, | |
exportName: component.exportName, | |
}; | |
} | |
return key.startsWith("__") ? { ...obj, [key]: undefined } : obj; | |
}, | |
); | |
return newParams; | |
}; | |
const jsonStringifyWithBigIntSupport = (data: unknown) => { | |
if (data !== undefined) { | |
return JSON.stringify( | |
data, | |
(_, v) => typeof v === "bigint" ? `${v}#bigint` : v, | |
).replace(/"(-?\d+)#bigint"/g, (_, a) => a); | |
} | |
}; | |
export const createJsx = ({ | |
jsx, | |
h, | |
Fragment, | |
cloneElement, | |
prefix = "", | |
key: islandKey = "default", | |
}) => | |
( | |
type: Parameters<typeof jsx>[0], | |
params: Parameters<typeof jsx>[1], | |
key: Parameters<typeof jsx>[2], | |
...props | |
) => { | |
const islands = getIslands(islandKey); | |
const island = islands.get(type); | |
const isletData = !island ? null : { | |
url: `${prefix}/islands/${createIslandId(island.url)}.js`, | |
exportName: island.exportName, | |
props: jsonStringifyWithBigIntSupport({ | |
...transformVirtualNodeToStatic(params, islands), | |
children: undefined, | |
}), | |
}; | |
const isletId = island ? getHashSync(JSON.stringify(isletData)) : null; | |
if (island) { | |
storeFunctionExecution((isletId: string, isletData: unknown) => { | |
window._ISLET = Object.assign( | |
{ [isletId]: isletData }, | |
window._ISLET || {}, | |
); | |
}, ...[isletId, isletData]); | |
} | |
const className = island ? createIslandScript(prefix, island) : null; | |
const children = h(type, params, key, ...props); | |
const result = h(island ? "fragment" : Fragment, { | |
style: { display: "contents" }, | |
className, | |
...(island | |
? { "data-islet-type": "island", "data-islet-id": isletId } | |
: {}), | |
children: !island ? children : cloneElement(children, { | |
children: children.props.children | |
? [ | |
h("fragment", { | |
style: { display: "contents" }, | |
"data-islet-type": "slot", | |
children: children.props.children, | |
}), | |
] | |
: null, | |
}), | |
}); | |
return island ? result : result.props.children; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment