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
Whenxspec 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:
"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.
ESM vs CJS resolution
The extensionless specifier./AUTH.xspec resolves correctly in:
- CJS-mode files (files in a package without
"type": "module", or.ctsfiles) - Projects using
moduleResolution: "bundler"
"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: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.