Skip to content

Commit

Permalink
Islands hydrate themselves (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
sndrs authored Apr 28, 2023
2 parents e6de2ae + ff5963d commit 3aa8070
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 87 deletions.
14 changes: 8 additions & 6 deletions src/_site/components/GreenThenPink.island.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
<script>
import { onMount } from 'svelte'
let green = true
let pink = false
setTimeout(() => {
green = false
pink = true
}, 1000)
console.log('hello from GreenThenPink.island.svelte')
onMount(() => {
setTimeout(() => {
green = false
pink = true
}, 1000)
})
</script>

<p class:green class:pink>green then pink</p>
Expand Down
13 changes: 7 additions & 6 deletions src/_site/components/RedThenBlue.island.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
<script>
import { onMount } from 'svelte'
import GreenThenPink from './GreenThenPink.island.svelte'
let red = true
let blue = false
setTimeout(() => {
red = false
blue = true
}, 1000)
console.log('hello from RedThenBlue.island.svelte')
onMount(() => {
setTimeout(() => {
red = false
blue = true
}, 1000)
})
</script>

<p class:red class:blue>red then blue</p>
Expand Down
5 changes: 5 additions & 0 deletions src/_site/components/sub_dir/SubDir.island.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
import GreenThenPink from '../../components/GreenThenPink.island.svelte'
</script>

<GreenThenPink />
3 changes: 3 additions & 0 deletions src/_site/routes/nested-islands.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
<script>
import RedThenBlue from '../components/RedThenBlue.island.svelte'
import GreenThenPink from '../components/GreenThenPink.island.svelte'
import SubDir from '../components/sub_dir/SubDir.island.svelte'
</script>

Example of nested islands.

<RedThenBlue />
<GreenThenPink />
<GreenThenPink />
<SubDir />
8 changes: 4 additions & 4 deletions src/build.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as esbuild from "https://deno.land/x/[email protected]/mod.js";
import { svelte_components } from "./esbuild_plugins/svelte_components.ts";
import { svelte_internal } from "./esbuild_plugins/svelte_internal.ts";
import { build_routes } from "./esbuild_plugins/routes.ts";
import { build_routes } from "./esbuild_plugins/build_routes.ts";
import { ensureDir } from "https://deno.land/[email protected]/fs/ensure_dir.ts";
import { parse } from "https://deno.land/[email protected]/flags/mod.ts";
import { serve } from "https://deno.land/[email protected]/http/server.ts";
Expand Down Expand Up @@ -61,9 +61,9 @@ const routesESBuildConfig: esbuild.BuildOptions = {
entryPoints: await get_svelte_files({ dir: "routes/" }),
write: false,
plugins: [
svelte_components,
svelte_components(site_dir, base_path),
svelte_internal,
build_routes({ base_path }),
build_routes,
],
outdir: build_dir,
...baseESBuildConfig,
Expand All @@ -73,7 +73,7 @@ const islandsESBuildConfig: esbuild.BuildOptions = {
entryPoints: await get_svelte_files({ dir: "components/" }),
write: true,
plugins: [
svelte_components,
svelte_components(site_dir, base_path),
svelte_internal,
],
outdir: build_dir + "components/",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ interface SSROutput {
css?: { code: string };
}

export const build_routes = (
{ base_path }: { base_path: string },
): Plugin => ({
export const build_routes: Plugin = {
name: "mononykus/build-routes",
setup(build) {
build.onEnd(async (result) => {
Expand All @@ -29,14 +27,28 @@ export const build_routes = (
};

const { html, css: _css, head } = module.default.render();

// remove any duplicate module imports (in cases where a page uses an island more than once)
const modules = new Set();
const deduped_head = head.replace(
/<script[\s\S]*?<\/script>/g,
(module) => {
if (modules.has(module)) {
return "";
}
modules.add(module);
return module;
},
);

const css = _css?.code ?? "";

const dist_path = route.path.replace(".js", ".html");
await ensureDir(dirname(dist_path));

await Deno.writeTextFile(
dist_path,
get_route_html({ html, css, head, base_path }),
get_route_html({ html, css, head: deduped_head }),
);
}));

Expand All @@ -47,4 +59,4 @@ export const build_routes = (
);
});
},
});
};
53 changes: 3 additions & 50 deletions src/esbuild_plugins/get_route_html.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,19 @@
import { normalize } from "https://deno.land/[email protected]/path/mod.ts";
import { format } from "npm:prettier";

// dummy value to put the var name in scope
const component_path = "";

// this function is stringified inline in the page
// putting it here gives us type safety etc
export const hydrate_island = async (target: Element) => {
try {
const name = target.getAttribute("name");
const props = JSON.parse(target.getAttribute("props") ?? "{}");
const load = performance.now();

const Component =
(await import(component_path + name + ".island.js")).default;
console.group(name);
console.info(
`Loaded in %c${Math.round((performance.now() - load) * 1000) / 1000}ms`,
"color: orange",
);

const hydrate = performance.now();
new Component({ target, props, hydrate: true });
target.setAttribute("foraged", "");

console.info(
`Hydrated in %c${
Math.round((performance.now() - hydrate) * 1000) / 1000
}ms%c with`,
"color: orange",
"color: reset",
props,
);
console.groupEnd();
} catch (_) {
console.error(_);
}
};

interface TemplateOptions {
css: string;
head: string;
html: string;
hydrator: string;
}
const template = ({ css, head, html, hydrator }: TemplateOptions) => `

const template = ({ css, head, html }: TemplateOptions) => `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
${head}
<script type="module">${hydrator}</script>
<style>${css}</style>
</head>
<body>
Expand All @@ -61,23 +22,15 @@ const template = ({ css, head, html, hydrator }: TemplateOptions) => `
</html>
`;

const island_hydrator = (base = "") => `
const component_path = "${base}";
const hydrate_island = ${hydrate_island.toString()};
document.querySelectorAll("one-claw[name]:not(one-claw one-claw)").forEach(hydrate_island);
`;

export const get_route_html = ({ html, css, head, base_path }: {
export const get_route_html = ({ html, css, head }: {
html: string;
css: string;
head: string;
base_path?: string;
}) => {
const page = template({
css,
head,
html,
hydrator: island_hydrator(normalize(`/${base_path}/components/`)),
});

try {
Expand Down
87 changes: 71 additions & 16 deletions src/esbuild_plugins/svelte_components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,42 @@ import {
resolve,
} from "https://deno.land/[email protected]/path/mod.ts";
import type { Plugin } from "https://deno.land/x/[email protected]/mod.js";
import { normalize } from "https://deno.land/[email protected]/path/mod.ts";
import { compile } from "npm:svelte/compiler";
import type { ComponentType } from "npm:svelte";

const filter = /\.svelte$/;
const name = "mononykus/svelte";

/** force wrapping the actual component in a synthetic one */
const one_claw_synthetic = "?one-claw-synthetic";
const ssr_island = "?ssr_island";

const OneClaw = ({ path, name }: { path: string; name: string }) =>
const OneClaw = (
{ path, name, module_src }: {
path: string;
name: string;
module_src: string;
},
) =>
`<!-- synthetic component -->
<script>
import Island from "${path}";
</script>
<one-claw props={JSON.stringify($$props)} name="${name}">
<Island {...$$props} />
</one-claw>
<style>
one-claw { display: contents }
</style>`;
<script>
import Island from "${path}";
</script>
<svelte:head>
<script type="module" src="${module_src}"></script>
</svelte:head>
<one-claw props={JSON.stringify($$props)} name="${name}">
<Island {...$$props} />
</one-claw>
<style>
one-claw { display: contents }
</style>
`;

export const svelte_components: Plugin = {
export const svelte_components = (
site_dir: string,
base_path: string,
): Plugin => ({
name,
setup(build) {
const generate = build.initialOptions.write ? "dom" : "ssr";
Expand All @@ -50,7 +65,7 @@ export const svelte_components: Plugin = {
) {
return {
path: resolve(dirname(importer), path),
suffix: one_claw_synthetic,
suffix: ssr_island,
};
}
}
Expand All @@ -61,8 +76,14 @@ export const svelte_components: Plugin = {
.replace(/(\.island)?\.svelte$/, "")
.replaceAll(/(\.|\W)/g, "_");

const source = suffix === one_claw_synthetic
? OneClaw({ path, name })
const module_src = normalize("/" + base_path + path.split(site_dir)[1])
.replace(
/svelte$/,
"js",
);

const source = suffix === ssr_island
? OneClaw({ path, name, module_src })
: await Deno.readTextFile(path);

const { js: { code } } = compile(source, {
Expand All @@ -74,7 +95,41 @@ export const svelte_components: Plugin = {
filename: basename(path),
});

if (generate === "dom" && path.endsWith(".island.svelte")) {
const hydrator = (name: string, Component: ComponentType) => {
try {
document.querySelectorAll(
`one-claw[name='${name}']:not(one-claw one-claw)`,
).forEach((target) => {
const load = performance.now();
console.groupCollapsed(
`Hydrating %c${name}%c`,
"color: orange",
"color: reset",
);
console.log(target);
const props = JSON.parse(target.getAttribute("props") ?? "{}");
new Component({ target, props, hydrate: true });
console.log(
`Done in %c${
Math.round((performance.now() - load) * 1000) / 1000
}ms`,
"color: orange",
);
console.groupEnd();
});
} catch (_) {
console.error(_);
}
};

return ({
contents:
`${code};(${hydrator.toString()})("${name}", ${name}_island)`,
});
}

return ({ contents: code });
});
},
};
});

0 comments on commit 3aa8070

Please sign in to comment.