Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .changeset/6683-app-shell-side-effects-array.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
---
'@object-ui/app-shell': minor
---

`@object-ui/app-shell` now publishes a precise `sideEffects` ARRAY.

**What this means for a consumer.** Until now the package declared no
`sideEffects` field at all, which every bundler reads as "assume every module in
this package does something when it is imported" — so nothing in the package
could be tree-shaken, and importing one named export from the barrel pulled in
the barrel's whole reachable graph. The package now names exactly the modules
that DO something on import: its entry forms (including `./styles.css`, which a
bundler must never drop) and the ten modules that register SDUI widgets, admin
components and metadata resources at load time. Everything else is now
shakeable, so a consumer's bundler may drop the parts of `@object-ui/app-shell`
their app does not use.

⚠️ **If your build depends on a module of this package being evaluated for its
side effects without importing anything from it, and that module is not one of
the ten named**, it may now be dropped from your bundle. Import the value you
need by name, or call the registration explicitly. Measured on this repo's own
console: 56,668 gzipped bytes left the eager closure and every SDUI registration
stayed present.

`"sideEffects": false` was NOT adopted and remains disproven by measurement: it
drops three live SDUI widget registrations (`mcp:connect-agent`,
`cloud:onboarding-next`, `cloud:ai-model-status`) to zero chunks on a green
build with no warning anywhere.

Two gates ship with the array, because an INCOMPLETE array fails silently inside
a consumer's bundle and would otherwise have no witness:
`scripts/check-side-effects-array.mjs` re-derives the enumeration from the module
bodies and fails when the array and the derivation disagree in either direction,
and `scripts/check-sdui-registration-pins.mjs` weighs the built console for every
registration the array promises to keep.
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -263,6 +263,27 @@ jobs:
if: steps.relevant.outputs.should_run == 'true'
run: pnpm check:self-import

# `sideEffects` is a PUBLISHED CONTRACT, and an ARRAY form of it fails in
# the one direction nothing can witness: an INCOMPLETE array drops a
# registration inside a CONSUMER's bundle, with no error, no warning and
# exit 0 — the same failure mode as `"sideEffects": false`, only quieter.
# `@object-ui/app-shell` declares one because both simpler answers are
# measurably wrong for it (objectui#6683; `false` drops three live SDUI
# widget registrations, measured in objectui#6535). This gate re-derives
# the enumeration from the module bodies and fails when the array and the
# derivation disagree in EITHER direction — a missing registrar, or a
# stale name whose module no longer registers anything.
#
# Placed here rather than with the build steps for the same reason as the
# two above: it parses sources with `typescript` and reads package
# manifests, so it needs the install and nothing built. The artifact half
# of the same contract — do the registrations survive a real bundler —
# cannot run here at all and lives in `performance-budget.yml`, which
# builds the console.
- name: Verify every `sideEffects` array names exactly its registering modules
if: steps.relevant.outputs.should_run == 'true'
run: pnpm check:side-effects-array

# Node's ESM resolver does not extension-search relative specifiers, so an
# extensionless `./SchemaRenderer` in a published `dist/` is unloadable
# under plain Node — `@object-ui/react`'s entry died with
Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/performance-budget.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,6 +41,14 @@ on:
- 'scripts/check-eager-closure-budget.mjs'
- 'scripts/render-budget-comment.mjs'
- 'scripts/invoked-as.mjs'
# The SDUI registration pins (objectui#6683) and their two dependencies.
# Same rule as the three above — this job runs them, so an edit to any of
# them must be able to turn this gate red on its OWN PR rather than on
# somebody else's next `packages/**` change.
- 'scripts/check-sdui-registration-pins.mjs'
- 'scripts/check-side-effects-array.mjs'
- 'scripts/component-registrations.mjs'
- 'scripts/js-comment-mask.mjs'
pull_request:
branches: [main, develop]
paths:
Expand DownExpand Up@@ -81,6 +89,14 @@ on:
- 'scripts/check-eager-closure-budget.mjs'
- 'scripts/render-budget-comment.mjs'
- 'scripts/invoked-as.mjs'
# The SDUI registration pins (objectui#6683) and their two dependencies.
# Same rule as the three above — this job runs them, so an edit to any of
# them must be able to turn this gate red on its OWN PR rather than on
# somebody else's next `packages/**` change.
- 'scripts/check-sdui-registration-pins.mjs'
- 'scripts/check-side-effects-array.mjs'
- 'scripts/component-registrations.mjs'
- 'scripts/js-comment-mask.mjs'

concurrency:
group: bundle-analysis-${{ github.event.pull_request.number || github.ref }}
Expand DownExpand Up@@ -330,6 +346,24 @@ jobs:
echo "✅ Budget OK: entry chunk and eager closure are both within budget"
echo "budget_status=pass" >> "$GITHUB_OUTPUT"

# The ARTIFACT half of the `sideEffects` contract (objectui#6683). The
# static gate in `ci.yml` proves the array agrees with the module bodies;
# it cannot prove that a real bundler reading that array still EMITS the
# registrations, and that is the question the hazard turns on —
# `"sideEffects": false` is statically coherent and drops three live SDUI
# widget registrations to 0 chunks on a green build (objectui#6535).
#
# The key set is derived from the array itself, so this step needs no
# list of its own. It runs after the budget step and never instead of it:
# a dropped registration and a size regression are different verdicts, and
# a size win bought by deleting a feature must not read as a size win.
#
# Not `if: always()` — with no `dist/` the checker exits 2 and says so,
# which is the honest verdict for a run that measured nothing, but there
# is no reason to spend it on a build that already failed.
- name: Pin the SDUI registrations the `sideEffects` array promises
run: pnpm check:sdui-registration-pins

- name: Generate package size report
id: size-report
# NOT `always()`. `always()` also fires on a cancelled run, where
Expand Down
Loading
Loading