Basilic
Architecture

ESM & TypeScript Strategy

ESM-first packages, dual-mode exports, TypeScript 7 for tsc, TypeScript 6 for tools that need the compiler API.

Packages are "type": "module". ESM-only — tsup uses format: ['esm']; no CJS emit.

The same @repo/* graph is loaded by a bundler (Next: can compile TS) and by naked Node (Fastify / Vercel: native ESM, .js only). That split is Pattern A vs B. prepack switches remaining src/ paths to dist/ for npm. Details: Publishing.

Dual-mode exports

Pattern A (@repo/core, @repo/react, @repo/ui): import points at src/ in the workspace. Next.js lists them in transpilePackages. Workspace build is a no-op (@repo/ui never runs tsup locally).

Pattern B (@repo/utils, @repo/error, @repo/email): source for bundlers → src/; node / import / default for compiled dist/ so serverless resolvers do not hit ERR_MODULE_NOT_FOUND.

@repo/cli: bin entry is dist/ only (Node CLI, not imported by apps).

Pattern B JS is tsup (format: ['esm']); .d.ts is tsc --declaration (dts: false on tsup).

Prefer subpath imports (@repo/utils/async). Do not deep-import src/.

TypeScript 7 and 6

TypeScript 7 is the native Go compiler ("@typescript/native": "npm:typescript@^7.0.2", bin tsc). Package checktypes and emit scripts invoke that binary. ESLint and Next.js keep TypeScript 6 via "typescript": "npm:@typescript/typescript6@^6.0.2" where the programmatic API is required. Next.js 16.3 would call typescript/bin/tsc; set experimental.useTypeScriptCli: false so it uses the TS 6 API instead.

Workspace build for Pattern A packages (@repo/core, @repo/react) is a no-op; prepack runs tsup and declaration emit for npm publish. Next.js apps that also depend on TypeScript 6 for the compiler API invoke the native CLI explicitly: node ../../node_modules/@typescript/native/bin/tsc --noEmit.

Shared configs live in tools/typescript (base.json, nextjs.json, react-library.json). ts-reset is on. types: ["*"] restores automatic @types/* (TS 7 defaults types to []).

Resolution

RuntimemoduleResolutionRelative imports
Next.js 16, React librariesbundlerextensionless OK
Fastify / NodeNodeNextuse .js in the import path (emit), never .ts

from './env.js' inside env.ts is the NodeNext emit specifier (the file on disk after tsc is .js). It is not CJS and not a second source file. Never write .ts in those import paths.

Node dev: tsx. Next: transpilePackages plus webpack conditionNames: ['source', …] and .js.ts extensionAlias (required in Next 16.3.3 — Turbopack lacks resolveExtensionAlias). Scripts pass --webpack until Turbopack supports workspace .js imports. Import @repo/*, not relative paths into packages/.

Until

Delete these when the blocker ships:

  • --webpack / extensionAlias — until Turbopack resolveExtensionAlias
  • TS 6 alias / useTypeScriptCli: false — until Next.js and ESLint use the TypeScript 7 compiler API
  • Pattern B source condition — until Node can load workspace TypeScript through exports (type-stripping alone does not rewrite .js.ts)

Generated OpenAPI client (packages/core/src/gen/) and skill templates are excluded from hand-maintained TS rules. Conventions: .cursor/rules/base/typescript.mdc.

On this page