Unified tool for importing TypeScript modules at runtime.
It's a common need for tools to support importing TypeScript modules at runtime. For example, to support configure files written in TypeScript.
There are so many ways to do that, each with its own trade-offs and limitations. This library aims to provide a simple, unified API for importing TypeScript modules, providing an easy-to-use API, and making it easy to switch between different loaders.
By default, it also provides a smart "auto" mode that decides the best loader based on the environment, trying to ease out their limitations and provide the best experience.
The goal is for this library to swallow the complexity of the underlying implementations, where you can just focus on the feature set you need. This library will keep up-to-date with the latest loaders and the runtime environment.
npm i importx
const mod = await import('importx').then(x => x.import('./path/to/module.ts', import.meta.url))
You can turn the second argument of import
into an object to provide options:
const mod = await import('importx').then(x => x.import('./path/to/module.ts', {
parentURL: import.meta.url, // *required
// The following options are their default values
cache: null, // false, if you want to always get a new module
listDependencies: false, // true, if you need to get the list of dependencies
loader: 'auto', // most of the time, you don't need to change this as they will be chosen automatically
}))
When loader
is not provided in the options explicitly, it will read from IMPORTX_LOADER
environment variable, and then fall back to the auto
.
Automatically choose the best loader based on the environment (if the below graph doesn't render, click here to view it on GitHub).
graph TD
A((Auto)) --> Cache
Cache --> |false| RuntimeTsx
Cache --> |true / null| IsTS
IsTS --> |Yes| SupportTs
IsTS --> |No| Native1
SupportTs --> |No| RuntimeTsx
SupportTs --> |Yes| Native2
RuntimeTsx --> |Yes| Tsx
RuntimeTsx --> |No| ListDeps
ListDeps --> |Yes| BundleRequire
ListDeps --> |No| Jiti
IsTS{{"Is importing a TypeScript file?"}}
SupportTs{{"Supports native TypeScript?"}}
Cache[["Cache enabled?"]]
Native1(["native import()"])
Native2(["native import()"])
RuntimeTsx{{"Is current runtime supports tsx?"}}
ListDeps[["Need to list dependencies?"]]
Tsx([tsx loader])
Jiti([jiti loader])
BundleRequire([bundle-require loader])
classDef auto fill:#0f82,stroke:#0f83,stroke-width:2px;
classDef question fill:#f9f2,stroke:#f9f3,stroke-width:2px;
classDef cache fill:#eb527120,stroke:#eb527133,stroke-width:2px;
classDef native fill:#8882,stroke:#8883,stroke-width:2px;
classDef ts fill:#09f2,stroke:#09f3,stroke-width:2px;
classDef tsx fill:#0fe2,stroke:#0fe3,stroke-width:2px;
classDef jiti fill:#ffde2220,stroke:#ffde2230,stroke-width:2px;
classDef bundle fill:#5f32,stroke:#5f33,stroke-width:2px;
class A auto;
class RuntimeTsx question;
class Cache,ListDeps cache;
class Native1,Native2,Native3 native;
class IsTS,SupportTs ts;
class Tsx tsx;
class Jiti jiti;
class BundleRequire bundle;
linkStyle default stroke:#8888
Use the native import()
to import the module. According to the ESM spec, importing the same module multiple times will return the same module instance.
Use tsx
's tsImport
API to import the module. Under the hood, it registers Node.js loader API and uses esbuild to transpile TypeScript to JavaScript.
- Native Node.js loader API, consistent and future-proof.
- Get the file list of module dependencies. Helpful for hot-reloading or manifest generation.
- Supports scoped registration, does not affect the global environment.
- Requires Node.js
^18.18.0
,^20.6.0
or above. Does not work on other runtime yet.
Use jiti
to import the module. It uses a bundled Babel parser to transpile modules. It runs in CJS mode and has its own cache and module runner.
- Self-contained, does not depend on esbuild.
- Own cache and module runner, better and flexible cache control. Works on the majority of Node-compatible runtimes.
Does not support top-level await yet(supported in v2)- Runs in CJS mode (transpiles all TS/ESM to CJS)
Use bundle-require
to import the module. It uses esbuild
to bundle the entry module, saves it to a temporary file, and then imports it.
- Get the file list of module dependencies. Helpful for hot-reloading or manifest generation.
- It creates a temporary bundle file when importing (will external
node_modules
). - Can be inefficient where there are many TypeScript modules in the import tree.
- Imports are using esbuild's resolution, which might have potential misalignment with Node.js.
- Always import a new module, does not support module cache.
By definition, ESM modules are always cached by the runtime, which means you will get the same module instance when importing the same module multiple times. In some scenarios, like a dev server watching for config file changes, the cache may not be desired as you want to get the new module with the latest code on your disk.
importx
allows you to specify if you want to have the module cache or not, by providing the cache
option:)
const mod = await import('importx')
.then(x => x.import('./path/to/module.ts', {
cache: false, // <-- this
parentURL: import.meta.url,
}))
Setting cache: null
(default) means you don't care about the cache (if you only import the module once).
Note that some loaders always have a cache, and some loaders always have no cache. With the auto
loader, we will choose the best loader based on your need. Otherwise, an unsupported combination will throw an error. For example:
// This will throw an error because `bundle-require` does not support cache.
const mod = await import('importx')
.then(x => x.import('./path/to/module.ts', {
cache: true,
loader: 'bundle-require',
parentURL: import.meta.url,
// ignoreImportxWarning: true // unless you have this
}))
You can get the extra module information by passing the module instance to getModuleInfo
:
await import('importx')
.then(async (x) => {
const mod = await x.import('./path/to/module.ts', import.meta.url)
const info = x.getModuleInfo(mod)
console.log(
info.loader, // the final loader used
info.timestampInit, // timestamp when the module is initialized
info.timestampLoad, // timestamp when the module is imported
info.dependencies, // list of dependencies (available only in `tsx` and `bundle-require` loader),
(info.timestampLoad - info.timestampInit) // time taken to load the module (in ms)
)
})
In cases like loading a config file for a dev server, where you need to watch for changes in the config file and reload the server, you may want to know the module's dependencies to watch for changes in them as well.
tsx
and bundle-require
loaders support listing the dependencies of the module. You can get the list of dependencies by getting the module info. To ensure you use always have the dependencies list in auto
mode, you can set the listDependencies
option to true
:
const mod = await import('importx')
.then(x => x.import('./path/to/module.ts', {
listDependencies: true,
parentURL: import.meta.url,
}))
Since v0.4, importx supports fallback loaders when previous loaders fail to load the module. By default ['jiti']
will be used as it's the most compatible loader. You can customize the fallback loaders by setting the fallbackLoaders
option:
const mod = await import('importx')
.then(x => x.import('./path/to/module.ts', {
fallbackLoaders: ['jiti', 'tsx'],
parentURL: import.meta.url,
}))
You can also disable fallback loaders by setting it to false
:
const mod = await import('importx')
.then(x => x.import('./path/to/module.ts', {
fallbackLoaders: false,
parentURL: import.meta.url,
}))
Importing a TypeScript module with importx
:
Generated with version
v0.4.4
at 2024-09-27T00:27:46.691Z
native | tsx | jiti | bundle-require | |
---|---|---|---|---|
node | Import: ❌ Cache: ❌ No cache: ❌ Deps: ❌ CTS Import: ❌ ESM/CJS Mixed: ❌ Const Enum: ❌ Import ESM Dep: ❌ |
Import: ✅ Cache: ✅ No cache: ✅ Deps: ✅ CTS Import: ✅ ESM/CJS Mixed: ✅ Const Enum: ✅ Import ESM Dep: ✅ |
Import: ✅ Cache: ✅ No cache: ✅ Deps: ✅ CTS Import: ✅ ESM/CJS Mixed: ✅ Const Enum: ✅ Import ESM Dep: ✅ |
Import: ✅ Cache: ❌ No cache: ✅ Deps: ✅ CTS Import: ❌ ESM/CJS Mixed: ✅ Const Enum: ✅ Import ESM Dep: ✅ |
tsx | Import: ✅ Cache: ✅ No cache: ❌ Deps: ❌ CTS Import: ✅ ESM/CJS Mixed: ✅ Const Enum: ✅ Import ESM Dep: ✅ |
Import: ✅ Cache: ✅ No cache: ✅ Deps: ✅ CTS Import: ✅ ESM/CJS Mixed: ✅ Const Enum: ✅ Import ESM Dep: ✅ |
Import: ✅ Cache: ✅ No cache: ✅ Deps: ✅ CTS Import: ✅ ESM/CJS Mixed: ✅ Const Enum: ✅ Import ESM Dep: ✅ |
Import: ✅ Cache: ❌ No cache: ✅ Deps: ✅ CTS Import: ❌ ESM/CJS Mixed: ✅ Const Enum: ✅ Import ESM Dep: ✅ |
deno | Import: ✅ Cache: ✅ No cache: ❌ Deps: ❌ CTS Import: ❌ ESM/CJS Mixed: ❌ Const Enum: ❌ Import ESM Dep: ✅ |
Import: ❌ Cache: ❌ No cache: ❌ Deps: ❌ CTS Import: ❌ ESM/CJS Mixed: ❌ Const Enum: ❌ Import ESM Dep: ❌ |
Import: ❌ Cache: ❌ No cache: ❌ Deps: ❌ CTS Import: ❌ ESM/CJS Mixed: ❌ Const Enum: ❌ Import ESM Dep: ❌ |
Import: ✅ Cache: ❌ No cache: ✅ Deps: ✅ CTS Import: ❌ ESM/CJS Mixed: ✅ Const Enum: ✅ Import ESM Dep: ✅ |
bun | Import: ✅ Cache: ✅ No cache: ❌ Deps: ❌ CTS Import: ✅ ESM/CJS Mixed: ✅ Const Enum: ✅ Import ESM Dep: ✅ |
Import: ❌ Cache: ❌ No cache: ❌ Deps: ❌ CTS Import: ❌ ESM/CJS Mixed: ❌ Const Enum: ❌ Import ESM Dep: ❌ |
Import: ✅ Cache: ❌ No cache: ❌ Deps: ✅ CTS Import: ✅ ESM/CJS Mixed: ✅ Const Enum: ✅ Import ESM Dep: ✅ |
Import: ✅ Cache: ❌ No cache: ✅ Deps: ✅ CTS Import: ❌ ESM/CJS Mixed: ✅ Const Enum: ✅ Import ESM Dep: ✅ |
native | tsx | jiti | bundle-require | |
---|---|---|---|---|
Cache: true |
✅ | ✅ | ✅ | ❌ |
Cache: false |
❌ | ✅ | ✅ | ✅ |
List dependencies | ❌ | ✅ | ✅ | ✅ |
Runtimes other than Node.js | ✅ | ❌ | ✅ | ✅ |
Native ESM Import | ✅ | ✅ | ✅ | ✅ |
Top-level await | ✅ | ✅ | ✅ | ✅ |
Runtime module type* | ESM | ESM | CJS | ESM/CJS |
*This indicates what's the module type for each loader to evaluate the modules. For
CJS
, it means the loader transpiles the module to CJS and executes it in CJS mode, which may have some limitations like top-level await.
MIT License © 2024-PRESENT Anthony Fu