node-url-import
v0.1.0
Published
Node.js hook to import modules from HTTP(S) URLs
Maintainers
Readme
node-url-import
Node.js hook that lets you import (and require) modules directly from HTTP(S) URLs like Deno.
import { add } from "https://esm.sh/lodash-es";
console.log(add(1, 2)); // 3// Remote TypeScript works too (Node 22.18+)
import { join } from "https://deno.land/[email protected]/path/mod.ts";// CJS require also works (Node 24+)
const { default: ms } = require("https://esm.sh/[email protected]");
console.log(ms("2 days")); // 172800000Works with static imports, dynamic import(), CJS require(), and relative imports inside remote modules. Fetched modules are cached on disk so subsequent runs are instant.
Usage
CLI
Like ts-node — a drop-in wrapper around node with the hook pre-loaded:
npm i -g node-url-import
# OR
npx node-url-import
node-url-import [options] [node-flags] <script-or-url> [script-args...]Flags before the script are split into node-url-import options and Node.js flags. Flags after the script are forwarded to the remote script as-is.
# Run a local file that imports from URLs
node-url-import ./app.mjs
# Run a remote URL directly
node-url-import https://raw.esm.sh/armor64/dist/cli.js
# Re-fetch all remote modules (bypass cache)
node-url-import -r ./app.mjs
# Pass Node.js flags (e.g. --inspect)
node-url-import --inspect ./app.mjs
# Forward flags to the remote script
node-url-import https://raw.esm.sh/nrm-lite/cli.mjs -h
# Write a lock file (Deno-compatible format)
node-url-import --lock ./app.mjs
# Error if lock file is out of date
node-url-import --lock --frozen ./app.mjs
# Clear the disk cache
node-url-import --clear-cacheLoader hook
Register the hook yourself with --import:
node --import node-url-import/register ./app.mjsOn Node 24+, CJS files can require() HTTP URLs too:
node --import node-url-import/register ./app.cjsOr the legacy --loader flag (ESM only):
node --loader node-url-import/loader ./app.mjsnpm: specifiers
Modules can use Deno-style npm: specifiers to import from node_modules. The version is stripped and the module is resolved by Node's default resolver:
import ms from "npm:ms";
import ms from "npm:[email protected]"; // version stripped → "ms"
import add from "npm:lodash-es@4/add"; // → "lodash-es/add"
import node from "npm:@types/node@22"; // → "@types/node"Programmatic API
import { fetchModule, clearCache } from "node-url-import";
const { source, contentType } = await fetchModule(
"https://esm.sh/lodash-es/add",
);
console.log(source);
await clearCache();How it works
- On Node 22.15+, synchronous hooks (
module.registerHooks()) intercept bothimportandrequirecalls for HTTP(S) URLs. On older Node, async hooks (module.register()) support ESMimportonly. resolvefollows redirects to the final URL so that relative imports inside a remote module (e.g.import "./util.js") resolve against the correct base. Bare specifiers like"node:fs"fall through to Node's default resolver.- Fetched sources are cached in Deno's remote module cache (
$DENO_DIR/remote/, defaulting to~/Library/Caches/deno/remote/on macOS and~/.cache/deno/remote/on Linux), using the same file layout as Deno — so both runtimes share the same cache. - For
require()(synchronous), cache hits are served viareadFileSync. On cache miss, a persistent worker thread fetches asynchronously while the main thread blocks viaAtomics.wait. - Cached modules are used forever until
--reloadis passed or--clear-cacheis used (same as Deno). - Module format is inferred from the
Content-Typeheader and URL extension, defaulting to ESM. TypeScript files (.ts,.tsx,.mts,.cts) are loaded using Node's built-in type stripping on Node 22.18+. - A Deno-style progress bar is shown on stderr during downloads (only on TTY).
Node.js version compatibility
| Feature | Minimum Node.js |
|---------|----------------|
| ESM import (via module.register) | v18.19.0 / v20.6.0 |
| ESM import (via module.registerHooks) | v22.15.0 / v23.5.0 |
| Remote TypeScript (.ts, .tsx, .mts, .cts) | v22.18.0 |
| CJS require of HTTP URLs | v24+ |
Options
| Flag | Description |
| -------------------- | -------------------------------------------------------------------------- |
| -r, --reload | Bypass cache, re-fetch all remote modules |
| --lock | Check/write a lock file (default: ./deno.lock) |
| --lock-file <path> | Lock file path (implies --lock) |
| --frozen | Error if lock file is out of date (use with --lock) |
| --tmp | Write fetched modules to a tmp dir with readable paths (CLI only) |
| --clear-cache | Purge the disk cache |
Any unrecognized flags before the script are forwarded to Node.js (e.g. --inspect, --env-file).
Lock file
When --lock is passed, node-url-import writes a lock file recording the SHA-256 hash of every fetched remote module. The format is compatible with Deno's deno.lock:
{
"version": "5",
"redirects": {
"https://raw.esm.sh/armor64/dist/cli.js": "https://raw.esm.sh/[email protected]/dist/cli.js"
},
"remote": {
"https://raw.esm.sh/[email protected]/dist/cli.js": "8513c973713ea38737552521eb52529ac880555fbb6fbba8ff8571cec27872c5"
}
}redirects: maps original URLs to their final redirected URLsremote: maps final URLs to SHA-256 hashes of the source
On subsequent runs with --lock, fetched content is verified against the recorded hashes. Use --frozen to error on any mismatch instead of updating.
TypeScript
Remote TypeScript modules are supported on Node 22.18+ via Node's built-in type stripping. Files with .ts, .tsx, .mts, or .cts extensions (and URLs served with text/typescript or application/typescript content-type) are automatically loaded with the module-typescript format:
node-url-import https://deno.land/[email protected]/path/mod.tsType declarations
TypeScript does not resolve https:// specifiers out of the box. Add an ambient module declaration to a .d.ts file included in your tsconfig.json:
declare module 'https://*' {
const mod: unknown;
export default mod;
export * from 'module';
}For finer-grained types, declare specific remote modules explicitly:
declare module 'https://esm.sh/lodash-es' {
export { add, merge } from 'lodash-es';
}Node.js --experimental-network-imports
Node.js v20 shipped a built-in --experimental-network-imports flag that allows HTTPS imports without a loader. It was removed in Node.js v22.
Environment variables
| Variable | Description | Default |
| ------------------------- | ------------------------------------------------------------------------- | ------------------------------- |
| DENO_DIR | Deno's root cache directory (shared with Deno) | ~/Library/Caches/deno (macOS), ~/.cache/deno (Linux) |
| URL_IMPORT_RELOAD=1 | Same as --reload | |
| URL_IMPORT_LOCK=<path> | Same as --lock <file> | |
| URL_IMPORT_FROZEN=1 | Same as --frozen | |
| URL_IMPORT_TMP=1 | Same as --tmp | |
