Skip to content

Commit

Permalink
binding: native support for typescript
Browse files Browse the repository at this point in the history
  • Loading branch information
terrablue committed Jul 22, 2024
1 parent 9715d7b commit 059480b
Show file tree
Hide file tree
Showing 23 changed files with 260 additions and 57 deletions.
12 changes: 6 additions & 6 deletions packages/binding/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@
},
"type": "module",
"exports": {
".": {
"default": "./src/exports.js"
},
"./go": "./src/bindings/go/exports.js",
"./python": "./src/bindings/python/exports.js",
"./ruby": "./src/bindings/ruby/exports.js"
"./*/common": "./src/server/*/common.js",
"./errors/*": "./src/errors/*.js",
"./*": {
"runtime": "./src/server/*/runtime.js",
"default": "./src/server/*/default.js"
}
}
}
5 changes: 0 additions & 5 deletions packages/binding/src/bindings/common/exports.js

This file was deleted.

3 changes: 0 additions & 3 deletions packages/binding/src/bindings/common/peers.js

This file was deleted.

4 changes: 0 additions & 4 deletions packages/binding/src/bindings/exports.js

This file was deleted.

30 changes: 0 additions & 30 deletions packages/binding/src/bindings/typescript/module.js

This file was deleted.

6 changes: 0 additions & 6 deletions packages/binding/src/errors.js

This file was deleted.

1 change: 0 additions & 1 deletion packages/binding/src/exports.js

This file was deleted.

48 changes: 48 additions & 0 deletions packages/binding/src/server/python/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { dependencies, name } from "@primate/binding/python/common";
import depend from "@primate/core/depend";

const routes_re = /def (?<route>get|post|put|delete)/gu;
const get_routes = code => [...code.matchAll(routes_re)]
.map(({ groups: { route } }) => route);

const make_route = route => `async ${route.toLowerCase()}(request) {
const ${route}_fn = pyodide.globals.get("${route}");
return to_request(await ${route}_fn(to_request(pyodide.toPy, request)));
}`;

const make_package = pkg => `await pyodide.loadPackage("${pkg}", {
messageCallback: _ => _,
});\n`;

const js_wrapper = async (path, routes, packages) => `
import { File } from "rcompat/fs";
import to_request from "@primate/binding/python/to-request";
import to_response from "@primate/binding/python/to-response";
import wrap from "@primate/binding/python/wrap";
import { loadPyodide as load } from "pyodide";
const pyodide = await load({ indexURL: "./node_modules/pyodide" });
const file = await File.text(${JSON.stringify(path)});
${packages.map(make_package)}
pyodide.runPython(wrap(file));
export default {
${routes.map(route => make_route(route)).join(",\n")}
};
`;

export default ({ extension, packages }) => async (app, next) => {
await depend(dependencies, `primate:${name}`);

app.bind(extension, async (directory, file) => {
const path = directory.join(file);
const base = path.directory;
const js = path.base.concat(".js");
const code = await path.text();
const routes = get_routes(code);
// write .js wrapper
await base.join(js)
.write(await js_wrapper(`${path}`, routes, packages));
});

return next(app);
};
5 changes: 5 additions & 0 deletions packages/binding/src/server/python/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const name = "python";

export const default_extension = ".py";

export const dependencies = ["pyodide"];
7 changes: 7 additions & 0 deletions packages/binding/src/server/python/default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { default_extension, name } from "@primate/binding/python/common";
import build from "./build.js";

export default ({ extension = default_extension, packages = [] } = {}) => ({
name: `primate:${name}`,
build: build({ name, extension, packages }),
});
78 changes: 78 additions & 0 deletions packages/binding/src/server/python/make_request.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { unwrap, unwrap_async } from "./unwrap.js";

const dispatchers = ["path", "query", "headers", "cookies"];

const wrap_dispatchers = (toPy, request) =>
Object.fromEntries(dispatchers.map(dispatcher =>
[dispatcher, {
...request[dispatcher],
json() {
return toPy(request[dispatcher].json());
},
}]));

const wrap_store = (toPy, store) => {
return {
...store,
async validate(input) {
return toPy(await store.validate(unwrap_async(input)));
},
async get(value) {
return toPy(await store.get(unwrap_async(value)));
},
async count(criteria) {
return store.count(unwrap_async(criteria));
},
async find(criteria) {
return toPy(await store.find(unwrap_async(criteria)));
},
async exists(criteria) {
return store.exists(unwrap_async(criteria));
},
async insert(document) {
return toPy(await store.insert(unwrap_async(document)));
},
async update(criteria, document) {
const ua = unwrap_async;
return toPy(await store.update(ua(criteria), ua(document)));
},
async save(document) {
return toPy(await store.save(unwrap_async(document)));
},
async delete(criteria) {
return store.delete(unwrap_async(criteria));
},
schema: {
async create(description) {
return store.schema.create(unwrap_async(description));
},
async delete() {
return store.schema.delete();
},
},
};
};

const is_store = value => value.connection !== undefined;
const wrap_stores = (toPy, object) => Object.entries(object)
.reduce((reduced, [key, value]) => ({
...reduced,
[key]: is_store(value) ? wrap_store(toPy, value) : wrap_stores(toPy, value),
}), {});

const wrap_session = (toPy, { session }) => ({
...session,
create(data) {
session.create(unwrap(data));
},
json() {
return toPy(session.json());
},
});

export default (toPy, request) => ({
...request,
...wrap_dispatchers(toPy, request),
session: wrap_session(toPy, request),
store: wrap_stores(toPy, request.store),
});
31 changes: 31 additions & 0 deletions packages/binding/src/server/python/make_response.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import error from "@primate/core/handlers/error";
import redirect from "@primate/core/handlers/redirect";
import view from "@primate/core/handlers/view";

import { unwrap } from "./unwrap.js";

const handlers = {
view({ name, props = {}, options = {} }) {
return view(name, props, options);
},
redirect({ location, options }) {
return redirect(location, options);
},
error({ body, options }) {
return error(body, options);
},
};

const handle_handler = response => {
const { __handler__ : handler, ...args } = response;
return handlers[handler]?.(args) ?? error();
};

const handle_other = response => response;

export default raw_response => {
const response = unwrap(raw_response);
return response.__handler__ === undefined
? handle_other(response)
: handle_handler(response);
};
3 changes: 3 additions & 0 deletions packages/binding/src/server/python/runtime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { name } from "@primate/binding/python/common";

export default () => ({ name: `primate:${name}` });
17 changes: 17 additions & 0 deletions packages/binding/src/server/python/unwrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const dict_converter = value => Object.fromEntries(value);

const normalize = response =>
response instanceof Map ? Object.fromEntries(response.entries()) : response;
const qualify = (response, destroy = true) => {
if (response?.toJs !== undefined) {
const py_proxy = response;
const converted = py_proxy.toJs({ dict_converter });
destroy && py_proxy.destroy();
return converted;
}
return response;
};

export const unwrap = response => normalize(qualify(response));

export const unwrap_async = response => normalize(qualify(response, false));
5 changes: 5 additions & 0 deletions packages/binding/src/server/python/wrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { File } from "rcompat/fs";

const wrap = await new File(import.meta.url).directory.join("wrap.py").text();

export default code => `${wrap}\n${code}`;
23 changes: 23 additions & 0 deletions packages/binding/src/server/python/wrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class Primate():
@staticmethod
def view(name, props = None, options = None):
return {
"__handler__": "view",
"name": name,
"props": props,
"options": options,
}
@staticmethod
def redirect(location, options = None):
return {
"__handler__": "redirect",
"location": location,
"options": options,
}
@staticmethod
def error(body, options = None):
return {
"__handler__": "error",
"body": body,
"options": options,
}
18 changes: 18 additions & 0 deletions packages/binding/src/server/typescript/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { name } from "@primate/binding/typescript/common";
import { dim } from "rcompat/colors";
import compile from "./compile.js";

const module = `@primate:${name}`;

export default ({ extension }) => (app, next) => {
app.bind(extension, async (directory, file) => {
app.log.info(`compiling ${dim(file)} to JavaScript`, { module });

const path = directory.join(file);
const base = path.directory;
const js = path.base.concat(".js");
await base.join(js).write(await compile(await path.text()));
});

return next(app);
};
3 changes: 3 additions & 0 deletions packages/binding/src/server/typescript/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const name = "typescript";

export const default_extension = ".ts";
7 changes: 7 additions & 0 deletions packages/binding/src/server/typescript/default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { default_extension, name } from "@primate/binding/typescript/common";
import build from "./build.js";

export default ({ extension = default_extension } = {}) => ({
name: `@primate:${name}`,
build: build({ extension }),
});
3 changes: 3 additions & 0 deletions packages/binding/src/server/typescript/runtime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { name } from "@primate/binding/typescript/common";

export default () => ({ name: `@primate:${name}` });
4 changes: 4 additions & 0 deletions packages/core/src/build/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default async (log, root, config) => {
const error = await path.routes.join("+error.js");

return {
bindings: {},
roots: [],
targets: { web },
importmaps: {},
Expand Down Expand Up @@ -98,5 +99,8 @@ export default async (log, root, config) => {
target(name, handler) {
this.targets[name] = handler;
},
bind(extension, handler) {
this.bindings[extension] = handler;
},
};
};
4 changes: 2 additions & 2 deletions packages/core/src/build/hooks/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ const post = async (app, target) => {

const directory = app.runpath(location.routes);
for (const path of await directory.collect()) {
await app.extensions[path.extension]
?.route(directory, path.debase(`${directory}/`), types);
await app.bindings[path.extension]
?.(directory, path.debase(`${directory}/`), types);
}
// copy framework pages
await app.stage(defaults, location.pages, html);
Expand Down

0 comments on commit 059480b

Please sign in to comment.