UNPKG

@ayonli/jsext

Version:

A JavaScript extension package for building strong and modern applications.

260 lines (232 loc) 8.47 kB
/** * Utility functions for working with JavaScript modules. * @module */ import { isBrowserWindow, isDedicatedWorker, isSharedWorker } from "./env.ts"; import { equals, extname } from "./path.ts"; import { getObjectURL } from "./module/util.ts"; /** * @deprecated There was some misunderstanding of this function in the past, it * should not be used in the user space anymore. */ const _getObjectURL = getObjectURL; export { _getObjectURL as getObjectURL }; /** * Performs interop on the given module. This functions is used to fix CommonJS * module imports in Node.js ES module files. * * By default, this function will check the module object for characteristics of * CommonJS modules and perform interoperability smartly. * * But sometimes, this behavior is not guaranteed, for example, when the `module` * is an ES module and it does have a default export as an object, or the * `module.exports` is the only export in the CommonJS file. In this case, * this function will be confused and may return an undesired object. * * To fix this, you can set the `strict` parameter to `true`, so this function * will only return the `exports` object when the module also has an `__esModule` * property, which is a common pattern generated by TypeScript for CommonJS * files. * * Or you can set the `strict` parameter to `false`, so this function will * always return the `default` object if it exists in `module`, which is the * target that Node.js uses to alias the `module.exports` object for CommonJS * modules. * * @example * ```ts * import { interop } from "@ayonli/jsext/module"; * * const { decode } = await interop(() => import("iconv-lite"), false); * ``` */ export function interop<T extends { [x: string]: any; }>( module: () => Promise<T>, strict?: boolean ): Promise<T>; /** * @example * ```ts * import { interop } from "@ayonli/jsext/module"; * * const { decode } = await interop(import("iconv-lite"), false); * ``` */ export function interop<T extends { [x: string]: any; }>( module: Promise<T>, strict?: boolean ): Promise<T>; /** * @example * ```ts * import { interop } from "@ayonli/jsext/module"; * * const { decode } = interop(await import("iconv-lite"), false); * ``` */ export function interop<T extends { [x: string]: any; }>(module: T, strict?: boolean): T; export function interop<T extends { [x: string]: any; }>( module: T | Promise<T> | (() => Promise<T>), strict: boolean | undefined = undefined ): T | Promise<T> { if (typeof module === "function") { return module().then(mod => interop(mod, strict)) as Promise<T>; } else if (module instanceof Promise) { return module.then(mod => interop(mod, strict)) as Promise<T>; } else if (typeof module === "object" && module !== null && !Array.isArray(module)) { if (typeof module["default"] === "object" && module["default"] !== null && !Array.isArray(module["default"]) ) { const hasEsModule = module["__esModule"] === true || module["default"]["__esModule"] === true; if (hasEsModule) { return module["default"]; } else if (strict) { return module; } const moduleKeys = Object.getOwnPropertyNames(module) .filter(x => x !== "default" && x !== "__esModule").sort(); const defaultKeys = Object.getOwnPropertyNames(module["default"]) .filter(x => x !== "default" && x !== "__esModule").sort(); if (String(moduleKeys) === String(defaultKeys)) { return module["default"]; } else if (strict === false && !moduleKeys.length) { return module["default"]; } } } return module; } /** * Checks if the current file is the entry of the program. * * @example * ```ts * import { isMain } from "@ayonli/jsext/module"; * * if (isMain(import.meta)) { * console.log("This is the main module."); * } * ``` */ export function isMain(importMeta: ImportMeta): boolean; /** * @example * ```ts * // CommonJS * const { isMain } = require("@ayonli/jsext/module"); * * if (isMain(module)) { * console.log("This is the main module."); * } * ``` */ export function isMain(module: NodeJS.Module): boolean; export function isMain(importMeta: ImportMeta | NodeJS.Module): boolean { if ("main" in importMeta && typeof importMeta["main"] === "boolean") { return importMeta["main"] as boolean; } if ("serviceWorker" in globalThis && "url" in importMeta) { // @ts-ignore return globalThis["serviceWorker"]["scriptURL"] === importMeta.url; } else if ((isDedicatedWorker || isSharedWorker) && "url" in importMeta && typeof location === "object" && location ) { return importMeta.url === location.href; } if (typeof process === "object" && Array.isArray(process.argv) && process.argv.length) { if (!process.argv[1]) { // Node.js REPL or the program is executed by `node -e "code"`, // or the program is executed by itself. return ["<repl>", "[eval]"].includes((importMeta as NodeJS.Module)["id"]); } const filename = "url" in importMeta ? importMeta.url : importMeta["filename"]; const urlExt = extname(filename); let entry = process.argv[1]!; if (!extname(entry) && urlExt) { // In Node.js, the extension name may be omitted when starting the script. entry += urlExt; } return equals(filename, entry, { ignoreFileProtocol: true }); } return false; } const importCache = new Map<string, Promise<void>>(); /** * Imports a script from the given URL to the current document, useful for * loading 3rd-party libraries dynamically in the browser. * * NOTE: This function is only available in the browser. * * @example * ```ts * import { importScript } from "@ayonli/jsext/module"; * * await importScript("https://code.jquery.com/jquery-3.7.1.min.js"); * * console.assert(typeof jQuery === "function"); * console.assert($ === jQuery); * ``` */ export function importScript(url: string, options: { type?: "classic" | "module"; } = {}): Promise<void> { if (!isBrowserWindow) { return Promise.reject(new Error("This function is only available in the browser.")); } url = new URL(url, location.href).href; let cache = importCache.get(url); if (cache) { return cache; } cache = new Promise<void>((resolve, reject) => { getObjectURL(url).then(_url => { const script = document.createElement("script"); script.src = _url; script.type = options.type === "module" ? "module" : "text/javascript"; script.setAttribute("data-src", url); script.onload = () => setTimeout(resolve, 0); script.onerror = () => reject(new Error(`Failed to load script: ${url}`)); document.head.appendChild(script); }).catch(reject); }); importCache.set(url, cache); return cache; } /** * Imports a stylesheet from the given URL to the current document, useful for * loading 3rd-party libraries dynamically in the browser. * * NOTE: This function is only available in the browser. * * @example * ```ts * import { importStylesheet } from "@ayonli/jsext/module"; * * await importStylesheet("https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.min.css"); * ``` */ export function importStylesheet(url: string): Promise<void> { if (!isBrowserWindow) { return Promise.reject(new Error("This function is only available in the browser.")); } url = new URL(url, location.href).href; let cache = importCache.get(url); if (cache) { return cache; } cache = new Promise<void>((resolve, reject) => { getObjectURL(url, "text/css").then(_url => { const link = document.createElement("link"); link.href = _url; link.rel = "stylesheet"; link.setAttribute("data-src", url); link.onload = () => setTimeout(resolve, 0); link.onerror = () => reject(new Error(`Failed to load stylesheet: ${url}`)); document.head.appendChild(link); }).catch(reject); }); importCache.set(url, cache); return cache; }