npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@typed/virtual-modules

v1.0.0-beta.1

Published

Synchronous, type-safe virtual module primitives for TypeScript Language Service and compiler-host integrations.

Readme

@typed/virtual-modules

Synchronous, type-safe virtual module primitives for TypeScript Language Service and compiler-host integrations.

Purpose

Virtual modules are module IDs that do not map to files on disk; instead, plugins generate their content when the module is resolved. Use them for type-safe generated modules (e.g. route matchers, API clients), scaffolding from file structure, or config injection. For example, import { routes } from "router:./routes" lets a plugin scan the routes directory and generate typed route descriptors from the file layout. This package provides the core primitives: the plugin system, type-aware code generation (via TypeInfoApi), and adapters that integrate with the TypeScript Language Service (editors) and CompilerHost (tsc, vmc).

Install

pnpm add @typed/virtual-modules

Peer / dev: typescript (project supplies its own).

API overview

  • PluginManager – First-match plugin resolution; resolveModule(id, importer) returns resolved / unresolved / error.
  • VirtualModulePlugin – Contract: name, shouldResolve(id, importer?), build(id, importer?, api); sync only.
  • TypeInfoApicreateTypeInfoApiSession({ ts, program, maxTypeDepth? })api.file(relativePath, options) returns a FileSnapshotResult (check result.ok before using result.snapshot); api.directory(relativeGlobs, options) returns type snapshots. Both support optional watch descriptors.
  • NodeModulePluginLoader – Load plugins from disk via Node resolution (sync CJS / preloaded); load(plugin | { modulePath, baseDir }).
  • LanguageServiceAdapterattachLanguageServiceAdapter({ ts, languageService, languageServiceHost, resolver, projectRoot, watchHost?, debounceMs? }); patches resolution, script snapshot/version, fileExists, readFile, and diagnostics; returns a handle with dispose(). Adapter state is bounded by necessary files: records whose importer file no longer exists are evicted.
  • CompilerHostAdapterattachCompilerHostAdapter({ ts, compilerHost, resolver, projectRoot, watchHost?, debounceMs? }); patches resolution, getSourceFile/getSourceFileByPath, fileExists, readFile, hasInvalidatedResolutions; returns a handle with dispose(). Same eviction of records when the importer file no longer exists.

Building plugins

Plugin contract

A VirtualModulePlugin implements name, shouldResolve(id, importer)boolean, and build(id, importer, api)VirtualModuleBuildResult (sync only). The host (Language Service adapter, CompilerHost adapter, or Vite plugin) provides api (TypeInfoApi) when it supplies createTypeInfoApiSession; otherwise plugins receive a no-op api that returns safe defaults (file(){ ok: false }, directory()[]). Hosts should always supply createTypeInfoApiSession when plugins use the API for correct behavior. Rule: build() must not trigger module resolution—re-entrancy is detected and surfaces as diagnostics (see Production considerations).

Minimal static plugin (no api usage):

const configPlugin: VirtualModulePlugin = {
  name: "config",
  shouldResolve(id) {
    return id === "virtual:config";
  },
  build() {
    return `export const env = "dev";`;
  },
};

Type snapshots: api.file() and api.directory()

api.file(relativePath, { baseDir, watch? })FileSnapshotResult. Always check result.ok; on success use result.snapshot (TypeInfoFileSnapshot: filePath, exports as ExportedTypeInfo[]). On failure, result.error is 'file-not-in-program' | 'path-escapes-base' | 'invalid-input'. Path contract: baseDir must be absolute; relativePath must not escape it.

build(_id, importer, api) {
  const baseDir = dirname(importer);
  const result = api.file("types.ts", { baseDir });
  if (!result.ok) {
    return `export const names = [];`;
  }
  const names = result.snapshot.exports.map((e) => e.name);
  return `export const names = ${JSON.stringify(names)};`;
}

api.directory(relativeGlobs, { baseDir, recursive, watch? })readonly TypeInfoFileSnapshot[]. Globs are relative to baseDir (e.g. "*.ts" or ["**/*.ts", "**/*.tsx"]).

build(_id, importer, api) {
  const baseDir = dirname(importer);
  const snapshots = api.directory("*.ts", { baseDir, recursive: true });
  const paths = snapshots.map((s) => s.filePath);
  return `export const paths = ${JSON.stringify(paths)};`;
}

ExportedTypeInfo has name, type (serialized TypeNode tree), and optional declarationKind, declarationText, docs. The type field is used for structural checks below.

Resolving a single export: api.resolveExport()

api.resolveExport(baseDir, filePath, exportName)ExportedTypeInfo | undefined. Use when you know the file and need one export by name without a full snapshot.

const foo = api.resolveExport(baseDir, "./mod.ts", "foo");

Structural assignability: api.isAssignableTo() and type targets

api.isAssignableTo(node, targetId, projection?)boolean. Checks whether the type behind node (a serialized TypeNode from this API) is assignable to a pre-resolved target. Used to enforce contracts (e.g. "handler return type must be Effect").

TypeProjectionStep (optional third arg): { kind: "returnType" }, { kind: "param", index }, { kind: "typeArg", index }, { kind: "property", name } to navigate to a sub-type.

Assignability only works when the host creates the TypeInfo session with type targets:

  • typeTargetSpecs: { id, module, exportName, typeMember? }. Host resolves from the program; module specifier must match user imports exactly.
  • typeTargets: pre-resolved { id, type: ts.Type }.

Plugins that need assignability declare typeTargetSpecs on the plugin; the host merges them (e.g. via collectTypeTargetSpecs) and passes them into createTypeInfoApiSession. If the program does not import the target modules, use createTypeTargetBootstrapContent(typeTargetSpecs) and include the generated file in rootNames (see Production considerations).

// Validate handler return type is Effect (projection navigates to return type)
const handlerExport = snapshot.exports.find((e) => e.name === "handler");
if (handlerExport && !api.isAssignableTo(handlerExport.type, "Effect", [{ kind: "returnType" }])) {
  return { errors: [{ code: "CONTRACT-001", message: "handler must return Effect", pluginName: name }] };
}

Watch descriptors and errors

Watch: api.file(..., { watch: true }) or api.directory(..., { watch: true }) records descriptors; the host calls session.consumeDependencies()WatchDependencyDescriptor[] for invalidation.

Build errors: Return { errors: [{ code, message, pluginName }] } for structured failures. Use isVirtualModuleBuildError(result) to detect.

Reference implementations

Production considerations

  • Path contractbaseDir must be an absolute path; relativePath (and resolved paths) must not escape baseDir. Path containment is enforced: escaping returns an error (path-escapes-base or invalid-input).
  • Plugin contractbuild() must not trigger module resolution. Re-entrancy is detected and may surface as unresolved resolution or a diagnostic (re-entrant-resolution).
  • TypeScript – Tested with TypeScript 5.x. The host supplies its own typescript instance. TypeInfoApi uses a small set of internal TypeScript APIs (see Internal API usage in production-readiness) for type-target resolution and assignability fallbacks; these are centralized in internal/tsInternal.ts and may require adjustment when upgrading TS.
  • Type targets and resolution – Type targets (e.g. for assignableTo) are resolved from the same TypeScript program only: “remote” here means types from other packages already in the program (e.g. in node_modules), not from a registry. Resolution matches the exact module specifier used in imports; path/alias mapping is not applied. For targets not imported by app code, use createTypeTargetBootstrapContent(specs) and include the generated file in the program’s rootNames.
  • Watch debouncing – Optional debounceMs on adapter options coalesces rapid file/glob watch events to avoid recomputation storms.

Errors and gotchas

Full reference: virtual-modules-errors-and-gotchas. Summary:

  • api.directory() may throw if TypeScript internal APIs throw during type serialization for a matched file. Not a typed error.
  • api.file() returns a FileSnapshotResult: either { ok: true, snapshot } or { ok: false, error, path? }. Possible error values: 'file-not-in-program' (path not in the program), 'path-escapes-base' (path would escape baseDir), 'invalid-input' (e.g. empty baseDir, null bytes, or invalid relativePath). Always check result.ok before using result.snapshot.
  • Adapter diagnostics – The Language Service adapter may attach diagnostics for plugin failures (e.g. plugin-build-threw, plugin-should-resolve-threw), invalid build output (invalid-build-output), invalid options (invalid-options), re-entrant resolution (re-entrant-resolution), or virtual module rebuild failures. Plugin names and messages are included.
  • Loader errorsNodeModulePluginLoader.load() returns structured errors with codes such as module-not-found, module-load-failed, invalid-plugin-export, path-escapes-base, and invalid-request (e.g. empty baseDir or specifier).

Status

Early implementation. Covers plugin manager, TypeInfo API, Node loader, and LS/CompilerHost adapters. See package spec and tests for behavior and testing strategy.