Skip to content

refactor(core-web): TS strict mode across 21 projects — utils, dotcms-js, sdk-*, data-access, ui, portlets, dotcdn - #36957

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

refactor(core-web): TS strict mode across 21 projects — utils, dotcms-js, sdk-*, data-access, ui, portlets, dotcdn#36957
nicobytes wants to merge 35 commits into
mainfrom
35932-enable-strict-mode

Conversation

@nicobytes

@nicobytes nicobytes commented Aug 7, 2026

Copy link
Copy Markdown
Member

What

Twenty-one steps of the strict-mode rollout (epic #35932), plus groundwork on a twenty-second, and partial progress on libs/ui:

Issue Project Change
#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
#35953 ui Partial — library program 0 (from 122), specs 90 (from 427). Unblocks 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.

Test plan

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

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:

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 #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 54s —— View job


Code Review

Reviewed the substantive (non-mechanical) changes in this PR against origin/main, and rechecked the prior findings from Copilot and @oidacra. This is a large PR but the vast majority is type-only or genuinely behavior-neutral. Focused on the handful of changes that alter runtime behavior.

New Issues

No new bugs found. The behavior-affecting changes I checked are sound:

  • paginator.service.ts:65_sortOrder deliberately left undefined (not defaulted to ASC); correct, since getParams gates direction on truthiness and OrderDirection.ASC === 1 is truthy — defaulting would start sending a param that was previously omitted.
  • dot-page-state.service.ts:283const [page, user] = result ?? [null, null] correctly handles the handleSetPageStateFailedundefined path; if (page && user) is a safe tightening (forkJoin already guaranteed both).
  • dot-router.service.ts:461 — switching from in-place mutation navExtras.queryParams['r'] = … to a spread copy is actually safer (tolerates queryParams being undefined) and preserves the r cache-buster.
  • dot-localstorage.service.tsString(value) and the item === null early return both preserve prior runtime behavior (setItem coerces to string anyway; parseInt(null)NaNJSON.parse(null)null).
  • dot-content-types-info.service.ts:146result = '' default is falsy-equivalent to the previous uninitialized undefined for all consumers.

Existing (from @oidacra's review — still present, non-blocking)

  • 🟡 Medium: libs/utils/src/lib/shared/FieldUtil.ts:33-36defaultValue/hint/values changed nullundefined. JSON.stringify drops undefined keys, so ROW/COLUMN/TAB dividers sent via the layout save now omit these keys instead of sending explicit null. However, this is very likely safe: regexCheck was already undefined on origin/main (unchanged context line in the diff) and the save path works today, so the backend already tolerates a missing key on these fields. Worth a one-line confirmation of Jackson missing-key-vs-null equivalence for PUT /api/v3/contenttype/{id}/fields/move, then this can be closed out.

  • 🟡 Medium: libs/dotcms-webcomponents/.../dot-material-icon-picker.tsx:24, dot-html-to-image.tsx:28, dot-time.tsx:77 — three members annotated ! that the code treats as genuinely optional (selectedSuggestionIndex is assigned undefined; previewImg gated by !this.previewImg in render(); classNames only set in a listener). Per this PR's own !-for-runtime-injected / ?-for-optional split, these should be ?. Inert today (strict is not enabled in dotcms-webcomponents — groundwork only), but they will not compile once [10/44] Enable TS strict mode in dotcms-webcomponents #35943 flips the flag, so they're worth correcting now while the split is fresh rather than re-litigating later. Fix this →

  • 🟡 Medium: libs/utils/src/lib/services/dot-asset.service.ts:33-41 — each pushed promise ends in .catch((e) => e), so a rejected fetch resolves with an Error, making the settled type really Response | Error; the new Promise<Response>[] annotation compiles but codifies a contract the code doesn't honor (downstream res.json() would throw on the error object). Pre-existing runtime behavior — the annotation just makes the mismatch look intentional. Promise<Response | Error>[] (or Promise.allSettled) would be more truthful.

Resolved

  • libs/sdk/create-app/src/utils/index.ts:124 — Copilot's misleading-message finding is fixed: throw now says "requires at least 1 attempt" and the comment clarifies retries is the total attempt count.
  • core-web/CLAUDE.md — Copilot's hard-coded libs/<project>/tsconfig.lib.json path is fixed: now uses a <projectRoot> placeholder with explicit caveats for apps (tsconfig.app.json) and projects without a tsconfig.lib.json.
  • libs/dotcms-js/src/lib/core/site.service.ts:37@oidacra asked about the unguarded this.selectedSite.identifier deref (regression class from Fix the Site Selector disappearing #32019). This PR only adds a ! to the field declaration; it does not touch the deref, so no new risk is introduced. The TODO(#35939) correctly defers the honest Site | undefined typing (a public-API change) to a follow-up.

Overall: no blocking issues. The three Medium items are all pre-existing-behavior or inert-groundwork concerns already raised by @oidacra; none block merge. Nice work keeping the diff free of new any/@ts-ignore.

@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>
#35952 #35954)

Four projects verified against the rollout bar. All four already carried
all six flags, and none was masked by a TS6053/TS2688 config error, so
their counts were real.

Three needed no change at all — portlets-dot-locales-data-access (#35950),
portlets-dot-experiments-data-access (#35952) and, after the one fix below,
portlets-dot-analytics-data-access (#35954). Two of them reported 36 errors
before #35948; every one was data-access leaking through imports, so
fixing that library took them to 0 with nothing touched here.

The one real defect was in global-store (#35951): src/index.ts re-exported
WebSocketStatus with `export {}`, but it is a type, so under isolatedModules
that is TS1205. global-store's own configs never reported it — no spec there
imports ./index, so the file was never in its own program. It only surfaced
from consumers, showing up as the single spec error attributed to
portlets-dot-analytics-data-access. `export type` fixes both issues at once.

Generalisation worth carrying: a barrel file can hold an isolatedModules
error that only its consumers ever see. Second instance after
dot-ai.service.ts in #35948.

None of the four is enforced — no build target, and :test does not
type-check since isolatedModules puts ts-jest in transpile-only mode.

tsc clean on lib and spec for all four; lint and test green (global-store
187 tests, analytics-data-access 217).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nicobytes and others added 2 commits August 17, 2026 13:12
Partial progress on ui, the rollout's biggest bottleneck: ~109 of its
errors leak into each of its 26 dependents, so this unblocks a large part
of the epic. tsconfig.lib.json is now at 0 (from 122); tsconfig.spec.json
is at 123 (from 427) and continues in a follow-up commit.

Flags added to libs/ui/tsconfig.json (it had none of the six).

Production fixes follow the policies agreed for the epic — zero values for
TS2564 where one exists, definite assignment only for FormGroup/Observable/
ViewChild, and guards rather than assertions where null is reachable:

- dot-icon's `size` deliberately became `size?: number` instead of `= 0`.
  The template binds [style.font-size.px]="size", so 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 simply wrong: `formEl` was declared HTMLFormElement while
  the template says #formEl="ngForm" (it is an NgForm); getVariableIndexChanged
  declared `number` while its own JSDoc documented `number | null`;
  getDefaultBundle returned null under a non-nullable type.
- Real defect found: dot-add-to-bundle invoked getDefaultBundle twice for the
  same value. Now once.
- tsconfig.lib.json excluded a non-existent src/test.ts but not test-setup.ts,
  **/*.test.ts or __mocks__/, so test files were compiled into the library
  program. That alone accounted for 15 errors.
- htmldiff-js ships no types; added a minimal module declaration rather than
  silencing the import.

Specs went 427 -> 123 mostly by asserting at the point of declaration rather
than at every use: 53 usages of one `select` local came from a single line, so
95 declaration-level assertions cleared roughly 300 errors.

One behaviour change worth naming: the gravatar directive now clears the
PrimeNG avatar with `undefined` instead of `null`, because Avatar declares
image?/label? as optional strings and null was never assignable. Five spec
assertions moved from toBeNull to toBeUndefined to match.

lint clean (8 pre-existing warnings); test unchanged at 81 suites / 820
passing, verified against the pre-change baseline.

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

Also fixes an htmldiff-js type leak that was inflating every consumer of
libs/ui by one error.

The ambient declaration added for htmldiff-js in #35953 lived under
libs/ui/src, so it was only in ui's own program; every project that compiles
ui's sources still reported TS7016. Registering it through tsconfig.base.json
makes it global. It sits in a root-level types/ folder rather than inside a
project, because mapping it into libs/ui made @nx/enforce-module-boundaries
demand a relative import.

Same shape as the known virtual:sdk-version leak from libs/sdk/client — an
ambient declaration parked next to its consumer instead of somewhere every
program can see it.

portlets-dot-usage (#35969): added the two missing flags; one real error, a
UsageSummary fixture missing the required lastUpdated.

edit-content-bridge (#35960): added the missing `strict`; two real errors.
The dialog ref is now captured in a local so its non-nullness is evident
instead of asserted. In the spec, `reconcileOnFormEvent` is assigned inside a
nested callback, which TypeScript's control-flow analysis cannot see, so it
narrowed the variable to `null` and reported it as not callable; definite
assignment states the intent.

Both projects: tsc clean on lib and spec, lint clean, tests green (97 and 21).
ui unchanged at 820 passing.

Neither is enforced — no build target, and :test does not type-check.

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

claude Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Claude Code is working…

I'll analyze this and get back to you.

View job run

19 errors, and 8 of them shared one root cause: dispatchLoading's switch had
no default branch, so under noImplicitReturns the updater's return type
included undefined, stopped resolving as a one-argument updater, and every
one of its six call sites reported TS2554 "Expected 0 arguments". An unknown
loader should leave state untouched, so `default: return state` fixes the
TS7030, the TS2345 and all six call sites at once.

The rest: definite assignment for a ViewChild and two fields built in
ngOnInit, a form control the component creates itself, two implicit-any
parameters, and chart.js tick callbacks that receive `string | number`
rather than `number`.

Also guards two conditions in libs/ui's dot-action-menu-button template.
`actions` is an optional @input used as `actions.length`, which had never
been null-checked: libs/ui has no build target, so its templates are only
verified when a consuming app compiles them, and turning on `strict` here is
what made ngtsc check them. Guarded with `actions?.length`, which is exactly
equivalent for both conditions.

Worth noting for the remaining apps: `tsc -p` does not check templates, so
per-project error counts taken that way miss this class entirely. Only a
build does.

app and spec configs at 0, lint clean, and dotcdn:build passes — this project
has a build target, so unlike most of the rollout its flags are enforced.
ui unchanged at 820 tests passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dot-tags (#35968), dot-locales (#35965) and dot-es-search (#35963). All
three were dominated by a single pattern each, and all three only became
this small after libs/ui was cleared — they reported 238, 243 and 246
errors before that, almost all of it ui leaking through imports.

Shared theme across the three: mocking signal stores in specs.

- dot-tags: 18 of 22 errors were `store.x as jest.Mock` casts. A Signal does
  not overlap a jest.Mock, so those have to go through `unknown`. The one
  real error was a `catch` binding, which is `unknown` under strict.
- dot-es-search: all 28 were `store.x = jest.fn()` assignments onto
  signal-typed members; each now states the target type it is standing in for.
- dot-locales: DialogService.open() is typed as possibly null in this PrimeNG
  version, so the two `DynamicDialogRef` annotations were wrong — dropped them
  and guarded the two usages. The store spec mocked `Observable<void>` methods
  with `of(null)`. The rest were nullable spectator queries.

Flags added where missing (locales already had all six).

All three: tsc clean on lib and spec, lint clean, tests green (20, 102, 83).
None is enforced — no build target, and :test does not type-check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nicobytes and others added 3 commits August 17, 2026 16:07
…tics lib

dot-categories (#35962) reaches 0 on both configs. Two upstream fixes here
also unblock the projects that follow.

dot-categories: 15 of 31 errors were the signal-store-mock pattern seen in
`{ onClose: Subject }` literal does not overlap a DynamicDialogRef, so both
have to route through `unknown`. The fixtures were missing required fields
in two directions: DotCMSAPIResponse needs errors/messages/permissions/
i18nMessagesMap alongside `entity` (now a shared API_ENVELOPE rather than
repeated nine times), and DotCategoryDeleteResult needs deletedCount.
openRowMenu takes a MouseEvent, not an Event.

dot-analytics: library config now clean.
- Its two barrels re-exported types with `export {}`, which is TS1205 under
  isolatedModules. Third instance of this shape after dot-ai.service.ts
  (#35948) and global-store (#35951); the error positions identify exactly
  which names are types, so the split is mechanical.
- Added @types/d3-scale, @types/d3-selection and @types/d3-shape. All three
  d3 packages are direct dependencies with no bundled types, so the imports
  were implicitly any. These are maintained DefinitelyTyped packages, so
  installing them beats declaring the modules by hand.

Still open: dot-analytics has 16 spec errors and content-drive-ui has 3 lib
plus 13 spec, all needing per-site reading rather than a pattern pass.

Tests green: 109, 333 and 245. Lint clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The rebase took the branch's lockfile to resolve the conflict; this puts
@types/d3-scale, @types/d3-selection and @types/d3-shape back into it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nicobytes nicobytes changed the title refactor(core-web): TS strict mode across utils-testing, utils, dotcms-js, sdk-create-app, sdk-react, sdk-analytics, sdk-angular, data-access + webcomponents groundwork refactor(core-web): TS strict mode across 21 projects — utils, dotcms-js, sdk-*, data-access, ui, portlets, dotcdn Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback 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