Skip to content

Commit

Permalink
feat: construct function call (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu authored Feb 25, 2023
1 parent fecdee1 commit d791507
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 27 deletions.
19 changes: 19 additions & 0 deletions src/builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as recast from "recast";
import { proxifyFunctionCall } from "./proxy/function-call";
import { literalToAst } from "./proxy/_utils";
import { Proxified } from "./types";

const b = recast.types.builders;

export const builder = {
functionCall(callee: string, ...args: any[]): Proxified {
const node = b.callExpression(
b.identifier(callee),
args.map((i) => literalToAst(i) as any)
);
return proxifyFunctionCall(node as any);
},
literal(value: any): Proxified {
return literalToAst(value);
},
};
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./code";
export * from "./types";
export * from "./format";
export * from "./error";
export * from "./builder";
33 changes: 29 additions & 4 deletions src/proxy/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,32 @@ import { MagicastError } from "../error";
import type { ESNode } from "../types";
import { ProxyUtils, Proxified } from "./types";

export const LITERALS_AST = new Set([
"Literal",
"StringLiteral",
"NumericLiteral",
"BooleanLiteral",
"NullLiteral",
"RegExpLiteral",
"BigIntLiteral",
]);

export const LITERALS_TYPEOF = new Set([
"string",
"number",
"boolean",
"bigint",
"symbol",
"undefined",
]);

const b = recast.types.builders;

export function isValidPropName(name: string) {
return /^[$A-Z_a-z][\w$]*$/.test(name);
}

const PROXY_KEY = "__magicast_proxy";
export const PROXY_KEY = "__magicast_proxy";

export function literalToAst(value: any, seen = new Set()): ESNode {
if (value === undefined) {
Expand All @@ -19,10 +38,19 @@ export function literalToAst(value: any, seen = new Set()): ESNode {
// eslint-disable-next-line unicorn/no-null
return b.literal(null) as any;
}
if (LITERALS_TYPEOF.has(typeof value)) {
return b.literal(value) as any;
}
if (seen.has(value)) {
throw new MagicastError("Can not serialize circular reference");
}
seen.add(value);

// forward proxy
if (value[PROXY_KEY]) {
return value.$ast;
}

if (value instanceof Set) {
return b.newExpression(b.identifier("Set"), [
b.arrayExpression([...value].map((n) => literalToAst(n, seen)) as any),
Expand Down Expand Up @@ -51,9 +79,6 @@ export function literalToAst(value: any, seen = new Set()): ESNode {
) as any;
}
if (typeof value === "object") {
if (PROXY_KEY in value) {
return value.$ast;
}
return b.objectExpression(
Object.entries(value).map(([key, value]) => {
return b.property(
Expand Down
24 changes: 3 additions & 21 deletions src/proxy/proxify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,15 @@ import { proxifyArray } from "./array";
import { proxifyFunctionCall } from "./function-call";
import { proxifyObject } from "./object";
import { Proxified, ProxifiedModule } from "./types";

const _literalTypes = new Set([
"Literal",
"StringLiteral",
"NumericLiteral",
"BooleanLiteral",
"NullLiteral",
"RegExpLiteral",
"BigIntLiteral",
]);

const _literals = new Set([
"string",
"number",
"boolean",
"bigint",
"symbol",
"undefined",
]);
import { LITERALS_AST, LITERALS_TYPEOF } from "./_utils";

const _cache = new WeakMap<ESNode, Proxified<any>>();

export function proxify<T>(node: ESNode, mod?: ProxifiedModule): Proxified<T> {
if (_literals.has(typeof node)) {
if (LITERALS_TYPEOF.has(typeof node)) {
return node as any;
}
if (_literalTypes.has(node.type)) {
if (LITERALS_AST.has(node.type)) {
return (node as any).value as any;
}

Expand Down
29 changes: 29 additions & 0 deletions test/builder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, it } from "vitest";
import { builder, parseCode } from "../src";
import { generate } from "./_utils";

describe("builder", () => {
it("functionCall", () => {
const call = builder.functionCall("functionName", 1, "bar", { foo: "bar" });
expect(call.$type).toBe("function-call");
expect(call.$callee).toBe("functionName");
expect(call.$args).toMatchInlineSnapshot(`
[
1,
"bar",
{
"foo": "bar",
},
]
`);

const mod = parseCode("");
mod.exports.a = call;

expect(generate(mod)).toMatchInlineSnapshot(`
"export const a = functionName(1, \\"bar\\", {
foo: \\"bar\\",
});"
`);
});
});
62 changes: 61 additions & 1 deletion test/function-call.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { parseCode } from "../src";
import { builder, parseCode, ProxifiedModule } from "../src";
import { generate } from "./_utils";

describe("function-calls", () => {
Expand Down Expand Up @@ -38,4 +38,64 @@ describe("function-calls", () => {
});"
`);
});

it("construct function call", () => {
// eslint-disable-next-line unicorn/consistent-function-scoping
const installVuePlugin = (mod: ProxifiedModule<any>) => {
// Inject export default if not exists
if (!mod.exports.default) {
mod.imports.$add({
imported: "defineConfig",
from: "vite",
});
mod.exports.default = builder.functionCall("defineConfig", {});
}

// Get config object, if it's a function call, get the first argument
const config =
mod.exports.default.$type === "function-call"
? mod.exports.default.$args[0]
: mod.exports.default;

// Inject vue plugin import
mod.imports.$add({
imported: "default",
local: "vuePlugin",
from: "@vitejs/plugin-vue",
});

// Install vue plugin
config.plugins ||= [];
config.plugins.push(
builder.functionCall("vuePlugin", {
jsx: true,
})
);
};

const mod1 = parseCode(`
import { defineConfig } from 'vite'
export default defineConfig({})
`);
const mod2 = parseCode("");

installVuePlugin(mod1);
installVuePlugin(mod2);

expect(generate(mod1)).toMatchInlineSnapshot(`
"import vuePlugin from \\"@vitejs/plugin-vue\\";
import { defineConfig } from \\"vite\\";
export default defineConfig({
plugins: [
vuePlugin({
jsx: true,
}),
],
});"
`);

expect(generate(mod2)).toEqual(generate(mod1));
});
});
16 changes: 15 additions & 1 deletion test/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, it, expect } from "vitest";
import { print } from "recast";
import { literalToAst } from "../src/proxy/_utils";
import { literalToAst, PROXY_KEY } from "../src/proxy/_utils";
import { parseCode } from "../src";

describe("literalToAst", () => {
// eslint-disable-next-line unicorn/consistent-function-scoping
Expand Down Expand Up @@ -38,6 +39,19 @@ describe("literalToAst", () => {
);
});

it("forward proxy", () => {
const mod = parseCode(`export default { foo: 1 }`);
const node = mod.exports.default;

expect(node[PROXY_KEY]).toBeTruthy();
expect(node).toMatchInlineSnapshot(`
{
"foo": 1,
}
`);
expect(literalToAst(node)).toBe(node.$ast);
});

it("circular reference", () => {
const obj: any = {};
obj.foo = obj;
Expand Down

0 comments on commit d791507

Please sign in to comment.