Skip to main content
xspec-generated modules are standard TypeScript — they require no xspec runtime dependency and work with the TypeScript compiler you already have. A small amount of tsconfig.json configuration ensures that types resolve correctly, go-to-definition lands in your .mdx sources, and emitted JavaScript runs under plain Node without any loader.

Generated file structure

When xspec build compiles a spec file named AUTH.mdx, it produces four files alongside it: Your source files import via ./AUTH.xspec. TypeScript resolves that specifier to AUTH.xspec.ts, which re-exports everything from the .impl.js companion. The .d.ts.map file is what makes your editor jump to the correct <S> section in the .mdx file when you use go-to-definition.

Minimal tsconfig.json

The following configuration is the verified minimum for working with xspec modules:
The critical addition relative to a default config is "specs/*.xspec.ts" in the include array. This instructs tsc to emit JavaScript for the generated entry points alongside their .impl.js companions so that Node can resolve the full module graph at runtime. Once compiled, you can run your code under plain Node — no custom loader, no xspec runtime package:

Compile in place — do not use outDir

xspec modules must be compiled in place, meaning emitted JavaScript sits beside its source. Do not use the outDir compiler option with xspec modules. The reason: when outDir is set, tsc copies NAME.xspec.js to the output directory but does not copy the NAME.xspec.impl.js companion. At runtime, the entry point tries to re-import from .impl.js and fails because the companion is missing from the output tree.
Do not set outDir in a project that imports xspec modules. If you need a separate output tree — for example for distribution — use a bundler instead of tsc --outDir. A bundler will inline both files correctly.

ESM vs CJS resolution

The extensionless specifier ./AUTH.xspec resolves correctly in:
  • CJS-mode files (files in a package without "type": "module", or .cts files)
  • Projects using moduleResolution: "bundler"
In a "type": "module" package, ESM resolution rules require explicit file extensions, which makes extensionless specifiers unresolvable by Node’s native ESM loader.
If your package uses "type": "module", consume xspec modules from CJS-mode files (.cts extension or a sub-package without "type": "module") or process them through a bundler. The bundler path works because bundlers resolve extensionless specifiers before any Node ESM rules apply.

Lint configuration for expression statements

Marker statements are bare expression statements — a node path used alone on a line with no assignment or call:
Some linters (ESLint’s no-unused-expressions rule, for example) flag expression statements as errors by default, since they typically indicate a forgotten assignment or a no-op. If your linter reports markers as unused expressions, configure it to allow expression statements that consist only of member access chains. With ESLint, you can use an inline disable comment on marker lines, or adjust the rule configuration to permit this pattern project-wide. The exact setting depends on your linter and rule version, but the principle is the same: the expression is intentional and must be left as a statement.
A project-wide ESLint override targeting *.ts files that allows member-expression statements is cleaner than per-line comments, especially in test files where markers tend to cluster together.