filing-cabinet
filing-cabinet is a synchronous Node.js resolver for source-code dependency strings. Give it an import such as ./button, the file containing that import, and the project root; it selects a resolver from the containing file's extension and returns the matching absolute file path. Version 6 understands CommonJS, ESM, AMD, webpack resolution, TypeScript and tsconfig paths, Sass, Less, Stylus, Vue, Svelte, Node package #imports, and custom extensions. It is infrastructure for dependency graphs, codemods, and analysis CLIs, not code that belongs in a browser bundle.
filing-cabinet earns its install when one analysis tool must understand a genuinely mixed frontend codebase. For ordinary Node or TypeScript resolution, a focused resolver is smaller, easier to type, and less likely to execute project configuration unexpectedly.
Use it if
- You are building a dependency graph, unused-file checker, codemod, or editor tool that must resolve imports the way several source ecosystems do
- One project mixes JavaScript, TypeScript, Vue or Svelte files, and stylesheet imports and you want one synchronous resolver interface
- You need to honor RequireJS, webpack, tsconfig path aliases, package #imports, or a custom package.json entry field
- You need to register a resolver for an extra source extension while retaining the built-in resolver table
- You need a lightweight single-purpose resolver: version 6 installs TypeScript 6, enhanced-resolve, resolve, AMD, Sass, Stylus, tsconfig, CLI, and module-detection packages even when your tool only follows ordinary Node imports
- Your runtime is older than Node 20.19 or 22.12: the package's engines field requires >=20.19.0 or >=22.12.0, and version 6 is an ES module
- You need TypeScript declarations for your own strict build: version 6 publishes no types or typings field, and the README presents JavaScript signatures rather than a declared CabinetOptions interface
- You analyze untrusted repositories in-process: webpackConfig is loaded as executable code, function configs are invoked, and the first item of an exported config array is used, so a hostile project config can run with your analyzer's permissions
- You want errors to distinguish missing files from misconfiguration: the documented contract returns an empty string when resolution fails, and source paths such as CommonJS lookup deliberately catch resolution errors
Setup reality
npm install filing-cabinet brings a pure JavaScript install with no native build or credentials, but it is not a small dependency: version 6 directly includes TypeScript 6, Commander 14, enhanced-resolve, resolve, tsconfig-paths, AMD lookup, Sass and Stylus lookup, module detection, and path helpers. Your Node version must satisfy >=20.19.0 or >=22.12.0. The package is type: module and exports its root; ESM uses a default import, while CommonJS must read require('filing-cabinet').default. Every resolution call is synchronous and needs partial, filename, and directory. filename is the importing source file, not the candidate target, and directory is the project root used to locate node_modules. A miss is '', so check truthiness before passing the result to fs. JavaScript module style is detected by parsing the source unless config, webpackConfig, or a pre-parsed ast selects a path; large graph crawls should reuse ASTs where available. A webpack config path is loaded and may execute code, and an array export uses only its first config. TypeScript aliases require the correct tsConfig; when passing a config object, also provide tsConfigPath so relative baseUrl and paths have a real location. Type resolution can return .d.ts unless noTypeDefinitions is true. nodeModulesConfig can prefer a field such as module or run a packageFilter, which can make results differ from Node runtime resolution. Custom register and unregister calls modify a process-global resolver table, so tests and concurrent analyses need cleanup.
Patterns
Resolve a relative JavaScript importresolve-relative-import
import path from 'node:path';
import cabinet from 'filing-cabinet';
const root = process.cwd();
const resolved = cabinet({
partial: './button',
filename: path.join(root, 'src/app.js'),
directory: root,
});
if (!resolved) throw new Error('dependency not found');The return value is an absolute path or an empty string. filename must identify the file containing the import so relative resolution has the right base.
Load version 6 from CommonJSrequire-from-commonjs
const { default: cabinet } = require('filing-cabinet');
const resolved = cabinet({
partial: './config',
filename: '/workspace/src/index.js',
directory: '/workspace',
});Version 6 is an ES module. require returns a namespace object, so the documented CommonJS form destructures default.
Resolve a package through its module fieldprefer-package-module-field
const resolved = cabinet({
partial: 'some-package',
filename: '/workspace/src/app.js',
directory: '/workspace',
nodeModulesConfig: { entry: 'module' },
});This deliberately differs from normal Node main resolution. Use it when analyzing bundler inputs, not when predicting what require will load at runtime.
Select package entries with packageFilterfilter-package-entry
const resolved = cabinet({
partial: 'some-package',
filename: '/workspace/src/app.js',
directory: '/workspace',
nodeModulesConfig(pkg) {
pkg.main = pkg.browser ?? pkg.module ?? pkg.main;
return pkg;
},
});The callback can mutate package metadata before resolution. Its policy must match the runtime or bundler whose dependency graph you are modeling.
Resolve with a tsconfig fileresolve-typescript-import
const resolved = cabinet({
partial: '@app/models/user',
filename: '/workspace/src/service.ts',
directory: '/workspace',
tsConfig: '/workspace/tsconfig.json',
});A TypeScript resolution may return a .d.ts file. Set noTypeDefinitions when the graph should follow executable JavaScript instead of declarations.
Prefer JavaScript over type declarationsresolve-runtime-file
const resolved = cabinet({
partial: 'some-library',
filename: '/workspace/src/app.ts',
directory: '/workspace',
tsConfig: '/workspace/tsconfig.json',
noTypeDefinitions: true,
});noTypeDefinitions only affects TypeScript lookup. If TypeScript cannot map a declaration back to executable code, the result may still be empty.
Pass an in-memory TypeScript configuse-parsed-tsconfig
const resolved = cabinet({
partial: '@lib/math',
filename: '/virtual/src/app.ts',
directory: '/virtual',
tsConfig: {
compilerOptions: { baseUrl: '.', paths: { '@lib/*': ['src/lib/*'] } },
},
tsConfigPath: '/virtual/tsconfig.json',
fileSystem: virtualFs,
});tsConfigPath supplies the location used to interpret relative baseUrl and paths. Without it, path mapping from an object config is ignored.
Resolve through a webpack configresolve-webpack-alias
const resolved = cabinet({
partial: '@ui/Button',
filename: '/workspace/src/app.js',
directory: '/workspace',
webpackConfig: '/workspace/webpack.config.cjs',
});The config is loaded as code, function exports are invoked, and only the first entry of an array export is used. Never do this in-process for an untrusted repository.
Resolve a package #imports aliasresolve-package-imports
const resolved = cabinet({
partial: '#internal/logger',
filename: '/workspace/src/app.js',
directory: '/workspace',
});Version 6 automatically handles package.json imports entries beginning with # for both JavaScript and TypeScript containing files.
Resolve an import found in a Vue fileresolve-vue-style-import
const resolved = cabinet({
partial: './styles/theme.scss',
filename: '/workspace/src/App.vue',
directory: '/workspace',
});The Vue and Svelte resolver dispatches by dependency suffix; scss, sass, and less use sass-lookup, while styl uses stylus-lookup.
Add and remove a custom resolverregister-custom-extension
cabinet.register('.py', ({ partial, filename }) => {
return resolvePythonImport(partial, filename) || '';
});
try {
const resolved = cabinet({
partial: 'my_package.tools',
filename: '/workspace/app.py',
directory: '/workspace',
});
} finally {
cabinet.unregister('.py');
}Registration changes a process-global table. Always unregister in tests or temporary analyses so later calls do not inherit the custom behavior.
Check whether an extension has a resolverinspect-supported-extensions
if (cabinet.supportedFileExtensions.includes('.tsx')) {
const resolver = cabinet.getLookup('.tsx');
console.log(typeof resolver === 'function');
}The array updates when register or unregister is called. Unknown containing-file extensions fall back to a generic dependency-path resolver.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| resolve | npm | You only need the classic Node require.resolve algorithm with packageFilter control and a much narrower dependency set |
| enhanced-resolve | npm | You specifically need webpack-style aliases, extensions, condition names, and plugin-driven resolution |
| tsconfig-paths | npm | Your only nonstandard requirement is resolving TypeScript baseUrl and paths mappings |