@ayonli/jsext
Version:
A JavaScript extension package for building strong and modern applications.
316 lines (275 loc) • 9.97 kB
text/typescript
/**
* The `module` module purified for the web.
* @module
*/
import { isBrowserWindow, isDedicatedWorker, isSharedWorker } from "../env.ts";
import { NotSupportedError, NetworkError } from "../error.ts";
import { equals, extname } from "../path.ts";
import { getObjectURL } from "./util.ts";
/**
* 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>;
}
let exports = module["module.exports"];
if (typeof exports !== "undefined") {
return exports as T;
} else if (isExportsObject(exports = module["default"])) {
const hasEsModule = module["__esModule"] === true
|| exports["__esModule"] === true;
if (hasEsModule) {
return exports as T;
} else if (strict) {
return module;
}
const exportNames = (x: string) => x !== "default" && x !== "__esModule";
const moduleKeys = Object.getOwnPropertyNames(module)
.filter(exportNames).sort();
const defaultKeys = Object.getOwnPropertyNames(exports)
.filter(exportNames).sort();
if (String(moduleKeys) === String(defaultKeys) ||
(strict === false && !moduleKeys.length)
) {
return exports as T;
}
}
return module;
}
function isExportsObject(module: unknown): module is { [x: string]: unknown; } {
return typeof module === "object" && module !== null && !Array.isArray(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"] ?? process.argv[1]);
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.
*
* This function uses cache to avoid loading the same module multiple times.
*
* 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 | URL, options: {
type?: "classic" | "module";
} = {}): Promise<void> {
if (!isBrowserWindow) {
return Promise.reject(
new NotSupportedError("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 NetworkError(`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.
*
* This function uses cache to avoid loading the same module multiple times.
*
* 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 | URL): Promise<void> {
if (!isBrowserWindow) {
return Promise.reject(
new NotSupportedError("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 NetworkError(`Failed to load stylesheet: ${url}`));
document.head.appendChild(link);
}).catch(reject);
});
importCache.set(url, cache);
return cache;
}
const wasmCache = new Map<string | WebAssembly.Module, Promise<WebAssembly.Exports>>();
export async function importWasm<T extends WebAssembly.Exports>(
module: string | URL | WebAssembly.Module,
imports: WebAssembly.Imports | undefined = undefined
): Promise<T> {
if (module instanceof WebAssembly.Module) {
let cache = wasmCache.get(module);
if (!cache) {
cache = WebAssembly.instantiate(module, imports).then(ins => ins.exports);
wasmCache.set(module, cache);
}
return await cache as T;
}
const url = typeof module === "string" ? module : module.href;
if (typeof WebAssembly.instantiateStreaming === "function") {
let cache = wasmCache.get(url);
if (!cache) {
cache = fetch(url)
.then(res => WebAssembly.instantiateStreaming(res, imports))
.then(ins => ins.instance.exports);
wasmCache.set(url, cache);
}
return await cache as T;
} else {
let cache = wasmCache.get(url);
if (!cache) {
cache = fetch(url)
.then(res => res.arrayBuffer())
.then(buf => WebAssembly.instantiate(buf, imports))
.then(ins => ins.instance.exports);
wasmCache.set(url, cache);
}
return await cache as T;
}
}