Skip to content

Add support for creating body function at runtime #46

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/javascript/transpile.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import {expect, it} from "vitest";
import {transpile} from "./transpile.js";

it("transpiles JavaScript expressions", () => {
it("transpiles JavaScript expressions", async () => {
expect(transpile("1 + 2", "js")).toMatchSnapshot();
expect(transpile("1 + 2", "js", {concreteBody: true}).body()).toEqual(3);
expect(transpile("x + y", "js")).toMatchSnapshot();
expect(transpile("x + y", "js", {concreteBody: true}).body(3, 5)).toEqual(8);
expect(transpile("await z", "js")).toMatchSnapshot();
expect(await transpile("await z", "js", {concreteBody: true}).body(Promise.resolve(7))).toEqual(7);
expect(transpile("display(1), display(2)", "js")).toMatchSnapshot();
});

it("transpiles JavaScript programs", () => {
it("transpiles JavaScript programs", async () => {
expect(transpile("const x = 1, y = 2;", "js")).toMatchSnapshot();
expect(transpile("const x = 1, y = 2;", "js", {concreteBody: true}).body()).toEqual({x: 1, y: 2});
expect(transpile("x + y;", "js")).toMatchSnapshot();
expect(transpile("await z;", "js")).toMatchSnapshot();
expect(await transpile("await z", "js", {concreteBody: true}).body(Promise.resolve(7))).toEqual(7);
});

it("transpiles static npm: imports", () => {
Expand Down Expand Up @@ -41,6 +46,9 @@ it("transpiles Observable JavaScript imports", () => {

it("transpiles import.meta.resolve", () => {
expect(transpile('import.meta.resolve("npm:d3")', "js")).toMatchSnapshot();
expect(transpile('import.meta.resolve("npm:d3")', "js",{concreteBody: true}).body()).toEqual("https://cdn.jsdelivr.net/npm/d3/+esm");
expect(transpile('import.meta.resolve("./test")', "js", {resolveLocalImports: true})).toMatchSnapshot();
expect(transpile('import.meta.resolve("./test")', "js", {resolveLocalImports: false})).toMatchSnapshot();
expect(transpile('import.meta.resolve("./test")', "js",{resolveLocalImports: false, concreteBody: true}).body()).toEqual("./test");
});

56 changes: 52 additions & 4 deletions src/javascript/transpile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,18 @@ import {parseJavaScript} from "./parse.js";
import {Sourcemap} from "./sourcemap.js";
import {transpileTemplate} from "./template.js";

const FunctionConstructors = {
regular: Object.getPrototypeOf(function () { }).constructor,
async: Object.getPrototypeOf(async function () { }).constructor,
generator: Object.getPrototypeOf(function* () { }).constructor,
asyncGenerator: Object.getPrototypeOf(async function* () { }).constructor,
} as const;

type ConcreteFunction = (...args: unknown[]) => unknown;

export type TranspiledJavaScript = {
/** the source code of a JavaScript function defining the primary variable */
body: string;
/** the source code of a JavaScript function defining the primary variable, or the function itself when concreteBody is true */
body: string | ConcreteFunction;
/** any unbound references in body; corresponds to the body arguments, in order */
inputs?: string[];
/** if present, the body returns an object of named outputs; alternative to output */
Expand All @@ -28,8 +37,21 @@ export type TranspileOptions = {
resolveLocalImports?: boolean;
/** If true, resolve file using import.meta.url (so Vite treats it as an asset). */
resolveFiles?: boolean;
/** If true, creates a function instance rather than a string for the body. */
concreteBody?: boolean;
};

// Overloads for better typing of body depending on concreteBody
export function transpile(
input: string,
mode: Cell["mode"],
options: TranspileOptions & { concreteBody: true }
): Omit<TranspiledJavaScript, "body"> & { body: ConcreteFunction };
export function transpile(
input: string,
mode: Cell["mode"],
options?: TranspileOptions
): Omit<TranspiledJavaScript, "body"> & { body: string };
export function transpile(
input: string,
mode: Cell["mode"],
Expand All @@ -47,6 +69,29 @@ function transpileMode(input: string, mode: Exclude<Cell["mode"], "ojs">): strin
return transpileTemplate(input, tag, raw);
}

function constructFunction(bodyStr: string) {
const { body } = parseJavaScript(bodyStr);
if (body.type !== "FunctionExpression" && body.type !== "ArrowFunctionExpression") {
throw new Error(`Unsupported function type: ${body.type}`);
}

const func = body.async && body.generator ?
FunctionConstructors.asyncGenerator :
body.async ?
FunctionConstructors.async :
body.generator ?
FunctionConstructors.generator :
FunctionConstructors.regular;

const params = body.params?.map((param) => bodyStr.slice(param.start, param.end)).join(", ") ?? "";
const isBlock = body.body.type === "BlockStatement";
const { start, end } = body.body;
const inner = isBlock
? bodyStr.slice(start + 1, end - 1)
: `return ${bodyStr.slice(start, end)}`;
return func(params, inner);
}

export function transpileJavaScript(
input: string,
options?: TranspileOptions
Expand All @@ -65,7 +110,10 @@ export function transpileJavaScript(
if (outputs.length > 0) output.insertRight(input.length, `\nreturn {${outputs}};`);
if (cell.expression) output.insertRight(input.length, `\n)`);
output.insertRight(input.length, "\n}");
const body = String(output);
let body: string | ConcreteFunction = String(output);
const autodisplay = cell.expression && !(inputs.includes("display") || inputs.includes("view"));
return {body, inputs, outputs, autodisplay};
if (options?.concreteBody) {
body = constructFunction(body as string);
}
return { body, inputs, outputs, autodisplay };
}