File
Blob: src/pyodide/internal/serializeJsModule.ts
| 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 */ |
| 9 | import { IS_CREATING_SNAPSHOT } from 'pyodide-internal:metadata'; |
| 10 | |
| 11 | export type SerializedJsModule = { |
| 12 | jsModule: true; |
| 13 | moduleName: string; |
| 14 | accessorList: string[]; |
| 15 | }; |
| 16 | |
| 17 | const importName = Symbol('importName'); |
| 18 | const getAccessorList = Symbol('getAccessorList'); |
| 19 | const getObject = Symbol('getObject'); |
| 20 | const getPrototypeOfKey = 'Reflect.getProtoTypeOf'; |
| 21 | |
| 22 | export 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 | |
| 38 | interface JsModules { |
| 39 | [a: string]: JsModules; |
| 40 | } |
| 41 | |
| 42 | export 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. |
| 64 | export 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 | } |