👷 migrate module builds from TypeScript compiler API to tsdown#4767
👷 migrate module builds from TypeScript compiler API to tsdown#4767thomas-lebeau wants to merge 9 commits into
Conversation
🎉 All green!🧪 All tests passed 🎯 Code Coverage (details) 🔗 Commit SHA: f510dab | Docs | Datadog PR Page | Give us feedback! |
5af4990 to
bf28c82
Compare
Bundles Sizes Evolution
|
8533ccc to
31c1d92
Compare
| "build": "yarn workspaces foreach --all --parallel --topological-dev run build", | ||
| "build:bundle": "yarn workspaces foreach --all --parallel --topological-dev run build:bundle", |
There was a problem hiding this comment.
question: why do we need those -dev? Also build:bundle don't need --topological because all bundles are built including all source files, not built artifacts. Paths are resolved from tsconfig.base.json.
There was a problem hiding this comment.
because I added browser-worker as a dev dependency, in Theory this is needed because it need to builds the worker before building browser-rum.
In practice, needsWorkerRebuild() in buildEnv deals about it. Ideally I would get rid of it, but it is still needed because unit tests don't build first.
| define: Object.fromEntries( | ||
| referencedBuildEnvKeys().map((key) => [`__BUILD_ENV__${key}__`, JSON.stringify(getBuildEnvValue(key))]) | ||
| ), |
There was a problem hiding this comment.
suggestion: We can probably define all build env even if they are unused?
define: Object.fromEntries(buildEnvKeys.map((key) => [`__BUILD_ENV__${key}__`, getBuildEnvValue(key)])),There was a problem hiding this comment.
The filtering is there specifically because WORKER_STRING isn't a pure value like the others — evaluating it has a side effect. getBuildEnvValue('WORKER_STRING') reads packages/browser-worker/bundle/worker.js (and rebuilds the worker if it's missing/stale). Only browser-rum actually references __BUILD_ENV__WORKER_STRING__ (in deflateWorker.ts).
Since tsdown's define is eager — every listed key is computed up front, for every package — defining all keys unconditionally makes every package's build read the worker bundle, even though only browser-rum needs it. In the parallel yarn build that's a problem: browser-debugger/logs/etc. end up reading worker.js while browser-worker is concurrently rebuilding it (its build does fs.rm('./bundle') first), which gives a flaky ENOENT.
There was a problem hiding this comment.
I see! Thank you for the explanation. Makes sense then, however this worker string brings a lot of complexity, if we were able to build it fast and in-memory we wouldn't need to cache it on disk, so no dependency needed and no concurrent access to the files. Let's keep it in mind :)
| }) | ||
|
|
||
| context = combine( | ||
| callMonitored(() => { |
There was a problem hiding this comment.
nitpick: we should be able to remove the monitored decorator no?
There was a problem hiding this comment.
we could yes. I think it's good to have a decorator for any modern consumer that might want to use it.
To use it in the browser-sdk while building with tsdown we would need to add a dependency for the decorator pollyfill. tsc inline it OOTB, but not tsdown and I though it's not worth it complicating the build to inline it for just this one usage.
Replace the TypeScript compiler API (`ts.createProgram`/`program.emit`) with tsdown (Rolldown-based) for transpiling package modules to CJS and ESM. Declarations are still emitted by `tsc` (`--emitDeclarationOnly`): Rolldown's declaration bundler restructures modules in ways that break older TypeScript consumers (inline `type` modifiers, `.js` import extensions, rewritten re-exports that trip `isolatedModules` on const enums). Keeping tsc for `.d.ts` makes the declaration output identical to before, preserving the TS 3.8+ compatibility matrix. Declarations are emitted only next to the CJS output, which every package's `types` field already points at. oxc lowers the one decorator in the codebase (`@monitored`) to a helper imported from `@oxc-project/runtime` and cannot inline it. To avoid adding a runtime dependency, the used helpers are vendored into the output and their imports rewritten to local copies. Build-time constants (`__BUILD_ENV__*__`) are inlined via tsdown's `define` option instead of post-processing emitted files. ESM output uses the `.mjs` extension, so the `--esm-type-module` flag (which wrote `esm/package.json`) is no longer needed.
- Replace @monitored decorator on Logger.logImplementation with callMonitored to avoid tsdown emitting oxc runtime helpers - Remove vendorOxcRuntimeHelpers workaround from build-package.ts - Drop @oxc-project/runtime devDependency and its LICENSE-3rdparty.csv entry
Declare @datadog/browser-worker as a devDependency of browser-rum and switch the build command to --topological-dev so yarn's workspace topological ordering guarantees browser-worker is built before browser-rum. Previously, the parallel build could start browser-rum before browser-worker produced bundle/worker.js, causing a flaky ENOENT error in getBuildEnvValue.
- add getEntries() to pick entry globs based on whether the package uses src/index.ts + src/entries or top-level src modules - set tsdown `root` to ./src so unbundle output mirrors the src layout (e.g. src/entries/main.ts -> cjs/entries/main.js) instead of being flattened to the entries' common ancestor - drop the `deps.neverBundle` external override
- bundles no longer depend on each other's build output, so parallel ordering is unnecessary after the tsdown migration
- move js-core time.ts to src/entries/time.ts so its public surface lives under src/entries - drop getEntries() heuristic; always derive tsdown entries from src/index.ts + src/entries/*.ts - update js-core exports, time/package.json, and tsconfig path to the new location
- update js-core AGENTS.md: sub-paths live under src/entries/, ESM output uses .mjs (no esm/package.json needed), and tsconfig paths point at src/entries - add TODO in build-package.ts explaining why declarations use emitDeclarations instead of tsdown dts (needs TS 4.7+ for bundler resolution and inline type)
9f852fb to
edc5440
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f510dab3a5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| platform: 'neutral', | ||
| unbundle: true, | ||
| dts: false, | ||
| tsconfig: '../../tsconfig.base.json', |
There was a problem hiding this comment.
Set an ES2020 target for tsdown
issue: Passing the tsconfig here does not make tsdown use compilerOptions.target; per tsdown's target option docs, when no engines.node is present it applies no syntax lowering. Several packages have no engines field and the sources already contain ES2021 logical assignment (for example worker ??= in packages/browser-rum/src/boot/recorderApi.ts and ||= in segment.ts), which the previous TypeScript module build lowered because tsconfig.base.json targets ES2020. As a result, npm consumers that serve or bundle these module files without transpiling node_modules can now receive syntax above the SDK's ES2020 output target.
Useful? React with 👍 / 👎.
| }) | ||
|
|
||
| context = combine( | ||
| callMonitored(() => { |
There was a problem hiding this comment.
Preserve pre-init Logger exceptions
issue: The old @monitored decorator only wrapped the method after startMonitorErrorCollection() was installed; before Logs init (or for consumers instantiating the exported Logger directly) it invoked the original method and let errors from the log strategy surface. callMonitored() always catches and swallows those errors, so a throwing handleLogStrategy before initialization now fails silently instead of preserving the previous public Logger behavior.
Useful? React with 👍 / 👎.
| // and remove emitDeclarations. The bundler output needs inline `type` modifiers (TS 4.5+) and | ||
| // `.mjs`/`.js` extensioned import paths that only resolve under `node16`/`bundler` module | ||
| // resolution (TS 4.7+), so 4.7 is the floor where it works. | ||
| await tsdownBuild({ |
There was a problem hiding this comment.
Keep const enums erased in module output
issue: Switching the JS module emit to tsdown here makes exported const enums become runtime enum objects instead of the string/number literals that the previous TypeScript emit produced. The codebase relies on erasing these for size (for example LifeCycleEventType is documented as avoiding a ~2KB minified cost), and dependent packages import browser-core/rum-core const enums such as ErrorHandling, DOM_EVENT, and LifeCycleEventType, so npm consumers now pull those enum objects into their bundles instead of inlined constants.
Useful? React with 👍 / 👎.
Motivation
The TypeScript compiler API (
ts.createProgram) used for building package modules is slow and verbose. This was called out in a TODO comment inbuild-package.ts:This migrates module transpilation to tsdown (backed by Rolldown/Rust).
Changes
tsdowninstead ofts.createProgram/program.emit.unbundle: truepreserves per-file output (matching tsc).outDirkeeps the./cjsand./esmlayout. ESM now uses.mjs, so the--esm-type-moduleflag (which wroteesm/package.json) is removed.defineinlines build-time constants (__BUILD_ENV__*__) at transpile time, replacing the oldreplaceBuildEnvInDirectorypost-processing.tsc(--emitDeclarationOnly,dts: falsein tsdown). Rolldown's declaration bundler restructures modules in ways that break older TypeScript consumers — inlinetypemodifiers (TS 4.5+),.js/.mjsextensions on declaration imports (unresolvable undermoduleResolution: node< TS 4.7), andexport ... fromre-exports rewritten intoimport+ localexport(tripsisolatedModuleson re-exportedconst enums). Keeping tsc for.d.tsmakes declaration output identical to before, preserving the full TS 3.8+ compatibility matrix. Declarations are emitted only next to the CJS output, since every package'stypesfield (and thetypescondition inexports) already points at./cjs.Test instructions
yarn build→yarn typecheck— passes.scripts/cli typecheck test/apps/vanilla— passes.yarn test:compat:tsc— all checks pass (TS 3.8.2, isolatedModules, 4.1.6, latest, exactOptionalPropertyTypes, ESNext).yarn build:apps— all test apps build.Checklist