English | 简体中文
A small but powerful Node.js module loader: import multiple files at once using glob patterns. It supports:
- Bulk importing modules by file pattern (resolved at runtime, no bundler required)
- Lazy loading (default) and eager loading
- Importing only a specific named export or the default export (
&import=) - Importing and parsing YAML directly (
.yaml/.yml)
In Node.js projects, you often want to import all modules under a directory (e.g. services/plugins/routes/configs). Writing a bunch of import statements by hand is tedious and easy to miss.
globload intercepts imports with the ?glob query at runtime via Node.js module hooks, and expands them into “the aggregated results of importing multiple modules”, so you get:
- Less repetitive imports
- No need to maintain an import list when files are added/removed
- No need for build tools like Vite/Rollup/Webpack, and no extra compilation step
# pnpm
pnpm add globload
# npm
npm install globload
# yarn
yarn add globloadglobload needs to register its hook script at Node startup. The recommended way is using --import:
node --import globload your-app.jsThen, simply append ?glob to an import path to trigger expansion.
Note: “import” here includes both static imports (
import ... from "...") and dynamic imports (import("...")). In other words, as long as the path includes?glob,globloadwill expand it no matter which style you use.
Assume your project structure looks like this:
your-project/
├── src/
│ ├── app.js
│ └── services/
│ ├── user.js
│ └── product.js
└── package.json
With only ?glob, the import is lazy: globload exports an object whose values are functions; calling a function triggers the actual import().
// src/app.js
import services from "./services/*.js?glob";
// services is like:
// {
// 'src/services/product.js': () => import('file:///.../src/services/product.js'),
// 'src/services/user.js': () => import('file:///.../src/services/user.js')
// }
// Note: keys are paths relative to process.cwd()
for (const pathKey in services) {
const mod = await services[pathKey]();
if (mod.getUser) console.log(mod.getUser());
}The same rule applies to dynamic import() (note that dynamic import returns a module namespace object; you need to read default from it):
// src/app.js
const { default: services } = await import("./services/*.js?glob");
for (const pathKey in services) {
const mod = await services[pathKey]();
if (mod.getUser) console.log(mod.getUser());
}Append &eager after ?glob to import all matched modules immediately:
// src/app.js
import services from "./services/*.js?glob&eager";
// services is like:
// {
// 'src/services/product.js': { ...exports },
// 'src/services/user.js': { ...exports }
// }
for (const pathKey in services) {
const mod = services[pathKey];
if (mod.getUser) console.log(mod.getUser());
}Use &import= to select either the default export or a specific named export:
?glob&import=default: take the default export from each module?glob&import=setup: take thesetupnamed export from each module
Lazy + default export:
import defaults from "./services/*.js?glob&import=default";
for (const pathKey in defaults) {
const d = await defaults[pathKey](); // returns the default export
console.log(d);
}Eager + named export:
import setups from "./services/*.js?glob&eager&import=setup";
for (const pathKey in setups) {
const setup = setups[pathKey]; // the setup export itself
if (typeof setup === "function") setup();
}When a matched file is YAML, globload parses its content into a JavaScript object.
Lazy YAML:
import configs from "./config/*.yaml?glob";
const db = await configs["src/config/database.yaml"]();
console.log(db.host);Eager YAML:
import configs from "./config/*.yaml?glob&eager";
console.log(configs["src/config/database.yaml"].port);Use &import= to read a top-level YAML key:
import items from "./config/*.yaml?glob&eager&import=user";
// items looks like:
// { 'src/config/database.yaml': 'admin' }For YAML, &import=default is equivalent to omitting &import, i.e. returning the whole parsed object.
If you use import attributes in a globload import statement (e.g. with { type: "json" } for JSON modules), those attributes are forwarded to every dynamic import() generated by globload.
import data from "./data/*.json?glob&eager" with { type: "json" };After expansion, it’s roughly equivalent to the following (illustrative; path resolution details omitted). The key point is the attributes appear on every dynamic import:
// Suppose it matches:
// - src/data/user.json
// - src/data/product.json
const data = {
"src/data/user.json": await import("file:///.../src/data/user.json", {
with: { type: "json" },
}),
"src/data/product.json": await import("file:///.../src/data/product.json", {
with: { type: "json" },
}),
};globload feels similar to bulk import features provided by frontend bundlers (e.g. Vite’s import.meta.glob), but the key difference is:
import.meta.glob: compile-time analysis and transformation; requires a bundling/build pipelineglobload: runtime parsing and loading via Node.js module hooks; no bundler required
So globload is a better fit for pure Node.js backend projects, scripts, or any scenario where you don’t want to introduce a build step.
- Node.js: see the
engines.nodefield inpackage.json(currently^18.19.0 || >=20.10.0).
Issues and PRs are welcome.