> ## Documentation Index
> Fetch the complete documentation index at: https://xspec.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Setting Up TypeScript Compiler for xspec Modules

> Configure TypeScript to typecheck and emit xspec-generated modules correctly, with guidance on tsconfig, compile-in-place, ESM vs CJS, and linting expression statements.

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:

| File                       | Purpose                                                          |
| -------------------------- | ---------------------------------------------------------------- |
| `AUTH.xspec.ts`            | TypeScript entry point; re-exports from `AUTH.xspec.impl.js`     |
| `AUTH.xspec.impl.js`       | Plain JavaScript implementation (the actual runtime values)      |
| `AUTH.xspec.impl.d.ts`     | Declaration file carrying the full typed API                     |
| `AUTH.xspec.impl.d.ts.map` | Declaration map enabling go-to-definition into the `.mdx` source |

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:

```jsonc theme={null}
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true
  },
  "include": ["src", "test", "specs/*.xspec.ts"]
}
```

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:

```sh theme={null}
tsc -p tsconfig.json   # typechecks all references, emits JS beside sources
node src/print.js      # runs under plain Node — no loader, no dependency
```

## 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.

<Warning>
  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.
</Warning>

## 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.

<Note>
  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.
</Note>

## Lint configuration for expression statements

Marker statements are bare expression statements — a node path used alone on a line with no assignment or call:

```ts theme={null}
AUTH.auth.login.valid   // valid marker; a bare expression statement
```

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.

<Tip>
  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.
</Tip>
