Skip to content

v8 N-14: publish per-file tsc output (TypeScript 7, no bundler) - #617

Draft
tlgimenes wants to merge 3 commits into
v8-13-drop-v7from
v8-14-tsc-build
Draft

tlgimenes wants to merge 3 commits into
v8-13-drop-v7from
v8-14-tsc-build

Conversation

@tlgimenes

@tlgimenes tlgimenes commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The 8 publishable packages (@decocms/blocks, plus @decocms/apps-{shopify,vtex,wake,magento,algolia,resend,sfmc-personalization}) publish raw .ts. Plain Node can't import .ts from node_modules, so these can't use them without a TypeScript loader: Playwright, scripts, Node servers, and Next without transpilePackages. Studio's e2e fixture currently needs tsx register() just to load @decocms/blocks/protocol.

Approach: compile file by file, no bundler

  • Each package now has a tsconfig.build.json (extends tsconfig.build.base.json: rootDir src, outDir dist, module/moduleResolution: NodeNext, rewriteRelativeImportExtensions, declaration + declarationMap + sourceMap). Tests and fixtures are excluded.
  • The compiler is the TypeScript 7 native compiler (typescript7, an npm alias of [email protected]), run through scripts/tsc.mjs. The plain typescript name stays on 5.x because TS 7 has no JS compiler API, and deco schema (plus Next type-checks and a few conformance tests) load typescript through that API.
  • --clean dist runs before every build, so a deleted or renamed source never leaves stale output behind.
  • Why there's still one module instance: each src/x.ts becomes exactly one dist/x.js, and relative imports are rewritten from ./x.ts to ./x.js. The module graph in dist matches the source graph edge for edge. No file is inlined into another, so a registry or singleton module loads once, which is what CLAUDE.md requires after the tsup incident. On top of that, createCMS keys its instance on Symbol.for on globalThis, so even two forced copies of cms.js return the same CMS.
  • Declaration maps point back to src, so the schema ids that deco schema builds from types stay @decocms/blocks/src/v8/types.ts@Seo instead of dist/...d.ts.

Exports and conditions

  • Every subpath is now { "types": "./dist/….d.ts", "source": "./src/….ts", "default": "./dist/….js" }. source is for tools that compile TypeScript and want .ts.
  • Conditional exports keep their order: /protocol/storage/fs still lists node before default, and Workers/browser conditions resolve to unsupported.js, as the original map did.
  • files ships dist plus src, because the source condition and the declaration maps both point into src. Tests are not shipped.
  • Workspace dev stays fast. The root tsconfig (customConditions: ["source"]) and vitest resolve the source condition, so typechecks and tests run on .ts with no build step. examples/nextjs-smoke consumes the built dist like any npm package, and bun install builds the workspace.

deco bin without tsx

bin/deco.js imports dist/v8/cli/main.js directly, so plain Node, Bun and npx run it with no loader. tsx is no longer a runtime dependency. typescript stays an optional peer, and only deco schema loads it, when it runs.

Release and CI

  • Release: semantic-release's prepareCmd is sync-versions.mjs && bun run build before each npm publish. Each package also has "prepack": "npm run build".
  • CI runs typecheck, then build, then a step that imports every built export of every package with plain node.
  • CLAUDE.md's package-exports section now states the rule: per-file tsc output, no bundler, and why that keeps one module instance.
  • Also in this PR: @opentelemetry/api is now a devDependency of packages/blocks, and knip ignores it. The v7 removal dropped it, but the telemetry docs' snippet fixture still imports it, so observability.test.ts failed on a fresh install.

Verification

I packed the 8 packages the way the release does (sync-versions at 8.99.0, bun run build, npm pack) and installed the tarballs into a clean folder outside the repo. The checks below used Node 26.9.

  • Tarballs: blocks has 402 files, apps-wake 46, apps-vtex 31, and the other apps 6 to 16 each. None contain test files.
  • Plain Node import of every subpath: all 19 entry points import and run in plain Node, and in Bun too. They are @decocms/blocks and its subpaths /analytics, /cli, /fetch, /secrets, /protocol, /protocol/keys, /protocol/server, /protocol/storage/fs and /protocol/conformance, the 7 app clients, and @decocms/apps-wake/storefront and /storefront/types.
  • Smoke test: createCMS plus forRelease().resolve("Card") resolves a nested reference. A missing block returns NOT_FOUND.
  • One instance of each module: loading 10 entry points together pulled in 83 distinct @decocms files. dist/v8/cms.js loaded exactly once, and no src/*.ts file loaded. With two forced copies of cms.js, both createCMS calls return the same instance.
  • Conditions: Node with --conditions=workerd or browser resolves to dist. esbuild with the Wrangler set (workerd,worker,browser, --platform=browser) picks protocol/storage/fs/unsupported.js. --platform=node picks the real fs storage.
  • deco CLI: npx deco --help works. On a fixture site, deco schema, deco content and deco check all succeed (2 blocks saved, 0 errors). The schema ids point at src/*.ts.
  • Consumer types: a file importing every subpath typechecks with --strict --skipLibCheck false under NodeNext and under Bundler, on TS 5.9.3 and on TS 7.0.2. It includes a @ts-expect-error line that fails if the types come through as any.
  • Studio: I ran Studio's blocks-protocol e2e fixture without tsx installed. runConformance against createContentHandler(createFsStorage(...)) gave 33 passed, 0 failed, 14 skipped.
  • Repo: bun install --frozen-lockfile, bun run typecheck, bun run build, bun run test (1740 passed, 104 skipped) and bun run check all pass.

Things to know, none caused by this PR: running npm pack without sync-versions leaves workspace:* in the app tarballs, and the release always runs sync first. Running Node with --conditions=source against node_modules fails with ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING, which is expected.


Stack: v8 ← 01 ← … ← 12 ← 13 ← 14

🤖 Generated with Claude Code

https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig


Summary by cubic

The 8 publishable packages (@decocms/blocks and the 7 @decocms/apps-*) now publish compiled JavaScript instead of raw .ts, so plain Node (scripts, Playwright, Node servers, Next without transpilePackages) can import them from node_modules without a TypeScript loader.

Build and publish

  • Each package compiles file-for-file with the TypeScript 7 native compiler, no bundler: every src/x.ts becomes one dist/x.js plus .d.ts and maps, with relative imports rewritten to .js, so module boundaries (and the registry singleton) match the source exactly.
  • Every export map entry is now { types, source, default }; files ships dist and src (tests excluded), and declaration maps point back to src so deco schema ids keep their src/... paths.
  • The root build compiles @decocms/blocks before the apps, which resolve it through its dist types; a parallel build raced blocks' tsc and failed in prepare.
  • prepack and the release prepareCmd build before each publish; new CI type-checks, builds, and imports every built export with plain Node.
  • The nextjs-smoke example drops transpilePackages.

CLI and tooling

  • bin/deco.js now runs the compiled dist/v8/cli/main.js, so deco works under plain Node, Bun, and npx with no loader; tsx is no longer a runtime dependency.
  • Builds and type-checks use TypeScript 7 (typescript7 alias); typescript stays on 5.x because deco schema, Next type-checks, and a few conformance tests load it through its JS compiler API, which TypeScript 7 lacks.
  • In-repo type-checks and vitest resolve packages through the new source condition, so local work keeps running .ts without a prior build.
  • Re-adds @opentelemetry/api as a devDependency of packages/blocks to fix a fresh-install observability.test.ts failure.

Written for commit 013607c. Summary will update on new commits.

View guided diff Turn on auto-fix

tlgimenes and others added 3 commits October 3, 2026 18:54
The 8 packages published raw .ts, which plain Node (scripts, Playwright,
Node servers, Next without transpilePackages) can't import from
node_modules. Each package now compiles file-for-file with the TypeScript 7
native compiler: every src/**/*.ts(x) becomes one dist/**/*.js plus .d.ts
and maps, imports left as imports, so module identity and singletons are
exactly the source's (no bundle, no inlined second copy).

- tsconfig.build.json per package (rootDir src, outDir dist, NodeNext,
  rewriteRelativeImportExtensions, declaration + maps, tests excluded);
  relative imports in emitted files now carry .ts extensions.
- exports: { types, source, default } per subpath (node/default order kept
  for /protocol/storage/fs); files ship dist + src; main/types point at dist.
- deco bin runs dist/v8/cli/main.js; tsx is no longer a runtime dependency.
  typescript stays the 5.x peer that `deco schema` loads (TS 7 has no JS API);
  builds use the `typescript7` alias through scripts/tsc.mjs.
- `deco schema` names a compiled .d.ts after its source (declaration map),
  so schema definition ids are the same from dist or source.
- Workspace: root prepare builds on install; type-checks and vitest resolve
  @decocms/* through the "source" condition; tests/globalSetup.ts rebuilds
  blocks' dist for the CLI tests that spawn the bin.
- Release prepareCmd builds after the version sync; each package's prepack
  rebuilds. New CI workflow: typecheck, build, plain-Node import of every
  built export.
- nextjs-smoke drops transpilePackages. CLAUDE.md documents the build rule.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…servability conformance fixture

The v7 removal dropped it from dependencies, but the telemetry docs' snippet
fixture still imports it for typechecking, so a fresh install failed
observability.test.ts. Dev-only; knip ignores it since only the fixture uses it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…ot to decocms-blocks

The apps resolve @decocms/blocks/* through its dist types, so a parallel
--filter build raced blocks' tsc and failed with TS2307 in prepare.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant