Shared base tsconfig.json for InfiniteToken TypeScript packages. Holds the compiler flags every repo agrees on (strictness, declaration output, consistent casing). Also ships opt-in tsup build config factories for a library, a CLI, or both — see tsup config below.
| Export | Use for |
|---|---|
@infinitetoken/tsconfig / @infinitetoken/tsconfig/tsconfig.json |
Universal core — strictness-only flags (strict, declaration, noUnusedLocals, etc). Rarely used directly; every variant below extends it. |
@infinitetoken/tsconfig/node |
Node/kit packages, RN library packages (via /react-native, below) — target: esnext, module: preserve, moduleResolution: bundler, types: [jest, node] |
@infinitetoken/tsconfig/server |
Express/server apps (unpublished) — module: commonjs, resolveJsonModule: true, types: [jest, node], and relaxes six strictness flags (declaration, declarationMap, noUnusedLocals, noUnusedParameters, noImplicitReturns, noFallthroughCasesInSwitch) back to false |
@infinitetoken/tsconfig/react-native |
React Native packages (bare RN, and RN libraries published to npm) — extends /node, adds target: ESNext, lib: [DOM, ESNext], jsx: react-jsx, sourceMap: true, allowSyntheticDefaultImports: true, resolveJsonModule: true, noEmit: true, customConditions: [react-native] |
@infinitetoken/tsconfig/expo |
Expo apps — extends /react-native, adds moduleDetection: force, allowJs: true |
{
"extends": "@infinitetoken/tsconfig/node",
"compilerOptions": {
"rootDir": "./src"
},
"include": ["src/**/*"]
}That's the whole file for a package that builds with tsup (and its dts: true, via @infinitetoken/tsconfig/tsup/lib below) — every other compiler option now comes from the preset. rootDir/include (and exclude, if you actually need one, and outDir if tsc itself ever emits — see below) are the only things that ever need to be local, because of a real TypeScript limitation: an inherited path-valued compilerOptions entry, or an inherited include/exclude, resolves relative to the file that declared it, not the file that extends it. If this package tried to ship rootDir/include/exclude itself, an unoverridden consumer would have TypeScript looking for source files inside node_modules/@infinitetoken/tsconfig/. target/module/moduleResolution/types/lib/etc. don't have this problem — plain compilerOptions values merge normally through extends, which is exactly why the presets above can carry them for you.
You very likely don't need an exclude array at all. include: ["src/**/*"] already scopes the whole program to src/ — anything living outside it (node_modules, dist, .claude/worktrees, examples, etc.) was never going to be included in the first place, so excluding it again is redundant. Only add exclude if you have something that genuinely lives inside src/ that you need to keep out of the program.
You very likely don't need outDir either, for the same reason. It's only load-bearing when tsc itself emits — --emitDeclarationOnly, or a plain build via tsc. If the package builds with tsup and lets its own dts: true handle declarations (the normal setup in this fleet — see tsup config), tsc only ever runs --noEmit for type-checking, and outDir has nothing to do — confirmed with a byte-identical dist/ diff with and without it. Add it back only if typecheck/build genuinely calls tsc without --noEmit.
rootDir is narrower than the note above once said — it's only required when something runs tsc with actual declaration emission enabled (--declaration/--emitDeclarationOnly, or a plain non---noEmit build): without it, that specific invocation fails with TS5011 ("the common source directory... rootDir must be explicitly set"). Verified directly: raw tsc --declaration --emitDeclarationOnly throws TS5011 on a rootDir-less config, every time. But tsup's own dts: true step does not go through this same check — confirmed by running npx tsup --dts directly against a rootDir-less config: clean build, byte-identical output, no error or warning. So in the fleet's actual, normal setup (typecheck = tsc --noEmit only, build = tsup), neither script ever emits declarations via raw tsc, and rootDir genuinely is dead weight — confirmed with byte-identical dist/ diffs across every repo in the fleet audited so far. Add it back only if a script in this specific package ever runs tsc with declaration emission directly (not through tsup) — check package.json's scripts, don't assume.
react-native/expo consumers don't need to think about any of this — those two presets set noEmit: true themselves, because an Expo/RN app's own tsc is always typecheck-only (Metro/EAS does the actual build), so a bare "typecheck": "tsc" script just works with no flags and no rootDir/outDir.
A typical Expo app's tsconfig.json — paths still has to be written out by hand (path-valued options can't be centralized, same rule as above), but this is the shape that's converged on independently across every real Expo app in this fleet:
{
"extends": "@infinitetoken/tsconfig/expo",
"compilerOptions": {
"paths": {
"@/*": ["./*"],
"@/components/*": ["./src/components/*"],
"@/hooks/*": ["./src/hooks/*"],
"@/utils/*": ["./src/utils/*"]
},
"types": ["react-native", "jest"]
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": [".claude/worktrees/**"]
}A few things worth knowing before copying this in:
includeis repo-root-scoped (**/*.ts/**/*.tsx), notsrc/**/*— the"@/*": ["./*"]root alias implies files outsidesrc/(app config, root scripts) may need to type-check too.typeshas to be the app's own full list regardless of whatexpo/react-nativeset — TypeScript'stypesoption is fully replaced, not merged, by the most-derived config. Only add"node"to it if@types/nodeis actually installed — otherwisetscfails immediately with "Cannot find type definition file for 'node'."- Add your own real folder aliases to
paths(@/redux/*,@/context/*, etc.) — the three above are just examples. - If
app.jsonhas"experiments": { "typedRoutes": true }, also add".expo/types/**/*.ts"and"expo-env.d.ts"toinclude. TypeScript'sincludeglobs don't traverse dot-directories by default (verified directly —**/*.tssilently skips anything under.expo/), so without that entrytscwon't see Expo Router's generated route-param types. It's not a hard failure either way —Hrefjust falls back to a looser, unchecked type — but it is a silent loss of the type-checking the feature is supposed to provide. - The first migration in an app almost never a pure config swap.
expo/tsconfig.base(what every app currently extends) sets no strictness at all —strict,noUnusedLocals,noUnusedParameters, andnoImplicitReturnsall come from@infinitetoken/tsconfig's base preset, for the first time, the moment an app switches. Expect real, pre-existing violations to surface immediately (unusedReactimports left over from before the new JSX transform, unused variables, a branch genuinely missing areturn) — verified directly against one real app in this fleet, which surfaced 10 such errors on the firsttscrun. Budget a small cleanup pass per app, not just a one-lineextendschange.
@infinitetoken/tsconfig/skia-paths — opt-in, only relevant to apps that import @shopify/react-native-skia (an Expo/RN app rendering to a Canvas; AirHockey, BoxHockey, and Pong in this fleet so far). It works around a real typecheck failure, not a hypothetical one:
@shopify/react-native-skia ships no exports map, so its top-level "react-native" field resolves bare imports to raw, untranspiled src/index.ts. The /react-native and /expo presets' own customConditions: ["react-native"] (matching Metro's real resolution) correctly follows that field, which pulls the package's own web/WithSkiaWeb.tsx into the typecheck program — and that file has a genuine unused React import upstream (present in the latest published version too, not just one pinned release) that fails under this package's base preset's noUnusedLocals. Weakening customConditions would break other, unrelated deps' own exports-map "react-native" conditions; weakening noUnusedLocals would turn off a check that's doing real work everywhere else. Instead, the fragment redirects the package — and its own directly-imported /src/web deep path, used to lazy-load the CanvasKit WASM runtime on web — to their compiled .d.ts counterparts for typecheck purposes only:
{
"@shopify/react-native-skia": ["./node_modules/@shopify/react-native-skia/lib/typescript/src/index.d.ts"],
"@shopify/react-native-skia/src/web": ["./node_modules/@shopify/react-native-skia/lib/typescript/src/web/index.d.ts"]
}This can't be pulled in through extends the way the presets above are. TypeScript does not merge nested compilerOptions.paths objects across an extends chain — verified directly against this package's own installed TypeScript (6.0.3): whichever config sets paths last in resolution order wins the whole object, not just the keys it names. A two-file fixture where the override's paths didn't even share a key with the base's still ended up with only the override's entry; the base's was silently dropped, not merged, and the same thing happens between two array-extends entries with no local override at all. Since every real consumer of this fragment already sets its own paths (its @/* aliases), adding @infinitetoken/tsconfig/skia-paths to an extends array alongside those aliases would just make one of the two disappear, not combine them.
So the opt-in is a spread, not an extends entry — the same shape as spreading @infinitetoken/tsconfig/tsup/lib-cli's own array result into a bigger tsup config in tsup config below. Anywhere that already runs through Node, spread it directly:
const skiaPaths = require('@infinitetoken/tsconfig/skia-paths')
const paths = { ...appOwnPaths, ...skiaPaths }A plain hand-authored tsconfig.json has no require() to run — the fragment is JSON, not executable — so there the "spread" is inlining its two entries verbatim next to your own aliases, with src/skia-paths.json in this package as the one canonical copy to diff against (instead of three independently-drifting copies of the same block) whenever @shopify/react-native-skia's own layout changes:
{
"extends": "@infinitetoken/tsconfig/expo",
"compilerOptions": {
"paths": {
"@/*": ["./*"],
"@/components/*": ["./src/components/*"],
// spread from @infinitetoken/tsconfig/skia-paths — see that package's README before editing
"@shopify/react-native-skia": ["./node_modules/@shopify/react-native-skia/lib/typescript/src/index.d.ts"],
"@shopify/react-native-skia/src/web": ["./node_modules/@shopify/react-native-skia/lib/typescript/src/web/index.d.ts"]
}
},
"include": ["**/*.ts", "**/*.tsx"]
}Opt-in — only relevant if the package builds with tsup. Unlike tsconfig.json, a tsup.config file is executable code with no path-resolution sharing limitation, so these are plain factory functions — same shape as @infinitetoken/jest-config's presets, a bare function you call, nothing invoked by name off an object. Use a tsup.config.cjs (not .ts) so it loads directly with require() instead of going through tsup's ESM-bundling loader. Pick the export that matches what the package actually ships:
| Export | Use for |
|---|---|
@infinitetoken/tsconfig/tsup/lib |
A library with no CLI — one entry, cjs+esm, declarations, cleans dist/ |
@infinitetoken/tsconfig/tsup/cli |
A CLI with no separate library export (main/exports point straight at the built bin) — one entry, esm-only, Node shebang banner, no declarations |
@infinitetoken/tsconfig/tsup/lib-cli |
A package that ships both — a real, separately-importable library and a CLI bin — the library entry (cjs+esm, declarations, cleans dist/) plus the CLI entry (esm-only, shebang, no declarations) |
The plain library case — one entry point, nothing else:
// tsup.config.cjs
module.exports = require('@infinitetoken/tsconfig/tsup/lib')()Pass a different entry or override anything tsup accepts: require('@infinitetoken/tsconfig/tsup/lib')('src/main.ts', { minify: true }).
A CLI-only package:
// tsup.config.cjs
module.exports = require('@infinitetoken/tsconfig/tsup/cli')()A package with both a library and a CLI:
// tsup.config.cjs
module.exports = require('@infinitetoken/tsconfig/tsup/lib-cli')()That returns the full two-entry array (createTsupLibCliConfig(libEntry?, cliEntry?, { lib?, cli? }) — defaults to src/index.ts / src/cli.ts). A package with an extra entry beyond those two (e.g. a separate subpath export) can spread it and append its own:
// tsup.config.cjs
const { defineConfig } = require('tsup')
const createTsupLibCliConfig = require('@infinitetoken/tsconfig/tsup/lib-cli')
module.exports = defineConfig([...createTsupLibCliConfig(), { entry: ['src/shapes.ts'], format: ['esm', 'cjs'], dts: true }])All three default dts: true on every entry that represents real importable code (never on a CLI entry — nobody imports a bin as a typed library) — a package doing tsup && tsc --emitDeclarationOnly as two separate steps, rather than letting tsup's own entry-scoped dts option handle it, will end up shipping declarations for every file tsconfig.json's include matches, __tests__ included, not just the real entry point.
Tag-based, using npm trusted publishing (OIDC, no token required):
npm version patch # or minor / major
git push --follow-tags