///
import { denoPlugins } from "https://deno.land/x/esbuild_deno_loader@0.8.2/mod.ts";
import * as esbuild from "https://deno.land/x/esbuild@v0.19.4/wasm.js";
import {
collectAndCleanScripts,
getHashSync,
scripted,
storeFunctionExecution,
} from "https://deno.land/x/scripted@0.0.3/mod.ts";
import { walk } from "https://deno.land/std@0.204.0/fs/walk.ts";
import {
dirname,
join,
relative,
toFileUrl,
} from "https://deno.land/std@0.204.0/path/mod.ts";
import { getIslands, IslandDef } from "https://deno.land/x/islet@0.0.22/client.ts";
interface Snapshot {
build_id: string;
files: Record;
}
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 = (fn: T, key = "default") =>
`/islands/${createIslandId(getIslands(key).get(fn)?.url!)}.js`;
export const config = {
routeOverride: "/islands/:id*",
};
function deepApply(data: T, applyFn): T {
function isObject(object: unknown): object is Record {
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).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 {
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[0]>;
openKv: () => ReturnType;
dev?: boolean;
}
const esbuildState = ((
done = false,
ongoingPromise: null | Promise = 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>;
interface SnapshotReader {
getPaths: () => string[];
read: (path: string) => Promise | 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();
const dependencies = new Map();
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(Object.entries(json.files));
const files = new Map();
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 => {
const scripts = collectAndCleanScripts();
const code = minify ? await transformScript(scripts) : scripts;
const script = ``;
if (html instanceof ReadableStream) {
return html.pipeThrough(new SuffixTransformStream(script));
}
return `${
html.replace(
html.includes("