| import module from "node:module"; | ||
| import process from "node:process"; | ||
| import path from "node:path"; | ||
| import remapping from "@jridgewell/remapping"; | ||
| import picomatch from "picomatch"; | ||
| import { loadConfigSync } from "unconfig"; | ||
| import { Buffer } from "node:buffer"; | ||
| import { fileURLToPath, pathToFileURL } from "node:url"; | ||
| import { createDebug } from "obug"; | ||
| //#region src/plugin.ts | ||
| function normalizePluginHook(plugin, key) { | ||
| const hook = plugin?.[key]; | ||
| if (!hook) return {}; | ||
| if (typeof hook === "function") return { handler: hook }; | ||
| return hook; | ||
| } | ||
| //#endregion | ||
| //#region node_modules/.pnpm/@antfu+utils@9.3.0/node_modules/@antfu/utils/dist/index.mjs | ||
| function toArray(array) { | ||
| array = array ?? []; | ||
| return Array.isArray(array) ? array : [array]; | ||
| } | ||
| //#endregion | ||
| //#region src/plugin-filter.ts | ||
| function getMatcherString(glob, cwd) { | ||
| if (glob.startsWith("**") || path.isAbsolute(glob)) return path.normalize(glob); | ||
| const resolved = path.resolve(cwd, glob); | ||
| return path.normalize(resolved); | ||
| } | ||
| function patternToIdFilter(pattern) { | ||
| if (pattern instanceof RegExp) return (id) => { | ||
| const normalizedId = path.normalize(id); | ||
| const result = pattern.test(normalizedId); | ||
| pattern.lastIndex = 0; | ||
| return result; | ||
| }; | ||
| const matcher = picomatch(getMatcherString(pattern, process.cwd()), { dot: true }); | ||
| return (id) => { | ||
| return matcher(path.normalize(id)); | ||
| }; | ||
| } | ||
| function patternToCodeFilter(pattern) { | ||
| if (pattern instanceof RegExp) return (code) => { | ||
| const result = pattern.test(code); | ||
| pattern.lastIndex = 0; | ||
| return result; | ||
| }; | ||
| return (code) => code.includes(pattern); | ||
| } | ||
| function createFilter(exclude, include) { | ||
| if (!exclude && !include) return; | ||
| return (input) => { | ||
| if (exclude?.some((filter) => filter(input))) return false; | ||
| if (include?.some((filter) => filter(input))) return true; | ||
| return !include || include.length <= 0; | ||
| }; | ||
| } | ||
| function normalizeFilter(filter) { | ||
| if (typeof filter === "string" || filter instanceof RegExp) return { include: [filter] }; | ||
| if (Array.isArray(filter)) return { include: filter }; | ||
| return { | ||
| exclude: filter.exclude ? toArray(filter.exclude) : void 0, | ||
| include: filter.include ? toArray(filter.include) : void 0 | ||
| }; | ||
| } | ||
| function createIdFilter(filter) { | ||
| if (!filter) return; | ||
| const { exclude, include } = normalizeFilter(filter); | ||
| const excludeFilter = exclude?.map(patternToIdFilter); | ||
| const includeFilter = include?.map(patternToIdFilter); | ||
| return createFilter(excludeFilter, includeFilter); | ||
| } | ||
| function createCodeFilter(filter) { | ||
| if (!filter) return; | ||
| const { exclude, include } = normalizeFilter(filter); | ||
| const excludeFilter = exclude?.map(patternToCodeFilter); | ||
| const includeFilter = include?.map(patternToCodeFilter); | ||
| return createFilter(excludeFilter, includeFilter); | ||
| } | ||
| function createFilterForId(filter) { | ||
| const filterFunction = createIdFilter(filter); | ||
| return filterFunction ? (id) => !!filterFunction(id) : void 0; | ||
| } | ||
| function createFilterForTransform(idFilter, codeFilter) { | ||
| if (!idFilter && !codeFilter) return; | ||
| const idFilterFunction = createIdFilter(idFilter); | ||
| const codeFilterFunction = createCodeFilter(codeFilter); | ||
| return (id, code) => { | ||
| let fallback = true; | ||
| if (idFilterFunction) fallback &&= idFilterFunction(id); | ||
| if (!fallback) return false; | ||
| if (codeFilterFunction) fallback &&= codeFilterFunction(code); | ||
| return fallback; | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region src/utils/config.ts | ||
| function loadConfig() { | ||
| const { config } = loadConfigSync({ | ||
| sources: [{ | ||
| files: "unloader.config", | ||
| extensions: [ | ||
| "ts", | ||
| "mts", | ||
| "cts", | ||
| "js", | ||
| "mjs", | ||
| "cjs", | ||
| "json", | ||
| "" | ||
| ] | ||
| }], | ||
| cwd: process.cwd(), | ||
| defaults: {} | ||
| }); | ||
| return config; | ||
| } | ||
| //#endregion | ||
| //#region src/utils/map.ts | ||
| function attachSourceMap(map, code) { | ||
| if (map) { | ||
| if (!map.sourcesContent || map.sourcesContent.length === 0) map.sourcesContent = [code]; | ||
| code += `\n//# sourceMappingURL=${toUrl(map)}`; | ||
| } | ||
| return code; | ||
| } | ||
| function toUrl(map) { | ||
| return `data:application/json;charset=utf-8;base64,${Buffer.from(map.toString()).toString("base64")}`; | ||
| } | ||
| //#endregion | ||
| //#region src/utils/url.ts | ||
| function urlToPath(url) { | ||
| if (!url) return url; | ||
| return url.startsWith("file://") ? fileURLToPath(url) : url; | ||
| } | ||
| function pathToUrl(isRequire, path) { | ||
| if (isRequire || path.startsWith("file://") || path.startsWith("data://") || path.startsWith("node:")) return path; | ||
| return pathToFileURL(path).href; | ||
| } | ||
| //#endregion | ||
| //#region src/hooks.ts | ||
| function createHooks() { | ||
| let config; | ||
| let deactivated = false; | ||
| let pluginContext; | ||
| function init(context, inlineConfig) { | ||
| pluginContext = context; | ||
| config = inlineConfig || loadConfig(); | ||
| for (const plugin of config.plugins || []) { | ||
| const { handler } = normalizePluginHook(plugin, "options"); | ||
| const result = handler?.call(context, config); | ||
| assertSync(result, plugin.name, "options"); | ||
| config = result || config; | ||
| } | ||
| for (const plugin of config.plugins || []) { | ||
| const { handler } = normalizePluginHook(plugin, "buildStart"); | ||
| const result = handler?.call(context); | ||
| assertSync(result, plugin.name, "buildStart"); | ||
| context.debug(`loaded plugin: %s`, plugin.name); | ||
| } | ||
| return config; | ||
| } | ||
| function resolve(specifier, context, nextResolve) { | ||
| if (deactivated) return nextResolve(specifier, context); | ||
| const isRequire = !Array.isArray(context.conditions) || context.conditions[0] === "require"; | ||
| pluginContext?.debug(`resolving %s with context %o`, specifier, context); | ||
| if (config?.plugins && pluginContext) for (const plugin of config.plugins) { | ||
| const resolveFn = createResolve(nextResolve, isRequire, pluginContext.debug); | ||
| const { handler, filter } = normalizePluginHook(plugin, "resolveId"); | ||
| const filterFn = createFilterForId(filter?.id); | ||
| const id = urlToPath(specifier); | ||
| if (filterFn && !filterFn(id)) continue; | ||
| const result = handler?.call({ | ||
| resolve: resolveFn, | ||
| ...pluginContext | ||
| }, id, urlToPath(context.parentURL), { | ||
| conditions: context.conditions, | ||
| attributes: context.importAttributes | ||
| }); | ||
| assertSync(result, plugin.name, "resolveId"); | ||
| if (result) { | ||
| let output; | ||
| if (typeof result === "string") output = { | ||
| url: pathToUrl(isRequire, result), | ||
| importAttributes: context.importAttributes, | ||
| shortCircuit: true | ||
| }; | ||
| else output = { | ||
| url: pathToUrl(isRequire, result.id), | ||
| format: result.format, | ||
| importAttributes: result.attributes || context.importAttributes, | ||
| shortCircuit: true | ||
| }; | ||
| pluginContext.debug(`resolved %s to %s with format %s`, specifier, output.url, output.format); | ||
| return output; | ||
| } | ||
| } | ||
| return nextResolve(specifier, context); | ||
| } | ||
| function load(url, context, nextLoad) { | ||
| if (deactivated || !config?.plugins) return nextLoad(url, context); | ||
| pluginContext?.debug(`load %s with context %o`, url, context); | ||
| let result; | ||
| const defaultFormat = context.format || "module"; | ||
| const maps = []; | ||
| for (const plugin of config.plugins) { | ||
| const { handler, filter } = normalizePluginHook(plugin, "load"); | ||
| const filterFn = createFilterForId(filter?.id); | ||
| const id = urlToPath(url); | ||
| if (filterFn && !filterFn(id)) continue; | ||
| const loadResult = handler?.call(pluginContext, id, { | ||
| format: context.format, | ||
| conditions: context.conditions, | ||
| attributes: context.importAttributes | ||
| }); | ||
| assertSync(loadResult, plugin.name, "load"); | ||
| if (loadResult) { | ||
| if (isModuleSource(loadResult)) result = { | ||
| source: loadResult, | ||
| format: defaultFormat, | ||
| shortCircuit: true | ||
| }; | ||
| else { | ||
| if (loadResult.map) maps.unshift(loadResult.map); | ||
| result = { | ||
| source: loadResult.code, | ||
| format: loadResult.format || defaultFormat, | ||
| shortCircuit: true | ||
| }; | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| result ||= nextLoad(url, context); | ||
| for (const plugin of config.plugins) { | ||
| const { handler, filter } = normalizePluginHook(plugin, "transform"); | ||
| const filterFn = createFilterForTransform(filter?.id, filter?.code); | ||
| const code = result.source || ""; | ||
| const id = urlToPath(url); | ||
| if (filterFn && (typeof code !== "string" || !filterFn(id, code || ""))) continue; | ||
| const transformResult = handler?.call(pluginContext, code, id, { | ||
| format: result.format, | ||
| conditions: context.conditions, | ||
| attributes: context.importAttributes | ||
| }); | ||
| assertSync(transformResult, plugin.name, "transform"); | ||
| if (transformResult) if (isModuleSource(transformResult)) result = { | ||
| ...result, | ||
| source: transformResult | ||
| }; | ||
| else { | ||
| if (transformResult.map) maps.unshift(transformResult.map); | ||
| result = { | ||
| ...result, | ||
| source: transformResult.code, | ||
| format: transformResult.format || result.format | ||
| }; | ||
| } | ||
| } | ||
| if (maps.length && typeof result.source === "string") { | ||
| const code = attachSourceMap(remapping(maps, () => null), result.source); | ||
| result.source = code; | ||
| } | ||
| return result; | ||
| } | ||
| const deactivate = () => { | ||
| deactivated = true; | ||
| }; | ||
| return { | ||
| init, | ||
| resolve, | ||
| load, | ||
| deactivate | ||
| }; | ||
| } | ||
| function createResolve(nextResolve, isRequire, debug) { | ||
| return (source, importer, options) => { | ||
| if (!path.isAbsolute(source) && importer) source = path.resolve(importer, "..", source); | ||
| try { | ||
| const resolved = nextResolve(pathToUrl(isRequire, source), { | ||
| parentURL: importer, | ||
| conditions: options?.conditions, | ||
| importAttributes: options?.attributes | ||
| }); | ||
| debug("resolved %s to %s with format %s", source, resolved.url, resolved.format); | ||
| return { | ||
| id: urlToPath(resolved.url), | ||
| attributes: resolved.importAttributes, | ||
| format: resolved.format | ||
| }; | ||
| } catch (error) { | ||
| debug("error resolving %s: %o", source, error); | ||
| return null; | ||
| } | ||
| }; | ||
| } | ||
| function isModuleSource(v) { | ||
| return typeof v === "string" || ArrayBuffer.isView(v) || v instanceof ArrayBuffer; | ||
| } | ||
| function assertSync(value, plugin, hook) { | ||
| if (value != null && typeof value === "object" && typeof value.then === "function") throw new Error(`Plugin "${plugin}" returned a Promise from "${hook}" hook. unloader only supports synchronous hooks.`); | ||
| } | ||
| //#endregion | ||
| //#region src/utils/context.ts | ||
| const sharedPluginContext = { | ||
| error: (message) => { | ||
| throw typeof message === "string" ? new Error(message) : message; | ||
| }, | ||
| meta: { unloaderVersion: "0.9.0" } | ||
| }; | ||
| //#endregion | ||
| //#region src/utils/debug.ts | ||
| const debug = createDebug("unloader"); | ||
| //#endregion | ||
| //#region src/api.ts | ||
| function register(inlineConfig) { | ||
| const registerHooks = module.registerHooks; | ||
| if (!registerHooks) throw new Error(`This version of Node.js (${process.version}) does not support module.registerHooks(). Please upgrade to Node v22.15 or v23.5 and above.`); | ||
| const { init, resolve, load, deactivate } = createHooks(); | ||
| if (init({ | ||
| ...sharedPluginContext, | ||
| log: (message) => console.info(message), | ||
| debug | ||
| }, inlineConfig).sourcemap && !process.sourceMapsEnabled) process.setSourceMapsEnabled(true); | ||
| registerHooks({ | ||
| resolve, | ||
| load | ||
| }); | ||
| return deactivate; | ||
| } | ||
| //#endregion | ||
| export { loadConfig as n, normalizePluginHook as r, register as t }; |
+31
-31
| import { ImportAttributes, ModuleFormat, ModuleSource } from "node:module"; | ||
| import { QuansyncAwaitableGenerator } from "quansync"; | ||
| import { QuansyncFn } from "quansync/macro"; | ||
| import { Arrayable } from "@antfu/utils"; | ||
| import { MessagePort, Transferable } from "node:worker_threads"; | ||
| //#region node_modules/.pnpm/@antfu+utils@9.3.0/node_modules/@antfu/utils/dist/index.d.mts | ||
| /** | ||
| * Array, or not yet | ||
| */ | ||
| type Arrayable<T> = T | Array<T>; | ||
| /** | ||
| * Function | ||
| */ | ||
| //#endregion | ||
| //#region src/plugin-filter.d.ts | ||
@@ -15,3 +20,2 @@ type StringOrRegExp = string | RegExp; | ||
| //#region src/plugin.d.ts | ||
| type Awaitable<T> = T | Promise<T>; | ||
| type FalsyValue = null | undefined | false | void; | ||
@@ -48,4 +52,3 @@ interface ResolveMeta { | ||
| interface PluginContext { | ||
| port?: MessagePort; | ||
| log: (message: any, transferList?: Transferable[]) => void; | ||
| log: (message: any) => void; | ||
| debug: (...args: any[]) => void; | ||
@@ -57,5 +60,4 @@ error: (message: string | Error) => never; | ||
| } | ||
| type ConditionalAwaitable<C, T> = (C extends true ? T : Awaitable<T>) | QuansyncAwaitableGenerator<T>; | ||
| type ResolveFn<Sync = false> = (source: string, importer?: string, options?: ResolveMeta) => ConditionalAwaitable<Sync, ResolvedId | null>; | ||
| interface Plugin<Sync = false> extends Partial<PluginHooks<Sync>> { | ||
| type ResolveFn = (source: string, importer?: string, options?: ResolveMeta) => ResolvedId | null; | ||
| interface Plugin extends Partial<PluginHooks> { | ||
| name: string; | ||
@@ -67,7 +69,7 @@ } | ||
| } | ||
| type HookFilterExtension<K$1 extends keyof FunctionPluginHooks<false>> = K$1 extends "transform" ? { | ||
| type HookFilterExtension<K extends keyof FunctionPluginHooks> = K extends "transform" ? { | ||
| filter?: HookFilter | undefined; | ||
| } : K$1 extends "load" ? { | ||
| } : K extends "load" ? { | ||
| filter?: Pick<HookFilter, "id"> | undefined; | ||
| } : K$1 extends "resolveId" ? { | ||
| } : K extends "resolveId" ? { | ||
| filter?: { | ||
@@ -77,14 +79,14 @@ id?: StringFilter<RegExp> | undefined; | ||
| } | undefined : {}; | ||
| interface FunctionPluginHooks<Sync> { | ||
| options: (this: PluginContext, config: UnloaderConfig<Sync>) => UnloaderConfig<Sync> | FalsyValue; | ||
| buildStart: (this: PluginContext) => ConditionalAwaitable<Sync, void>; | ||
| interface FunctionPluginHooks { | ||
| options: (this: PluginContext, config: UnloaderConfig) => UnloaderConfig | FalsyValue; | ||
| buildStart: (this: PluginContext) => void; | ||
| resolveId: (this: PluginContext & { | ||
| resolve: ResolveFn<Sync>; | ||
| }, source: string, importer: string | undefined, options: ResolveMeta) => ConditionalAwaitable<Sync, string | ResolvedId | FalsyValue>; | ||
| resolve: ResolveFn; | ||
| }, source: string, importer: string | undefined, options: ResolveMeta) => string | ResolvedId | FalsyValue; | ||
| load: (this: PluginContext, id: string, options: ResolveMeta & { | ||
| format: ModuleFormat | (string & {}) | null | undefined; | ||
| }) => ConditionalAwaitable<Sync, ModuleSource | LoadResult | FalsyValue>; | ||
| }) => ModuleSource | LoadResult | FalsyValue; | ||
| transform: (this: PluginContext, code: ModuleSource, id: string, options: ResolveMeta & { | ||
| format: string | null | undefined; | ||
| }) => ConditionalAwaitable<Sync, ModuleSource | LoadResult | FalsyValue>; | ||
| }) => ModuleSource | LoadResult | FalsyValue; | ||
| } | ||
@@ -94,22 +96,20 @@ type ObjectHook<T, O = {}> = T | ({ | ||
| } & O); | ||
| type PluginHooks<Sync> = { [K in keyof FunctionPluginHooks<Sync>]: ObjectHook<FunctionPluginHooks<Sync>[K], HookFilterExtension<K>> }; | ||
| declare function normalizePluginHook<K$1 extends keyof PluginHooks<false>>(plugin: Plugin<false>, key: K$1): Partial<{ | ||
| handler?: FunctionPluginHooks<false>[K$1]; | ||
| } & HookFilterExtension<K$1>>; | ||
| type PluginHooks = { [K in keyof FunctionPluginHooks]: ObjectHook<FunctionPluginHooks[K], HookFilterExtension<K>> }; | ||
| declare function normalizePluginHook<K extends keyof PluginHooks>(plugin: Plugin, key: K): Partial<{ | ||
| handler?: FunctionPluginHooks[K]; | ||
| } & HookFilterExtension<K>>; | ||
| //#endregion | ||
| //#region src/utils/config.d.ts | ||
| interface UnloaderConfig<Sync = false> { | ||
| interface UnloaderConfig { | ||
| sourcemap?: boolean; | ||
| plugins?: Plugin<Sync>[]; | ||
| plugins?: Plugin[]; | ||
| } | ||
| declare const loadConfig: QuansyncFn<UnloaderConfig, []>; | ||
| declare function loadConfig(): UnloaderConfig; | ||
| //#endregion | ||
| //#region src/api.d.ts | ||
| declare function register(inlineConfig?: string): Promise<() => Promise<void>>; | ||
| declare function registerSync(inlineConfig?: UnloaderConfig<true>): () => void; | ||
| declare function register(inlineConfig?: UnloaderConfig): () => void; | ||
| //#endregion | ||
| //#region src/index.d.ts | ||
| declare function defineConfig(config: UnloaderConfig): UnloaderConfig; | ||
| declare function defineSyncConfig(config: UnloaderConfig<true>): UnloaderConfig<true>; | ||
| //#endregion | ||
| export { Awaitable, ConditionalAwaitable, FalsyValue, FunctionPluginHooks, HookFilter, HookFilterExtension, LoadResult, ObjectHook, Plugin, PluginContext, PluginHooks, ResolveFn, ResolveMeta, ResolvedId, UnloaderConfig, defineConfig, defineSyncConfig, loadConfig, normalizePluginHook, register, registerSync }; | ||
| export { FalsyValue, FunctionPluginHooks, HookFilter, HookFilterExtension, LoadResult, ObjectHook, Plugin, PluginContext, PluginHooks, ResolveFn, ResolveMeta, ResolvedId, UnloaderConfig, defineConfig, loadConfig, normalizePluginHook, register }; |
+2
-8
@@ -1,4 +0,2 @@ | ||
| import { i as normalizePluginHook, r as loadConfig } from "./context-DRCo_iRa.mjs"; | ||
| import { n as registerSync, t as register } from "./api-CXxT_bvN.mjs"; | ||
| import { n as loadConfig, r as normalizePluginHook, t as register } from "./api-B_siCmJt.mjs"; | ||
| //#region src/index.ts | ||
@@ -8,7 +6,3 @@ function defineConfig(config) { | ||
| } | ||
| function defineSyncConfig(config) { | ||
| return config; | ||
| } | ||
| //#endregion | ||
| export { defineConfig, defineSyncConfig, loadConfig, normalizePluginHook, register, registerSync }; | ||
| export { defineConfig, loadConfig, normalizePluginHook, register }; |
@@ -1,8 +0,5 @@ | ||
| import "./context-DRCo_iRa.mjs"; | ||
| import { t as register } from "./api-CXxT_bvN.mjs"; | ||
| import { t as register } from "./api-B_siCmJt.mjs"; | ||
| //#region src/register.ts | ||
| await register(); | ||
| register(); | ||
| //#endregion | ||
| export { }; | ||
| export {}; |
+17
-22
| { | ||
| "name": "unloader", | ||
| "type": "module", | ||
| "version": "0.8.3", | ||
| "version": "0.9.0", | ||
| "description": "Node.js loader with a Rollup-like interface.", | ||
@@ -20,9 +20,4 @@ "author": "Kevin Deng <sxzz@sxzz.moe>", | ||
| "./register": "./dist/register.mjs", | ||
| "./register-sync": "./dist/register-sync.mjs", | ||
| "./worker": "./dist/worker.mjs", | ||
| "./package.json": "./package.json" | ||
| }, | ||
| "main": "./dist/index.mjs", | ||
| "module": "./dist/index.mjs", | ||
| "types": "./dist/index.d.mts", | ||
| "files": [ | ||
@@ -39,24 +34,24 @@ "dist" | ||
| "@jridgewell/remapping": "^2.3.5", | ||
| "birpc": "^4.0.0", | ||
| "obug": "^2.1.1", | ||
| "picomatch": "^4.0.3", | ||
| "quansync": "^1.0.0", | ||
| "unconfig": "^7.4.2" | ||
| "picomatch": "^4.0.4", | ||
| "unconfig": "^7.5.0" | ||
| }, | ||
| "inlinedDependencies": { | ||
| "@antfu/utils": "9.3.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@antfu/utils": "^9.3.0", | ||
| "@quansync/fs": "^1.0.0", | ||
| "@sxzz/eslint-config": "^7.4.3", | ||
| "@sxzz/prettier-config": "^2.2.6", | ||
| "@types/node": "^24.10.4", | ||
| "@types/picomatch": "^4.0.2", | ||
| "@typescript/native-preview": "7.0.0-dev.20251219.1", | ||
| "bumpp": "^10.3.2", | ||
| "eslint": "^9.39.2", | ||
| "@sxzz/eslint-config": "^7.8.4", | ||
| "@sxzz/prettier-config": "^2.3.1", | ||
| "@types/node": "^25.6.0", | ||
| "@types/picomatch": "^4.0.3", | ||
| "@typescript/native-preview": "7.0.0-dev.20260415.1", | ||
| "bumpp": "^11.0.1", | ||
| "eslint": "^10.2.0", | ||
| "magic-string": "^0.30.21", | ||
| "prettier": "^3.7.4", | ||
| "tsdown": "^0.18.1", | ||
| "prettier": "^3.8.3", | ||
| "tsdown": "^0.21.8", | ||
| "tsdown-preset-sxzz": "^0.5.0", | ||
| "tsx": "^4.21.0", | ||
| "typescript": "^5.9.3", | ||
| "unplugin-quansync": "^0.5.1" | ||
| "typescript": "^6.0.2" | ||
| }, | ||
@@ -63,0 +58,0 @@ "prettier": "@sxzz/prettier-config", |
+5
-36
@@ -33,4 +33,3 @@ # unloader | ||
| ```bash | ||
| node --import unloader/register ... # For ESM only, support both sync and async hooks | ||
| node --require unloader/register-sync ... # For both ESM and CJS, only support sync hooks | ||
| node --import unloader/register ... | ||
| ``` | ||
@@ -51,20 +50,9 @@ | ||
| unloader supports both ESM and CJS, however, async hooks are only supported in | ||
| ESM. To support both ESM and CJS, please make sure all hooks are synchronous, or | ||
| use [quansync](https://github.com/quansync-dev/quansync). | ||
| unloader supports both ESM and CJS. All hooks are synchronous. | ||
| Here is an example of using sync hooks and quansync: | ||
| <details> | ||
| <summary>Show code</summary> | ||
| ```ts | ||
| import { readFileSync } from 'node:fs' | ||
| import { readFile } from '@quansync/fs' | ||
| import { quansync } from 'quansync' | ||
| import type { Plugin } from 'unloader' | ||
| // sync usage | ||
| const pluginSync: Plugin<true> = { | ||
| const plugin: Plugin = { | ||
| name: 'my-plugin', | ||
@@ -84,23 +72,4 @@ resolveId(source, importer, options) { | ||
| } | ||
| // quansync usage | ||
| const pluginQuansync: Plugin = { | ||
| name: 'my-plugin', | ||
| resolveId: quansync(function* (source, importer, options) { | ||
| const result = yield this.resolve(`${source}.js`, importer, options) | ||
| if (result) { | ||
| console.log(result) | ||
| return result | ||
| } | ||
| }), | ||
| load: quansync(function* (id) { | ||
| const contents = yield readFile(id, 'utf8') | ||
| console.log(contents) | ||
| return contents | ||
| }), | ||
| } | ||
| ``` | ||
| </details> | ||
| ### Example | ||
@@ -125,3 +94,3 @@ | ||
| }, | ||
| async resolveId(source, importer, options) { | ||
| resolveId(source, importer, options) { | ||
| if (source.startsWith('node:')) return | ||
@@ -135,3 +104,3 @@ | ||
| // Feature: try resolve with different extensions | ||
| const result = await this.resolve(`${source}.js`, importer, options) | ||
| const result = this.resolve(`${source}.js`, importer, options) | ||
| if (result) return result | ||
@@ -138,0 +107,0 @@ }, |
| import { n as createHooks, t as sharedPluginContext } from "./context-DRCo_iRa.mjs"; | ||
| import module from "node:module"; | ||
| import process from "node:process"; | ||
| import { createBirpc } from "birpc"; | ||
| import { createDebug } from "obug"; | ||
| //#region src/utils/debug.ts | ||
| const debug = createDebug("unloader"); | ||
| //#endregion | ||
| //#region src/rpc.ts | ||
| const mainFunctions = { | ||
| log(...messages) { | ||
| console.info(...messages); | ||
| }, | ||
| debug(...args) { | ||
| debug(...args); | ||
| } | ||
| }; | ||
| function createRpc(port) { | ||
| return createBirpc(mainFunctions, { | ||
| post: (data) => port.postMessage(data), | ||
| on: (fn) => port.on("message", fn) | ||
| }); | ||
| } | ||
| //#endregion | ||
| //#region src/api.ts | ||
| async function register(inlineConfig) { | ||
| if (!module.register) throw new Error(`This version of Node.js (${process.version}) does not support module.register(). Please upgrade to Node v20.6 and above.`); | ||
| const { port1, port2 } = new MessageChannel(); | ||
| const rpc = createRpc(port1); | ||
| port1.unref(); | ||
| const data = { | ||
| port: port2, | ||
| inlineConfig | ||
| }; | ||
| const transferList = [port2]; | ||
| module.register("./worker.mjs", { | ||
| parentURL: import.meta.url, | ||
| data, | ||
| transferList | ||
| }); | ||
| if (await rpc.ping()) process.setSourceMapsEnabled(true); | ||
| return () => rpc.deactivate(); | ||
| } | ||
| function registerSync(inlineConfig) { | ||
| const registerHooks = module.registerHooks; | ||
| if (!registerHooks) throw new Error(`This version of Node.js (${process.version}) does not support module.registerHooks(). Please upgrade to Node v22.15 or v23.5 and above.`); | ||
| const { init, resolve, load, deactivate } = createHooks(); | ||
| const context = { | ||
| ...sharedPluginContext, | ||
| log: (message) => console.info(message), | ||
| debug | ||
| }; | ||
| if (init.sync(context, inlineConfig).sourcemap && !process.sourceMapsEnabled) process.setSourceMapsEnabled(true); | ||
| registerHooks({ | ||
| resolve: resolve.sync, | ||
| load: load.sync | ||
| }); | ||
| return deactivate; | ||
| } | ||
| //#endregion | ||
| export { registerSync as n, register as t }; |
| import process from "node:process"; | ||
| import path from "node:path"; | ||
| import remapping from "@jridgewell/remapping"; | ||
| import { getIsAsync } from "quansync"; | ||
| import { quansync } from "quansync/macro"; | ||
| import picomatch from "picomatch"; | ||
| import { loadConfig } from "unconfig"; | ||
| import { Buffer } from "node:buffer"; | ||
| import { fileURLToPath, pathToFileURL } from "node:url"; | ||
| //#region src/plugin.ts | ||
| function normalizePluginHook(plugin, key) { | ||
| const hook = plugin?.[key]; | ||
| if (!hook) return {}; | ||
| if (typeof hook === "function") return { handler: hook }; | ||
| return hook; | ||
| } | ||
| //#endregion | ||
| //#region node_modules/.pnpm/@antfu+utils@9.3.0/node_modules/@antfu/utils/dist/index.mjs | ||
| function toArray(array) { | ||
| array = array ?? []; | ||
| return Array.isArray(array) ? array : [array]; | ||
| } | ||
| //#endregion | ||
| //#region src/plugin-filter.ts | ||
| function getMatcherString(glob, cwd) { | ||
| if (glob.startsWith("**") || path.isAbsolute(glob)) return path.normalize(glob); | ||
| const resolved = path.resolve(cwd, glob); | ||
| return path.normalize(resolved); | ||
| } | ||
| function patternToIdFilter(pattern) { | ||
| if (pattern instanceof RegExp) return (id) => { | ||
| const normalizedId = path.normalize(id); | ||
| const result = pattern.test(normalizedId); | ||
| pattern.lastIndex = 0; | ||
| return result; | ||
| }; | ||
| const matcher = picomatch(getMatcherString(pattern, process.cwd()), { dot: true }); | ||
| return (id) => { | ||
| return matcher(path.normalize(id)); | ||
| }; | ||
| } | ||
| function patternToCodeFilter(pattern) { | ||
| if (pattern instanceof RegExp) return (code) => { | ||
| const result = pattern.test(code); | ||
| pattern.lastIndex = 0; | ||
| return result; | ||
| }; | ||
| return (code) => code.includes(pattern); | ||
| } | ||
| function createFilter(exclude, include) { | ||
| if (!exclude && !include) return; | ||
| return (input) => { | ||
| if (exclude?.some((filter) => filter(input))) return false; | ||
| if (include?.some((filter) => filter(input))) return true; | ||
| return !include || include.length <= 0; | ||
| }; | ||
| } | ||
| function normalizeFilter(filter) { | ||
| if (typeof filter === "string" || filter instanceof RegExp) return { include: [filter] }; | ||
| if (Array.isArray(filter)) return { include: filter }; | ||
| return { | ||
| exclude: filter.exclude ? toArray(filter.exclude) : void 0, | ||
| include: filter.include ? toArray(filter.include) : void 0 | ||
| }; | ||
| } | ||
| function createIdFilter(filter) { | ||
| if (!filter) return; | ||
| const { exclude, include } = normalizeFilter(filter); | ||
| const excludeFilter = exclude?.map(patternToIdFilter); | ||
| const includeFilter = include?.map(patternToIdFilter); | ||
| return createFilter(excludeFilter, includeFilter); | ||
| } | ||
| function createCodeFilter(filter) { | ||
| if (!filter) return; | ||
| const { exclude, include } = normalizeFilter(filter); | ||
| const excludeFilter = exclude?.map(patternToCodeFilter); | ||
| const includeFilter = include?.map(patternToCodeFilter); | ||
| return createFilter(excludeFilter, includeFilter); | ||
| } | ||
| function createFilterForId(filter) { | ||
| const filterFunction = createIdFilter(filter); | ||
| return filterFunction ? (id) => !!filterFunction(id) : void 0; | ||
| } | ||
| function createFilterForTransform(idFilter, codeFilter) { | ||
| if (!idFilter && !codeFilter) return; | ||
| const idFilterFunction = createIdFilter(idFilter); | ||
| const codeFilterFunction = createCodeFilter(codeFilter); | ||
| return (id, code) => { | ||
| let fallback = true; | ||
| if (idFilterFunction) fallback &&= idFilterFunction(id); | ||
| if (!fallback) return false; | ||
| if (codeFilterFunction) fallback &&= codeFilterFunction(code); | ||
| return fallback; | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region src/utils/config.ts | ||
| const loadConfig$1 = quansync(function* () { | ||
| const { config } = yield loadConfig({ | ||
| sources: [{ | ||
| files: "unloader.config", | ||
| extensions: [ | ||
| "ts", | ||
| "mts", | ||
| "cts", | ||
| "js", | ||
| "mjs", | ||
| "cjs", | ||
| "json", | ||
| "" | ||
| ] | ||
| }], | ||
| cwd: process.cwd(), | ||
| defaults: {} | ||
| }); | ||
| return config; | ||
| }); | ||
| //#endregion | ||
| //#region src/utils/map.ts | ||
| function attachSourceMap(map, code) { | ||
| if (map) { | ||
| if (!map.sourcesContent || map.sourcesContent.length === 0) map.sourcesContent = [code]; | ||
| code += `\n//# sourceMappingURL=${toUrl(map)}`; | ||
| } | ||
| return code; | ||
| } | ||
| function toUrl(map) { | ||
| return `data:application/json;charset=utf-8;base64,${Buffer.from(map.toString()).toString("base64")}`; | ||
| } | ||
| //#endregion | ||
| //#region src/utils/url.ts | ||
| function urlToPath(url) { | ||
| if (!url) return url; | ||
| return url.startsWith("file://") ? fileURLToPath(url) : url; | ||
| } | ||
| function pathToUrl(isRequire, path$1) { | ||
| if (isRequire || path$1.startsWith("file://") || path$1.startsWith("data://") || path$1.startsWith("node:")) return path$1; | ||
| return pathToFileURL(path$1).href; | ||
| } | ||
| //#endregion | ||
| //#region src/hooks.ts | ||
| function createHooks() { | ||
| let config; | ||
| let deactivated = false; | ||
| let pluginContext; | ||
| const init = quansync(function* (context, inlineConfig) { | ||
| pluginContext = context; | ||
| config = inlineConfig || (yield loadConfig$1()); | ||
| for (const plugin of config.plugins || []) { | ||
| const { handler } = normalizePluginHook(plugin, "options"); | ||
| config = handler?.call(context, config) || config; | ||
| } | ||
| for (const plugin of config.plugins || []) { | ||
| const { handler } = normalizePluginHook(plugin, "buildStart"); | ||
| yield handler?.call(context); | ||
| context.debug(`loaded plugin: %s`, plugin.name); | ||
| } | ||
| return config; | ||
| }); | ||
| const resolve = quansync(function* (specifier, context, nextResolve) { | ||
| if (deactivated) return nextResolve(specifier, context); | ||
| const isRequire = !Array.isArray(context.conditions) || context.conditions[0] === "require"; | ||
| pluginContext?.debug(`resolving %s with context %o`, specifier, context); | ||
| if (config?.plugins && pluginContext) for (const plugin of config.plugins) { | ||
| const resolve$1 = createResolve(nextResolve, isRequire, pluginContext.debug); | ||
| const isAsync = yield getIsAsync(); | ||
| const { handler, filter } = normalizePluginHook(plugin, "resolveId"); | ||
| const filterFn = createFilterForId(filter?.id); | ||
| const id = urlToPath(specifier); | ||
| if (filterFn && !filterFn(id)) continue; | ||
| const result = yield handler?.call({ | ||
| resolve: isAsync ? resolve$1 : resolve$1.sync, | ||
| ...pluginContext | ||
| }, id, urlToPath(context.parentURL), { | ||
| conditions: context.conditions, | ||
| attributes: context.importAttributes | ||
| }); | ||
| if (result) { | ||
| let output; | ||
| if (typeof result === "string") output = { | ||
| url: pathToUrl(isRequire, result), | ||
| importAttributes: context.importAttributes, | ||
| shortCircuit: true | ||
| }; | ||
| else output = { | ||
| url: pathToUrl(isRequire, result.id), | ||
| format: result.format, | ||
| importAttributes: result.attributes || context.importAttributes, | ||
| shortCircuit: true | ||
| }; | ||
| pluginContext.debug(`resolved %s to %s with format %s`, specifier, output.url, output.format); | ||
| return output; | ||
| } | ||
| } | ||
| return nextResolve(specifier, context); | ||
| }); | ||
| const load = quansync(function* (url, context, nextLoad) { | ||
| if (deactivated || !config?.plugins) return nextLoad(url, context); | ||
| pluginContext?.debug(`load %s with context %o`, url, context); | ||
| let result; | ||
| const defaultFormat = context.format || "module"; | ||
| const maps = []; | ||
| for (const plugin of config.plugins) { | ||
| const { handler, filter } = normalizePluginHook(plugin, "load"); | ||
| const filterFn = createFilterForId(filter?.id); | ||
| const id = urlToPath(url); | ||
| if (filterFn && !filterFn(id)) continue; | ||
| const loadResult = yield handler?.call(pluginContext, id, { | ||
| format: context.format, | ||
| conditions: context.conditions, | ||
| attributes: context.importAttributes | ||
| }); | ||
| if (loadResult) { | ||
| if (isModuleSource(loadResult)) result = { | ||
| source: loadResult, | ||
| format: defaultFormat, | ||
| shortCircuit: true | ||
| }; | ||
| else { | ||
| if (loadResult.map) maps.unshift(loadResult.map); | ||
| result = { | ||
| source: loadResult.code, | ||
| format: loadResult.format || defaultFormat, | ||
| shortCircuit: true | ||
| }; | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| result ||= yield nextLoad(url, context); | ||
| for (const plugin of config.plugins) { | ||
| const { handler, filter } = normalizePluginHook(plugin, "transform"); | ||
| const filterFn = createFilterForTransform(filter?.id, filter?.code); | ||
| const code = result.source || ""; | ||
| const id = urlToPath(url); | ||
| if (filterFn && (typeof code !== "string" || !filterFn(id, code || ""))) continue; | ||
| const transformResult = yield handler?.call(pluginContext, code, id, { | ||
| format: result.format, | ||
| conditions: context.conditions, | ||
| attributes: context.importAttributes | ||
| }); | ||
| if (transformResult) if (isModuleSource(transformResult)) result = { | ||
| ...result, | ||
| source: transformResult | ||
| }; | ||
| else { | ||
| if (transformResult.map) maps.unshift(transformResult.map); | ||
| result = { | ||
| ...result, | ||
| source: transformResult.code, | ||
| format: transformResult.format || result.format | ||
| }; | ||
| } | ||
| } | ||
| if (maps.length && typeof result.source === "string") { | ||
| const code = attachSourceMap(remapping(maps, () => null), result.source); | ||
| result.source = code; | ||
| } | ||
| return result; | ||
| }); | ||
| const deactivate = () => { | ||
| deactivated = true; | ||
| }; | ||
| return { | ||
| init, | ||
| resolve, | ||
| load, | ||
| deactivate | ||
| }; | ||
| } | ||
| function createResolve(nextResolve, isRequire, debug) { | ||
| return quansync(function* (source, importer, options) { | ||
| if (!path.isAbsolute(source) && importer) source = path.resolve(importer, "..", source); | ||
| try { | ||
| const resolved = yield nextResolve(pathToUrl(isRequire, source), { | ||
| parentURL: importer, | ||
| conditions: options?.conditions, | ||
| importAttributes: options?.attributes | ||
| }); | ||
| debug("resolved %s to %s with format %s", source, resolved.url, resolved.format); | ||
| return { | ||
| id: urlToPath(resolved.url), | ||
| attributes: resolved.importAttributes, | ||
| format: resolved.format | ||
| }; | ||
| } catch (error) { | ||
| debug("error resolving %s: %o", source, error); | ||
| return null; | ||
| } | ||
| }); | ||
| } | ||
| function isModuleSource(v) { | ||
| return typeof v === "string" || ArrayBuffer.isView(v) || v instanceof ArrayBuffer; | ||
| } | ||
| //#endregion | ||
| //#region package.json | ||
| var version = "0.8.3"; | ||
| //#endregion | ||
| //#region src/utils/context.ts | ||
| const sharedPluginContext = { | ||
| error: (message) => { | ||
| throw typeof message === "string" ? new Error(message) : message; | ||
| }, | ||
| meta: { unloaderVersion: version } | ||
| }; | ||
| //#endregion | ||
| export { normalizePluginHook as i, createHooks as n, loadConfig$1 as r, sharedPluginContext as t }; |
| export { }; |
| import "./context-DRCo_iRa.mjs"; | ||
| import { n as registerSync } from "./api-CXxT_bvN.mjs"; | ||
| //#region src/register-sync.ts | ||
| registerSync(); | ||
| //#endregion | ||
| export { }; |
| import { InitializeHook, LoadHook, ResolveHook } from "node:module"; | ||
| import { MessagePort } from "node:worker_threads"; | ||
| //#region src/worker.d.ts | ||
| interface Data { | ||
| port: MessagePort; | ||
| inlineConfig?: string; | ||
| } | ||
| declare const threadFunctions: { | ||
| deactivate(): void; | ||
| ping(): Promise<boolean>; | ||
| }; | ||
| type ThreadFunctions = typeof threadFunctions; | ||
| declare const initialize: InitializeHook; | ||
| declare const resolve: ResolveHook; | ||
| declare const load: LoadHook; | ||
| //#endregion | ||
| export { Data, ThreadFunctions, initialize, load, resolve }; |
| import { n as createHooks, t as sharedPluginContext } from "./context-DRCo_iRa.mjs"; | ||
| import process from "node:process"; | ||
| import { createBirpc } from "birpc"; | ||
| //#region src/worker.ts | ||
| let data; | ||
| const { promise: promisePong, resolve: pong } = Promise.withResolvers(); | ||
| const hooks = createHooks(); | ||
| const threadFunctions = { | ||
| deactivate() { | ||
| hooks.deactivate(); | ||
| }, | ||
| ping() { | ||
| return promisePong; | ||
| } | ||
| }; | ||
| let rpc; | ||
| const initialize = async (_data) => { | ||
| data = _data; | ||
| const { port } = data; | ||
| rpc = createBirpc(threadFunctions, { | ||
| post: (data$1) => port.postMessage(data$1), | ||
| on: (fn) => port.on("message", fn) | ||
| }); | ||
| const context = { | ||
| ...sharedPluginContext, | ||
| port, | ||
| log: (...args) => rpc.log(...args), | ||
| debug: (...args) => rpc.debug(...args) | ||
| }; | ||
| let inlineConfig; | ||
| if (data.inlineConfig) inlineConfig = (await import(data.inlineConfig)).default; | ||
| pong(!!((await hooks.init(context, inlineConfig)).sourcemap && !process.sourceMapsEnabled)); | ||
| }; | ||
| const resolve = hooks.resolve.async; | ||
| const load = hooks.load.async; | ||
| //#endregion | ||
| export { initialize, load, resolve }; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
4
-33.33%14
-6.67%21706
-18.21%8
-38.46%344
-18.48%174
-15.12%- Removed
- Removed
- Removed
Updated
Updated