@ayonli/jsext
Version:
A JavaScript extension package for building strong and modern applications.
260 lines (232 loc) • 8.47 kB
text/typescript
/**
* 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;
}