dotCMS / dotCMS/core

strict mode: Enable TypeScript strict mode across the core-web workspace

Open
#35,932 4 comments 0 reactions 1 assignee View on GitHub

@nicobytes is already working on this.

Since Aug 24, 2026.

  • #36957 by @nicobytes — closed without merging
Team : Falcon Type : New Functionality
Dominant language
Java
Stars
970
Forks
486
Avg merge
3d 33m
Merged PRs (30d)
170

Description

Description

Enable TypeScript strict mode incrementally across the entire core-web Nx workspace, one project at a time, rather than a big-bang global strict: true.

Today core-web/tsconfig.base.json has "strict": false, and it stays that way — the rollout never flips it globally. Each project opts in via its own tsconfig.json, keeping every PR small and reviewable.

The rollout follows the project dependency graph bottom-up (leaf libraries first, applications last). When a project is activated, all of its internal dependencies are already strict, so the type errors surfaced are genuinely its own instead of leaked any from upstream.

Approach (updated)

The original plan was to use typescript-strict-plugin with an opt-in paths list. That approach was dropped. The bootstrap issue (#35933) was closed without the plugin ever landing — it is absent from package.json, pnpm-lock.yaml, and main — and the first merged rollout PR (#36879, dotcms-models) instead used per-project tsconfig.json flags. That is now the established pattern:

"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true

There is no tsc-strict script and no // @ts-strict-ignore escape hatch. Sub-issues still referencing either are stale — ignore those acceptance criteria.

What enforces strict: for Rollup libraries that emit declarations ("declaration": true), @rollup/plugin-typescript is in the build chain and reports type errors, so the existing build target is the gate — CI runs nx run-many -t build via the build-test execution in core-web/pom.xml. Angular libraries typecheck through ng-packagr. Vite-based projects are the exception: their builds use esbuild and skip type checking, which is why the Nx Vite plugin infers a separate typecheck target for them. Note that lint does not catch type errors — ESLint reports lint rules, not TS diagnostics.

See core-web/CLAUDE.md → "TypeScript Strict Mode" for the per-project procedure.

This Epic tracks one task per TS project (42 total), each its own small PR, executed in strict dependency order.

Excluded from scope: dotcms-scss (SCSS only), dotcms-ui-e2e (e2e), mcp-server, and the empty root core-web project.

Removed from the rollout — dead libraries

Two projects were dropped after investigation showed they are dead code that does not compile. Removal is tracked in #36950:

Project Issue Why
dotcms #35936 (closed) Legacy initDotCMS SDK. 0 dependents, tagged skip:build/lint/test, broken dotcms-models import, last npm publish 2022-10-10. Superseded by @dotcms/client.
dot-layout-grid #35937 (closed) Legacy NgGrid grid. 0 dependents, no build target at all, imports ComponentFactoryResolver (removed from Angular), peer deps pinned to Angular 6/7. Last consumer deleted 2024-08-22. Superseded by GridStack in libs/template-builder.

Process note: the sub-issues were generated from the Nx dependency graph without checking whether each project was still alive. An audit of all 44 confirmed the problem is bounded to these two — every other library in the rollout has real dependents — but future automated rollouts should verify liveness before generating issues.

Progress
Issue Project Outcome
#35934 dotcms-models Done — PR #36879 (established the per-project pattern)
#35935 sdk-types Already compliant before the epic; PR #36957 shipped the docs
#35936 dotcms Dead library — closed, removal tracked in #36950
#35937 dot-layout-grid Dead library — closed, removal tracked in #36950
#35938 sdk-create-app Done — PR #36957 (2 errors)
#35939 dotcms-js Done — PR #36957 (38 errors)
#35940 utils Done — PR #36957 (32 lib + 17 spec errors)
#35941 sdk-uve Already compliant before the epic — closed, no diff
#35942 sdk-client Already compliant before the epic — closed, no diff
#35943 dotcms-webcomponents Groundwork only in PR #36957 — Stencil decorator members prepared; strict still off, ~250 errors across 38 files remain
#35944 utils-testing Done — PR #36957 (flags were present but inert: a stale types: ["jasmine"] aborted all type checking)
#35945 sdk-react Done — PR #36957 (14 TS4111 accesses; build verified as a real gate by negative test)
#35946 sdk-analytics Done — PR #36957 (18 TS4111 in source + 29 in specs, 14 of them pre-existing drift; not enforced — negative test shows the Vite build ignores type errors)
#35947 sdk-angular Already compliant — PR #36957 removed dead next/ tsconfig refs that made tsconfig.spec.json unverifiable (TS6053); build verified as a real gate by negative test
#35948 data-access Done — PR #36957. Flags were present but inert (no build target, so nothing read its tsconfig): 36 lib + 47 spec errors fixed. Blast radius measured across all 7 strict dependents — zero new errors, 218 removed, three went 36 → 0. Not enforced
#35949 sdk-experiments Done — flags only; enforcement proved by negative test (Rollup)
#35950 portlets-dot-locales-data-access Already compliant — no diff; its 36 errors were data-access leaking
#35951 global-store Done — one export type; the barrel's TS1205 was only visible from consumers
#35952 portlets-dot-experiments-data-access Already compliant — no diff
#35953 ui Partial — library program 0 (from 122), specs 90 (from 427). The rollout's bottleneck: ~109 errors leaked into each of its 26 dependents
#35954 portlets-dot-analytics-data-access Compliant — its single error lived in global-store's barrel, closed by #35951
#35960 edit-content-bridge Done (2 errors). Enforced — has a build target
#35962 portlets-dot-categories-portlet Done (31) — signal-mock pattern plus fixture drift in two directions
#35963 portlets-dot-es-search-portlet Done (30) — all 28 spec errors were one assignment pattern
#35965 portlets-dot-locales-portlet Done (27) — flags were present but unsatisfied; two wrong annotations
#35968 portlets-dot-tags-portlet Done (22) — 18 were Signal↔jest.Mock casts
#35969 portlets-dot-usage Done (1) — plus a global fix for the htmldiff-js type leak
#35970 dotcdn Done (19), 8 from one missing switch default. Enforced. First app to go strict — surfaced template errors in libs/ui
#35953 ui Done — 549 → 0. The rollout's bottleneck: ~109 errors leaked into each of its 26 dependents, so clearing it dropped seven projects from 219–275 to 0–59
#35956 new-block-editor Done (60) — 38 were TS4111 on TipTap attrs; EditorView sourced from @tiptap/pm/view; @types/turndown added
#35959 content-drive-ui Done (59) — a drop with no active node emitted a payload its own type forbids
#35961 portlets-dot-analytics Done (42) — Spectator's typed props vs Angular's input aliases; two type-only barrels; @types/d3-* added
#35966 portlets-dot-plugins-portlet Done — reported 733, had 63. moduleResolution: node10 broke 256 imports. Also corrects libs/portlets/CLAUDE.md, which recommended that config
#35955 block-editor Done (442, estimated 743) — four wrong declarations explained most of it; test suite was already red and stayed exactly as red (#37091)
#35973 dotcms-block-editor Done — three tsconfigs had never type-checked anything (TS2688); unmasking them caught a template defect no tsc -p can see
#35971 edit-ema-ui Done (119) — two stores declared their pre-render state non-nullable; a spec caught a wire-payload change
#35958 template-builder Done (141) — plus 27 from a fixtures file being compiled as production code
An Omit that erased a whole interface (#35955)

block-editor had DotMenuItem extends Omit<MenuItem, 'icon'>. PrimeNG's MenuItem carries a [key: string]: any index signature, so keyof MenuItem is string | number, and Exclude<string | number, 'icon'> removes nothing — Omit collapsed the type to its index signatures alone and discarded every declared member. id, label, command, disabled were all any, in a type exported to five dependents.

It is worth stating how this was established, because the obvious probe gives the wrong answer:

type Omitted = Omit<MenuItem, 'icon'>;

// Useless: true either way, because the index signature admits any string key.
type HasLabel = 'label' extends keyof Omitted ? 'PRESERVED' : 'WIPED';

// Decisive: `any` means only the index signature survived.
declare const viaOmit: Omitted['label'];    // any                → tsc reports nothing
declare const viaDirect: MenuItem['label']; // string | undefined → tsc reports it

Omit<T, K> is unsafe on any type with a string index signature. Worth grepping for elsewhere in the monorepo — it is a silent any factory, and nothing in the toolchain flags it.

Estimates were high because the graph was still dirty

block-editor was estimated at ~743 own errors and had 442. The difference is the libs/ui cascade: fixing that project removed ~300 errors from block-editor without anyone touching it. The same will be true of the remaining large projects, so their headline numbers should be re-measured, not planned against.

The cost of skipping the runtime check

block-editor:test is red on main — 16 suites, 37 tests, all Angular 22 migration debt (#37091). Strict mode landed anyway, with the suite verified unchanged by failing-test name rather than by count.

That is a real gap, not a formality. Earlier in this epic the compiler accepted a "completed" fixture that a test then rejected, because the omission the fixture made was the exact thing the test asserted on. On block-editor that check did not exist. Any project whose suite is red should have it repaired before, not after.

Two more ways to measure nothing (#35973, #35943)

Deprecated options abort too. TS5101 (baseUrl) and TS5107 (moduleResolution: node10) are config-level errors under TypeScript 6, so they stop the compiler before semantic checking exactly like TS2688 and TS6053. dotcms-webcomponents reports 2 errors without --ignoreDeprecations 6.0 and 279 with it. It cannot set the option in its tsconfig — Stencil bundles TypeScript 5.8.3, which only accepts "5.0", while the workspace runs 6.0.3, which requires "6.0".

The general rule is now in core-web/CLAUDE.md: any TS5xxx, TS6xxx or TS2688 error is a configuration error. tsc never reached the code, so whatever count follows is meaningless.

Check every tsconfig, not just spec. apps/dotcms-block-editor had "types": ["jasmine"] in three configs — spec, editor, and the tsconfig.json they share. All three had aborted on TS2688 since forever.

Templates are only gated where a build target exists

Closing dotcms-block-editor (#35973) turned out to matter far more than its zero own errors suggest. tsc -p was clean on all three configs, and then nx run dotcms-block-editor:build failed:

TS2339: Property 'contentlet' does not exist on type 'never'
  libs/block-editor/.../suggestions-list-item.component.html

libs/block-editor has no build target, so its templates had never been type-checked by anything — tsc -p does not check them, and it was the only gate #35955 had. The small consumer app is what puts them under a real gate.

Worth carrying forward: for any project without a build target, "0 errors" means 0 errors in TypeScript files. Its templates are unverified until some consumer with a build compiles them.

A spec file that disabled type checking across three libraries (#35974)

libs/edit-content/.../dot-edit-content-field.component.spec.ts declared:

/* We need this declare to dont have import errors from CommandType of Tiptap */
declare module '@tiptap/core' {
    interface Commands {
        [key: string]: { [key: string]: (...args) => any };
    }
}

Module augmentations are global to the TypeScript program, so this single spec gave TipTap's Commands a string index signature for every file compiled with it — which is why editor.chain().focus(), a real declared command, reported "Property 'focus' comes from an index signature".

edit-content's program pulls 249 files from block-editor and all of new-block-editor, so under strict flags it produced 256 errors in those two libraries (149 + 107). Both are clean under their own configs; nothing could see this until a consumer went strict.

Measured with and without it on the current non-strict config: 27 lib / 48 spec errors either way. The comment's premise was stale — it suppressed nothing and cost 256 unchecked sites. Removed.

A declare module in a spec file is a repo-wide change. Worth grepping for others.

Re-measured: the remaining estimates were wrong in both directions

Every project left in the rollout, measured by writing the flags into its own tsconfig.json (what the rollout actually does) rather than forcing them on the CLI — which matters, because CLI flags override a project's deliberate overrides:

Issue Project Own errors Epic estimate
#35976 dotcms-binary-field-builder 0
#35967 portlets-dot-query-tool-portlet 3
#35971 edit-ema-ui 119 185
#35958 template-builder 141 220
#35972 portlets-content-drive 269 316
#35943 dotcms-webcomponents 279 ~278
#35964 portlets-dot-experiments-portlet 293 387
#35957 dot-rules 381 657
#35975 portlets-edit-ema-portlet 790 1112
#35974 edit-content 943 1175
#35977 dotcms-ui 2187 ~1002

Roughly 5,400 rather than the ~6,000 implied, and dotcms-ui is the one that was under-estimated. dotcms-binary-field-builder and portlets-dot-query-tool-portlet are effectively free once edit-content lands — which makes edit-content the highest-leverage remaining project, not one of the two giants.

A fixtures file in the lib build (#35958)

template-builder reported 27 inherited errors, all Cannot find name 'jest' in libs/utils-testing. The cause was local: tsconfig.lib.json excludes *.spec.ts but not src/**/utils/mocks.ts, so a fixtures file importing @dotcms/utils-testing sat in the lib program with types: [].

edit-content reports the same 27 from the same cause, and it is worth grepping for others: a mocks.ts under src/ is production code unless the lib config says otherwise.

Zero values are not always the right default (#35971)

The bulk TS2564 policy — "string means '', boolean means false" — is wrong whenever the consumer's own default is not the zero value. @Input() showDiff: boolean became = false, but the dotDiff pipe defaults to true and DotContentCompareStore seeds showDiff: true. Diffing silently switched off and 4 specs failed.

Check what reads the field before picking its default.

Never let a type error change a payload (#35971)

publishContentletAndWaitForIndex takes { [key: string]: string | number }, and dot-favorite-page.store sends inode: formData.inode || null. The obvious fix is ?? '' — and it is wrong: the spec asserts inode: null because the endpoint distinguishes null from an empty string. The signature was the thing that needed widening.

This is the second time in this epic that the pull toward "make the error go away" would have altered behaviour. The first was a fixture that deliberately omitted a field its own test asserted on. Both were caught by running the tests, which is why a red suite (as in block-editor, #37091) is a real cost and not a formality.

On production non-null assertions

Bulk narrowing passes put 27 ! into edit-content's production files and 9 into template-builder's, each tripping @typescript-eslint/no-non-null-assertion — the rule that exists to discourage precisely that. Lint went from 11 warnings to 41, and from clean to 9.

They were reverted or converted to guards. Where a guard is not obvious, the error belongs on the remaining count rather than under an assertion: edit-content went back up because of this, deliberately. Strict mode is only worth the effort if the nullability is actually handled.

Enforcement is uneven and worth tracking. Only some of these projects have CI that actually verifies the flags.

  • Enforced through their build: sdk-types, sdk-create-app, sdk-uve, sdk-client, sdk-react, sdk-angular, sdk-experiments, edit-content-bridge and dotcdn — the Nx rollup, esbuild, tsc and ng-packagr executors all type-check. sdk-angular was confirmed by negative test (a deliberate type error fails nx run sdk-angular:build with TS2322).
  • Declared but unverified: dotcms-js, utils, utils-testing and now data-access have no build target and are tag-excluded from lint and test, so their flags are documentation only — a regression there would not be caught. sdk-analytics lands in the same bucket for a different reason: it does build, but through Vite, and a negative test confirmed a deliberate type error does not fail nx run sdk-analytics:buildvite-plugin-dts emits declarations without failing on diagnostics. With five projects now in this state, this needs a single decision on whether nx affected -t typecheck gets wired into core-web/pom.xml.
  • The opposite case: dotcms-webcomponents has no skip:build, so Stencil type-checks it on every PR. There the gate is real and strict is all-or-nothing — it cannot be switched on until the error count reaches zero.

A related trap found along the way: a tsconfig that names a types entry which is not installed makes tsc emit TS2688 and stop before semantic checking, so it reports one error no matter what the code does. That is what hid utils-testing's real errors, and it means a stable error count across a change proves nothing in that situation.

A second blind spot, found in #35946: the Nx-inferred typecheck target runs only tsconfig.lib.json, never tsconfig.spec.json. Combined with Vite projects transforming tests through babel-jest (types stripped, never checked), spec type errors are invisible to every gate — sdk-analytics had accumulated 14 of them unnoticed. Remaining projects should be measured against both configs, and should expect a similar hidden backlog.

A third finding, from #35947 — verify the compiler actually reached the code before trusting any count. Two of the fourteen projects triaged so far were silently unchecked: utils-testing via TS2688 (a types entry that is not installed) and sdk-angular via TS6053 (a files entry pointing at a deleted file). Both abort tsc before semantic checking, so it reports one error regardless of the code. sdk-angular's tsconfig.spec.json had never completed a single semantic pass. Note the asymmetry: a non-matching include glob is harmless, a missing files entry is fatal. The remaining ~30 projects' tsconfigs should be swept for both variants before their error counts are believed. core-web/CLAUDE.md now documents both.

A third, larger blind spot, found in #35948: nx run <project>:test does not type-check — anywhere in core-web. jest-preset-angular runs on ts-jest, and ts-jest copies TypeScript's isolatedModules into its own transpile-only switch (config-set.js:229), which stops it building the language-service host that diagnostics need (ts-compiler.js:74). This repo's Jest guidance requires isolatedModules: true in every tsconfig.spec.json, so the flag is set everywhere. data-access is the proof: 84 tsc errors alongside 754 passing tests. Consequences: (a) passing tests have never been evidence that specs type-check, so any prior per-project conclusion resting on that is void — the #35947 comment has been corrected on that basis; (b) every project in this rollout should be measured with tsc -p <projectRoot>/tsconfig.spec.json --noEmit, never with :test; (c) removing isolatedModules would switch checking on monorepo-wide, which is an epic-level call and was deliberately not made inside #35948. core-web/CLAUDE.md documents this alongside the two masking notes above.

A fourth measurement trap, from #35966: check moduleResolution before believing any count. dot-plugins reported 733 errors and had 63. Its tsconfig.spec.json used module: "commonjs" + moduleResolution: "node10", which cannot resolve the @dotcms/* subpath exports — so 256 imports failed and everything downstream collapsed to unknown (227 TS2571, 115 TS18046, 85 TS2339). Aligning it with dot-tags took it to 1. Every remaining project was swept and dot-plugins was the only one affected, so the large counts for block-editor, dot-rules, edit-content and edit-ema-portlet are real work rather than artefacts.

Root cause worth fixing at the source: libs/portlets/CLAUDE.md recommended exactly this configuration. Its anti-patterns table said to omit strict: true ("causes issues with Angular compiler") and to use "module": "commonjs" in tsconfig.spec.json, while dot-tags — which the same guide calls the canonical reference — carries both and compiles clean. Corrected in batch three, so the next portlet does not inherit it.

The libs/ui cascade is the clearest evidence for working strictly bottom-up. It had none of the six flags and 549 own errors, ~109 of which leaked into each of its 26 dependents. Clearing it took portlets-dot-usage from 219 to 0 without touching that project, and portlets-dot-tags-portlet turned out to have 1 error of its own rather than 238. Re-measure after every upstream lands.

Dependencies added during the rollout. Four direct dependencies shipped no types, so their imports were implicitly any: d3-scale, d3-selection, d3-shape (#35961) and turndown (#35956). Their DefinitelyTyped packages are maintained, so they were installed rather than hand-declared. A fifth, htmldiff-js, has no DefinitelyTyped package and is declared in a root-level types/ folder registered through tsconfig.base.json — not next to its consumer, which is what made it leak one error into every consumer of libs/ui (#35969).

Two findings from batch two that change how the remaining projects should be measured.

1. tsc -p does not check templates. Every per-project count in this epic was taken with tsc, which misses template errors entirely — those are only verified when an app builds, and most projects here have no build target. Measured by probe across the three remaining apps: 2, 8 and 23 template errors against 350–2500 .ts errors. Real, but ~1% of the volume, so it does not change the plan. Found via #35970, the first app to go strict, whose build failed on libs/ui's templates rather than its own.

2. Upstream leakage inflates every count by roughly 60%. Measuring own-vs-inherited errors across the remaining 24 projects gave 8,916 own against 23,272 reported. The uniform ~109 seen in every portlet was libs/ui leaking through imports — portlets-dot-tags-portlet had 1 error of its own, not 238. Clearing libs/ui's library program dropped seven projects from 219–275 down to 0–59. The practical rule: fix strictly bottom-up and re-measure after each upstream lands, and never plan from a raw count. Corollary already visible — dotcms-block-editor, dotcms-binary-field-builder and portlets-dot-query-tool-portlet have almost no errors of their own but cannot reach 0 until block-editor, edit-content, new-block-editor and edit-ema are done.

Also worth noting: tsc -p on an app includes every file matching its include, while a build only compiles what is reachable from the entry point. For dotcms-ui that is 3,165 vs 1,002 — roughly two thousand errors sit in files that are never compiled or shipped.

Execution order (high level)
Layer 0  dotcms-models, sdk-types, ...
Layer 1  dotcms-js, utils, sdk-uve, ...
Layer 2  utils-testing, sdk-react, ...
Layer 3  data-access, ...
Layer 4  global-store
Layer 5  ui
Layer 6  block-editor + portlets-*
Layer 7  edit-ema-ui, portlets-content-drive
Layer 8  edit-content
Layer 9  portlets-edit-ema-portlet
Layer 10 dotcms-ui (app)

The full ordered list lives in the sub-issues, numbered [NN/44] (numbering kept as originally generated; two of those numbers are now closed as not applicable).

Desired Outcome

  • All in-scope core-web projects compile clean under strict with zero errors.
  • Every strict project is actually enforced by a CI target (build or typecheck) — strict flags that nothing verifies are worthless.
  • No regressions: nx affected -t build,lint stays green throughout.
  • New any usage trends toward zero.

Target Personas

  • Developer teams
  • Content teams
  • DevOps teams
  • System administrators (dotCMS)

Links

  • Strategy: bottom-up topological order derived from nx graph.
  • Established pattern: #36879 (dotcms-models) — per-project tsconfig flags.
  • Dead-code removal: #36950

And a pattern worth stating up front for the remaining 29: flags present ≠ flags applied. data-access had all six flags plus Angular's strict template options for some time while carrying 36 errors, because a library with no build target never has its own tsconfig read by anything, and its dependents compile its sources under their own non-strict configs. Before assuming a project is done because its tsconfig looks right, check whether nx show project <p> lists a build target — and if not, run tsc -p yourself.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.