Skip to content

Commit

Permalink
react: add hydration
Browse files Browse the repository at this point in the history
  • Loading branch information
terrablue committed Aug 1, 2023
1 parent 32a3775 commit 46ec7cf
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 10 deletions.
4 changes: 3 additions & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
"dependencies": {
"@babel/core": "^7.22.9",
"@babel/preset-react": "^7.22.5",
"babel-plugin-react-require": "^4.0.1",
"babel-plugin-replace-import-extension": "^1.1.3",
"esact": "^0.18.2-p1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"runtime-compat": "^0.24.1"
"runtime-compat": "^0.25.0"
},
"peerDependencies": {
"primate": "0.21"
Expand Down
12 changes: 12 additions & 0 deletions packages/react/src/client/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default (component, props) => `
import {hydrateRoot} from "react-dom/client";
import React from "react";
import * as components from "app";
const data = JSON.parse(${JSON.stringify(JSON.stringify(props))});
const component = components.${component};
const is_class = component?.prototype instanceof React.Component;
const create = (MaybeClass, props) =>
is_class ? new MaybeClass(props) : MaybeClass(props);
hydrateRoot(document.body, create(component, data));
`;
1 change: 1 addition & 0 deletions packages/react/src/client/exports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {default as client} from "./client.js";
88 changes: 79 additions & 9 deletions packages/react/src/module.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,74 @@
import crypto from "runtime-compat/crypto";
import {Path} from "runtime-compat/fs";
import {Response, Status, MediaType} from "runtime-compat/http";
import ReactDOMServer from "react-dom/server";
import React from "react";
import babel from "@babel/core";
import {client} from "./client/exports.js";

const render = (component, attributes) =>
ReactDOMServer.renderToString(React.createElement(component, attributes));
// do not hard-depend on node
const packager = import.meta.runtime?.packager ?? "package.json";
const library = import.meta.runtime?.library ?? "node_modules";

const encoder = new TextEncoder();
const hash = async (string, algorithm = "sha-256") => {
const base = 16;
const target_pad_length = 2;
const target_slice = 12;
const bytes = await crypto.subtle.digest(algorithm, encoder.encode(string));
return Array.from(new Uint8Array(bytes))
.map(byte => byte.toString(base).padStart(target_pad_length, "0"))
.join("")
.slice(0, target_slice);
};
const normalize = async path => `react_${await hash(path)}`;

const render = (component, props) =>
ReactDOMServer.renderToString(React.createElement(component, props));

const import$ = async (module, submodule, importname, app) => {
const {config: {build: {modules}}, build: {paths}} = app;
const parts = module.split("/");
const path = [library, ...parts];
const pkg = await Path.resolve().join(...path, packager).json();
const {browser} = pkg.exports["."];
const dependency = Path.resolve().join(...path, browser[submodule]);
const to = paths.client.join(modules, submodule);
await to.file.create();
await dependency.file.copy(`${to.join("client.js")}`);
const client = new Path(`${to}`.replace(paths.client, _ => ""), "client.js");
app.importmaps[importname] = `${client}`;
};

const handler = (name, props = {}, {status = Status.OK} = {}) => async app => {
const {build, config} = app;
const target = build.paths.server.join(config.build.app).join(`${name}.js`);
const body = render((await import(target)).default, props);
const data = {data: props};
const body = render((await import(target)).default, data);
const component = await normalize(name);

const code = client(component, data);
const type = "module";

await app.publish({code, type, inline: true});

const headers$ = await app.headers();

return new Response(await app.render({body}), {
status,
headers: {...app.headers(), "Content-Type": MediaType.TEXT_HTML},
headers: {...headers$, "Content-Type": MediaType.TEXT_HTML},
});
};

const jsx = ".jsx";
const js = ".js";
const cwd = new Path(import.meta.url).directory.path;
const presets = ["@babel/preset-react"];
const plugins = [
["react-require"],
["replace-import-extension", {extMapping: {[jsx]: js}}],
];

export default _ => ({
name: "@primate/react",
register(app, next) {
Expand All @@ -28,11 +79,6 @@ export default _ => ({
const source = app.build.paths.components;
const target = app.build.paths.server.join(app.config.build.app);
await target.file.create();
const jsx = ".jsx";
const js = ".js";
const cwd = new Path(import.meta.url).directory.path;
const presets = ["@babel/preset-react"];
const plugins = [["replace-import-extension", {extMapping: {[jsx]: js}}]];
const components = await source.list(filename => filename.endsWith(jsx));
await Promise.all(components.map(async component => {
const file = await component.file.read();
Expand All @@ -43,4 +89,28 @@ export default _ => ({

return next(app);
},
async publish(app, next) {
const source = app.build.paths.components;
const target = app.build.paths.client.join(app.config.build.app);
await target.file.create();

// create client components
const components = await source.list(filename => filename.endsWith(jsx));
await Promise.all(components.map(async component => {
const name = component.path.replace(`${source}/`, "");
const file = await component.file.read();
const {code} = babel.transformSync(file, {cwd, presets, plugins});
const to = target.join(`${component.path}.js`.replace(source, ""));
await to.file.write(code);
const imported = await normalize(name);
app.bootstrap({
type: "script",
code: `export {default as ${imported}} from "./${name}.js";\n`,
});
}));

await import$("esact", "react", "react", app);
await import$("esact", "react-dom", "react-dom/client", app);
return next(app);
},
});

0 comments on commit 46ec7cf

Please sign in to comment.