Conversation
…itions Part of #37571. React runtime - DotCMSEditableText now loads @tinymce/tinymce-react through a dynamic import (new TinyMCEEditor module) behind a Suspense boundary, so live-mode consumers no longer download the editor they can never open. - Dev mode and the Analytics-ready flag are resolved once per layout tree in DotCMSPageProvider and shared through the page context. Previously every contentlet registered its own dotcms:analytics:ready window listener and ran its own UVE state lookup. - useCheckVisibleContent is gated behind the dev-mode flag. Its getBoundingClientRect() forces a synchronous layout and only feeds the editor's empty-contentlet placeholder, so production stopped paying for it. - The page context value is memoized, and Contentlet wraps its mapped component in Suspense so consumers can map content types to React.lazy/next/dynamic components without supplying their own boundary. Measured on a 10-contentlet page: 10 analytics listeners and 10 getBoundingClientRect() calls in production, down to 1 and 0. Covered by the new DotCMSLayoutBody.runtime.test.tsx, which fails against the previous code. Packaging - @nx/rollup generates "import": "./x.cjs.mjs", an interop bridge backed by a single non-analysable CommonJS file, and every modern bundler resolves import before module. A shared writeBundle plugin now rewrites the generated map so import resolves to the real ESM artifact and require keeps pointing at the CommonJS build, with types first so it is not shadowed. Applied to client, uve and types, which also gain sideEffects: false. - @dotcms/react declares sideEffects as an allow-list rather than false, because its CSS is injected by JavaScript and a blanket false would let bundlers drop the grid styles. Build fixes found on the way - nx build sdk-react could not complete at all: @nx/rollup 23 replaced the postcss file filter with a picomatch matcher that never matches the absolute module ids rollup passes, so stylesheets reached rollup untransformed and it failed parsing Column.module.css as JavaScript. Swapped in rollup-plugin-postcss, already a workspace dependency. - Style injection is emitted inline instead of importing style-inject, which rollup was copying into a node_modules directory inside the package. npm always strips node_modules from the tarball, so published CSS chunks would have imported a file that did not exist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s root Part of #37571. @dotcms/client — additive subpath entrypoints - createDotCMSClient constructs page, navigation, content and AI in its constructor, so every API is statically reachable and no amount of sideEffects metadata removes them from a page-only bundle. - Adds @dotcms/client/page, /navigation, /content and /ai, each exporting a focused factory. Verified against the built output: the closure of @dotcms/client/page is page-api + client-context + internal, with no content-api (55KB) and no ai-api (10.6KB). - Validation, URL normalization and the Authorization header move to a shared createClientContext so the root factory and the subpaths behave identically, down to their error messages. createDotCMSClient and its .page/.nav/.content/ .ai properties are unchanged; all 300 client tests pass untouched. @dotcms/analytics — framework-neutral root (breaking) - src/index.ts was `export * from './lib/react'`, so the root and ./react were the same module and importing the package pulled in react, next/navigation and @dotcms/uve. The ~4,300-line neutral engine had no entrypoint of its own. - The root now exports initializeContentAnalytics, getAnalyticsConfig and the shared types. Confirmed against the built artifact: the root's transitive closure has no react and no next/*. React bindings stay at ./react, which already existed and is what the README has always documented. - Declares next as an optional peer dependency: ./react has always imported next/navigation without saying so. - Fixes the exports map, which listed `types` after `import` (resolvers take the first match, so the types condition was unreachable), and the typesVersions entry that disagreed with it. - MIGRATION.md and a README entrypoint table cover the change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds sdk-bundle-budgets, which runs after the SDKs are built, bundles a one-line import of each public entry point with esbuild, and asserts on what actually ships. Unit tests cannot see any of this — it is a property of the built package and its exports map, not of the source. Probes and what each forbids are declared in src/probes.ts; gzip ceilings live in budgets.json so a change to one is visible in review. Current numbers, against the issue's measured baseline: react-layout-only 6.28 KB gzip (issue measured ~11.6 KB, with TinyMCE) react-hook-only 4.26 KB gzip (issue measured ~10.4 KB, with TinyMCE) client-page-only 7.56 KB gzip no ai-api, no content-api, no lucene analytics-neutral 20.88 KB gzip no react, no next uve-only 3.03 KB gzip Also asserts, per package: `import` never resolves to Nx's *.cjs.mjs bridge, `types` is declared first so it is not shadowed by an earlier condition, and a sideEffects field is present. Two details worth knowing: - Module lists come from metafile.outputs[..].inputs filtered to bytesInOutput > 0, not metafile.inputs. The latter lists every file esbuild parsed, including everything a barrel made it look at and then tree-shook, and would report TinyMCE as present in a layout bundle that ships none of it. - The test target sets cache: false. It reads build artifacts, which Nx does not track as inputs, so a cached run replayed a stale pass — both negative checks below silently "passed" until this was fixed. Verified the gate fails on the conditions it is meant to catch: dropping sideEffects from the built @dotcms/react fails 3 tests and makes both React probes leak TinyMCE and the block-editor blocks; restoring the *.cjs.mjs bridge in @dotcms/client fails the ESM-condition and types-ordering assertions. There is also a guard that the Row/Column stylesheets stay in a layout bundle. The sideEffects allow-list is what keeps them, and every other assertion here would still pass if a blanket `false` silently dropped the grid styles. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Part of #37571. Component maps - examples/nextjs and examples/nextjs-experiments map each content type through next/dynamic; examples/astro uses React.lazy, which works without the consumer supplying a boundary now that DotCMSLayoutBody wraps each contentlet in one. - The CustomNoComponent fallback stays eager — it renders when no mapping matches and should not wait on a network round-trip. - examples/astro's content-types barrel re-exported every component with `export *`. That alone made all of them statically reachable from the one importer, which would have defeated the lazy map no matter how it was written. It now exports only the map. - Same fix in the Astro header barrel, which re-exported AISearchDialog and so pulled the dotCMS AI client into the header. AI search - The dialog was imported eagerly and rendered unconditionally in all three examples. It now loads on first open and stays mounted after that, so its behaviour once opened is unchanged. Verification - examples/scripts/check-initial-bundle.mjs resolves the chunks a route actually downloads — Next.js from the client-reference manifests plus rootMainFiles, Astro by requiring a chunk to have no static importer AND be reached through import() — and asserts two markers are absent from them but still present elsewhere in the build, so a feature that disappeared cannot pass by accident. - Each example maps UnusedComponentProbe, a content type no page uses, as the deterministic fixture. - The TinyMCE needle is `tinymceScriptSrc`, not the bare string "tinymce": @dotcms/uve legitimately ships the editor's URL and toolbar config, and matching those reported a failure where none existed. - core-web/tools/copy-sdk-to-examples.mjs copies the locally built SDKs over the installed ones so an example build validates the working tree instead of the published packages. It never edits a package.json, so the manifests keep floating on `latest` and validate-sdk-package-shapes stays green. Measured, published 26.9.14-1 vs this branch, gzip: DotCMSLayoutBody only 11.29 KB -> 6.13 KB (-46%), TinyMCE gone useEditableDotCMSPage only 10.19 KB -> 4.16 KB (-59%), TinyMCE gone page fetching 10.30 KB -> 7.39 KB (-28%), via @dotcms/client/page analytics root 23.31 KB -> 20.82 KB, React modules 4 -> 0 The two React figures line up with the audit in the issue (~11.6 KB and ~10.4 KB), which is a useful check that this is measuring the same thing. Incidental fixes needed to get the examples building at all - examples/astro did not type-check: 14 errors, reproduced against the published packages, so they predate this work. Three views destructured useEditableDotCMSPage without defaulting, and three pages used `"error" in pageResponse` to narrow — which cannot work, because a successful response also carries an optional `error` (the deprecated first GraphQL error). Added isPageFetchError, which tests for the thrown DotErrorPage instance. astro check is now clean and `npm run build` completes. - examples/nextjs-experiments had the same missing-default issue in two views. Docs - The React SDK README's "Component Mapping" section claimed lazy loading was supported while showing an eager snippet in a variable named DYNAMIC_COMPONENTS. It now shows both next/dynamic and React.lazy, and warns about barrel re-exports. - Example READMEs and CLAUDE.md files updated to match, so the pattern consumers copy is the optimized one. - examples/astro/README.md referred to a non-existent DotCMSBodyLayout in six places. - libs/sdk/react/CLAUDE.md documented Jest and a jest.config.ts that does not exist; the project runs Vitest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… existing apps Follow-up to #37571, prompted by a customer report: ~135 mapped components loading on every page in a Next.js app built from our old example. The SDK changes in this PR do not fix that on their own. The component map lives in consumer code, so upgrading @dotcms/react cannot make a static map lazy — the app has to change. What this adds is the guidance and the measurement to do it. - check-initial-bundle.mjs now reports initial-route raw and gzip totals, and takes --report to print them without asserting. - The React SDK README gains a "Migrating an existing app" section with the measured effect, the barrel-export trap, the customRenderers map, and how to verify the result. Measured on examples/nextjs scaled to 135 mapped content types with one realistic component each: static imports 892.7 KB raw / 250.4 KB gzip 135 of 135 in the initial route next/dynamic 792.6 KB raw / 238.1 KB gzip 0 of 135 in the initial route The structural result is the point: a static map puts every mapped component in the initial bundle, a dynamic one puts none there. The 100 KB is what 135 modest components cost; real component libraries are heavier, and the saving scales with them rather than with the count. Worth recording for whoever reads this later: an earlier version of the experiment generated 135 near-identical components and measured almost no gzip difference, because near-identical modules compress to nearly nothing when bundled together. The components had to be given genuinely distinct markup and copy before the measurement meant anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five findings from reviewing the preceding commits. Two are defects in that
work, two are structural, one tightens a guard.
The provider useMemo did not memoize
- DotCMSLayoutBody defaulted `components` and `slots` to inline `{}` literals,
producing new object identities on every render. Those feed the page context's
useMemo, so it was invalidated every render and the memo added cost without
ever holding — re-rendering every container and contentlet in the tree.
- Defaults are hoisted to module scope. Measured with a context probe: distinct
context values across two renders goes from 2 to 1.
- DotCMSLayoutBody.runtime.test.tsx now asserts it, and fails when the defaults
are put back inline.
Style injection deduplicated on a colliding key
- The injected guard keyed on `css.length`, so a second stylesheet that happened
to be the same byte length as one already on the page would silently not
inject. Row (105 bytes) and Column (1586) do not collide today, which is luck
rather than design. It now compares stylesheet content.
One exports-map patcher instead of two
- The React build had its own package.json patcher duplicating what
tools/rollup/patch-exports.cjs already does (types first, sideEffects). The
shared plugin now handles ESM-only packages and takes extraConditions, so
React declares `react-server` through it and the bespoke copy is deleted.
- Verified behaviour-preserving: the published exports and sideEffects of react,
client, uve and types are byte-identical before and after.
The shared plugin was in the wrong place
- It lived at libs/sdk/rollup-shared/, which made it a pseudo-project under
libs/ and tripped @nx/enforce-module-boundaries. It is build tooling, so it
moves to tools/rollup/ alongside generate-vite-configs.mjs and
copy-sdk-to-examples.mjs.
Deferred-existence guard was too permissive
- check-initial-bundle.mjs searched all of .next for the "still exists
somewhere" check, so a marker surviving only in the server bundle would pass
while having vanished from the browser entirely. It now searches .next/static.
Also adds an assertion that every export condition points at a file that was
actually emitted — the react-server target is a hardcoded path and nothing
checked it resolved.
Not changed, but confirmed while reviewing:
- sideEffects: false is accurate for client, uve, types and analytics. A bare
side-effect import of each bundles to 0 bytes.
- The virtual:sdk-version TS2307 diagnostic during sdk-vue's dts step is
pre-existing: it appears identically with the pre-subpath client.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
❌ Linked Issue Needs Team LabelThis PR is linked to issue #37571, but that issue has no How to fix this:Apply a This comment was automatically generated by the issue linking workflow |
|
Claude finished @fmontes's task in 4m 23s —— View job PR Review — perf(sdk): stop shipping unused code to browser SDK consumers
I reviewed the SDK runtime changes, the TinyMCE split, the CSS-injection publishing fix, the bundle/publint gates, and the example conversions. This is careful, well-tested work — the reasoning is captured in the code comments and the guards are verified by breaking them. One genuine interaction is worth a look before merge; everything else is clean. New Issues
Notes (non-blocking, not defects)
The runtime test ( Unrelated: the automation comment about the linked issue #37571 missing a • |
Removes @dotcms/client/page, /navigation, /content and /ai, added in c52fe01. They worked, and the budget probe proved a page-only import dropped ai-api and the query builders. But measured alone, the saving is 3-5 KB gzip, and only for a consumer whose browser-side code needs exactly one area: createDotCMSClient 8.06 KB gzip /page 5.16 KB /content 4.64 KB /navigation 2.94 KB /ai 3.35 KB Two problems with shipping that. None of our examples used the subpaths — all three still build one shared client from the root — so we would have published four public entrypoints with nothing demonstrating them. And the split only pays off if you avoid sharing a single client instance across areas, which is the opposite of the pattern the README documents. That is public API surface we would carry indefinitely for a benefit nobody in this repository takes. Better to wait until a consumer asks, with a real usage shape to design against. This leaves #37571's "importing page-only client functionality does not retain AI search and unrelated query-builder code" unmet. Deliberate, and called out in the PR. Also reverts the createClientContext extraction that came with it: without the subpath factories it had a single caller, so it relocated the client's setup without reducing anything. client.ts is byte-identical to its pre-subpath state. @dotcms/client keeps the packaging fix from d8fbf3a: `import` resolves to index.esm.js, `require` to index.cjs.js, `types` first, sideEffects declared. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Removes tools/rollup/patch-exports.cjs and the React package.json patcher it replaced. Both rewrote the generated exports map so `import` resolved to *.esm.js instead of Nx's *.cjs.mjs interop bridge. Research says that was solving a problem we do not have. Nx's shape is deliberate, and its comment says so: If CJS format is used, make sure `import` (from Node) points to same instance of the package. Otherwise, packages that are required to be singletons (like React, RxJS, etc.) will break. Reserve `module` entry for bundlers to accommodate tree-shaking. So `import` is for Node and `module` — written first — is for bundlers. Still the behaviour on nx master today. The only related report, nrwl/nx#20009, is a runtime break in the bridge, closed; nobody has filed a tree-shaking complaint about this shape, which should have been a hint. Measured, rather than argued: - A package with Nx's exact generated shape, resolved by esbuild with default conditions, lands on index.esm.js and drops unused exports. esbuild honours `module` and finds it first. - examples/nextjs built against the patched packages vs Nx's original shape: 713.4 KB raw / 212.8 KB gzip against 713.9 KB / 213.0 KB. Two tenths of a kilobyte, in exchange for reversing upstream's singleton protection. The bundle-budget probes are unchanged after removing it (react-layout-only 6294 vs 6282 bytes gzip, noise), which is the same result from the other direction. What actually produced the wins, corrected: DotCMSLayoutBody 11.3 -> 6.1 KB and useEditableDotCMSPage 10.2 -> 4.2 KB came from the TinyMCE split and React's sideEffects allow-list. @dotcms/react is ESM-only, so Nx already pointed its `import` at ESM and the patch never applied to it. The exports rewrite contributed nothing to any headline number. What replaces ~110 lines of build tooling: - `sideEffects` moves into the source package.json of client, uve, types and react. Verified it propagates to dist untouched, so it needs no plugin. - React sets generateExportsField: false and hand-writes its exports map, which is the only one needing a condition (react-server) Nx cannot generate. Nothing mutates a package.json at build time any more. Also drops the two budget assertions that pinned the exports map's shape. The per-probe check that a bundle contains no CommonJS artifacts stays — it tests resolution behaviour rather than re-litigating Nx's decision. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces hand-written packaging assertions with publint, which found two real defects on its first run. Both pre-existing. @dotcms/analytics shipped ESM with no module type Node: "[MODULE_TYPELESS_PACKAGE_JSON] ... doesn't parse as CommonJS. Reparsing as ES module because module syntax was detected. This incurs a performance overhead." It worked only through Node's syntax-detection fallback; every consumer paid a reparse and older Node fails outright. Adds "type": "module". Invisible to a bundler probe or an example build — it is a Node resolution concern. `types` was shadowed in the generated exports maps Export conditions match in declaration order, and @nx/rollup writes `module` before `types`, so TypeScript can resolve to JavaScript instead of declarations. publint reports it as an error. Nx offers no option to change the order, so tools/rollup/types-first.cjs moves the key after the fact. That plugin reorders keys and nothing else. Every condition still points where Nx pointed it — `module` at the ESM build for bundlers, `import` at the CJS interop bridge so Node keeps one instance. Deliberate upstream, left alone. This is not a return of the exports rewrite removed in 5a7f414: that changed targets for no measurable benefit, this fixes an ordering bug an external linter flags. All five packages are now publint-clean. The gate fails on errors *and* warnings, with an explicit ACCEPTED list. That distinction is load-bearing: the analytics defect was reported as a warning, so an errors-only gate would have let through the exact bug that justified adopting the tool. Verified by removing "type": "module" from the built package and watching the suite fail with the three offending paths named. Two findings are accepted for now, each with a reason in the code: INVALID_REPOSITORY_VALUE (repo-wide convention) and EXPORTS_TYPES_INVALID_FORMAT (needs .d.mts output and per-condition `types`; a wider build change, and types resolve correctly today). sdk-bundle-budgets keeps only what publint cannot do: proving a named module is absent from a bundle. Its export-target-exists assertion is dropped as redundant. Worth recording, since it is the reason the probes survive: a size budget is not a substitute for the absence assertion. Removing `sideEffects` from the built @dotcms/react and measuring with size-limit moved DotCMSLayoutBody 4.3 KB -> 4.3 KB and useEditableDotCMSPage 4.0 -> 4.6 KB under rolldown, and 4.25 -> 4.25 and 3.96 -> 4.55 under esbuild. Every configuration passed its budget. The named assertion fails loudly and says which modules came back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Gives consumers a way to see whether their content-type components actually load
on demand, which prompted this whole effort — a customer followed our old
example and ended up with 135 components on every page.
There is no built-in way to check. `next build` with Turbopack prints no First
Load JS column, `next build --experimental-analyze` emits a route diagnostic
that never names components, and `@next/bundle-analyzer` is a webpack plugin
that produces nothing at all on a Turbopack build. All three were tried.
So `npm run analyze` resolves the chunks a route actually downloads — from the
build manifest plus the client-reference manifests — and reports the totals and
how many more load on demand. Passing a string reports whether that code is in
the initial download or deferred.
The script insists on a string from the component's output rather than its name,
and the README explains why: the map keys live in the initial chunk by design.
`Banner: dynamic(() => import("./Banner"))` compiles to a loader that decides
when to fetch the component, so grepping for "Banner" finds those few bytes and
says nothing about where the component went. The first version of this script
reported Banner as eagerly loaded for exactly that reason.
Matches examples/scripts/check-initial-bundle.mjs on the same build (14 chunks,
713.4 KB raw, 212.8 KB gzip). That script stays the CI guard with its
pass/fail assertions; this one is for a consumer looking at their own app.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both bundle scripts unioned every route's initial chunks and labelled the total "initial route JavaScript". Routes share most of the app shell, so the union is larger than anything a visitor downloads — on examples/nextjs it reported 212.8 KB gzip where the heaviest real route is 178.6 KB and the main content page is 172.7 KB. A 23% over-report here, and worse on an app with more distinct routes. npm run analyze now prints a row per route. The CI script keeps the union, which is what its assertions need — a module must be absent from every route — but says so instead of calling it a route. This corrects the before/after figures quoted in the PR. Measured per route on the main content page, same 14 content types both ways: static imports 619.7 KB raw / 181.3 KB gzip, 6 on-demand chunks next/dynamic 594.7 KB raw / 172.7 KB gzip, 20 on-demand chunks 8.6 KB gzip, not the 28.9 KB the union implied. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing referenced it — no npm script, no CI job, no documentation, only its own usage comment. It was written to verify example builds against local SDK changes during development and then committed, which is not a reason to carry it. The workflow it automated is a copy loop over dist/libs/sdk into an example's node_modules. Anyone who needs it can write it, and CI never builds examples at all, so nothing in the pipeline loses a step. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixes #37571
The problem
DotCMSEditableTextimported it at top level, but only renders an editor in UVE edit mode — published pages carried it for nothing. One hook import cost ~10 KB gzip.sideEffects. Without it bundlers must assume every module matters, so importing one export from the barrel kept the whole barrel. Tree-shaking couldn't work regardless of how the code was written.Contentletrepeated work per instance. Each registered its owndotcms:analytics:readylistener, calledgetUVEState(), and rangetBoundingClientRect()— a forced reflow feeding only the dev-mode placeholder, so production computed and discarded it. 60 contentlets, 60 times.What changed
TinyMCEEditor.tsx, loaded vialazy()on entering edit mode. ComplementssideEffects: that handles consumers who never use editable text, this handles the many who do but serve published pages.sideEffectsdeclared.falsefor client/uve/types. React gets an allow-list instead — its CSS modules inject styles on import, and a blanketfalsewould drop the grid silently. A test asserts those stylesheets survive.DotCMSPageProviderresolvesisDevModeandisAnalyticsActiveonce and shares them via context;useCheckVisibleContentis gated behind dev mode. One listener and one UVE lookup per page instead of per contentlet, and no reflow in production. Memoizing the context value also required hoistingDotCMSLayoutBody's default props — inline{}defeated it.Contentletprovides aSuspenseboundary.React.lazysuspends and throws without one;next/dynamicbrings its own, plainlazydoesn't. Consumers can now map lazy components in any framework with no extra wiring — this is what makes the Astro example work.@dotcms/analyticsroot is framework-neutral. It wasexport * from './lib/react', so any import pulled React andnext/navigation.next/dynamic/React.lazy, and the READMEs explain why — copying the old snippet is how this reached production.Before and after
Against published
26.9.14-1, gzip:DotCMSLayoutBodyuseEditableDotCMSPageMatches the audit in the issue, so we measured the same thing.
Runtime, 10 contentlets: analytics listeners 10 → 1,
getBoundingClientRect()in production 10 → 0.Component map —
examples/nextjs, its real 14 content types, same components both ways, measured on the main content route:/[[...slug]]initial JSnext/dynamic−25 KB raw / −8.6 KB gzip on a site with 14 content types. Scaled to 135, the same conversion moves that route 710.5 → 615.7 KB raw and 189.5 → 177.8 KB gzip, with 141 on-demand chunks instead of 6.
The gzip deltas are modest because the framework shell dominates a small example. The structural change is the durable part: every mapped component leaves the initial download, so the saving grows with how heavy the components actually are — and real component libraries are heavier than this example's.
What this does not fix
Upgrading won't make an existing consumer's map lazy — it's their code, resolved at their build time. One line per content type; the React README has the migration and two traps: a barrel re-exporting content types undoes it, and
customRenderershas the same shape. Eithernext/dynamicorReact.lazyworks.examples/nextjsshipsnpm run analyzeso a consumer can check their own build:Pass a string from a component's output and it reports whether that code is downloaded up front or deferred. There is no built-in alternative: Turbopack prints no First Load JS column,
--experimental-analyzenever names components, and@next/bundle-analyzeris a webpack plugin that emits nothing on a Turbopack build. All three were tried.A publishing bug
React's CSS modules inject via a
style-injecthelper. WithpreserveModules, rollup copied that helper intodist/libs/sdk/react/node_modules/.pnpm/…and pointed the import there. Resolves locally, but npm stripsnode_modulesfrom the tarball — published stylesheet chunks would have imported a missing file and thrown for every consumer. Injection is now emitted inline.Validation
publintruns against every built package. It checks what a bundle probe can't see — exports resolution, emitted files, module type — and found two pre-existing defects on its first run, both fixed here:@dotcms/analyticsshipped ESM with no"type": "module". Node reparsed every file on import ([MODULE_TYPELESS_PACKAGE_JSON]); older Node fails outright.typeswas shadowed in the generated exports maps. Conditions match in declaration order and@nx/rollupwritesmodulefirst, so TypeScript can resolve to JavaScript instead of declarations.tools/rollup/types-first.cjsreorders the key — targets untouched.The gate fails on errors and warnings, with an explicit accept-list. That matters: the analytics defect was a warning, so an errors-only gate would have missed the very bug that justified the tool.
sdk-bundle-budgetskeeps only what publint can't do — proving a named module is absent from a bundle. A layout-only import must contain no TinyMCE, editable text or block-editor renderers; a hook-only import no layout modules or CSS; neutral analytics no React or Next. Plus gzip budgets inbudgets.json.A size budget is not a substitute for that. Removing
sideEffectsfrom the built@dotcms/reactand measuring withsize-limitmovedDotCMSLayoutBody4.3 → 4.3 KB anduseEditableDotCMSPage4.0 → 4.6 KB under rolldown, 4.25 → 4.25 and 3.96 → 4.55 under esbuild. Every configuration passed its budget. The named assertion fails and says which modules came back.Module lists come from esbuild's metafile filtered to bytes actually contributed — the raw input list includes everything a barrel made it parse and then tree-shake, which would report TinyMCE as present in a bundle shipping none.
examples/scripts/check-initial-bundle.mjsresolves a route's real initial chunks (Next client-reference manifests; for Astro, no static importer and reached viaimport()) and proves anUnusedComponentProbeand the editor are absent.sdk-bundle-budgetsand the publint gate run in the existing frontend CI job. The example check does not — CI never builds the examples — so it is a local check, run when changing a component map or how the SDK is bundled. Every check was verified by breaking what it protects.Breaking change
@dotcms/analyticsroot is now neutral. ImportinguseContentAnalyticsorDotContentAnalyticsfrom root needs/react— the subpath already existed and is what the README shows, so only undocumented usage breaks.MIGRATION.mdincluded; package is0.0.1-beta, unused in this repo.Also fixed, pre-existing
nx build sdk-reactdidn't work at all.@nx/rollup23's postcss filter never matches absolute module ids, so rollup parsedColumn.module.cssas JavaScript. Only SDK with CSS.examples/astrohad 14 type errors and failed its own build, reproduced against published packages. Fixed, since it now has to build to be verified.Not included
@dotcms/client/page,/navigation,/content,/ai— built, measured at 3–5 KB gzip each, removed. No example used them, and the saving only lands if you avoid sharing a client instance, which the README recommends doing. Four entrypoints to support forever with nothing demonstrating them. That acceptance criterion is deliberately unmet.Needs a human
Offline checks only, so unproven:
Untouched:
examples/angularandangular-ssrdon't build before or after — published@dotcms/angulardoesn't match their pinned Angular. Needs its own issue.🤖 Generated with Claude Code