Skip to content

Commit b54fdcf

Browse files
authored
feat: use proxy for top level module (#8)
* feat: able to manipulate exports * feat: add delete operation * chore: lint
1 parent 446f7ed commit b54fdcf

File tree

6 files changed

+177
-37
lines changed

6 files changed

+177
-37
lines changed

src/code.ts

+12-8
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,35 @@
11
import { promises as fsp } from "node:fs";
22
import { print, parse, Options as ParseOptions } from "recast";
3-
import { ModuleNode } from "./module";
43
import { getBabelParser } from "./babel";
5-
import { ESNode, ParsedFileNode } from "./types";
4+
import { ESNode, ParsedFileNode, ProxifiedModule } from "./types";
5+
import { proxifyModule } from "./proxy/module";
66

7-
export function parseCode(code: string, options?: ParseOptions): ModuleNode {
7+
export function parseCode<T = any>(
8+
code: string,
9+
options?: ParseOptions
10+
): ProxifiedModule<T> {
811
const node: ParsedFileNode = parse(code, {
912
parser: options?.parser || getBabelParser(),
1013
...options,
1114
});
12-
return new ModuleNode(node);
15+
return proxifyModule(node);
1316
}
1417

1518
export function generateCode(
16-
node: { ast: ESNode } | ESNode,
19+
node: { $ast: ESNode } | ESNode,
1720
options?: ParseOptions
1821
): { code: string; map?: any } {
19-
const { code, map } = print("ast" in node ? node.ast : node, {
22+
const ast = "$ast" in node ? node.$ast : node;
23+
const { code, map } = print(ast, {
2024
...options,
2125
});
2226
return { code, map };
2327
}
2428

25-
export async function loadFile(
29+
export async function loadFile<T = any>(
2630
filename: string,
2731
options: ParseOptions = {}
28-
): Promise<ModuleNode> {
32+
): Promise<ProxifiedModule<T>> {
2933
const contents = await fsp.readFile(filename, "utf8");
3034
options.sourceFileName = options.sourceFileName ?? filename;
3135
return parseCode(contents, options);

src/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
export * from "./code";
2-
export * from "./module";
32
export * from "./types";

src/module.ts

-28
This file was deleted.

src/proxy/module.ts

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import * as recast from "recast";
2+
import { ESNode, ParsedFileNode } from "../types";
3+
import { ProxifiedModule } from "./types";
4+
import { createProxy, literalToAst, proxify } from "./_utils";
5+
6+
export function proxifyModule<T>(ast: ParsedFileNode): ProxifiedModule<T> {
7+
const root = ast.program as ESNode;
8+
if (root.type !== "Program") {
9+
throw new Error(`Cannot proxify ${ast.type} as module`);
10+
}
11+
12+
const findExport = (key: string) => {
13+
const type =
14+
key === "default" ? "ExportDefaultDeclaration" : "ExportNamedDeclaration";
15+
16+
for (const n of root.body) {
17+
if (n.type === type) {
18+
if (key === "default") {
19+
return n.declaration;
20+
}
21+
if (n.declaration && "declarations" in n.declaration) {
22+
const dec = n.declaration.declarations[0];
23+
if ("name" in dec.id && dec.id.name === key) {
24+
return dec.init as any;
25+
}
26+
}
27+
}
28+
}
29+
};
30+
31+
const updateOrAddExport = (key: string, value: any) => {
32+
const type =
33+
key === "default" ? "ExportDefaultDeclaration" : "ExportNamedDeclaration";
34+
35+
const node = literalToAst(value) as any;
36+
for (const n of root.body) {
37+
if (n.type === type) {
38+
if (key === "default") {
39+
n.declaration = node;
40+
return;
41+
}
42+
if (n.declaration && "declarations" in n.declaration) {
43+
const dec = n.declaration.declarations[0];
44+
if ("name" in dec.id && dec.id.name === key) {
45+
dec.init = node;
46+
return;
47+
}
48+
}
49+
}
50+
}
51+
52+
root.body.push(
53+
key === "default"
54+
? recast.types.builders.exportDefaultDeclaration(node)
55+
: (recast.types.builders.exportNamedDeclaration(
56+
recast.types.builders.variableDeclaration("const", [
57+
recast.types.builders.variableDeclarator(
58+
recast.types.builders.identifier(key),
59+
node
60+
),
61+
])
62+
) as any)
63+
);
64+
};
65+
66+
const exportsProxy = createProxy(
67+
root,
68+
{},
69+
{
70+
get(_, prop) {
71+
const node = findExport(prop as string);
72+
if (node) {
73+
return proxify(node);
74+
}
75+
},
76+
set(_, prop, value) {
77+
updateOrAddExport(prop as string, value);
78+
return true;
79+
},
80+
deleteProperty(_, prop) {
81+
const type =
82+
prop === "default"
83+
? "ExportDefaultDeclaration"
84+
: "ExportNamedDeclaration";
85+
86+
for (let i = 0; i < root.body.length; i++) {
87+
const n = root.body[i];
88+
if (n.type === type) {
89+
if (prop === "default") {
90+
root.body.splice(i, 1);
91+
return true;
92+
}
93+
if (n.declaration && "declarations" in n.declaration) {
94+
const dec = n.declaration.declarations[0];
95+
if ("name" in dec.id && dec.id.name === prop) {
96+
root.body.splice(i, 1);
97+
return true;
98+
}
99+
}
100+
}
101+
}
102+
return false;
103+
},
104+
}
105+
);
106+
107+
return {
108+
$ast: root,
109+
$type: "module",
110+
exports: exportsProxy,
111+
get imports() {
112+
throw new Error("Not implemented");
113+
},
114+
} as any;
115+
}

src/proxy/types.ts

+5
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,8 @@ export type Proxified<T = any> = T extends
3535
[K in keyof T]: Proxified<T[K]>;
3636
} & ProxyUtils
3737
: T;
38+
39+
export interface ProxifiedModule<T = Record<string, unknown>> {
40+
exports: Proxified<T>;
41+
imports: unknown[];
42+
}

test/index.test.ts

+45
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,49 @@ describe("magicast", () => {
205205
}
206206
`);
207207
});
208+
209+
it("manipulate exports", () => {
210+
const mod = parseCode("");
211+
212+
expect(mod.exports).toMatchInlineSnapshot(`{}`);
213+
expect(generate(mod)).toMatchInlineSnapshot('""');
214+
215+
mod.exports.default = { foo: "1" };
216+
217+
expect(generate(mod)).toMatchInlineSnapshot(`
218+
"export default {
219+
foo: \\"1\\",
220+
};"
221+
`);
222+
223+
mod.exports.default.foo = 2;
224+
225+
expect(generate(mod)).toMatchInlineSnapshot(`
226+
"export default {
227+
foo: 2,
228+
};"
229+
`);
230+
231+
mod.exports.named ||= [];
232+
mod.exports.named.push("a");
233+
234+
expect(generate(mod)).toMatchInlineSnapshot(`
235+
"export default {
236+
foo: 2,
237+
};
238+
239+
export const named = [\\"a\\"];"
240+
`);
241+
242+
// delete
243+
delete mod.exports.default;
244+
245+
expect(generate(mod)).toMatchInlineSnapshot(
246+
'"export const named = [\\"a\\"];"'
247+
);
248+
249+
delete mod.exports.named;
250+
251+
expect(generate(mod)).toMatchInlineSnapshot('""');
252+
});
208253
});

0 commit comments

Comments
 (0)