Skip to content

refactor(core-web): TS strict mode across all 44 projects — completes epic #35932 - #36957

Open
nicobytes wants to merge 140 commits into
mainfrom
35932-enable-strict-mode
Open

refactor(core-web): TS strict mode across all 44 projects — completes epic #35932#36957
nicobytes wants to merge 140 commits into
mainfrom
35932-enable-strict-mode

Conversation

@nicobytes

@nicobytes nicobytes commented Aug 7, 2026

Copy link
Copy Markdown
Member

What

Thirty-three steps of the strict-mode rollout (epic #35932), plus groundwork on one more (dotcms-webcomponents, not closed):

Issue Project Change
#35974 edit-content 101 + 208 errors. The spec config had opt-outs hiding 666 of them. Most fixes were declarations lagging behind bodies already written for null
#35976 dotcms-binary-field-builder Had none of the six flags. Its own tsconfig compiles nothing (include: []), so measuring that file reported a fake zero
#35967 portlets-dot-query-tool-portlet Two flags missing. Its noImplicitReturns-without-strict combination caught a TS7030 I introduced in edit-content
#35971 edit-ema-ui 119 errors. Two stores declared their pre-render state non-nullable; a spec caught me changing a wire payload
#35958 template-builder 141 errors, plus 27 that came from a fixtures file being compiled as production code
#35973 dotcms-block-editor Three tsconfigs had never type-checked anything (TS2688). Unmasking them surfaced a template defect that only a build can see
#35955 block-editor 442 own errors. Four wrong declarations explained most of them, including an Omit that silently erased every member of PrimeNG's MenuItem
#35970 dotcdn 19 errors, 8 of them from one missing switch default. First app to go strict — surfaced template errors in libs/ui
#35969 portlets-dot-usage One fixture field, plus a global fix for the htmldiff-js type leak
#35968 portlets-dot-tags-portlet 18 of 22 were Signal↔jest.Mock casts; one catch binding
#35965 portlets-dot-locales-portlet Flags were present but unsatisfied — two wrong DynamicDialogRef annotations, of(null) for Observable<void>
#35963 portlets-dot-es-search-portlet All 28 spec errors were jest.fn() assigned onto signal-typed members
#35962 portlets-dot-categories-portlet Fixture drift in two directions plus the signal-mock pattern
#35960 edit-content-bridge Dialog ref captured in a local; a control-flow blind spot in its spec
#35954 portlets-dot-analytics-data-access Compliant — its one error lived in global-store's barrel
#35952 portlets-dot-experiments-data-access Already compliant, no diff
#35951 global-store One-line export type that also closed #35954
#35950 portlets-dot-locales-data-access Already compliant, no diff
#35949 sdk-experiments Flags-only; enforcement proved by negative test
#35966 portlets-dot-plugins-portlet Reported 733, had 63 — moduleResolution: node10 broke 256 imports. Also corrects the portlets guide that recommended it
#35961 portlets-dot-analytics Spectator's typed props vs Angular's input aliases; two more type-only barrels; @types/d3-* added
#35959 content-drive-ui A drop with no active node emitted a payload its own type forbids
#35956 new-block-editor 38 of 60 were TS4111 on TipTap attrs; EditorView sourced from @tiptap/pm/view; @types/turndown added
#35953 ui 549 → 0. The rollout's bottleneck: ~109 errors leaked into each of its 26 dependents
#35948 data-access Flags were present but inert (no build target) — fixes the 36 lib + 47 spec errors hiding behind them
#35947 sdk-angular Already compliant — removes dead next/ tsconfig refs that made tsconfig.spec.json unverifiable (TS6053)
#35946 sdk-analytics Enable the 5 missing flags + fix 18 TS4111 in source and 29 in specs (14 pre-existing)
#35945 sdk-react Enable the 5 missing flags + fix 14 TS4111 index-signature accesses
#35944 utils-testing Strict was declared but inert — a stale types: ["jasmine"] aborted all type checking
#35940 utils Enable strict + fix the 32 (+17 spec) resulting type errors
#35939 dotcms-js Enable strict + fix the 38 resulting type errors
#35938 sdk-create-app Enable strict + fix the 2 resulting type errors
#35935 sdk-types No code needed — it was already strict. Documents the rollout pattern instead.

All three add the standard six flags to the project's own tsconfig.json, following the pattern established in #36879 (dotcms-models). tsconfig.base.json stays at "strict": false — the rollout never flips it globally.


dotcms-js (#35939)

The largest of the three: 38 errors across 11 files, in a layer-1 core library with 20 dependent projects, including the dotcms-ui admin app. Six of those dependents are already strict, so this library's loose types were leaking uncertainty into projects that had opted into rigour.

Most fixes correct types that were simply wrong, rather than silencing the compiler:

Site Was Reality
Auth.loginAsUser User The code has always passed null when nobody is impersonating, and every consumer already guards with auth.loginAsUser || auth.user. Now User | null.
StringUtils.getLine string Its own JSDoc says "null if it does not exists". Now string | null.
HttpRequestUtils.getQueryStringParam string Same — JSDoc already documented the null case.
RoutingService.getPortletURL string Returns Map.get(). Now string | undefined.
SiteService.switchSiteById Observable<Site> Emits of(null) when no site is found. Now Observable<Site | null>; its one consumer already handled null.
ResponseView.bodyJsonObject DotCMSResponse<T> Assigned from HttpResponse.body, which is nullable. The surrounding try/catch could never throw and has been removed.

LoginService.urls moved from Record<string, string> to inference-typed, which resolves all 8 TS4111 errors at once and gives each endpoint a named property.

Two definite-assignment assertions were used, each with a TODO: LoginService._auth and SiteService.selectedSite are assigned during init but not in the constructor. Modelling them as | undefined is the truthful type, but their public getters (auth, currentSite) are consumed by already-strict projects, so widening them is a public-API change that belongs in its own issue.

No new any, @ts-ignore, or @ts-expect-error anywhere in this PR.

⚠️ dotcms-js has no build target and is tag-excluded from lint and test, so nothing in CI verifies these flags. They document intent; they do not enforce it. This was an explicit scoping decision — no typecheck target or CI gate was added. The six already-strict consumers provide partial, incidental coverage only. Full reasoning in specs/35939-dotcms-js-strict-mode/spec.md, which is included in this PR.

Blast-radius verification

data-access (a strict consumer) went from 106 type errors to 68, with zero new errors introduced — the honest types upstream remove noise downstream. dotcms-ui typechecks clean apart from a pre-existing missing dotcms-webcomponents/loader dist.


utils (#35940)

32 errors across only 3 files, plus 17 more that appeared in the spec files once the flags propagated through tsconfig.spec.json (baseline there was 0). Both are fixed here — leaving the spec errors would have shipped a regression.

The bulk was one constant. EMPTY_FIELD assigned null to 18 members that DotCMSContentTypeField declares non-nullable:

  • Replaced with zero values of the declared types. Verified safe: nothing compares those members to null strictly — consumers use falsy checks such as isNewField's !field.id — so '', 0 and false behave identically at runtime.
  • clazz has no zero value (DotCMSClazz is a union of concrete Java class names), so EMPTY_FIELD and EMPTY_SYSTEM_FIELD are now Omit<DotCMSContentTypeField, 'clazz'>. They are partial templates, not valid fields, and the type now says so. The derived COLUMN_FIELD / ROW_FIELD / TAB_FIELD already supply their own clazz, so they remain complete.

Other fixes:

Site Change
getFieldsWithoutLayout Truthy .filter() did not narrow the optional row.columns. A type predicate clears the TS2532 and both TS2769 without a cast.
ellipsizeText Accepted null/undefined at runtime — its own guard and its tests say so — but declared string/number. Widened to match, with an explicit limit == null check so later comparisons narrow.
fallbackErrorMessages Typed { [key: number]: string }, mirroring the identical declaration already in libs/data-access/.../dot-upload.service.ts.
dot-utils.ts Bracket access for the six DotCMSContentlet index-signature reads in getImageAssetUrl.
dot-asset.service.ts Explicit types for promises and the two fetchAsset params.

The nine as unknown as casts added are all in spec files, on inputs the tests deliberately pass as invalid, matching the idiom those files already used.

⚠️ Same enforcement gap as dotcms-js: utils has no build target and is tag-excluded from lint and test, so nothing in CI verifies these flags. Accepted trade-off, consistent with #35939.

Blast-radius verification

data-access (strict consumer) went from 68 type errors to 36, zero new. utils-testing (strict) unchanged at its 1 pre-existing error — the Omit did not break its EMPTY_SYSTEM_FIELD spread.


sdk-create-app (#35938)

Two errors, both from flags beyond plain strict:

  • src/index.ts:393process.env.DEBUG needs bracket access under noPropertyAccessFromIndexSignature (TS4111). It is the only process.env.* dot access in the project.
  • src/utils/index.ts:41fetchWithRetry tripped noImplicitReturns (TS7030). The loop returns on success and throws on the last attempt, but with retries < 1 the loop never runs and the function fell through returning undefined. Its only caller (isDotcmsRunning, src/index.ts:506) already guarded with if (res && …), so nothing broke in practice — but the signature was lying. Throwing after the loop closes the gap and narrows the return type to Promise<AxiosResponse>.

No build or CI wiring was needed here. The @nx/esbuild:esbuild executor type-checks before bundling (skipTypeCheck defaults to false and is not overridden), and CI already builds this project via nx run-many -t build (build-test in core-web/pom.xml). The same build runs in the SDK release pipeline (cicd_release-sdk.ymlnx run-many --projects='sdk-*'), so the flags are enforced on every release.


sdk-types (#35935)

libs/sdk/types/tsconfig.json has carried strict: true plus the four extra safety flags since the library was created (#31967), and tsc --noEmit passes with zero errors. It is also already enforced: tsconfig.lib.json sets "declaration": true, so @rollup/plugin-typescript sits in the Rollup chain and fails the build on a strict violation.

So no code change was required. What was missing was documentation, added here to core-web/CLAUDE.md:

  • A ## TypeScript Strict Mode section covering the per-project flags, what actually enforces them, and the Vite exception (esbuild skips type checking, which is why the Nx Vite plugin infers a separate typecheck target).
  • Fixes a line that forbade "strict": true in project tsconfigs. It sat under the Jest config guidance but read as a blanket ban, contradicted docs/frontend/TYPESCRIPT_STANDARDS.md, and blocked the epic outright. The restriction now points at tsconfig.spec.json, which is what it meant.


utils-testing (#35944)

The six strict flags were already in tsconfig.json — but completely inert. tsconfig.lib.json declared "types": ["jasmine"], that package is not installed, so tsc emitted TS2688: Cannot find type definition file for 'jasmine' and stopped before semantic checking. The project reported exactly one error regardless of what the code did.

The reference was stale: nothing uses jasmine, two files use jest.*, and @types/jest is installed. Switching to "types": ["jest"] removed the abort and 27 spurious Cannot find name 'jest' errors, leaving 5 real ones:

Site Fix
clean-up-dialog.ts Untyped fixture param → typed structurally as { nativeElement: unknown }, since only that property is touched (no need to pull in Angular's ComponentFixture)
dot-page-state.service.mock.ts _lock: boolean = nullboolean | null
dot-page-tools.mock.ts ×3 Mock entries carried a tags array that DotPageTool does not declare. Verified nothing in the repo reads .tags off a page tool, so the dead field was removed rather than added to the model in dotcms-models

tsc -p libs/utils-testing/tsconfig.lib.json --noEmit now exits 0 with no CLI overrides — the check is real rather than short-circuited.

Verified across consumers of the touched mocks (cleanUpDialog in 7 files, page-tools mock in 3): data-access 751 tests passed, edit-ema-ui 338 passed.


dotcms-webcomponents (#35943) — groundwork only, not closed

Strict is not enabled here. ~250 errors remain across 38 files, and unlike the other projects this one has no skip:build, so Stencil type-checks it on every PR — flipping the flag early turns CI red. What landed is the part that is correct on its own.

The decorator split, which is the load-bearing decision. Stencil declares runtime-injected members without initializers, colliding with strictPropertyInitialization (139 of the original 375 errors). The fix cannot be uniform:

Decorator Count Fix Why
@Event 57 ! Internal; the runtime creates the EventEmitter
@Element 27 ! Internal; the host element
@State 25 ! Internal component state
@Prop 30 ? Public API

Using ! on @Prop made Stencil emit 28 props as required in components.d.ts — breaking for any TS/JSX consumer. With ? the generated API moves required → optional, which is backward compatible. Measured in the generated file, not assumed.

Two traps recorded on the issue

Stencil under-reports. Its build shows ~10 files / ~39 errors per run, not the total. Measured at the same commit: Stencil 39 errors / 10 files vs tsc 250 / 38. Size this work with tsc, not with build output.

--skip-nx-cache does not clear Stencil's cache. Builds can report green against stale .stencil output. This bit me: 0117273504 annotated a prop, passed a "clean" build, and was actually broken — reverted in f22afce383 after verifying twice with .stencil and the Nx cache cleared.

That prop (dot-binary-text-field's value) is genuinely contradictory: handleFilePaste assigns a File, other paths assign strings, and the template feeds it to an <input value> that accepts neither. No annotation describes the current code — the render path has to be fixed first. Left untyped with a TODO(#35943) so it is not re-annotated in isolation.


sdk-react (#35945)

strict: true was already present; the five companion flags were not. Adding them surfaced 14 errors, all TS4111 — dot access on a type carrying an index signature — resolved with bracket notation. Two origins, same fix:

  • 13 from node.attrs, declared Record<string, any> in @dotcms/types. That type is deliberately left alone: block editor attributes really are dynamic, and it lives in a layer-0 project whose consumers would all be affected.
  • 1 from CSS Modules (styles.row in Row.tsx), whose generated type is also a Record<string, string>.

No behaviour change — bracket access compiles to the same property lookup.

The flags here are genuinely enforced, and that was proved rather than assumed. Reverting one access to dot notation fails the build with @rollup/plugin-typescript TS4111, confirming TypeScript sits in the Rollup chain. The project carries no skip: tags, so CI builds, lints and tests it on every PR, and the same build runs in the SDK release pipeline.

One error remains under plain tsc and is expected: Cannot find module 'virtual:sdk-version' in sdk-client — a Vite virtual module that raw tsc cannot resolve but the build can. It predates this change and is unrelated to strict mode. Worth knowing when measuring, or the count reads 15 instead of 14.


sdk-analytics (#35946)

Same starting shape as sdk-react: strict: true already present, the five companion flags absent. But this one is not enforced, and that was established by test rather than inference.

18 errors in production source, all TS4111 from noPropertyAccessFromIndexSignature — dot access on HTMLElement.dataset (DOMStringMap) and on a Record<string, unknown> of payload properties. Bracket notation throughout, reads and writes alike. Spread across dot-analytics.utils.ts (10), dot-analytics.click-tracker.ts (4), dot-analytics.impression-tracker.ts (3), dot-analytics.click.utils.ts (1).

29 errors in specs — 15 more of the same mechanical dataset fix, plus 14 that were pre-existing drift rather than strict-mode fallout. Confirmed pre-existing: they persist identically under --strict false. Two independent gaps had let them accumulate unseen — the inferred typecheck target runs only tsconfig.lib.json, and jest.config.ts transforms via babel-jest, which strips types without checking them.

Root cause Count
ANALYTICS_CONTENTLET_CLASS no longer exported — renamed to CONTENTLET_CLASS 2
Pageview fixture put device inside data and omitted required locale_id 4
Untyped jest.fn() inferring never for mockResolvedValue / mockRejectedValue 3
Location mock missing host 1
jest.spyOn(...).mockImplementation() called with no argument 2
mockInitialize inferred as zero-arg 1
result.custom — not on EnrichedTrackPayload 1
TS2589 excessively deep instantiation 1

The pageview fixture was the instructive one: with device misplaced and locale_id missing, the pageview member of the DotCMSEvent union stopped matching, so TypeScript fell through to the impression member and reported a misleading "doc_encoding does not exist on DotCMSContentImpressionPageData". One coherent fix cleared four errors. Fixtures were corrected rather than production types widened; no source bug hid behind any of them.

⚠️ Negative test says the build is not a gate. Unlike sdk-react, this project builds through Vite. It does run dts({ tsconfigPath: 'tsconfig.lib.json' }) with vite-plugin-dts@4.5.4, which invokes the TS compiler to emit declarations — so the build plausibly could have enforced the flags. It does not: a deliberate const __strictProbe: number = "definitely not a number"; in a lib source file did not fail nx run sdk-analytics:build --skip-nx-cache. vite-plugin-dts emits declarations without failing on diagnostics, and CI never invokes typecheck. Probe reverted immediately.

So sdk-analytics joins dotcms-js and utils as strict but unenforced. Wiring nx affected -t typecheck into core-web/pom.xml was deliberately left out — it is monorepo-wide and belongs to the epic, not to project 13 of 44. Both gaps are raised on #35932.

This also corrects the pattern proposed in #35942 — that every libs/sdk/* project was already strict and already enforced. That holds for the Rollup-built SDK libs, which type-check through @rollup/plugin-typescript (as sdk-react proved). It does not hold for Vite-built ones: sdk-analytics inherited strict from the shared tsconfig lineage but neither the other five flags nor a type-checking build.

0 internal dependents — the only references to @dotcms/analytics outside the lib are doc comments in libs/sdk/uve/src/internal/constants.ts. No blast radius.


sdk-angular (#35947)

No flags were added — all six were already there, plus Angular's strictTemplates, strictInjectionParameters and strictInputAccessModifiers. Re-adding them would have been a cosmetic diff. The real defect was dead config.

Both tsconfig.lib.json and tsconfig.spec.json referenced a next/ directory that existed and was removed — the references landed on 2025-03-21 (09e879b2ac) and outlived the directory. One of them was fatal:

error TS6053: File '.../libs/sdk/angular/next/test-setup.ts' not found.
  The file is in the program because:
    Part of 'files' list in tsconfig.json

tsc aborts on that before semantic checking, so tsconfig.spec.json had never completed a single semantic pass and any error count taken from it was meaningless. The asymmetry is the lesson: a non-matching include glob is harmless, a missing files entry is fatal — which is why tsconfig.lib.json, whose next/ references were only in include/exclude, kept working.

Removed the dangling references from both. Both configs now report 0 own errors; the one remaining error in each is the pre-existing, unrelated virtual:sdk-version from sdk-client documented in the sdk-react section above.

The spec config coming out clean was predicted, not lucky: jest-preset-angular@17ts-jest@29.4.6 with diagnostics enabled and transpile-only unset already type-checked all 21 spec files against these exact compilerOptions — just per-file, never as a whole program. That is the opposite of sdk-analytics below, where babel-jest stripped types and hid 14 errors. Same rollout, two projects, and the test transformer decided whether anything was checked at all.

Negative test confirms the build is a real gate. A deliberate const __strictProbe: number = "definitely not a number"; in lib/store/dotcms.store.ts fails nx run sdk-angular:build with TS2322 and exit code 1 — @nx/angular:package runs ngtsc. Probe reverted, file byte-identical to git. No typecheck target was added; per CLAUDE.md that is redundant when the build already type-checks.

Production source is clean without escape hatches: 0 @ts-ignore / @ts-expect-error, 0 non-null assertions, and 2 anys that are the same exported declaration (DynamicComponentEntity = Promise<Type<any>>, lib/models/index.ts:12). Type<any> is idiomatic Angular for dynamically-loaded components and the type is public API, so narrowing it is a separate change, not strict-mode work. 0 internal dependents.

CLAUDE.md now documents the TS6053 masking variant next to the existing TS2688 one. Two of the fourteen projects triaged so far were masked this way — #35944 via TS2688, #35947 via TS6053 — so error counts from the remaining projects should not be trusted until their tsconfigs are checked for this.


data-access (#35948)

First non-isolated project in the rollout: 27 direct dependents, 6 of them already strict.

All six flags had been in libs/data-access/tsconfig.json for some time, and they were completely inert. The project has no build target, so its own tsconfig is never read by anything, and its 27 dependents compile these sources under their own non-strict configs. So 36 errors sat in a layer-3 shared services hub with CI fully green — matching the 106 → 68 → 36 drift measured incidentally in the dotcms-js and utils sections above.

Config Before After
tsconfig.lib.json 36 0
tsconfig.spec.json 84 (47 own + 37 lib pulled in) 0

Production source (36)

  • paginator.service.ts (14) — 8 uninitialised fields given zero values; four header reads take ?? '' (identical NaN outcome); private setLinks(linksString: string) widened to string | null since its body already did linksString?.split(',') || []; the file-local interface Links gained an index signature because the Link-header parser stores whatever rel the server sends.

    _sortOrder was deliberately left optional rather than defaulted. getParams() gates the direction query param on truthiness, and OrderDirection.ASC === 1 is truthy where undefined was not — defaulting it would have made every paginated request in the admin UI start sending a param it previously omitted.

  • dot-page-state.service.ts (12) — six declarations widened because they were simply wrong; the service really does emit null. The interesting one is handleSetPageStateFailed, declared Observable<DotHttpErrorHandled> but ending in map(() => undefined). Because it genuinely emits undefined, the caller's = [null, null] destructuring default is reachable and load-bearing, not dead code. Declaring the honest type made the whole switchMap typable; it now destructures explicitly instead of fighting an annotation. if (page) became if (page && user) — which forkJoin already guaranteed.
  • dot-router (5), dot-localstorage (3), dot-content-types-info (2) — nullable getters (previousUrl, storedRedirectUrl), localStorage reads, and a string index narrowed to keyof.

Specs (47)

25 came from three lines. The fake Router declared navigate = jest.fn(() => ...), which infers zero parameters, so every toHaveBeenCalledWith(...) was a TS2554.

Gotcha for the remaining projects: this file's jest is @types/jest, which uses jest.fn<TReturn, TArgs>two type parameters. @jest/globals (as in sdk-analytics) uses jest.fn<Fn>. Wrong arity gives TS2743.

Two of the rest were real bugs hiding behind disabled suites:

  • dot-global-message.service.spec.ts imported DotMessageService from dot-alert-confirm.service, which does not export it. The suite is xdescribed, so it never ran.
  • dot-ai.service.ts — a production file — did export { DotAiProviderConfig } on a type, invalid under isolatedModules. Only the spec config sets that flag, so only it surfaced the error.

Also: dot-page-layout.service.spec.ts was calling save(id, mockDotLayout()), but save takes a DotTemplateDesigner and posts it verbatim — the spec was testing a payload shape production never sends (edit-ema-layout.component.ts:111 sends the real one). And the dot-content-drive fixture used an offset field removed from DotContentDriveSearchRequest, copied from the model's own stale JSDoc example, which is fixed here too. dot-personas now reuses the existing mockDotPersona from @dotcms/utils-testing instead of hand-rolling 21 fields.

No new any, no @ts-ignore / @ts-expect-error anywhere in the diff.

Blast radius — measured, not assumed

Every strict dependent was counted before and after. Zero new errors, 218 removed, and three went fully clean because they were carrying nothing but this library's leakage:

Strict dependent Before After
global-store 36 0
portlets-dot-analytics-data-access 36 0
portlets-dot-experiments-data-access 36 0
image-editor 148 111
portlets-dot-analytics 151 115
portlets-dot-locales-portlet 147 111
utils-testing 0 0

This is the "high leverage" the issue predicted, quantified.

⚠️ Still unenforced, consistent with dotcms-js and utils above — no build target, so nothing catches a regression of these 83 fixes. That is now four projects in this state; raised on #35932 rather than solved per-project.

A repo-wide finding: test never type-checks specs

data-access uses jest-preset-angularts-jest@29.4.6 against tsconfig.spec.json, which looks like it type-checks. It does not, because that tsconfig sets isolatedModules: true:

  • ts-jest/.../config/config-set.js:229 reads TypeScript's isolatedModules into ts-jest's own flag.
  • ts-jest/.../compiler/ts-compiler.js:74 builds the language-service host only if (!isolatedModules).
  • _doTypeChecking() needs that host for getSemanticDiagnostics.

data-access is the proof: 84 tsc errors alongside 754 passing tests. Since core-web/CLAUDE.md mandates isolatedModules: true in every tsconfig.spec.json, no project's test target type-checks its specs anywhere in this monorepo — so "tests pass" has never been evidence of spec type-cleanliness. This corrects the justification given in the sdk-angular section above (that verdict was separately confirmed with tsc -p, so it stands). Removing the flag would enable checking monorepo-wide and is left to the epic.


Batch two: twelve more projects (#35949 #35950 #35951 #35952 #35954 #35960 #35962 #35963 #35965 #35968 #35969 #35970)

Bottom-up, and the ordering mattered more than the raw counts suggested.

libs/ui first, because it was the bottleneck (#35953 — still open)

ui had none of the six flags and 549 own errors, and ~109 of them leaked into each of its 26 dependents. Clearing its library program collapsed the portlets that follow:

Project Before ui After
portlets-dot-usage 219 0
portlets-dot-tags-portlet 238 22
portlets-dot-locales-portlet 243 27
portlets-dot-es-search-portlet 246 30
portlets-dot-categories-portlet 249 33
portlets-dot-analytics 266 50
content-drive-ui 275 59

ui's tsconfig.lib.json is at 0 (from 122) and its tsconfig.spec.json at 90 (from 427), so #35953 stays open. Notable findings there:

  • dot-icon's size became size?: number rather than = 0, because the template binds [style.font-size.px]="size" and 0 would have rendered invisible icons where undefined inherits.
  • dot-sidebar, dot-dropdown, dot-site-selector, dot-container-options and dot-trim-input all inject their host with { optional: true } and then used it unguarded. They now guard.
  • Types that were wrong rather than merely loose: formEl was declared HTMLFormElement while the template says #formEl="ngForm"; getVariableIndexChanged declared number while its own JSDoc documented number | null.
  • A real defect: dot-add-to-bundle invoked getDefaultBundle twice for the same value.
  • tsconfig.lib.json excluded a non-existent src/test.ts but not test-setup.ts, **/*.test.ts or __mocks__/, so test files were being compiled into the library program — 15 errors by itself.
  • Specs went 427 → 90 mostly by asserting at the point of declaration: 53 usages of one select local came from a single line.

Already compliant, verified rather than assumed

#35950 portlets-dot-locales-data-access and #35952 portlets-dot-experiments-data-access needed no change: all six flags present, both configs at 0, and neither masked by a TS6053/TS2688 config error. Both reported 36 errors before #35948 — purely data-access leaking.

#35949 sdk-experiments was a flags-only change. The cost was measured on the CLI before committing (0 with all six), and enforcement was proved by negative test: it builds with Rollup, so a deliberate type error fails the build with @rollup/plugin-typescript TS2322.

Barrel files that only their consumers could see

#35951 global-store re-exported WebSocketStatus — a type — with export {}, which is TS1205 under isolatedModules. Its own configs never reported it, because no spec there imports ./index, so the file was never in its own program. It surfaced only from consumers, showing up as the single spec error attributed to #35954 portlets-dot-analytics-data-access. One export type closed both issues.

Third and fourth instances of this shape followed in dot-analytics's two barrels. A barrel can carry an isolatedModules error that only its consumers ever see — worth a repo-wide sweep, raised on #35932.

An ambient declaration in the wrong place

The htmldiff-js declaration added for ui lived under libs/ui/src, so it was only in ui's own program and every consumer still reported TS7016. It is now registered through tsconfig.base.json from a root-level types/ folder — inside libs/ui made @nx/enforce-module-boundaries demand a relative import. Same shape as the known virtual:sdk-version leak from libs/sdk/client.

Signal stores are the dominant spec pattern

#35968 dot-tags (18 of 22), #35963 dot-es-search (all 28) and #35962 dot-categories (15 of 31) were all the same theme: a Signal<T> does not structurally overlap a jest.Mock, so casts must route through unknown, and assignments of jest.fn() onto signal-typed members must state the type they stand in for.

dot-categories also had fixtures incomplete in two directions — DotCMSAPIResponse needs four fields beside entity (now a shared API_ENVELOPE rather than repeated nine times) and DotCategoryDeleteResult needs deletedCount — plus four calls passing Event where openRowMenu takes a MouseEvent.

Wrong annotations, not loose ones

#35965 dot-locales: both dialog refs were annotated DynamicDialogRef, but DialogService.open() is typed as possibly null in this PrimeNG version. Its store spec mocked Observable<void> methods with of(null).

#35960 edit-content-bridge: the dialog ref is now captured in a local so its non-nullness is evident rather than asserted. In its spec, reconcileOnFormEvent is assigned inside a nested callback, which control-flow analysis cannot see, so TypeScript narrowed it back to null and called it not callable.

#35969 portlets-dot-usage: one fixture missing UsageSummary.lastUpdated.

The first app, and what it revealed about templates

#35970 dotcdn had 19 errors, and 8 shared one cause: dispatchLoading's switch had no default, so under noImplicitReturns the updater's return type included undefined, it stopped resolving as a one-argument updater, and all six call sites reported TS2554: Expected 0 arguments. default: return state fixed all eight.

More importantly, its build failed on libs/ui's templates, not on dotcdn:

libs/ui/.../dot-action-menu-button.component.html:1:14 - error TS2532: Object is possibly 'undefined'

libs/ui has no build target, so its templates had never been null-checked — they are only verified when a consuming app compiles them.

tsc -p does not check templates. Every per-project count in this rollout taken that way misses the class entirely. Probed across the three remaining apps by temporarily enabling the flags and building: 2, 8 and 23 template errors against 350–2500 .ts errors. Real, but ~1% of the volume, so it does not change the plan. Reported on #35932.

dotcdn and edit-content-bridge both have build targets, so their flags are genuinely enforced. The rest of this batch is not — no build target, and :test does not type-check.

Dependencies

Added @types/d3-scale, @types/d3-selection and @types/d3-shape. All three d3 packages are direct dependencies with no bundled types, so those imports were implicitly any. Maintained DefinitelyTyped packages, so installing beats hand-declaring the modules.


Batch three: libs/ui finished, plus four more (#35953 #35956 #35959 #35961 #35966)

libs/ui (#35953) — 549 → 0, and it unblocked most of what followed

Both configs are now clean: tsconfig.lib.json 122 → 0, tsconfig.spec.json 427 → 0. ~109 of its errors leaked into each of its 26 dependents, so clearing it collapsed seven projects from 219–275 down to 0–59. portlets-dot-tags-portlet had 1 error of its own, not 238.

Highlights beyond the mechanical work:

  • Types narrower than their own implementation. DotLocaleTagPipe guards with if (!languageId || !languagesMap), DotRelativeDatePipe with const time = date || Date.now(), and onAssignChange/onCommentChange with ?? '' — all three declared non-nullable parameters, making those guards unreachable and the specs that assert the null behaviour uncompilable.
  • A type contradicting the API. DotLanguageVariableEntry declared every language's value as always present; the API omits languages without a variable and the component already reads them with ?.value.
  • A spec referencing a removed type. dot-browsing.service.spec imported SiteEntity, which no longer exists — dot-site.model.ts says to use DotSite, and createFakeSite already returns it.
  • Real defects: dot-add-to-bundle invoked getDefaultBundle twice for the same value; five directives injected their host with { optional: true } and used it unguarded; formEl was declared HTMLFormElement while the template says #formEl="ngForm".
  • Test files were in the library program. tsconfig.lib.json excluded a non-existent src/test.ts but not test-setup.ts, **/*.test.ts or __mocks__/ — 15 errors by itself.

Two techniques worth reusing: fix at the point of declaration (53 usages of one select local came from a single line; 95 declaration-level assertions cleared ~300 spec errors), and read each TS2564 site rather than applying the policy blindlydot-icon's size became size?: number instead of = 0, because the template binds [style.font-size.px]="size" and 0 renders invisible icons where undefined inherits.

dot-plugins (#35966) — reported 733, had 63

tsconfig.spec.json used module: "commonjs" + moduleResolution: "node10", and tsconfig.json was missing moduleResolution: "bundler". node10 cannot resolve the @dotcms/* subpath exports, so 256 imports failed and everything downstream collapsed to unknown (227 TS2571, 115 TS18046, 85 TS2339). Aligning both with dot-tags took it from 733 to 1.

Swept the other remaining projects — 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.

libs/portlets/CLAUDE.md was telling people to configure it that way. 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 strict: true and module: "preserve" and compiles clean. Corrected, so the next portlet does not repeat it.

dot-analytics (#35961) and content-drive-ui (#35959)

Spectator's typed props disagrees with Angular for aliased signal inputs. Components declare $tableState = input.required({ alias: 'tableState' }). Spectator's InferInputSignals keys off the field name; Angular's setInput requires the alias. The specs passed the alias under an as unknown cast that made the props bag unknown, so removing it surfaced TS2561 with a "did you mean $tableState" hint — and following that hint broke 12 tests. The alias wins; the cast is narrowed to the props type derived from the factory, with a comment naming the conflict.

A drop with no destination. dot-tree-folder's onDrop read the nullable $activeDropNode() and emitted it as targetFolder, which both payload types declare non-null. A drop outside any folder emitted an invalid event; it now returns early.

Also: two more barrels re-exporting types with export {} (third and fourth instances after #35948 and #35951), and @types/d3-scale / @types/d3-selection / @types/d3-shape added — direct dependencies with no bundled types.

new-block-editor (#35956)

38 of 60 were TS4111 on TipTap node attrs, converted from the exact positions tsc reports. EditorView is annotated from @tiptap/pm/view, not top-level prosemirror-view — the file's own comment explains that TipTap 3.x nests its own copy and mixing the two yields TS2322; the comment now names the correct source instead of saying the import is avoided entirely. Three plugin state fields had init: () => null, pinning the state type to null. @types/turndown added.

On my own mistakes in this batch

Three self-inflicted breakages, all from over-broad regexes, all caught by running the suites: contentlet?.assetcontentlet?['asset'] (invalid syntax, which then masked every other error), a (view) replacement that hit call sites as well as declarations, and — the one that mattered — "completing" a fixture that deliberately omitted Action.name, which is exactly the case its test asserts on. tsc was happy with that last one; only the test caught it.

Dependencies added in this batch

@types/d3-scale, @types/d3-selection, @types/d3-shape, @types/turndown. All four are direct dependencies whose imports were implicitly any; these are maintained DefinitelyTyped packages, so installing them beats hand-declaring the modules.

Batch four: block-editor (#35955)

The first of the four large projects. Estimated at ~743 own errors; the real figure was 442 — clearing libs/ui had already removed ~300 of them without this project being touched. No inherited errors at all, so all 442 were local.

block-editor has no build target, so tsc -p on tsconfig.lib.json and tsconfig.spec.json is the acceptance test. There is no build to lean on.

Four wrong declarations, not 442 unrelated fixes

DotMenuItem extends Omit<MenuItem, 'icon'> erased every declared member of MenuItem. 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. id, label, command, disabled: all silently any. I probed it rather than assuming:

type Omitted = Omit<MenuItem, 'icon'>;
declare const viaOmit: Omitted['label'];    // any                → tsc reports nothing
declare const viaDirect: MenuItem['label']; // string | undefined → tsc reports it

MenuItem already declares icon?: string, so the omission bought nothing. Extending it directly cleared 12 TS4111 and gave every consumer real types back.

ImageNode referenced itself. addCommands used ImageNode.name inside ImageNode's own initializer, so TypeScript typed the whole extension any (TS7022). this.name is the same value and breaks the cycle — which then exposed a real Map inference problem in DotBlockEditorComponent._customNodes that the any had been hiding.

loadCustomBlocks had the wrong element type. Declared PromiseSettledResult<AnyExtension>[], but import(url) yields a module namespace — the element type is Record<string, AnyExtension>.

editor.storage.dotConfig was optional although getEditorExtensions() registers DotConfigExtension unconditionally, first in the list. Existing readers were split between ! and ?.. Declared required, and dot-config.types.ts — an unreferenced duplicate of the same module augmentation — deleted, since two copies that must stay in sync is exactly how this drifts.

Deleted rather than initialised

Two fields were never assigned and never read: FloatingActionsView.element and AIImagePromptView.tippyOptions. Giving them an initializer to satisfy TS2564 would have preserved dead code.

Seven errors were already present without any strict flags, two of them broken references:

  • asset-form.component.spec.ts tested ImageTabviewFormComponent, a component that does not exist on main either. Deleted.
  • dot-upload-asset.component.spec.ts imported DotUploadFileService from block-editor's shared barrel instead of @dotcms/data-access, so it provided a different token than the component injects.

Flagged, not changed

FloatingActionsView.update calls this.render().onExit(null), but ActionsMenu's onExit destructures editor from its argument. Preserved exactly, behind a cast and a FIXME(#35955) — changing runtime behaviour does not belong in a type-only pass.

placeholder.plugin keeps tr.getMeta(this). It looks wrong, but ProseMirror binds a state field's apply() to the Plugin instance, which is what makes it match the tr.setMeta(PlaceholderPlugin, …) calls. Annotated the this parameter to record that.

The test suite was already red — and stayed exactly as red

block-editor:test fails 16 suites / 37 tests on main. libs/block-editor is byte-identical between main and this branch, so this is Angular 22 migration debt, not something the epic introduced. Now tracked in #37091.

Per the agreed scope, this PR is types-only and left the suite alone. I verified that by name, not by count — a different set of 37 failures would have the same total:

=== only BEFORE (fixed by my changes) ===
=== only AFTER (new failures = regressions) ===

Same 38 entries, nothing added, nothing accidentally fixed. The cost is real and worth stating plainly: for this project I had no runtime safety net, which is the guard that caught my worst mistake earlier in this PR.

Dependents

Measured by checking out libs/block-editor at 82dbf4c9ad and re-running each dependent's tsc -p, so the delta isolates this change from the rest of the branch.

Dependent Before After
dotcms-block-editor 1 0
dotcms-ui 1 0
edit-ema-ui 2 0
edit-content 29 27
portlets-edit-ema-portlet 190 188

Zero new errors; three cleared outright.

dotcms-block-editor (#35973) — and the template gate block-editor never had

Three of this app's tsconfigs — spec, editor and the shared tsconfig.json they extend — declared "types": ["jasmine", "node"]. @types/jasmine is not installed, nor are karma-jasmine or jasmine-core, so each aborted with TS2688 before semantic checking. None had ever type-checked anything. The @angular/build:karma test target cannot run for the same reason, and the app has no .spec.ts files at all.

With that removed and the flags added, tsc -p was clean on all three — and the build failed:

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

SuggestionsListItemComponent.data was declared = null, which under strict infers the type null, so data?.contentlet narrowed to never. Typed properly now.

The wider point: libs/block-editor has no build target, so its templates had never been type-checked by anything. tsc -p does not check templates, and it was the only gate #35955 had. #35973 is what puts them under a real one — which is why closing a small app mattered more than its error count suggested.

A spec file that disabled type checking across three libraries

While measuring #35974, libs/edit-content/.../dot-edit-content-field.component.spec.ts turned out to declare:

/* 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 program. This one gave TipTap's Commands a string index signature for every file compiled alongside it — which is how editor.chain().focus(), a real declared command, became "Property 'focus' comes from an index signature".

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

The comment's premise no longer holds: measured with the augmentation present and absent on the current non-strict config, edit-content reports 27 lib / 48 spec errors either way. It suppressed nothing and cost 256 unchecked sites. Removed.

A sixth way to get a green signal that checked nothing

TS5101 (baseUrl) and TS5107 (moduleResolution: node10) are deprecation errors under TypeScript 6 — and, like TS2688 and TS6053, they abort before semantic checking. libs/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". No single value satisfies both, so the CLI flag is mandatory — the comment there now says so, and core-web/CLAUDE.md records the general rule: any TS5xxx/TS6xxx/TS2688 error is a configuration error, and the count after it means nothing.

Batch five: edit-ema-ui (#35971) and template-builder (#35958)

Two projects taken to 0, both with tests green throughout and lint back at its clean baseline.

A fixtures file compiled as production code

template-builder reported 27 inherited errors, all Cannot find name 'jest' in libs/utils-testing. utils-testing was not the problem: tsconfig.lib.json excludes *.spec.ts but not src/**/utils/mocks.ts, so a fixtures file importing @dotcms/utils-testing was in the lib program under types: []. Only specs import it and it is not in the public barrel, so it is now excluded from the lib build. edit-content reports the same 27 from the same cause.

sidebar is null when empty, and the model never said so

DotLayout.sidebar and DotTemplateLayoutProperties.sidebar were both declared non-nullable while TemplateBuilderComponent deliberately clears them:

sidebar: layoutProperties?.sidebar?.location?.length // Make it null if it's empty so it doesn't get saved
    ? layoutProperties.sidebar
    : null,

Two specs assert it. Widening the shared model surfaced three unguarded reads in the store — which is the point — and had zero impact on the eight strict projects that consume DotLayout.

Nearly changing a wire payload

publishContentletAndWaitForIndex takes { [key: string]: string | number }, and dot-favorite-page.store sends inode: formData.inode || null. My first fix was ?? ''. The spec caught it, because it asserts the payload contains inode: null — the endpoint distinguishes null from an empty string. The signature was wrong, not the call; widened it in data-access.

Same shape in DotContentCompareStore, which piped httpErrorManagerService.handle(err) out of catchError into a switchMap typed for contentlets. A handled error is not a contentlet array; both blocks now complete with EMPTY.

Two of my own mistakes, both caught by tests

A wrong zero value. The bulk TS2564 pass turned @Input() showDiff: boolean into = false. The dotDiff pipe defaults to true and the store seeds showDiff: true, so false silently disabled diffing and broke 4 specs. "Boolean means false" does not hold when the consumer's default is not the zero value.

Spectator props keying. Renaming the key from the alias contentlet to the declared member $contentlet type-checks and then fails at runtime in 11 specs, because Spectator applies the alias while InferInputSignals types props by the member name. The cast is unavoidable; it now carries a comment saying so.

No production non-null assertions left behind

Bulk narrowing introduced 27 in edit-content and 9 in template-builder, each tripping @typescript-eslint/no-non-null-assertion — the rule that exists to discourage exactly that. All were reverted or converted:

  • edit-content: reverted, errors put back on the remaining count where they want real guards
  • template-builder: converted — two DialogService.open() results, three subGridOpts.children reads, one child.containers filter, and a resizestart handler that asserted a four-link GridStack chain optional at every step
  • edit-ema-ui: five in dot-favorite-page replaced with form.controls['x'], which the form always builds

Batch six: edit-content (#35974) and the two it was blocking (#35976, #35967)

edit-content is the largest project closed so far after ui: 101 production errors and 208
spec errors to 0
, with 27 phantom "inherited" errors removed at source. 112 suites / 2218 tests
unchanged; lint at its pre-existing 11-warning baseline.

Its tsconfig.spec.json carried explicit "strict": false and
"noPropertyAccessFromIndexSignature": false opt-outs, which were hiding 666 of the 944 errors
the specs really had.

The dominant pattern was not missing guards — it was declarations lagging behind code already
written for null.
Six signalMethod handlers plus five utilities and BaseWrapperField.formControl
all opened with a !x guard while declaring the argument non-nullable.
RelationshipFieldStore.initialize had three comments saying "contentlet is null in manual
translation" over a type that said otherwise.

InputSignal<T> is not covariant. Widening BaseWrapperField to InputSignal<T | null> took
the count 74 → 94: all 17 subclasses would have needed byte-identical input types, and 5 legitimately
differ. The base only reads them, so it declares Signal<T | null> — that is covariant.

Two latent bugs the compiler surfaced: the workflow sidebar's Select button is not disabled, so
confirming without choosing emitted undefined through an output<string>(); and the category field
wrote undefined into the form value for selected categories with no inode.

Four fixtures could never have matched the code: canLock mocked with the raw HTTP envelope when the
service maps response.entity; getByInode mocked with the scheme-grouped shape; isDialogMode
mocked in three specs after being deleted from the store; and MultiSelect.valuesAsString, which does
not exist on PrimeNG 21 — so expect(...).toEqual(undefined) asserted nothing.

Three of my own changes were caught by the tests, and in each case the test documented the intent
better than the type did, so I changed my code rather than the assertion: a guard in
onCommentSubmitted (the test is named "should still call addComment … even if identifier is
undefined"
, and the adjacent tests assert the opposite for the history handlers); formValues: {}
where a spec asserts null; and a contentType guard that aborted the locales flow because the
fixture never set one — the guard is right, so the fixture now provides one.

Nine shared-model members were widened to admit the null the endpoints actually send (workflow
metadata/status/scheme, contentlet lockedBy/lockedByName, content-type field
defaultValue/values/rendered, categories description). Every reader already tolerated it and
the model was the outlier — WorkflowTask.status was the only non-nullable member alongside
belongsTo, description and dueDate, all three already annotated. Blast radius re-measured
across all 12 strict projects after every one: 0 errors.

Two things deliberately not done. DotCMSContentlet.title is genuinely nullable — two tests are
named "without title" — but widening it lights up 10 errors across block-editor and edit-ema-ui,
both already closed, so it needs its own issue. And onWorkflowActionFired did not get an inode
guard: it carries a comment warning that one silently blocks saving new content.

Also recorded on #35974: libs/edit-content has no build target, so its templates have never
been type-checked and its strictTemplates is inert — the same gap as libs/block-editor.

The two projects it was blocking land here too. dotcms-binary-field-builder needed no source
changes. portlets-dot-query-tool-portlet needed three signal-mock casts, and its combination of
noImplicitReturns without strict caught a TS7030 I had introduced in edit-content — a guard
that returned bare where the other path returns an Observable teardown.


Test plan

edit-ema-ui (#35971) and template-builder (#35958)

  • tsc -p — 0 on lib and spec for both (from 119 and 141)
  • edit-ema-ui:test — 20 suites / 343 tests green; template-builder:test — 15 suites / 148 tests unchanged
  • Both projects' lint back at their clean baselines, with zero production non-null assertions added
  • template-builder's 27 inherited errors traced to mocks.ts in the lib build, not to utils-testing
  • After widening DotLayout.sidebar and the data-access payload signature: dotcms-models, data-access, ui, block-editor, edit-ema-ui, utils-testing, utils, global-store all still 0

dotcms-block-editor (#35973)

  • tsc -p — 0 on tsconfig.app.json, tsconfig.spec.json and tsconfig.editor.json, all three of which previously aborted on TS2688
  • nx run dotcms-block-editor:build — clean (it failed first, on a template defect tsc -p cannot see)
  • libs/block-editor unaffected: 0/0, tests unchanged at 16 suites / 37 by name
  • libs/new-block-editor unaffected: 0
  • edit-content:test still green at 112 suites / 2218 tests after removing the poisoning augmentation

block-editor (#35955)

  • tsc -p libs/block-editor/tsconfig.lib.json --noEmit — 0 errors (from 296)
  • tsc -p libs/block-editor/tsconfig.spec.json --noEmit — 0 errors (from 443); 442 own errors deduped across both
  • nx run block-editor:test — unchanged at 16 suites / 37 tests failing, verified by failing-test name: identical 38 entries before and after
  • nx run block-editor:lint — unchanged at 11 errors, all in files this branch does not touch (git diff origin/main confirms)
  • Five dependents re-measured against 82dbf4c9ad: zero new errors, three go to 0
  • No build target on this project, so tsc -p is the gate — stated on the issue rather than implied

dotcms-js

  • pnpm exec tsc -p libs/dotcms-js/tsconfig.lib.json --noEmit — 0 errors (from 38)
  • All six already-strict consumers build green: data-access, global-store, portlets-dot-analytics, portlets-dot-analytics-data-access, portlets-dot-locales-portlet, utils-testing
  • data-access typecheck: 106 → 68 errors, zero new
  • dotcms-ui typecheck clean (one pre-existing unrelated error)
  • dotcms-js lint went from 42 to 41 problems (still tag-excluded)

sdk-create-app

  • tsc --noEmit clean on lib and spec
  • nx run sdk-create-app:build / :lint / :test green
  • CLI smoke test: node dist/libs/sdk/create-app/index.js --help works
  • Negative test: reverting the DEBUG fix makes nx run sdk-create-app:build fail with TS4111 — confirming the build gate is real

sdk-analytics

  • tsc --noEmit clean on tsconfig.lib.json (from 18) and tsconfig.spec.json (from 29)
  • nx run sdk-analytics:typecheck / :lint / :build / :build:standalone green
  • nx run sdk-analytics:test — 15 suites, 314 tests passed. Since jest never type-checked these specs, this was the real regression check on the fixture edits
  • Negative test: a deliberate type error does not fail nx run sdk-analytics:build — this project's build is not a gate

sdk-angular

  • tsc -p tsconfig.spec.json --noEmit now completes a semantic pass at all (previously TS6053), 0 own errors
  • tsc -p tsconfig.lib.json --noEmit 0 own errors
  • nx run sdk-angular:lint clean; :build green
  • nx run sdk-angular:test unchanged at 21 suites / 234 tests — the guard that no file dropped out of the program
  • Negative test: a deliberate type error does fail nx run sdk-angular:build (TS2322, exit 1) — ngtsc gates this project

data-access

  • tsc -p tsconfig.lib.json --noEmit 36 → 0; tsc -p tsconfig.spec.json --noEmit 84 → 0
  • nx run data-access:lint clean; :test unchanged at 79 suites / 754 tests
  • Blast radius: all 7 strict dependents counted before and after — zero new errors, 218 removed, three went 36 → 0
  • Runtime guard on the widened services: dotcms-ui 820 tests and ui 2184 tests pass
  • nx affected -t build green for all 6 affected projects

Batch two

Batch three

  • libs/ui both configs 0 (122 and 427 before); lint clean; 81 suites / 820 tests unchanged
  • dot-plugins 733 → 0; new-block-editor 60 → 0; dot-analytics 42 → 0; content-drive-ui 59 → 0
  • Tests green and unchanged: ui 820, analytics 333, content-drive-ui 245, plugins 81, new-block-editor 57
  • After libs/ui landed, re-verified the ten already-closed projects plus dotcdn for regressions
  • Swept every remaining project's moduleResolution; dot-plugins was the only one misconfigured

Both

  • pnpm exec nx format:check --base=origin/main green
  • No new any / @ts-ignore / @ts-expect-error (verified by diff grep)

Note: neither sdk-create-app nor dotcms-js has usable tests. sdk-create-app has zero test files (passWithNoTests: true); dotcms-js has 3 spec files that do not run (skip:test, and tsconfig.spec.json fails on a pre-existing jasmine types error). A green :test means nothing for either — the real verification is compilation.


Correction: a verification false negative (review follow-up)

A review comment caught a real regression this PR introduced, and the reason it slipped through matters for how the numbers above should be read.

libs/utils-testing/tsconfig.lib.json declares "types": ["jasmine"], and that package is not installed. tsc therefore emits TS2688: Cannot find type definition file for 'jasmine' and stops before semantic checking. So tsc -p libs/utils-testing/tsconfig.lib.json --noEmit reports exactly one error no matter what the code does.

The utils section originally reported "utils-testing unchanged at 1 pre-existing error" as evidence of no regression. That measurement proved nothing — nothing was being type-checked. Running the same config with --types node reveals 33 errors, including a genuine TS2741 caused by retyping EMPTY_SYSTEM_FIELD to Omit<DotCMSContentTypeField, 'clazz'>: the mock at dot-content-types.mock.ts:71 spreads it and never supplies clazz.

Fixed by giving the mock clazz: DotCMSClazzes.TEXT; that config is now at 32 errors, all pre-existing and unrelated.

Because the mock has ~103 consumers whose tests do run in CI, the runtime-value change was verified rather than assumed — clazz went null (pre-PR) → absent (this PR) → TEXT:

  • FieldUtil.isRow / isColumn / isTabDivider compare for equality and return false for all three values.
  • There is no !field.clazz or field.clazz === null anywhere in the repo.
  • Test runs: default-value-property 7/7; dot-content-types-edit 545 passed across 48 suites; data-access 751 passed across 79 suites.

The data-access figures reported elsewhere in this PR (106 → 68 for dotcms-js, 68 → 36 for utils) are not affected — that project has no unresolved types entry, so those runs were doing real semantic checking.

core-web/CLAUDE.md now documents this masking behaviour so the next person does not repeat it.

Other two comments

  • sdk-create-app — the throw said "requires at least 1 retry", but retries is the total attempt count (for (i = 0; i < retries; i++)), so retries = 1 is one attempt and zero retries. Reworded to "attempt".
  • CLAUDE.md verify snippet — hard-coded libs/<project>/tsconfig.lib.json, which resolves for neither nested projects (libs/sdk/create-app, which has no tsconfig.lib.json) nor apps (tsconfig.app.json). Replaced with a <projectRoot> placeholder and both caveats.

Notes for reviewers

Three sibling issues in this rollout turned out not to need the work as written, and were resolved separately:

Batch seven: the last five projects (#35943 #35957 #35964 #35972 #35975 #35977)

This batch finishes the epic. dotcms-ui was the largest single project in it — 1005 errors across app and spec.

dotcms-webcomponents (#35943) — now closed

The groundwork section above left this open. 104 errors to 0, then the flags on. It type-checks twice: once by the workspace's TypeScript 6.0.3 and once by Stencil, which bundles its own 5.8.3. TS6 re-declared Node.textContent as an asymmetric accessor — get(): string, set(value: string | null) — so element.textContent.replace(...) is clean under 6 and Object is possibly 'null' under 5.8. The project reached 0 on tsc -p and the Stencil build still failed. Where two compilers check the same sources, the build is the gate.

dot-rules (#35957), portlets-dot-experiments-portlet (#35964), portlets-content-drive (#35972), portlets-edit-ema-portlet (#35975)

  • dot-experiments: 81 lib + 289 spec. Two tsconfig defects first — a dead include and a missing it-spec exclude.
  • content-drive: 270 errors.
  • edit-ema/portlet: needed its test-only mocks.ts excluded from the lib config before the count meant anything.
  • dot-rules: flags on, lib + spec to 0.

dotcms-ui (#35977) — 1005 → 0

Production source reached 0 first, then the specs. The findings worth a reviewer's time:

Real defects, fixed:

Where What
DotContentTypeComponentStore.saveCopyDialog Combined the form fields with assetSelected$ (string | null) and passed that straight to saveCopyContentType, so a submit with nothing selected sent null as the content type to copy. The error-path test was reaching the effect without selecting — which is what surfaced it
DotAutocompleteTagsComponent.addItem this.value.unshift(this.value.pop()) unshifts undefined back into the tag array when the list is empty, and getStringifyLabels then reads .label off it
shouldClearDropdown(): boolean Returned dropdown && options.length && …options.length is a number, so a function declared : boolean could return 0
SearchableDropdownComponent.action @Input() action!: (event: Event) => void claimed the input is always bound; only one of its three hosts binds it, and the template's @if (action) is what handles the other two. Reported by the Angular compiler as TS2774, always true

Signatures corrected at their source rather than at the call: DotRouterService.currentPortlet (always sets id, three callers were paying for the optional); portletReload$ (a bare Subject, so unknown); DotEventsService.listen and PaginatorService.getWithOffset/getCurrentPage left unparameterised at four sites; LoginService.watchUser typed (params?: unknown) while its body always calls with an Auth; DotNavLogoService.setLogo, whose own navLogo?.startsWith had already assumed a nullable argument; ActionHeaderDeleteOptions.confirmHeader/confirmMessage, optional while feeding a confirm dialog that requires both — and nothing in the repo supplies deleteOptions at all.

DataTableColumn.icon?: (any) => string was not the any type — it is a parameter named any with no type at all, which is why TypeScript reported TS7051 rather than an implicit-any. Nobody had ever read it.

The app build found six errors tsc -p cannot see

tsc does not check templates. strictTemplates being off does not stop a project's own strictNullChecks from applying to template expressions, and the Angular compiler is the only thing that evaluates them. All six were real: two optional-sites accesses, a nullable form.get, an unnarrowed second options() call, the @if (action) above, and runningExperiment.scheduling.endDate in edit-ema/portlet — a library that measures 0 on its own configs, because a build-less library's templates are checked only when an app compiles them.

tsconfig.editor.json also reached 0

I expected to record this as a known limitation. It compiles what tsconfig.app.json excludes, and that is where things had been rotting unseen:

  • Seven dead files — six index.ts barrels and components.ts — re-exporting NgModules and a component deleted in the standalone migration. Zero importers; none could have compiled. Removed.
  • Four Storybook stories broken since the PrimeNG upgrade: PrimeNGConfig is now PrimeNG and its ripple flag is a signal.
  • libs/block-editor/NodeViewRenderer.ts, not this project's file. It declares override decorations!: readonly DecorationWithType[] — clean under block-editor's es2015 target, where a class field is an assignment, and TS2612 under dotcms-ui's ES2022, where useDefineForClassFields is on by default and the same field emits a defineProperty that shadows the base value with undefined. declare (which TypeScript will not accept alongside override) states the intent and emits nothing, so both configs agree. A new variant of "flags interact across projects": here it is the target that interacts.

One shared helper replaces eight casts

aliasedProps in apps/dotcms-ui/src/app/test/. Spectator keys props by the class property name ($field) while the ComponentRef.setInput underneath needs the alias (field), so a component following this repo's $name + { alias } convention cannot express its inputs through props at all. One spec already carried a comment saying exactly that.

Verification for this batch

Gate Result
dotcms-ui tsconfig.app.json / spec / editor 0 / 0 / 0
nx run dotcms-ui:build:production exit 0
nx run dotcms-ui:test 222 suites / 2189 passed / 32 skipped
nx run dotcms-ui:lint 1 pre-existing warning
ui 104 suites / 1349 passed
dotcms-models, data-access, ui, block-editor, edit-ema/portlet, edit-content tsc -p at 0

data-access's four PushPublishService failures and block-editor's 16/37 reproduce identically at HEAD — confirmed against a stashed tree, not assumed.

Correction: the commit trailers on the final batch cite the wrong issue

The commits for dotcms-ui are tagged (#35933). That is [00] Setup typescript-strict-plugin baseline + CI gate, which was already closed before this batch started. The correct issue is #35977 [44/44] Enable TS strict mode in dotcms-ui, and the Closes line below is right.

I did not rewrite the trailers — the branch is pushed and shared, and the Closes lines are what drive the automation. Flagging it so the trailer/issue mismatch is not a surprise in review or in git log.

Follow-ups filed rather than folded in

Four issues under epic #32713, so none of this rides along in an already-large PR:

Issue Why it is separate
#37120 strictTemplates in the four apps. Measured: 430 errors across 373 files, and 231 of those files are in libs/ — it is a ~27-library project, not an app change. The TODO(#35930) gating it points at an issue that is already closed
#37121 The last 11 projects outside this epic's 44, then flipping tsconfig.base.json to "strict": true — which is also how five of those eleven came to be created non-strict
#37122 The blanket as unknown as DotCMSContentType on dotcmsContentTypeBasicMock, which lets ~20 fields of the most-used content-type fixture disagree with the model unchecked. Found because two specs overrode host after the spread, putting the assignment outside the cast
#37123 Three production defects flagged in place and left behaving as before: an iframe listener pair leaked on every load (.bind(this) on both add and remove), a drop guard that compares undefined === null and so has never fired, and cleanTemplateItem deleting type before testing it

Closes #35943
Closes #35957
Closes #35964
Closes #35972
Closes #35975
Closes #35977

Closes #35971
Closes #35958
Closes #35973
Closes #35955
Closes #35966
Closes #35961
Closes #35959
Closes #35956
Closes #35953
Closes #35970
Closes #35969
Closes #35968
Closes #35965
Closes #35963
Closes #35962
Closes #35960
Closes #35954
Closes #35952
Closes #35951
Closes #35950
Closes #35949
Closes #35948
Closes #35947
Closes #35946
Closes #35945
Closes #35974
Closes #35976
Closes #35967
Closes #35944
Closes #35940
Closes #35939
Closes #35938
Closes #35935

nicobytes and others added 2 commits August 7, 2026 12:12
`sdk-types` needs no code change: `libs/sdk/types/tsconfig.json` has carried
`strict: true` plus the four extra safety flags since the library was created
(#31967), and `tsc -p tsconfig.lib.json --noEmit` passes with zero errors.

It is already enforced too. Because `tsconfig.lib.json` sets
`"declaration": true`, `@rollup/plugin-typescript` sits in the Rollup chain and
reports type diagnostics, so `sdk-types:build` fails on a strict violation —
verified by removing a constructor assignment and watching the build report
TS2564. CI builds every project via the `build-test` execution in
`core-web/pom.xml`, so the gate already runs on each PR. A dedicated
`typecheck` target would be redundant. `lint` does not catch this: ESLint
reports lint rules, not TS diagnostics.

What was actually missing is documentation, so the remaining 42 projects in
epic #35932 have a pattern to follow:

- Add a `## TypeScript Strict Mode` section covering the per-project flags,
  what enforces them, and the Vite exception (esbuild skips type checking,
  which is why the Nx Vite plugin infers a separate `typecheck` target).
- Fix the line that forbade `"strict": true` in project tsconfigs. It sat under
  the Jest config guidance but read as a blanket ban, contradicted
  `docs/frontend/TYPESCRIPT_STANDARDS.md`, and blocked the epic outright. The
  restriction now points at `tsconfig.spec.json`, which is what it meant.

Closes #35935

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add the standard per-project strict flags to `libs/sdk/create-app/tsconfig.json`,
following the pattern established in #36879 (dotcms-models). `tsconfig.base.json`
is left at `strict: false`.

Two errors surfaced, both from flags beyond plain `strict`:

- `src/index.ts:393` — `process.env.DEBUG` needs bracket access under
  `noPropertyAccessFromIndexSignature` (TS4111). It is the only `process.env.*`
  dot access in the project.
- `src/utils/index.ts:41` — `fetchWithRetry` tripped `noImplicitReturns`
  (TS7030). The loop returns on success and throws on the last attempt, but with
  `retries < 1` the loop never runs and the function fell through returning
  `undefined`. Its only caller already guarded with `if (res && ...)`, so nothing
  broke in practice, but the signature was lying. Throwing after the loop closes
  the gap and narrows the return type.

No build or CI wiring needed. The `@nx/esbuild:esbuild` executor type-checks
before bundling — `skipTypeCheck` defaults to false and is not overridden — and
CI already builds this project via `nx run-many -t build` (`build-test` in
core-web/pom.xml). The same build runs in the SDK release pipeline
(`cicd_release-sdk.yml` → `nx run-many --projects='sdk-*'`), so the flags are
enforced on every release.

Verified: tsc clean on lib and spec; `nx run sdk-create-app:build/lint/test`
green; `nx affected -t build,lint` green; `node dist/libs/sdk/create-app/index.js
--help` still works. Negative test — reverting the DEBUG fix makes
`nx run sdk-create-app:build` fail with TS4111, confirming the gate is real.

Closes #35938

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

claude Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Claude finished @nicobytes's task in 2m 43s —— View job


Code Review — TS strict-mode rollout (#36957)

  • Gather context (diff vs origin/main)
  • Recheck prior review findings
  • Scan changed source for runtime-behavior changes
  • Post review

Scope note: this PR touches 1,136 files; the overwhelming majority are mechanical type annotations, !x guards, and spec casts with no runtime effect and green tests. I focused the review on (a) the prior review findings and (b) the diffs that change runtime behavior or wire payloads, since those are the only places a type-only pass can introduce a regression.

New Issues

  • 🟡 Medium: core-web/libs/utils/src/lib/shared/FieldUtil.ts:38-41EMPTY_FIELD.defaultValue, .hint and .values went from null to undefined. JSON.stringify drops undefined-valued keys but keeps explicit null, so the derived COLUMN_FIELD/ROW_FIELD/TAB_FIELD (which spread EMPTY_SYSTEM_FIELD) now omit those three keys when serialized through the layout-save call (PUT /api/v3/contenttype/{id}/fields/move) instead of sending null. The PR body documents the null → zero-value changes and argues frontend consumers use falsy checks, but that reasoning covers ''/0/false, not the three fields that became undefined — and the frontend-consumer argument doesn't address the backend wire contract. This is the exact point raised in oidacra's review comment and it isn't in the PR's documented-changes list.
    • Assumption: the move/layout endpoint deserializes rows/columns/tab-dividers with Jackson.
    • What to verify: that a missing key deserializes identically to an explicit null for these fields on that endpoint (or restore null for the three keys to preserve the previous wire shape). If already confirmed, add it to the documented-changes list. Fix this →

Existing

  • 🟡 Medium: core-web/libs/utils/src/lib/services/dot-asset.service.ts:22 — the new promises: Promise<Response>[] annotation is optimistic: each promise ends in .catch((e) => e), so a rejected fetch resolves to an Error, and res.json() (line 48) would throw on it. The runtime behavior is unchanged (the .catch predates this PR), so this is not a regression — but the new type now hides a real failure mode that Promise<Response | Error>[] / Promise.allSettled would surface. Non-blocking; flagged only because the annotation is new.
  • 🟡 Medium (tracking): core-web/libs/dotcms-js/src/lib/core/site.service.ts:34 — the TODO(#35939) defers widening selectedSite/currentSite to "its own issue" but no dedicated follow-up issue is referenced. Given eventResponse() dereferences this.selectedSite.identifier unguarded on a surface with prior regression history, worth pointing the TODO at a filed issue so it isn't deferred indefinitely.

Resolved

  • core-web/libs/sdk/create-app/src/utils/index.ts:122 — error message reworded to "requires at least 1 attempt" and a clarifying comment added; copilot's wording concern addressed.
  • core-web/CLAUDE.md:127 — verify snippet now uses a <projectRoot> placeholder with explicit caveats for apps (tsconfig.app.json) and nested/tsconfig.json-only projects; copilot's concern addressed.
  • core-web/libs/dotcms-webcomponents/.../dot-material-icon-picker.tsx:25selectedSuggestionIndex is now ?: number (was !); oidacra's optional-vs-runtime-injected point addressed.

Note: previewImg!/classNames! (dot-html-to-image, dot-time) still use ! where ? would be more honest, but both are safe at runtime (!this.previewImg guard; {...undefined} spread), so no runtime bug — a type-honesty nit only.

No blocking issues found. The one item worth resolving before merge is the FieldUtil undefined-vs-null wire-payload question.

@github-actions github-actions Bot added Area : Documentation PR changes documentation files Area : Frontend PR changes Angular/TypeScript frontend code Area : SDK PR changes SDK libraries labels Aug 7, 2026
@nicobytes nicobytes changed the title 35932 enable strict mode refactor(core-web): enable TS strict mode in sdk-create-app + document the rollout (#35938, #35935) Aug 7, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR opts the sdk-create-app library into the workspace’s incremental TypeScript strict-mode rollout (issue #35932), and adjusts docs/runtime code to align with stricter typing and clearer failure modes.

Changes:

  • Enabled strict TypeScript compiler flags for core-web/libs/sdk/create-app via its project tsconfig.json.
  • Updated fetchWithRetry to throw when misconfigured with < 1 attempts to avoid an implicit undefined return path.
  • Updated strict-mode rollout documentation and adjusted DEBUG env access to bracket notation for noPropertyAccessFromIndexSignature.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
core-web/libs/sdk/create-app/tsconfig.json Enables strict compiler options at the project level for the strict-mode rollout.
core-web/libs/sdk/create-app/src/utils/index.ts Adds an explicit throw path for invalid retries values in fetchWithRetry.
core-web/libs/sdk/create-app/src/index.ts Switches DEBUG env access to process.env['DEBUG'] for strict-mode compatibility.
core-web/CLAUDE.md Documents the strict-mode rollout procedure and clarifies portlet tsconfig guidance.

Comment thread core-web/libs/sdk/create-app/src/utils/index.ts
Comment thread core-web/CLAUDE.md Outdated
Add the standard per-project strict flags to `libs/dotcms-js/tsconfig.json`,
following the pattern from #36879 (dotcms-models), and resolve the 38 errors
they surface across 11 files. `tsconfig.base.json` stays at `strict: false`.

Notable type corrections rather than mechanical silencing:

- `Auth.loginAsUser` was typed `User` but the code has always passed `null`
  when nobody is impersonating, and every consumer already guards with
  `auth.loginAsUser || auth.user`. Corrected to `User | null`.
- `StringUtils.getLine` and `HttpRequestUtils.getQueryStringParam` both
  document "null if it does not exist" but were typed `string`. Corrected.
- `RoutingService.getPortletURL` returns `Map.get()`, so `string | undefined`.
- `SiteService.switchSiteById` emits `of(null)` when no site is found, so
  `Observable<Site | null>`. Its one consumer already handles null.
- `ResponseView` now models `HttpResponse.body` as nullable instead of
  assigning `null` into a non-nullable field inside a `try/catch` that could
  never throw. The dead try/catch is removed.
- `LoginService.urls` is typed by inference instead of `Record<string, string>`,
  which keeps dot access valid and gives each endpoint a named property.

Two definite-assignment assertions were used, each with a TODO: `_auth` and
`selectedSite` are assigned during init but not in the constructor. Modelling
them as `| undefined` is the truthful type, but their public getters (`auth`,
`currentSite`) are consumed by already-strict projects, so widening them is a
public-API change that belongs in its own issue.

No new `any`, `@ts-ignore`, or `@ts-expect-error`.

Verified:
- `tsc -p libs/dotcms-js/tsconfig.lib.json --noEmit` — 0 errors
- All six already-strict consumers build green (data-access, global-store,
  portlets-dot-analytics, portlets-dot-analytics-data-access,
  portlets-dot-locales-portlet, utils-testing)
- `data-access` typecheck went from 106 errors to 68, with zero new errors
  introduced — the honest types upstream remove noise downstream
- `dotcms-ui` typecheck clean apart from a pre-existing missing
  `dotcms-webcomponents/loader` dist
- `nx format:check` green; dotcms-js lint went from 42 to 41 problems

Note: this project has no `build` target and is tag-excluded from lint and
test, so nothing in CI verifies these flags. That was an explicit scoping
decision — no `typecheck` target or CI gate was added. See
`specs/35939-dotcms-js-strict-mode/spec.md`.

Closes #35939

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nicobytes nicobytes changed the title refactor(core-web): enable TS strict mode in sdk-create-app + document the rollout (#35938, #35935) refactor(core-web): enable TS strict mode in dotcms-js and sdk-create-app + document the rollout (#35939, #35938, #35935) Aug 7, 2026
nicobytes and others added 3 commits August 7, 2026 14:16
Add the standard per-project strict flags to `libs/utils/tsconfig.json`,
following the pattern from #36879 (dotcms-models), and resolve the 32 errors
they surface across 3 files. `tsconfig.base.json` stays at `strict: false`.

The flags also propagate to `tsconfig.spec.json`, which surfaced 17 further
errors in the spec files (baseline was 0). Those are fixed here too rather
than left as a regression.

Notable changes:

- `EMPTY_FIELD` assigned `null` to 18 members that `DotCMSContentTypeField`
  declares non-nullable. Replaced with zero values of the declared types.
  Nothing compares those members to `null` strictly — consumers use falsy
  checks such as `isNewField`'s `!field.id` — so `''`, `0` and `false` behave
  identically at runtime.
- `clazz` has no zero value (`DotCMSClazz` is a union of concrete Java class
  names), so `EMPTY_FIELD` and `EMPTY_SYSTEM_FIELD` are now typed
  `Omit<DotCMSContentTypeField, 'clazz'>`. They are partial templates, not
  valid fields, and the type now says so. The derived `COLUMN_FIELD`,
  `ROW_FIELD` and `TAB_FIELD` already supply their own `clazz`.
- `getFieldsWithoutLayout` used a truthy `.filter()` that does not narrow the
  optional `row.columns`. Replaced with a type predicate, which clears the
  TS2532 and both TS2769 errors without a cast.
- `ellipsizeText` accepted `null`/`undefined` at runtime — its own guard and
  its tests document that — but declared `string` and `number`. Widened to
  match, with an explicit `limit == null` check so the later comparisons
  narrow.
- `fallbackErrorMessages` typed `{ [key: number]: string }`, mirroring the
  identical declaration already in `libs/data-access/.../dot-upload.service.ts`.
- `dot-utils.ts` uses bracket access for the six `DotCMSContentlet`
  index-signature reads in `getImageAssetUrl`.

No new `any`, `@ts-ignore`, or `@ts-expect-error`. The nine `as unknown as`
casts added are all in spec files, on inputs the tests deliberately pass as
invalid, matching the idiom those files already used.

Verified:
- `tsc -p libs/utils/tsconfig.lib.json --noEmit` — 0 errors (from 32)
- `tsc -p libs/utils/tsconfig.spec.json --noEmit` — 0 errors (from 17)
- `data-access` typecheck went from 68 errors to 36, zero new
- `utils-testing` unchanged at 1 pre-existing error (missing jasmine types)
- `dotcms-ui` typecheck clean apart from a pre-existing missing
  `dotcms-webcomponents/loader` dist
- `nx format:check` green

Note: `utils` has no `build` target and is tag-excluded from lint and test, so
nothing in CI verifies these flags — the same accepted trade-off as #35939.

Closes #35940

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nicobytes nicobytes changed the title refactor(core-web): enable TS strict mode in dotcms-js and sdk-create-app + document the rollout (#35939, #35938, #35935) refactor(core-web): enable TS strict mode in utils, dotcms-js and sdk-create-app + document the rollout Aug 7, 2026
nicobytes and others added 2 commits August 7, 2026 16:20
Addresses three review comments on #36957.

1. `dot-content-types.mock.ts` — real regression, now fixed.

`dotcmsContentTypeFieldBasicMock` spreads `EMPTY_SYSTEM_FIELD`, which #35940
retyped to `Omit<DotCMSContentTypeField, 'clazz'>`, leaving the mock without a
required property (TS2741). It now supplies `clazz: DotCMSClazzes.TEXT`; callers
that care already override it.

Why the original verification missed it: `libs/utils-testing/tsconfig.lib.json`
declares `"types": ["jasmine"]` and that package is not installed, so tsc emits
`TS2688: Cannot find type definition file for 'jasmine'` and stops before
semantic checking. The "1 error before, 1 after" measurement reported in #35940
therefore proved nothing — nothing was being checked. Running with
`--types node` reveals 33 errors, including the TS2741. It is 32 after this fix.

Verified the runtime-value change, since the mock has ~103 consumers whose
tests do run in CI: `clazz` went `null` (pre-PR) → absent (#35940) → `TEXT`.
`FieldUtil.isRow`/`isColumn`/`isTabDivider` compare for equality and return
false for all three, and there is no `!field.clazz` or `=== null` check
anywhere. Test runs: `default-value-property` 7/7, `dot-content-types-edit`
545 passed across 48 suites, `data-access` 751 passed across 79 suites.

2. `sdk-create-app/src/utils/index.ts` — the throw said "requires at least 1
retry", but `retries` is the total attempt count (`for (i = 0; i < retries)`),
so `retries = 1` means one attempt and zero retries. Reworded to "attempt" and
the ambiguity noted in the comment.

3. `core-web/CLAUDE.md` — the verify snippet hard-coded
`libs/<project>/tsconfig.lib.json`, which resolves for neither nested projects
(`libs/sdk/create-app`, which has no `tsconfig.lib.json`) nor apps
(`tsconfig.app.json`). Replaced with a `<projectRoot>` placeholder plus the two
caveats, a reminder that `tsconfig.spec.json` inherits the flags, and a warning
about unresolved `types` entries masking all semantic diagnostics.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`sdk-uve` needs no change for [08/44]. The six strict flags have been in
`libs/sdk/uve/tsconfig.json` since the library was created (`277cbbc8f7`,
#31242, Feb 2025) as a verbatim copy of `sdk-client`'s config, `tsc --noEmit`
is clean on both lib and spec, and there are zero `any`, `@ts-ignore` or
non-null assertions across 4518 lines.

It is also genuinely enforced, which is what separated `sdk-types` from
`dotcms-js` and `utils`. `rollup.config.cjs` sets `compiler: 'babel'`, but that
governs only transpilation — `@nx/rollup`'s `withNx` always inserts a
TypeScript plugin with `check`/`noEmitOnError` tied to `skipTypeCheck`, which
this project does not set. Two of the three type-checking paths run in CI, and
the `build-test` execution in `core-web/pom.xml` has no `<skip>` element, so it
cannot be turned off.

Issue closed as completed with the evidence; not linked to PR #36957 since
there is no diff and that PR did not resolve it.

Also records an incidental finding, left unfixed: `tsconfig.base.json:104`
maps `@dotcms/uve/types` to a file that does not exist, and nothing imports it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`sdk-client` needs no change for [09/44]. The six strict flags are already in
`libs/sdk/client/tsconfig.json`, `tsc --noEmit` is clean on both lib and spec,
and there are zero `any`, `@ts-ignore` or non-null assertions across 9600 lines
of production source.

Enforcement is unambiguous here, unlike the sibling projects that needed an
argument: `rollup.config.cjs` sets `compiler: 'tsc'` against `tsconfig.lib.json`
with no `skipTypeCheck`, so the build compiles with tsc directly against the
strict config. `tags` is empty and the `build-test` execution in
`core-web/pom.xml` has no `<skip>` element, so that build runs on every PR and
gates every SDK release.

Issue closed as completed with the evidence; not linked to PR #36957 since
there is no diff and that PR did not resolve it.

Also records an emerging pattern for the remaining issues: every `libs/sdk/*`
project checked so far is already strict and already enforced — they share a
tsconfig lineage (sdk-uve's config is a verbatim copy of this one) and all build
through Nx executors that type-check. The unfinished work is concentrated in
the non-SDK libraries and the apps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nicobytes and others added 18 commits August 19, 2026 15:22
The merge brought in main's new shared asset picker and moved the folder list
view and the dropzone into `libs/ui`. Those projects are already strict on this
branch, so the incoming code did not compile: 68 lib + 69 spec errors.

The dominant cause is the one this branch has been finding all along —
`DialogService.open` returns `DynamicDialogRef | null`, because PrimeNG refuses
to open a duplicate. Six new call sites subscribed to `.onClose` on the result.
Two of them also hold a re-entry guard, and releasing it only on close means a
refused open leaves the button dead for the rest of the session; both now
release it when nothing opened:

- `DotFileFieldComponent.#openAssetPicker` (`#assetPickerPending`)
- `DotWysiwygPluginService` (`imagePickerBusy`)

Three more real ones:

- `DotGeneratedAIImage.response` is null when generation failed, and both the
  file field's mapper and the block editor's insert read straight through it.
  The file field's pipe now filters on `response`, not just on the image.
- The wysiwyg drop handler read `event.dataTransfer.files[0]`; a drop need not
  carry a DataTransfer, and dragging text inside the editor is the common case.
- `DotFileFieldComponent.writeValue` was overridden as `(value: string)` while
  the base declares `T | null` and Angular calls it with null to clear a
  control — the spec that does exactly that is what caught it.

Two declarations were saying less than the code: `DotContentDrivePaginateEvent
.page` is optional (`onPaginate` already reads it as `page ?? 1`), and the
content-drive shell's drop target is optional on the shared `DotUploadFiles`,
since a drop can land before a folder is chosen.

On the ten conflicts I took main's side throughout, including where it had
reverted narrowing this branch added — `filter(Boolean)` over a type predicate
in the content-type filter, `l.multiple` over `!!l.multiple`. Those come back
here under compiler guidance rather than by hand. Main's move of the
folder-list-view models carried the narrowing forward intact, so nothing was
lost there.

Spec fixtures: main's new store spec used the component-spectator
`inject(token, true)` idiom in a `createServiceFactory`, whose `inject` takes
one argument; and its contentlet/folder stubs carry a handful of the required
fields, now asserted once through an `asset()` helper instead of at each site.

Verified at 0/0 on lib and spec for ui, edit-content, new-block-editor,
content-drive (portlet and ui), image-editor, dotcms-models and data-access,
dotcms-ui still 0/0, and test + lint green across all seven: 12/87, 104/1349,
1/54, 112/2243, 222/2189, 30/1147.

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

589 → 503, in two uniform passes over specs.

**44 × TS2722.** Every one is a spec invoking a callback it registered itself,
through a property the model declares optional: the `accept` handed to
`DotAlertConfirmService.confirm`, a PrimeNG `MenuItem.command`, a
`DotDialogActions.accept.action`. None is in production code — the one that
was is already fixed. `!` is what these are for, and it is permitted in
`*.spec.ts` by the workspace eslint config.

**39 × TS2322 across three specs.** The three `dot-custom-field-settings`
specs each carried a byte-identical `DotCMSContentTypeField` literal that put
`null` in eighteen fields the model declares non-nullable. All three now spread
`dotcmsContentTypeFieldBasicMock` and override only the four fields their tests
actually distinguish, which removes the triplication along with the errors.

One note on the measurement rather than the code: the first reading of this
batch came back as a flat `0` with no output, which is the fake-zero shape
CLAUDE.md warns about. It was neither — `pnpm` had failed because the shell was
in the repo root rather than `core-web`, and grepping the output instead of
checking the exit status hid it. Exit status is checked here.

Verified: tsconfig.app.json and tsconfig.spec.json at 0/0, tests at
222 suites / 2189 passed / 32 skipped, lint clean.

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

503 → 481. The two heaviest production files left, 11 errors each.

**iframe.component.** Legacy JSP pages hang callbacks off the frame's window by
name, which `Window`'s numeric index signature cannot express — that is stated
with a cast now. The rest were absences the code half-knew about:
`DotFunctionInfo.args` is optional (which is the only reason `setArgs` exists),
the frame's document need not have an `<html>` yet, and `onLoad` receives the
DOM `load` event, whose `target` is `EventTarget | null` and whose frame may
have no `contentDocument` at all. `parseInt('')` is `NaN`, so the `> 400` test
behaves exactly as it did with a missing title.

Flagged, not fixed: `handleIframeEvents` passes `this.emitKeyDown.bind(this)` to
both `removeEventListener` and `addEventListener`, and each `.bind` produces a
fresh function — so neither remove has ever removed anything, and every iframe
load adds another pair of listeners. Fixing it means changing listener identity,
which changes what the component emits; that is more than a strict-mode pass
should do. There is a comment at the site.

**searchable-dropdown.** The component reads whichever properties
`labelPropertyName` and `valuePropertyName` name, so a row is only ever known by
key: `value` and `options` are `Record<string, unknown>` now, which is what the
five index accesses through them were already assuming. `selectedOptionIndex` is
`number | null` — the third branch of `selectDropdownOption` already tested for
null while the two above it did arithmetic on the same field — and `paginate`
takes `| null`, which its own `event?.first ?? 0` had been admitting.

One thing worth writing down, because the first attempt got it wrong. Coercing
`setLabel`'s `valueString` to `''` broke a passing test: the template's
`[class.selected]` compares `item[getValueLabelPropertyName()]` against
`valueString`, and with no `labelPropertyName` configured both sides read
`undefined` and matched. Coercing one side to `''` makes the comparison false
and the clicked row loses its class. `getItemLabel` declared `string` and
returned `undefined` on that path — the flags exposed the lie, but the fix has
to keep both sides reading the property the same way, so this is a cast rather
than a coercion, with the reason at the site.

Verified: tsconfig.app.json and tsconfig.spec.json at 0/0, tests at
222 suites / 2189 passed / 32 skipped, lint clean.

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

481 → 419.

**DOTTestBed.** `configureTestingModule` merged its defaults by walking
`for...in` over `DEFAULT_CONFIG`, which made all eight of its accesses indexes
into `TestModuleMetadata`. The defaults only ever carry `imports` and
`providers`, so those are named now and the prepend-what-is-missing behaviour —
including handing back the defaults array itself when the caller passed none —
moves into one helper. Shared infrastructure, so this is verified by the whole
suite rather than by a single spec.

**Three declarations that production code already contradicted:**

- `DotNavLogoService.navBarLogo$` was `BehaviorSubject<string>` while `setLogo`
  publishes `null` for anything that is not a `/dA` asset path. That is how "no
  custom logo" is spelled and the nav header's template branches on it. 14 spec
  errors and one production lie, one change.
- `DotTemplate.layout` was required while an advanced template has no layout —
  which the template store already knew, falling back with
  `template.layout || EMPTY_TEMPLATE_DESIGN.layout`. No strict consumer
  regressed (template-builder, ui, edit-content, data-access, utils-testing,
  global-store all still 0).
- `FieldDragDropService`'s four dragula option callbacks were declared with
  required `HTMLElement`s where dragula passes `Element | undefined`, which
  strictFunctionTypes rejects. They now take what dragula gives and cast where
  they reach for `dataset`.

Flagged, not fixed: `wasDrop` in that service is `(target) => target === null`,
but `DragulaCustomEvent.target` is optional, so a drop arrives as `undefined`
and the predicate has always returned false — the `clearCurrentFullRowEl` branch
behind it is dead. Making it fire changes when the full-row highlight clears,
which is behaviour, not types. Comment at the site.

The rest is spec fixture drift: `MenuItem.label` and `DotTemplate.themeInfo` are
optional, a `MenuItemCommandEvent` is what a menu command receives, and two
workflow-action fixtures set too few fields for a single `as` to overlap.

Verified: tsconfig.app.json and tsconfig.spec.json at 0/0, tests at
222 suites / 2189 passed / 32 skipped plus 15/148 for template-builder, lint
clean across all three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
**content-types-form.** `getProp(item: string)` existed to turn an absent
property into `''` and was declared as if it never received one.

**dot-container-properties.store.** Two updaters annotated their callback with
the whole `DotContainerPropertiesState` while being declared over a subset —
the same contravariance mistake as the pages store. Two state fields were
non-nullable and seeded `null`: `container` is null until the resolver's
container lands (and on the create path until it is saved), `originalForm` until
the form is built from it. And `DotContainersService` swallows a failed request
into `of(null)`, so `catchError` never fires and both `saveContainer` and
`editContainer` read `.container` off nothing — the same defect family as the
list store and the templates service.

The component's subscriber annotated the full state where
`containerAndStructure$` selects two fields.

**dot-block-editor-settings.** `BlockOption` required `label` and `code`, but
`getEditorBlockOptions` maps from `DotMenuItem` where both are optional — its
own sort already reads `label ?? ''`. The custom-block parser computed its label
fallback *above* the guard that narrows `name`, so the fallback was
`string | undefined` rather than the `string` it looks like.

`settingsMap` was reached by variable key in two places, which first pushed it
to a `Record` and traded TS7053 for TS4111 on its named reads. Both lookups were
redundant: `saveSettings` already has the row it was looking up, and the
`ngOnInit` one matches on a key that comes from a *saved field variable*, which
need not name a setting the form shows — that is a `find` now, and the map keeps
its literal type.

Spec fixtures: four `as unknown` casts erased the `Partial<DotCMSContentTypeField>`
annotations they were assigned to, and three field-variable lists set the two
keys the component reads out of the five the model requires.

Verified: tsconfig.app.json and tsconfig.spec.json at 0/0, TS2304/TS2552 at 0,
tests at 222 suites / 2189 passed / 32 skipped, lint clean.

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

**dot-content-types-edit-resolver.** Resolves `DotCMSContentType | null` now,
which is what it does: `getContentType`'s error path reports through
`DotHttpErrorManagerService` and emits null. And `getDefaultContentType` builds
the seed for a content type that does not exist yet — `id`, `iDate`, `modDate`
and five more are filled in on save, which is why they are null against a model
that describes what the endpoint *returns*; that is stated with one cast and a
comment rather than eight.

**dot-contentlet-editor.service.** `_load` and `_keyDown` are null until an
action binds them and null again after `clear()`, and the subject carries null
for the same reason — the three url streams filter it out, which their
predicates were already deciding. `getActionUrl` emits null on the error path.

**dot-contentlet-wrapper.** The event-handler map is keyed by event name, so it
is a `Record` rather than a literal the iframe indexes by variable. Its
`if (!this.customEventsHandler)` guard read a field with no initialiser to
decide whether to assign it, which is what "used before being assigned" meant;
the guard is gone and the constructor just builds the map.

**dot-page-selector.** `message` and `currentHost` are both cleared as each
search restarts. `parseUrl` returns null for a query that is not a URL — which
`isHostAndPath` was checking with `url && ...` while the signature denied it —
and `getEmptyMessage` fell off the end of its switch for anything outside the
three search types.

**dot-container-code.** `contentTypeNamesById` is keyed by content type id.
`handleTabClick` and `removeItem` default their index to `null`, which their own
`index !== null` and `index - 1` branches already read; `removeItem` had no such
branch, so it now returns early instead of calling `removeAt(null - 1)`.

Spec side: the monaco stub sets the two methods `monacoInit` touches out of the
111 on `MonacoStandaloneCodeEditor`, so it is cast with that said.

Verified: tsconfig.app.json and tsconfig.spec.json at 0/0, tests at
222 suites / 2189 passed / 32 skipped, lint clean.

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

**The three validators** (`validateDateDefaultValue`, `validateRelationship`,
`noWhitespaceValidator`) declared their parameter as `UntypedFormControl` while
`ValidatorFn` hands them an `AbstractControl` — which strictFunctionTypes
rejects, and none of them reaches for anything only a control of that one kind
has. They take `AbstractControl` and return `ValidationErrors | null` now, which
is what the three entries in `PROPERTY_INFO` need.

**`const x = {}` built key by key** — the download-bundle request body, the
properties form's form-fields map, the apps save payload — is a `Record` (or, for
the apps one, the `DotAppsSaveData` it already returns).

**Option lists seeded `null`** against a declared `SelectItem[]`. Empty is the
honest "nothing loaded" in the push-publish form, whose two readers already
check `.length`.

Not in the download-bundle dialog, and the suite caught it: `if (this.filters)`
in its `ngOnInit` is the "have I fetched yet?" check, and an empty array is
truthy — seeding `[]` skipped the fetch and took 12 tests with it. `null` is
load-bearing there, so the field keeps it, with the reason written down, and the
two readers coerce. That is the third time on this branch that a "harmless"
seed turned out to be the thing driving a load; it is worth the habit of asking
what reads a field before widening it.

Also: `content-type-fields-properties-form`'s input has no default, so it reads
`| undefined` until the parent binds one — which both lifecycle hooks were
already checking for, one line after assigning it. The guard moved above the
assignment so the field stays non-nullable for its nine readers.
`DotListingDataTableComponent` was passing PrimeNG's `1 | -1 | null` sort order
straight into `OrderDirection`, calling three generic paginator methods that
default to `unknown`, and indexing a row by a caller-configured column name.

Verified: tsconfig.app.json and tsconfig.spec.json at 0/0, tests at
222 suites / 2189 passed / 32 skipped, lint clean.

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

343 → 328, three uniform patterns across the production tail.

**Six resolvers** hit the same pair: `route.paramMap.get(...)` returns
`string | null`, and `DotAppsService` / `DotContainersService` /
`DotContentletEditorService` swallow a failed request into `of(null)`. So each
resolves `T | null`, which is what the components behind them already read.
`DotAppsConfigurationResolver` was also declared `Resolve<Observable<DotApp>>` —
a double-wrapped `Observable` the Router would never have unwrapped.

Four more services declared a non-nullable `Observable<T>` over the same
`handleError`: `createCustomTool`, `addToLayout`, `addStarterPage`,
`removeStarterPage`. `DotMenuService.menu$` is null between `reloadMenu` and the
next fetch, and its `getDotMenuId` used `find`, which emits `undefined` for a
portlet id the loaded menu does not carry.

**Four `BehaviorSubject` seeds** typed as if they never held their seed:
`new BehaviorSubject(null)` in the workflow-task-detail and login-page-state
services (both cleared back to null by design), and `new BehaviorSubject([])`
inferring `never[]`. Plus the announcements store's `currentSite`, which is null
until the site resolves — its two helpers now say so rather than being handed a
null through a non-null parameter.

**Four `dialogActions` fields** update their accept button by spreading it
(`{ ...this.dialogActions.accept, disabled }`), which yields a partial without
`label` when the property is optional. All four always build one, so all four
require it — the same narrowing this branch already applied to
`dot-add-persona-dialog` and `dot-push-publish-dialog`.

Verified: tsconfig.app.json and tsconfig.spec.json at 0/0, tests at
222 suites / 2189 passed / 32 skipped, lint clean.

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

328 → 317.

`dom-autoscroller` ships no declarations and has no `@types` package, so
`ContentTypeFieldsDropZoneComponent`'s import was an implicit `any`. It now has
one in `core-web/types/` wired through `paths` in `tsconfig.base.json`, which is
the third entry there and follows `jstat` for the same reason: an ambient
`declare module` inside the importing project is invisible to a strict consumer
compiling those sources through a path mapping. Only what
`setUpDragulaScroll` passes is declared, so a change in what we use from the
package stays a compile error. Every strict consumer of the app's libs still
measures 0 (ui, edit-content, data-access, utils-testing, global-store,
template-builder, new-block-editor).

Four more tables indexed by strings the server supplies: the date validator's
formats and the field-variables blacklist are keyed by field clazz, and
`DotFilterPipe` matches on key names its caller passes in.
`NotificationIcons` lists the three levels that have an icon while
`DotNotification.level` is a plain string, so it carries an index signature and
its one reader already falls back.

Three more absences the code half-knew about: `getFieldType` misses for a clazz
the endpoint does not know (the dialog header already read it as
`currentFieldType?.label`), `fieldTypeLabel` was `null` standing in for "no
label" against a declared `string`, and the fields list prepended a line divider
the endpoint need not have returned.

Verified: tsconfig.app.json and tsconfig.spec.json at 0/0, tests at
222 suites / 2189 passed / 32 skipped, lint clean.

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

**`DotLicense.isCommunity` was declared as the literal `false`** — the model
asserted that no license is ever anything but community. Every other declaration
of the flag in the workspace is `boolean`, and both the test bed and the
config-service mock set it to `true`. That one character is what stopped
`app.component` from handing the config's license to `DotLicenseService`.
`libs/dotcms` stays at its 17 pre-existing errors; every other strict consumer
is still 0.

**`ColorUtil.rgb2hex` reassigned a `RegExpMatchArray` over its own `string`
parameter** — which is exactly what an untyped parameter hides. It uses a local
now, and a value containing "rgb" that is not a well-formed `rgb()` no longer
throws on `parts[1]`: it comes back unchanged, like anything else the function
cannot convert. That path was reachable and is the only behaviour change here.

**`DotParseHtmlService`** annotated two `childNodes` callbacks as `HTMLElement`.
`childNodes` yields `ChildNode`, which covers the text nodes between tags —
those have neither `tagName` nor `innerHTML` and fall through to being appended
as-is, which is what already happened at runtime.

**Four subscribers annotated wider than their stream emits.** `app.component`'s
config pipe has two branches emitting different literal shapes, and
`dot-apps-list`/`dot-binary-settings` were reading `DotAppsService.get`'s
failure `null` and the error handler's result as if they were the payload.

`DotAppsConfigurationDetailFormComponent`'s value transforms return three
different shapes — `STRING` returns the `{ value, disabled }` form a reactive
control also accepts — so `string | boolean` was never the whole story; that is
a named type now.

Flagged, not fixed: `StringFormat.formatMessage` loops to `args.length - 1`, so
the last argument is never substituted and a single argument substitutes nothing
at all. Changing which placeholders get filled is behaviour; there is a comment
at the site.

Verified: tsconfig.app.json and tsconfig.spec.json at 0/0, tests at
222 suites / 2189 passed / 32 skipped, lint clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
**`LoginService.watchUser` declared `func: (params?: unknown) => void`** while
its body calls the callback with `this.auth` and with every `auth$` emission —
always an `Auth`. That denial is why both callers who wanted the user
(`dot-my-account`, the toolbar-user store) had to annotate their own callback
with a type the signature would not accept. It takes `(auth: Auth) => void` now;
the two callers that ignore the argument still satisfy it, and the toolbar-user
spec's three-field `mockAuth` is what confirmed the signature is enforced.

`libs/dotcms-js` stays where it was: 0 on its lib config, and its spec config is
still masked by the `TS2688` jasmine entry CLAUDE.md documents — none of the 26
errors that mask hides mention `watchUser`.

**The four consumers of `DotLoginPageStateService.get()`**, which emits null
until `set()` has fetched the page state. Each now filters with a type predicate
rather than annotating the value as already present.

**Three `registerOnChange(fn)`** and one pipe parameter left untyped.

**Three rxjs sites that dropped a narrowing the runtime already does:**
`fromEvent` unparameterised so `merge` produced `Observable<unknown>`, a generic
paginator call defaulting to `unknown`, and `onNavigationEnd` filtering on
`event instanceof NavigationEnd` while returning `Observable<Event>` — the
predicate is a type guard now, so its one consumer no longer has to re-assert
what the filter established.

Verified: tsconfig.app.json and tsconfig.spec.json at 0/0, tests at
222 suites / 2189 passed / 32 skipped, lint clean. `dotcms-js:test` and
`dotcms-js:lint` fail as they already do at HEAD.

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

The dot-apps portlet carried 38 of the remaining strict errors, and most traced
to optionality that no caller ever produced.

`DotAppsConfigurationItemComponent.site` was an optional input, so the component
grew a `@if (site(); as site)` wrapper, two `!site()?.configured` host bindings,
and outputs declared `DotAppsSite | undefined`. It is only ever rendered from the
list's `@for` over real sites, and the list it emits through declares those same
outputs non-optional — the parent handlers take a plain site. Made the input
required and dropped the guard the requirement makes dead.

`DotAppsImportExportDialogStore.openExport` declared a non-null `app` while its
own export effect reads it as `exportAll: app ? false : true`. That is why the
export-all caller had to write `openExport(null as unknown as DotApp)`. The
parameter is nullable; the cast is gone.

`DotAppsService.exportConfiguration` declared `Promise<string | null>` and never
resolves null — both branches return a string. Narrowed, which removes the last
`| null` the dialog store had to absorb.

Two smaller ones in `dot-apps-configuration.component.ts`: `getWithOffset` was
unparameterised, so the paginated app arrived as `unknown`; and the
delete-all handler re-read `this.$app()` inside its callback instead of the local
the guard above it had already narrowed.

On the spec side, six `props: { formFields: … } as unknown` casts were working
around a real Spectator limitation: `props` is keyed by the class property name
(`$formFields`) while the `ComponentRef.setInput` underneath needs the public
alias (`formFields`), so an aliased signal input cannot satisfy both. Stated once
in a documented helper instead of six times as a bare cast.

Verified: `tsconfig.app.json` and `tsconfig.spec.json` at 0, `data-access` lib
and spec at 0, probe 288 → 250, dot-apps at 0. `dotcms-ui:test` on the dot-apps
pattern is 14 suites / 163 passed / 3 skipped; lint clean on dotcms-ui and
data-access.

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

`DotRouterService.currentPortlet` returned `PortletNav`, whose `id` is optional —
but that getter always sets it from `getPortletId`, which returns a plain string.
The optional `id` exists for the seeded `_routeHistory`, not for this getter. Three
callers were paying for it (`dot-content-types-edit`, `dot-contentlets`,
`dot-create-contentlet`, all passing it straight to `reloadData(portlet: string)`).
Narrowed the getter's return; the field on the model stays optional for the seed.

`DataTableColumn.icon?: (any) => string` was not the `any` type — it is a parameter
*named* `any` with no type at all, which is why TypeScript reported TS7051 rather
than an implicit-any. The one column that supplies it reads `icon` off the row.

`DotMdIconSelectorComponent.registerOnChange(fn: () => void)` declared the
zero-argument form of a contract Angular always calls with the control's value —
the fourth CVA on this branch with that same mistake.

The mechanical half:

- 19 × TS2454 "used before being assigned": `let result: boolean;` populated inside
  a synchronous `subscribe`. Declared `| undefined` instead, so a subscription that
  never fires now fails the assertion rather than comparing a value that was never
  produced.
- 15 untyped callback and helper parameters across 11 specs.
- `input<T>(undefined)` on `dot-content-type-fields-variables` made required: the
  only host is the edit-field dialog, whose `currentField` is not optional.

Process note: the real `tsconfig.spec.json` went red on this batch while the probe
kept falling. `getConfig(route)` in the content-types-edit spec takes the route
*data* — it is handed back as `{ data: of(route) }` — so annotating it
`Partial<ActivatedRoute>` type-checked under the probe and broke on the two callers
that pass `{ contentType }`. Both configs measured after every batch, by exit
status.

Verified: `tsconfig.app.json` and `tsconfig.spec.json` at 0, `data-access` lib at
0, probe 250 → 203. `nx run-many -t test lint -p dotcms-ui data-access` green at
222 suites / 2189 passed / 32 skipped.

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

The last 34 production errors. Two are guards that were never enforced and one is
a request the app could send with nothing to act on.

`DotContentTypeComponentStore.saveCopyDialog` combined the form fields with
`assetSelected$`, which is `string | null`, and passed that straight to
`saveCopyContentType` — so a submit with nothing selected sent `null` as the
content type to copy. `setAssetSelected` only ever takes a string, so `null`
means "the dialog was never opened for an asset"; the effect now filters on it.
The error-path test was reaching the effect without selecting, which is what
surfaced this: it now selects first, as the dialog does.

`DotAutocompleteTagsComponent.addItem` does `this.value.unshift(this.value.pop())`.
On an empty list that unshifts `undefined` back into the tag array, and
`getStringifyLabels` then reads `.label` off it. Only reachable if PrimeNG fires
`onSelect` with nothing selected, but the hole was real.

`shouldClearDropdown` returned `dropdown && options.length && !options.includes(v)`
— `options.length` is a number, so a function declared `: boolean` could return
`0`. It is used in an `if`, so no behaviour changed, but the signature was false.

Three more signatures corrected at their source rather than at the call:

- `DotRouterService.portletReload$` was a bare `Subject` (so `unknown`); it carries
  whatever `reloadCurrentPortlet(id?)` was given, and its one consumer's
  `reloadIframePortlet(portletId?)` already branches on absence.
- `DotEventsService.listen` and `PaginatorService.getCurrentPage`/`getWithOffset`
  are generic and four call sites left them unparameterised, taking `unknown` and
  then annotating the subscriber with the type the compiler had just discarded.
- `ActionHeaderDeleteOptions.confirmHeader`/`confirmMessage` were optional feeding
  a confirm dialog that requires both. Nothing in the repo supplies `deleteOptions`
  at all, so the optionality only ever weakened the call.

`dot-custom-event-handler` read `document.querySelector('html')`, which is
nullable; `document.documentElement` is the same node and is not. Its spec was
stubbing `querySelector` to observe the argument — there is nothing left to stub,
so it now asserts against the root element jsdom provides.

The rest is `!`-free narrowing at 20-odd sites: fields cleared to null on close
(`eventData`, `url`, `tempUploadedFile`, `lastDeletedTag`) declared `| null`; two
CVA no-ops written `=> undefined` (which infers a return type of `undefined`, so
no `void` handler satisfies them) given block bodies; and guards hoisted into
locals where TypeScript drops a property narrowing across a callback.

Verified: production source **0** under all six flags. `tsconfig.app.json` and
`tsconfig.spec.json` at 0, `data-access` lib at 0, probe 203 → 173 (all specs).
`nx run-many -t test lint -p dotcms-ui data-access` green at 222 suites / 2189
passed / 32 skipped and 80 / 781; lint back to the one pre-existing warning.

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

`mockResponseView` already returns an `HttpErrorResponse`, and two specs wrote
`new HttpErrorResponse(mockResponseView(400))` — handing one `HttpErrorResponse`
to the constructor as its own init object. That is also exactly why the types
disagreed: the class exposes `url: string | null` while the init literal wants
`string | undefined`. Dropping the redundant wrapper is the fix, not a cast.

Two families, both mechanical:

- 26 sites queried a node the test had already rendered and used it directly
  (`spectator.query`, `document.querySelector`, `form.get`). Asserted at the
  query rather than re-checked at each use; where a local carried an explicit
  `: HTMLButtonElement` annotation, the type argument moved onto
  `querySelector<T>` so the annotation is not restating what the call returns.
- 18 sites passed `null` for router arguments the implementation ignores. Both
  guards and both resolvers already name those parameters `_route`/`_state`, so
  the specs now say so once per file with a named constant instead of a bare
  `null` at each call. Two of them were not "unused" at all: `UrlSegment`'s
  second argument is its matrix parameter map, where `{}` is the real value, and
  `HttpResponse` never accepted a null `headers`.

Verified: `tsconfig.app.json` and `tsconfig.spec.json` at 0, probe 173 → 133.
`nx run-many -t test lint -p dotcms-ui` green at 222 suites / 2189 passed / 32
skipped, back to the one pre-existing lint warning after dropping three imports
the simplifications left unused.

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

`ContentTypeFieldsDropZoneComponent.saveFieldsHandler` declared
`fieldToSave: DotCMSContentTypeField`, whose `id` is required — while its own body
branches on `if (fieldToSave.id)` to decide between "edit an existing field" and
"emit a save for a new one", and the component has a `removeFieldsWithoutId()` for
the other half of that state. A field with no id is a first-class input here, so
the parameter is now `Partial<DotCMSContentTypeField>`.

The spec that exposed it was mutating the shared fixture: it deleted `id` off the
object the other tests read. Now it deletes from a copy.

`DotContainer.path` is optional but the API returns `path: null` for a DB
container — only file-based containers have one — which the service spec's fixture
reproduces faithfully. Widened to `string | null`; every reader already guards on
truthiness. Blast radius measured: `dotcms-models`, `data-access`, `ui`,
`edit-content` and `edit-ema/portlet` all stay at 0.

The `handleError`-null family again, this time in the specs: 21 subscribe
callbacks across 7 service and resolver specs annotated their parameter with the
non-null type while the service resolves `T | null`. Dropped the annotations so the
subscriber carries what the service emits, rather than restating something
narrower that the compiler then had to reject.

Plus the 11 layout-navigation sites in the drop-zone spec, where `columns` is
optional because a tab row has none.

Verified: `tsconfig.app.json` and `tsconfig.spec.json` at 0, probe 133 → 95.
`dotcms-ui` 222 suites / 2189 passed, `data-access` 80 / 781, `ui` 104 / 1349;
lint at the one pre-existing warning for dotcms-ui and the nine pre-existing for
`ui` (confirmed against a stashed tree).

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

`DotCMSContentType.host` was declared `string`, but seven fixtures across two
specs set `host: null` — and so does the shared `dotcmsContentTypeBasicMock`,
where it goes unnoticed because that whole object ends in
`as unknown as DotCMSContentType`. The form's own
`getProp(item) { return item || ''; }` exists for exactly this value. Widened to
`string | null`, with `getProp` following. Blast radius measured: `dotcms-models`,
`data-access`, `ui`, `utils-testing` and `edit-content` all stay at 0.

That blanket cast is worth naming: it lets some twenty fields of the most-used
content-type fixture disagree with the model without anything reporting it. Not
changed here — unpicking it belongs with the `utils-testing` project, which has
its own rollout issue.

`DotPushPublishFormComponent.data` was `@Input() data!: DotPushPublishDialogData`
while `ngOnInit` wraps every read in `if (this.data)` and the spec host clears it
to `null` between cases. Declared nullable, with the guard hoisted into a local so
the narrowing survives into the `loadFilters` callback.

Two spec simplifications rather than assertions: the expected POST body in
`dot-content-types-edit` is typed `Partial<DotCMSContentType>` instead of deleting
a required `workflows` off it, and the `setSelectedFolder` mock returns an empty
response instead of `of(null)` for an `Observable<DotCMSResponse<…>>`.

The rest is fixture narrowing: `find()` results asserted where the line above
already says `toBeDefined()`, `form.get()` chains, `columns` on layout rows, and an
index signature on the listing spec's row type — which the assertions look up by
the column's `fieldName`.

Process note: adding that index signature put `boolean` into the row's value
union, and the real `tsconfig.spec.json` went red on `new Date(cellValue)` while
the probe did not report it. Both configs measured after every batch.

Verified: `tsconfig.app.json` and `tsconfig.spec.json` at 0, probe 95 → 47.
`dotcms-ui` 222 suites / 2189 passed, `data-access` 80 / 781, `ui` 104 / 1349.
Lint unchanged: one warning for dotcms-ui, nine for `ui`, and `utils-testing:lint`
fails identically at HEAD (36 errors, confirmed against a stashed tree).

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

The six flags are on in `apps/dotcms-ui/tsconfig.json`, and all three of its
configs — `app`, `spec` and `editor` — report 0. Exactly one `"strict"` key, so
nothing silently wins.

**The app build found six errors that `tsc -p` cannot see.** `tsc` does not check
templates; the Angular compiler is the only thing that evaluates them, and
`strictTemplates` being off does not stop a project's own `strictNullChecks` from
applying to template expressions. All six were real:

- `apps.sites[0].name` and `apps.sites[0].configured` in the app-detail template —
  `DotApp.sites` is optional, and an app with no configured site has none.
- `form.get('workflows').disabled`, nullable like every other `get` on that form.
- `options() && options().secondary` — the second call is a fresh read the compiler
  cannot narrow from the first.
- `runningExperiment.scheduling.endDate` in `edit-ema/portlet`, a library that
  measures 0 on its own configs. A build-less library's templates are checked only
  when an app compiles them.
- `@if (action)` in the searchable dropdown, reported as TS2774 *always true*:
  `@Input() action!: (event: Event) => void` claimed the input is always bound,
  while only one of its three hosts binds it and the `@if` is what handles the
  other two. The `!` was asserting that check could never fail.

**`tsconfig.editor.json` reached 0 too**, which I had expected to record as a known
limitation. It compiles what `tsconfig.app.json` excludes, and that turned out to
be where things had been rotting unseen:

- Seven dead files — six `index.ts` barrels and `components.ts` — re-exporting
  NgModules and a component deleted in the standalone migration. Zero importers;
  none of them could have compiled. Removed.
- Four Storybook stories broken since the PrimeNG upgrade: `PrimeNGConfig` is now
  `PrimeNG` and its `ripple` flag is a signal.
- `libs/block-editor/NodeViewRenderer.ts`, which is not this project's file. It
  declares `override decorations!: readonly DecorationWithType[]`, clean under
  block-editor's own **es2015** target where a class field is an assignment, and
  TS2612 under dotcms-ui's **ES2022**, where `useDefineForClassFields` is on by
  default and the same field emits a `defineProperty` that shadows the base value
  with `undefined`. `declare` (which TypeScript will not accept alongside
  `override`) states the intent and emits nothing, so both configs agree.

Also in the tail: `DotPortletToolbarActions.primary` made nullable, because the
toolbar's template already reads it as `actions?.primary?.length` and three tests
pass `null` for "cancel only"; `DotNavLogoService.setLogo` widened to `string |
null`, which its own `navLogo?.startsWith` had already assumed; and
`DotContentletEditorService.createUrl$` narrowed to `Observable<string |
undefined>` — it maps an optional key off an action's data bag, and its one
consumer filters `undefined` out, which is the proof it arrives.

One shared helper replaces eight `as unknown` casts across six specs:
`aliasedProps` in `app/test/`. Spectator keys `props` by the class property name
(`$field`) while the `ComponentRef.setInput` underneath needs the alias (`field`),
so a component following this repo's `$name` + `{ alias }` convention cannot
express its inputs through `props` at all. One spec already carried a comment
saying exactly that.

Verified: `tsconfig.app.json`, `tsconfig.spec.json` and `tsconfig.editor.json` all
at 0; `dotcms-ui:build:production` succeeds (exit 0). `dotcms-ui:test` 222 suites
/ 2189 passed / 32 skipped, lint at the one pre-existing warning. `ui` 104 / 1349,
`edit-ema/portlet` and the five other affected libs at 0 on `tsc -p`.
`data-access`'s four `PushPublishService` failures and `block-editor`'s 16/37 both
reproduce identically at HEAD (confirmed against a stashed tree).

Closes #35933

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nicobytes nicobytes changed the title refactor(core-web): TS strict mode across 30 projects — template-builder, edit-ema-ui, block-editor, ui, data-access, sdk-*, portlets refactor(core-web): TS strict mode across 30 projects Aug 20, 2026
@nicobytes nicobytes changed the title refactor(core-web): TS strict mode across 30 projects refactor(core-web): TS strict mode across all 44 projects — completes epic #35932 Aug 20, 2026
nicobytes and others added 3 commits August 19, 2026 21:23
Formatting only, no behaviour and no type changes — the output of the repo's own
`nx format:write`, which CI checks.

- 28 files in `libs/sdk/vue`, which `main` carries at a different indentation than
  the workspace Prettier config produces.
- Two files in `apps/dotcms-ui` whose formatting my earlier strict-mode commits
  left unclean: an arrow body in `dot-pages/store/store.ts` and a method-chain
  break in `dot-apps-configuration-header.component.spec.ts`.

Kept as its own commit so it does not sit inside a type-change diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area : Documentation PR changes documentation files Area : Frontend PR changes Angular/TypeScript frontend code Area : SDK PR changes SDK libraries

Projects

Status: No status

3 participants