Skip to content
File

Blob: src/pyodide/internal/serializeJsModule.ts

typescript117 lines
1// Copyright (c) 2026 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5/* eslint-disable prefer-rest-params */
6/* eslint-disable @typescript-eslint/no-unsafe-member-access */
7/* eslint-disable @typescript-eslint/no-unsafe-argument */
8/* eslint-disable @typescript-eslint/no-unsafe-assignment */
9import { IS_CREATING_SNAPSHOT } from 'pyodide-internal:metadata';
10 
11export type SerializedJsModule = {
12 jsModule: true;
13 moduleName: string;
14 accessorList: string[];
15};
16 
17const importName = Symbol('importName');
18const getAccessorList = Symbol('getAccessorList');
19const getObject = Symbol('getObject');
20const getPrototypeOfKey = 'Reflect.getProtoTypeOf';
21 
22export function maybeSerializeJsModule(
23 obj_: any,
24 modules: Set<string>
25): SerializedJsModule | undefined {
26 const obj = obj_ as
27 | { [importName]: string; [getAccessorList]: string[] }
28 | undefined;
29 const moduleName = obj?.[importName];
30 if (!moduleName) {
31 return undefined;
32 }
33 modules.add(moduleName);
34 const accessorList: string[] = obj[getAccessorList];
35 return { jsModule: true, moduleName, accessorList };
36}
37 
38interface JsModules {
39 [a: string]: JsModules;
40}
41 
42export function deserializeJsModule(
43 obj: SerializedJsModule,
44 jsModules: JsModules
45): unknown {
46 const { accessorList, moduleName } = obj;
47 const result =
48 accessorList.reduce((x: JsModules, y: string): JsModules => {
49 if (y === getPrototypeOfKey) {
50 return Reflect.getPrototypeOf(x) as JsModules;
51 }
52 return x[y]!;
53 }, jsModules[moduleName]!) ?? null;
54 // Support stacked snapshots
55 return createImportProxy(moduleName, result, accessorList);
56}
57 
58// This tracks the information needed to "serialize" attributes of js modules. We need the name and
59// the sequence of attribute accesses. We store the name and accessorList under the importName and
60// getAccessorList symbols.
61//
62// If the receiver of a function call is an import proxy, this can cause the call to crash, so we
63// unwrap the receiver using the getObject symbol.
64export function createImportProxy<T>(
65 name: string,
66 mod: T,
67 accessorList: (string | symbol)[] = []
68): T {
69 if (!IS_CREATING_SNAPSHOT) {
70 return mod;
71 }
72 if (!mod || !['object', 'function'].includes(typeof mod)) {
73 return mod;
74 }
75 return new Proxy(mod, {
76 get(target: any, prop: string | symbol, _receiver): any {
77 if (prop === importName) {
78 return name;
79 }
80 if (prop === getAccessorList) {
81 return accessorList;
82 }
83 if (prop === getObject) {
84 return target;
85 }
86 // @ts-expect-error untyped Reflect.get
87 const orig = Reflect.get(...arguments);
88 const descr = Reflect.getOwnPropertyDescriptor(target, prop);
89 // We're required to return the original value unmodified if it's an own
90 // property with a non-writable, non-configurable data descriptor
91 if (descr && descr.writable === false && !descr.configurable) {
92 return orig;
93 }
94 // Or an accessor descriptor with a setter but no getter
95 if (descr && descr.set && !descr.get) {
96 return orig;
97 }
98 if (!['object', 'function'].includes(typeof orig)) {
99 return orig;
100 }
101 return createImportProxy(name, orig, [...accessorList, prop]);
102 },
103 apply(target: any, thisArg: any, argumentList: any[]): any {
104 // If thisArg is a GlobalsProxy it may break APIs that expect the receiver
105 // to be unmodified. Unwrap any GlobalsProxy before making the call.
106 thisArg = thisArg?.[getObject] ?? thisArg;
107 return Reflect.apply(target, thisArg, argumentList);
108 },
109 getPrototypeOf(target: object): any {
110 return createImportProxy(name, Reflect.getPrototypeOf(target), [
111 ...accessorList,
112 getPrototypeOfKey,
113 ]);
114 },
115 }) as T;
116}