Skip to content

feat(bundle): bundle the bit cli with esbuild - #10590

Draft
GiladShoham wants to merge 491 commits into
masterfrom
bit-bundle3
Draft

GiladShoham wants to merge 491 commits into
masterfrom
bit-bundle3

Conversation

@GiladShoham

@GiladShoham GiladShoham commented Aug 9, 2026

Copy link
Copy Markdown
Member

Bundles the CLI into a single 59 MB CJS file plus 10 externals that cannot be inlined, with a generated shim package per core aspect re-exporting its slice of the bundle. npm run bundle builds it; --sea also builds a node single executable.

1.2 GB / 141k files → 159 MB / ~2.8k files. bit --help ~0.53s warm.

The shims emit the same dist/*.aspect.js and dist/*.main.runtime.js filenames the aspect loader already discovers, so the runtime needed no changes. The one source fix is real and independent of bundling: hook-require patched module.constructor.prototype.require, which under any bundler installs an enumerable require on Object.prototype.

bit start now works too: it serves the pre-built UI/preview bundles instead of running a bundler at all (shouldServeBundleUi/writePreviewEntry hash-match and serve from the shipped artifacts/, no public/ written). Verified end to end from a fresh bit init + bit create workspace: UI shell, workspace/scope roots, and the component's own preview all served from the pre-bundle. @rspack/dev-server and @rspack/core itself (42 MB — was the single biggest external) are both fully excluded from the default build's package.json/node_modules, not just externalized — neither is reachable once bit start serves the pre-bundle instead of rebuilding.

Also adds a "UI vendor DLL" artifact, built alongside the existing UI/preview pre-bundle: BundleUiTask runs an additional rspack DllPlugin pass producing vendor.js + a portable vendor-manifest.json, covering React and every core-aspect package that ships browser (.ui.runtime/.preview.runtime) code. A new createUiVendorDllReference() lets a separate consuming project turn that manifest into real DllReferencePlugin options, so it can intercept already-compiled core UI code instead of recompiling it from source — fixing the gap where a bundled bit can't build a third-party UI root (e.g. an app like community-cloud). Design doc: bundle-plan/26-ui-vendor-dll-design.md; plan + decision log: bundle-plan/27-ui-vendor-dll-plan.md.

Also adds a CircleCI guard (scripts/bundle-size-guard.mjs + scripts/bundle-size-baseline.json) that fails the build if any part of the bundle grows more than 10% past its baseline — a direct response to an SSR bundle regression (6 MB → 53 MB) caught and fixed on this branch by externalizing @rspack/core in rspack.ssr.config.ts. Runs in two phases so the cheaper check fails fast: pre (no UI/preview build needed) in build_esbuild_bundle, right after the bundle is built and before the e2e fan-out starts; post in e2e_test_ui_prebundle, right after inject_ui_prebundle. Every check also has a whole-folder catch-all alongside the twelve named sub-paths. --update-baseline re-measures and rewrites the baseline file, the way to accept an intentional size increase later. Baseline is calibrated against a real CircleCI (Linux) run, not a local machine (native binaries for packages like esbuild/@swc/core differ by platform).

Current size breakdown (measured on a real CircleCI Linux run):

piece size
bit.app.js (the CLI bundle) 59.42 MB
shims (@teambit/* + vendored harmony deps), before UI/preview injected 10.11 MB — regular dist/ barrels 0.12 MB + browser barrels (browser/, real compiled dist per shim, for third-party bundlers) 7.32 MB + vendored harmony runtime deps/locators/types the rest
UI/preview pre-bundle (artifacts/), injected after 21.06 MB — app shell (workspace+scope, single combined build) 6.65 MB + SSR 6.35 MB (was ~53 MB before the @rspack/core externalize fix) + UI vendor DLL (vendor.js + manifest) 7.38 MB + preview 0.68 MB
combined dist/core-aspects (bundle + all shims, before → after injection) 81.54 MB → 102.57 MB
externals installed (node_modules) 68.31 MB
total shipped distribution (whole-folder check, before → after injection) ~149.9 MB → ~171 MB

The UI pre-bundle shrink from the original ~82.7 MB two-build layout down to the single shared compilation comes from upstream #10628 (SSR fix + minifier) and #10629 (single rspack compilation shared by both UI roots instead of two separate builds), both merged into this branch, plus #10631's bit start sanity e2e. Note the UI artifact no longer splits into separate ui-bundle/workspace/ui-bundle/scope directories — it's one shared ui-bundle/public/bit/ tree (with ssr/ and static/ under it) plus the sibling ui-vendor-dll/.

esbuild's own metafile.json (8.9 MB of build-analysis JSON, never read at runtime) is no longer written into the published package — still produced for local npm run bundle iteration and CI's diagnostic capture.

Producing the UI/preview pre-bundle locally needs a real bit build --tasks BundleUI,PreBundlePreview; it's now cached under a gitignored .bundle-cache/ (with a commit-hash + date meta.json) so node_modules wipes don't force re-deriving it every time.

Verified from an isolated dir: 40+ commands including create, status, tag, export, import, watch, server, start, and build --unmodified (all 9 tasks, rspack included). UI vendor DLL additionally verified against a real rebuilt CLI bundle and against a genuinely separate pnpm install with different peer dependencies, confirming real DllReferencePlugin interception (not just a clean build).

npm run e2e-test:bundle / :sea run the suite against the artifact; CircleCI builds it once in build_esbuild_bundle and shares it across the e2e nodes (gated to ^bit-bundle.* branches).

Full architecture, measurements, externals breakdown, script-vs-SEA analysis, the publishable package layout and open questions are in bundle-plan.md.

Draft: based on remove-core-envs-from-manifest, so the diff includes that branch. Opened to get CircleCI running. ui-vendor-dll (#10690) has been fast-forward merged into this branch and closed.

GiladShoham and others added 30 commits August 10, 2026 13:20
bit-cli-app-env is itself an env, so teambit.envs/env; cli-bundler is a plain node module rather
than an aspect, so node-babel-mocha.
a user's workspace resolves @teambit/<aspect> to a shim whose body is one dynamic require, so
without declarations every import degraded to any - no type checking, no autocomplete.

the .d.ts tree is copied verbatim from the package being shimmed rather than regenerated. that
preserves type identity: declarations re-export their siblings and other @teambit/* packages, and
those references resolve through the sibling shims, so every aspect sees the same Component and
Workspace. rolling them up per package would break exactly that. the whole tree is copied, not just
index.d.ts, because the re-exports are relative paths into it.

the shim's exports map points 'types' at the copied declarations - inheriting the original, which
points at .ts sources a shim does not have, would leave everything untyped.

capsules always carry declarations; this repo needs 'bit compile --generate-types'. both verified,
107/107 shims with 1722 files. an external workspace type-checks against them under noImplicitAny,
and a negative control confirms the types are enforced rather than silently any: ws.path resolves as
string and cm.toArray() as [Component, string][], the Component coming from a sibling shim.
…k status

docs.mdx explains each module and the traps: resolution is not path-joining, exports maps don't
extension-probe, a missing main runtime is legitimate so a resolution bug looks like a normal build.

plan gains 9e - the task runs green, what the first runs exposed, freshness confirmed correct, types
verified in an external workspace, and what is still open.
the task wrote to <capsule>/app-bundle, so the build produced a prototype dir that something would
later have had to lift into the package. it now builds into the capsule itself, which *is* what gets
published, giving the 9b layout directly: package.json + bin/bit + dist/<aspect>/index.js locators +
dist/core-aspects/{bundle,node_modules}.

inPlace changes three things: never clean (the out dir holds the component's own sources and dist -
cleanOutDir would delete them), merge into the real package.json instead of writing the
@teambit/bit-bundle-externals stand-in, and skip .npmrc, which only exists for the prototype's local
npm install.

the merge prunes the dependency surface to the externals alone - 168 declared dependencies replaced
by 7. leaving them would make a consumer's install re-download the very tree the bundle replaces,
and would resolve a second copy of every core aspect next to the shims, so @teambit/workspace could
resolve to a published package rather than the bundle slice. dev/peer/optional deps go too;
identity fields (name, version, componentId, engines) are untouched.

also excludes dist/core-aspects from the .d.ts copy: in place, @teambit/bit's source dir is the
capsule, whose dist now contains the generated shims, so an unfiltered glob copied all 106 shims'
declarations into the bit shim (1158 files instead of 18).

verified: the capsule's sources and dist survive, and the built package runs --version, init, status
and list from a fresh workspace.
MochaMain.createTester() has no callers anywhere in the repo or in any
resolved env - real consumers (node-babel-mocha, node-typescript-mocha)
already import MochaTester directly from @teambit/defender.mocha-tester,
which has no dependency on this aspect. Being an eager BitMain dependency,
it forced mocha's require chain onto every bit invocation for no reason.
mocha is no longer needed - the @teambit/mocha core aspect that pulled it
in unconditionally is gone (merged from remove-core-envs-from-manifest).
Rebuild verified: externalsInstalled 11 -> 10, coreAspects 106 -> 105, zero
require sites for mocha in bit.app.js.

bundle-plan.md records the webpack/mocha externals research and the mocha
removal end to end.
…s an install that re-keys them

pnpm keys a virtual-store directory by the package's peer-resolution hash, so
an install that changes the dependency set gives the same name@version a NEW
directory and deletes the one this process loaded its modules from. Node keeps
the loaded module objects, but not the files - so any require the loaded code
deferred past load time resolves against the deleted directory and throws
MODULE_NOT_FOUND.

This branch made that fatal: the envs that used to be core aspects are now
ordinary packages resolved out of the workspace's virtual store (on master
they resolve from bit's own installation, which no workspace install touches),
and @teambit/aspect defers require('./babel/babel-config') until getCompiler()
is called - which the install flow itself does right after the package-manager
run, when it compiles components and reloads envs. The install then dies with
"Cannot find module './babel/babel-config'", and since the env never loads,
bit create surfaces it as the misleading `template "react" was not found`
(8 of the 14 failed e2e shards in CI build 436404; the two unmasked ones fail
on bit install directly).

Replacing the in-memory instances instead does not work - verified by trying:
every reload path (reloadMovedEnvs, loading components as aspects) has to
consult the registered env to do its work, and consulting it is exactly what
throws. reloadMovedEnvs is additionally a no-op for these envs: it filters on
env.__path/env.id, which only plugin-loaded envs carry, never aspect-registered
ones.

So the fix follows the rule an OS applies to a running binary's deleted files:
what the process has loaded stays available for the process's lifetime. Before
the package-manager run, snapshot which virtual-store directories back entries
in require.cache; afterwards, restore any that vanished from their re-keyed
twin - same name@version, different peer hash - whose package content is
identical (same tarball; the peer set only affects the sibling dependency
symlinks, which are relative and stay valid). pnpmPruneModules learns to skip
directories backing require.cache entries so it does not re-delete a restored
one; a later command's process, which has nothing loaded from it, prunes it.

Reproduced and verified with deps-in-capsules.e2e.ts: the second install
re-keys a dozen loaded @teambit/*@1.0.1042 slots; without the fix the suite
fails on the babel-config require, with it all tests pass and the debug log
shows each removed slot restored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ound"

getTemplateWithIdOrEnvFallback swallowed every failure of the env fallback
with a bare catch and re-threw the generator's own "template X was not found".
Only "this aspect has no such template" is safe to replace that way; when the
env itself fails to load (a failed capsule install, a missing module in its
package), that failure is the actual cause, and hiding it sent the CI
investigation of build 436404 in the wrong direction for 8 of 14 shards.
Distinguish the two by matching the generator's own not-found error and let
everything else propagate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…modules

The preservation added in e792b9f keeps virtual-store directories the
running process loaded modules from requireable when an install re-keys them,
by scanning require.cache - which only CJS modules appear in. ESM modules
live in node's ESM module map, which has no enumeration API, so an ESM env
relocated by an install was still exposed to the same MODULE_NOT_FOUND on any
import it deferred past load time.

aspect-loader now records every file it loads through dynamic import() - the
env-plugin loader and loadEsm(), the only two such call sites - in a
Symbol.for-keyed global Set, and the preservation scans that set alongside
require.cache. A global symbol rather than a shared import because the reader
lives in the pnpm package manager, which aspect-loader must not depend on
(the dependency runs the other way), and Symbol.for resolves to the same key
even when a module is duplicated in node_modules; both sides document the
contract and point at each other.

Only ESM entry files are recorded, not their transitive static imports: those
are fully loaded into memory and never re-read, while the entry's own package
directory - where deferred imports and config-file reads point - is restored
wholly. The realpath is recorded alongside the given spelling so a load
reached through a node_modules symlink is attributed to the .pnpm directory
that owns it.

Verified: 55 specs across both components (4 new for the recorder, 2 new for
the reader), deps-in-capsules.e2e.ts still green, npm run lint green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The restores ran under an unbounded Promise.all, one recursive fs.copy per
removed directory, right after the engine has just saturated the disk -
review flagged the burst as a hazard in constrained CI/container
environments. The common case is zero removed directories and the checks
stay cheap; when there are any, serial restore bounds the I/O with no
meaningful cost (the deps-in-capsules repro restores 26 dirs and stays
green). Also logs an aggregate removed/restored/duration line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…from-manifest

The loaded-package preservation work this branch carried was upstreamed as
#10595, so master and the branch both hold it and every conflict is between
the branch's original and the reviewed version that landed. Master's version
wins throughout: it is the same work with the realpath spellings, the
patch-hash donor check, the slot-owner attribution and the prune early-return
added on top.

plugins.ts and aspect-loader's loadEsm merged cleanly into a double
recordLoadedEsmFile - the branch recorded before the load, master after it, on
the grounds that only a load that succeeded leaves something in memory worth
keeping files for. Kept master's single post-load call in both.

The branch-only aspect-loader changes (the versionless loaded-aspect lookup,
the core-aspect manifest guard, the requested-id-preserving def dedup) are
untouched - master never had them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s nested copy

An install that decides a hoisted copy satisfies what was nested deletes the
nested directory - and if this process loaded modules out of it, node keeps the
module objects but not the files, so any require the loaded code deferred past
load time throws MODULE_NOT_FOUND. `@teambit/aspect` defers
`require('./babel/babel-config')` until `getCompiler()`, which the install flow
itself calls right after the package-manager run, so the install dies with
`Cannot find module './babel/babel-config'`.

This is the same hazard #10595 fixed for the virtual store, reached by a
different route. That snapshot only scans `node_modules/.pnpm`, so a package
loaded from a root component's own node_modules was invisible to it and the
preservation was inert. root-components' hoisted-linker suite hit exactly this:
17 loaded directories under `.bit_roots/teambit.harmony_aspect/node_modules`
were removed by the second install.

`reloadMovedEnvs` does not cover it either - it skips any env without a
`__path`, which is only set for plugin-registered envs, and `@teambit/aspect`'s
AspectEnv is registered by its aspect provider. Reloading is no substitute
anyway: consulting the registered env is what triggers the deferred require.

So preserve these the same way: snapshot the package directories under the
workspace's node_modules that back loaded modules, and afterwards restore any
that vanished from a same-version copy found by walking the node_modules chain
up from where it used to be - what node itself would resolve now - bounded at
the workspace root. A donor must match on version, since the point is to keep
serving the files belonging to the modules already in memory. The loaded-files
plumbing shared with the virtual-store module moves to loaded-module-files.ts.

Verified with root-components.e2e.ts's hoisted-linker suite: 0 passing/1 failing
before (the before-all hook died on the babel-config require), 12 passing after,
with the debug log showing 17 of 17 removed directories restored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… of a nested capsule's

A nested capsule install is rooted at the capsule, but it was handed the root
links keyed by the capsules root - the directory one level *above* its root. That
describes an importer outside the workspace, and the package manager then has to
give the root project itself a directory dep path relative to itself. That path
is empty, so it comes out as `<name>@file:`, which its own parser rejects:

  Failed to build lockfile from resolved dependency graph: Resolved dependency
  path "@teambit/react@file:(...)" keys no lockfile entry:
  Failed to parse suffix: Empty path after `file:` scheme

Only the first nested capsule got the links (`index === 0`), so this needed that
capsule to be a named package - which is exactly what an env capsule is. The
mis-keying dates to #10453/#10457; it stayed latent while envs were core aspects
and no env capsule was installed this way.

Give the links the install they describe: one rooted at the capsules root. It
runs before the capsules rather than alongside them, so they can resolve the core
aspects it links while they install and nothing races it over the root's
node_modules. When a cyclic group exists it is already rooted there and keeps
owning them, unchanged.

Verified against optional-dependencies.e2e.ts, whose "before all" runs
`bit create react button --env teambit.react/react`: 0 passing/1 failing before,
10 passing after. The failure needs a cold capsule cache - once the env capsule
is cached the install is rooted at the capsules root instead and the bug is
skipped - so reproduce with `rm -rf ~/Library/Caches/Bit/capsules/*` first. On CI
this cluster accounts for 12 failing e2e tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…as a loadable env again

The suite asserts that an old-format env's dependency policy is missing after
the first install and applied after the second. It skips setCustomEnv's install
because it measures what each install does - which used to be fine, since the
envs the fixture needs were core aspects that were always present.

They are packages now, so with nothing installed the env cannot load at *any*
point and the policy is never applied: the second-install and recurring-install
assertions all fail. The chain fails one link at a time - @teambit/node first,
then teambit.react/react, then teambit.harmony/aspect - so the fixture's own
imports are only the first of what loading it needs.

State the chain as a workspace policy rather than installing it up front. The
first install then fetches it while the env is still unloadable - which is
exactly what makes it an "old env" for the first-install assertions - and the
second install finds it loadable. Installing the packages beforehand instead
makes the env load on the first install and applies the policy there, which
inverts the two first-install assertions.

Not env.jsonc, though it would sidestep the chain: `calculateEnvManifest` reads
the policy without loading the env, and `setOldNonLoadedEnvs` intersects with
`envsWithoutManifest`, so a manifest would statically apply the deps on the
first install and silence the warning - turning this suite into a duplicate of
env-jsonc-policies.e2e.ts and dropping the only coverage of the legacy path.

install.e2e.ts's old-envs suite: 2 passing/3 failing before, 5 passing after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… compiler

The suite's last assertion is that the component compiles into its package
directory, but the component takes the default env - which on this branch is the
empty env, with no compiler - so no dist is ever written. The file landed on
master after this branch named an env explicitly in the e2e suites that assert a
dist, so it never got that treatment.

Use the workspace-local ts env rather than a published one: it installs nothing,
so what the global store does or does not serve is still exactly what the
assertions measure. `setBitdevNodeEnv` would install its env package and fails
to load under the global virtual store ("The requested module '@teambit/component'
does not provide an export named 'ComponentMap'"), which is a separate matter
from this suite's subject.

The "building an aspect" block still fails and is untouched: its TSCompiler
type-checks bit's own repo sources through the linked core aspects, against the
`@types/react@17` the pinned `@teambit/aspect@1.0.1042` env brings, so react 18
APIs (`startTransition`, `useSyncExternalStore`) come back as missing exports.

global-virtual-store.e2e.ts: 4 passing/2 failing before, 6 passing/1 failing after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The suite deletes the component's dist artifact from the remote and asserts on
the error that follows, so the tag has to record one. It did not: the ts env
this branch put here compiles in the workspace but emits nothing in a capsule
build - its TSCompiler task runs and reports success while the capsule ends up
with no dist directory, so builder records only the schema and package-tar
artifacts and `artifacts.find(a => a.name === 'dist')` is undefined.

A workspace compile is what made that look fine elsewhere: it copies the files
it cannot compile into the dist, so a dist appears there regardless. A build
does not copy, so nothing appears.

Use the bitdev node env instead, the same env this file already relies on for
the --loose build below, and install it after the re-import rather than
importing it - it is a published package, not a scope component.

build-cmd.e2e.ts "dist file is deleted from the remote": the before hook threw
on `undefined.files` before, 2 passing after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…'s so the bridge is the only variable

The aspect built here uses the pinned legacy aspect env, which brings older
typings than this repo does - @types/react@17 against its 19, @types/mime@1
against its 2. The type-paths bridge maps the program's `react` to whatever the
workspace hoisted, which is correct for the workspace's own packages; but bit's
own sources share that program, because a core aspect in the dev repo exposes
`types: index.ts` rather than a built `.d.ts`, and they need the repo's typings.
So the compile failed on `startTransition` and `useSyncExternalStore` missing
from react, and `getType` missing from mime - none of which is about the
reachability this suite measures.

The mismatch is broad (13 of the 19 typings packages the workspace and the repo
share differ), so pinning per package as errors appear would be endless. Take
the versions from the running repo instead, at runtime, so a repo-side bump
cannot silently reintroduce the skew.

Filtering the errors by file was the other option and would have been wrong:
with the bridge stubbed out to return no paths, all 47 resulting errors land in
bit's own sources - the same files the version skew shows up in. Ignoring them
would leave a suite that passes with the bridge fully disabled. That the guard
still bites was verified both ways: bridge on with these versions pinned is 0
errors, bridge off with them pinned is still 47.

global-virtual-store.e2e.ts: 6 passing/1 failing before, 7 passing after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… an aspect

`createAspect` configures the component with the aspect env, which used to be a
core aspect and is now a package. This suite is the one createAspect caller that
also configures the aspect in workspace.jsonc, so bit has to load it - and with
the env's package absent the load fails, the component is reported with a
"failed loading env" issue, and the snap in the before hook throws before the
import under test ever runs.

The other callers scaffold an aspect without the workspace using it, so nothing
forces the env to load and they are unaffected.

bit-import-on-lanes.e2e.ts: the before hook threw before, 19 passing after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ests after fixing the oom issue in pnpm side
…undle

Instead of copying the three markdown templates next to bit.app.js at
build time, getDefaultRulesContent now requires them statically behind
a BIT_IS_BUNDLE check so esbuild's new .md text loader inlines them as
strings. Also fixes the pre-existing ignore-assets-plugin, which was
silently stripping all .md files (including these) to empty modules.

Template filenames are now exported from mcp-config-writer as a single
source of truth the bundler plugin imports, instead of a duplicated list.
… has not landed

e7cffe7 dropped e2e_test and e2e_test_bbit back to medium on the grounds that
the OOM was fixed on the pnpm side. The run it produced (#437962) says otherwise:
15 failures, 12 of them a bare `Killed`, 11 of those on the `bit install` of an
env package - @teambit/react.react-env@1.3.5, @teambit/env + @teambit/node.
2793 tests reported instead of 2948, the missing 155 being suites whose
before-hooks were killed before they could run.

The premise has not been met yet. pnpm/pnpm#13681 - the engine still peaking
about 2x pnpm v10, 4.4GB for a 3.8k-package graph - is open, so the peak does
not fit in medium's 4GB whichever way the fix lands. This branch is also pinned
to @pnpm/napi 12.0.0-rc.1, behind the rc.2/rc.3 that any such fix would ship in,
so it could not have picked one up.

Restored with the reasoning in the file so the next attempt starts from what to
re-measure: once #13681 closes and the engine is bumped here, drop this and
watch for `Killed`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…undles

- resolve an aspect's `artifacts/` from the running bit before bvm, so a bundled
  bit serves its own pre-bundles instead of a bvm install's
- build the pre-bundles from core aspects only, so their `.hash` is not keyed to
  bit's own dev workspace (react arrives there as an env, versioned)
- emit a shim file per non-main runtime; without one a bundled bit resolved zero
  preview aspects and hashed the empty string, so no artifact could ever match
- copy each aspect's `artifacts/` into its shim
- trim the ui-bundling externals: 31 -> 12 installed, 1.3 GB -> 322 MB

Also fixes two resolution bugs that blocked the build tasks: `@apollo/client` is
aliased to a single copy (it is a peer of @teambit/component, unresolvable from a
capsule store, and carries React context), and use-cloud-scopes gets the
`dist/esm.mjs` its exports map already declared.
…ndles

§17 covers why both pre-bundle hashes missed on this branch (react left the core
aspects, so bit's own workspace and a user's stopped agreeing), the alternatives
weighed for what belongs in the artifact, and the resulting 1.3 GB -> 322 MB.
Adds D12-D15, findings-log entries, and closes gap #1. Links #10596
for the 90 MB UI artifact.

Also carries the §16/§18 text that was already in the working tree.
GiladShoham and others added 30 commits September 8, 2026 13:40
…er check

Pre-phase baseline was captured on macOS and undercounted externals by
~8.7% vs. real CircleCI (Linux) numbers, eating most of the margin on
the first run. Recalibrated from the actual CI job's report.

Also adds total-pre/total-post checks measuring the entire out-dir, so
anything landing anywhere in the shipped folder trips the guard, not
just the named sub-paths.
Move the post-phase check + inject_ui_prebundle out of
e2e_test_ui_prebundle into a new check_ui_prebundle_size job - build
validation, not a test, so it shouldn't live in the e2e job. Persists
the already-injected bundle so e2e_test_ui_prebundle attaches it
directly instead of redoing the injection.
It builds the bundle, it doesn't set anything up. Updates the job key,
every requires: reference, and current-state docs; historical findings
log entries keep the old name since they describe what was true then.
React version compatibility check, process.cwd()-independent resolve
root, correct nested node_modules handling, per-package version-
collision handling, dependency-aware router-context safety, and no
longer leaking the generated entry file's build paths into the
shipped artifact - all in ui-vendor-dll.ts.

Also: fail (not warn) on an unresolvable harmony runtime dep and vendor
a missing browser-dist dependency in generate-shim-packages.ts; fail
the size guard on a silently-missing artifact instead of reading it as
0 bytes; wire the size guard into both bvm publish CI jobs; fix a
lint-staged glob that silently excluded root-level JS files.
## Proposed Changes

The component-peer dependency e2e test gives `comp1` a custom TypeScript
env but leaves `comp2` on the empty default env. With root components
enabled, `comp1` references `comp2` as a TypeScript project even though
the peer has no compiler to generate its tsconfig.

Assign the same custom env to `comp2` so both projects have a compiler.
This fixes the test fixture while preserving its peer-dependency and
hidden-peer assertions; it does not change the external compiler's
handling of non-TypeScript dependencies.

Validation: `npm run lint` and `git diff --check` pass. Focused e2e runs
were attempted under Node 22 with the local compiler reference-pruning
patch disabled, but both original and corrected setups hit 18 unrelated
local type errors mixing checkout types with an installed Bit version. A
clean CI e2e result is still needed.
The previous commit's import { tmpdir } from 'os' broke the real
browser UI/preview pre-bundle build: ui-vendor-dll.ts is reachable
from a browser compilation via the @teambit/ui barrel, and os has no
browser resolve fallback the way fs/path do. Write the scratch entry
file under dirname(outputPath) instead - no new Node builtin needed,
and it sits outside the artifact glob rather than merely being
cleaned up in time.

Also: buildUiVendorDll's new sourceRoot default broke two existing
unit tests under real capsule execution - they used lodash.compact/
lodash.flatten as fixtures, which aren't actual dependencies of
@teambit/ui and only resolved by cwd happenstance. Switched to
p-map-series/chalk, real declared dependencies guaranteed present
wherever the component's own install is.

Also updates bundle-size-baseline.json's shims-dist check for the
expected size growth from vendoring @teambit/base-react.navigation.link.
p-map-series/chalk still failed to resolve via buildUiVendorDll's new
sourceRoot default inside bit_pr's real build capsule, even though
p-map-series is a genuine dependency of @teambit/ui. Revert to the
original lodash.compact/lodash.flatten fixtures, explicitly resolved
off process.cwd() (confirmed via CI history that this exact pairing
passed before any of this sourceRoot work) - real BundleUiTask usage
never passes a sourceRoot, so it's unaffected and still gets the
correct default.

Also replaces the resolveContextProviderMismatchUnsafePackages
negative-case test's use of the live @teambit/preview component (whose
dependency graph disagreed between CI and local) with a fully
controlled fake package.
…om-manifest

# Conflicts:
#	.bitmap
#	pnpm-lock.yaml
#	scopes/dependencies/pnpm/pnpm.package-manager.ts
#	scopes/workspace/install/install.main.runtime.ts
…om-manifest

# Conflicts:
#	.bitmap
#	.circleci/config.yml
#	scopes/envs/envs/environments.main.runtime.ts
…nning bit's dists

the dev install re-resolves with a manifest bbit never used; when the two disagree pnpm
relinks the whole tree and replaces the workspace components' node_modules dirs, dists
included. the install's own post-install compile then dies on a deferred require.

also make the core-env e2e assertion follow DEFAULT_ENV instead of naming an env.
the install replaces the node_modules the process is loaded from, so every require it
defers past that point throws MODULE_NOT_FOUND - the compiler aspect first, then the
pager on the way out - long after the packages themselves have landed.
…alls

building them inside an env root fails with node-gyp-build exit 127 since pnpm 12.4.1;
nothing in the suite uses them.
…s unloaded

their packages depend on this repo's components, whose dists the install just replaced,
so the first pass falls back to tsc - whose eager requires pull UI-only packages into the
node runtime and break the next bit.
the released bbit installed the current version while the dev binary wanted the pinned
one; swapping a package this widely depended on re-keys peer hashes across the tree, so
the dev install relinked all of node_modules and replaced the dists bit runs from.

reverts the CI scaffolding that worked around the symptom.
… envs"

the released bbit resolves core aspects from its own bundle and ignores the policy, so it
still installed 1.0.1169 and the whole-tree relink was unchanged (+6152 either way).
…he container

nothing in that describe looks at the preview; bundling a react-based env's preview is the
tag's memory peak.
the tag is still OOM-killed with the preview task skipped, so the peak is elsewhere in the
build - keep the task's coverage.
… relink

Temporary, to be reverted. The dev-binary install in bit_pr /
check_circular_dependencies relinks the whole tree (6152 packages) instead of the
delta (1023 before the merge) and replaces the dists the running bit is loaded
from. Everything else on this branch is unchanged, so this isolates the engine as
the single variable.
… root

A phantom require of a legacy core env resolves from the requiring package up to
the workspace root, so a package already there satisfies it. Adding the pinned
legacy version on top replaced the version the rest of the tree was resolved
against: the package manager then re-linked every package whose peer resolution
moved with it - the whole tree - re-materializing the workspace components'
packages without the dists the running process is loaded from, which crashed the
next deferred require with MODULE_NOT_FOUND.

The bit repo installs the react env at the root, so it hit exactly this: the
install relinked 6280 packages instead of the 1023 delta.
The previous commit guarded only the phantom-require path; the react env reaches
the policy through the used-env path, so the downgrade still happened. Move the
guard into addLegacyCoreEnvsToPolicy, which both paths funnel through, and make
it version-aware: a root that already provides the package at the pinned version
or later needs nothing added.

These envs are dependencies of the bit package itself, so a bit that still ships
them as core aspects installs its own (newer) version at the root. Pinning the
older legacy version over it moved what the rest of the tree resolved its peers
against and re-linked all 6280 packages, rewriting the dists the running process
is loaded from.
Building them inside an env root fails with `node-gyp-build` exited with status
127 since pnpm 12.4.1, so an unrelated native addon decides whether a test can
scaffold its fixture. Five suites hit it on the react env alone.

Set neverBuiltDependencies once where every e2e workspace is created, and drop
the two per-call --disallow-scripts workarounds it replaces.
# Conflicts:
#	e2e/functionalities/peer-dependencies.e2e.ts
#	pnpm-lock.yaml
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v3 prs to merge for bit v3

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants