refactor(core-web): TS strict mode across all 44 projects — completes epic #35932 - #36957
refactor(core-web): TS strict mode across all 44 projects — completes epic #35932#36957nicobytes wants to merge 138 commits into
Conversation
`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 finished @nicobytes's task in 1m 58s —— View job Claude finished @nicobytes's task in 1m 48s —— View job SDK Compatibility Check
Result: no SDK-breaking change detected. This PR is the TS-strict-mode rollout — every touched file in
Per the task instructions, no comment or label is added since no breaking change was found. |
There was a problem hiding this comment.
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-appvia its projecttsconfig.json. - Updated
fetchWithRetryto throw when misconfigured with< 1attempts to avoid an implicitundefinedreturn 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. |
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>
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>
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>
…67 (#35933) `FieldPropertyService` is 12 methods over two object literals, and 21 of its errors were the same thing: both tables are keyed by strings the server supplies — a property name from the field-types endpoint, a field's `clazz` — so the lookups are index accesses, not property reads. `PROPERTY_INFO` and `DATA_TYPE_PROPERTY_INFO` now say so, each with a named row type. Typing the tables meant the service could no longer hide what it returns, and five of its signatures were wrong: - `getValidations` declared `ValidationErrors[]` while holding `ValidatorFn[]`, which is what the one caller hands the form builder. - `getDataTypeValues` declared `string[]` while returning `{text, value}[]`. - `getComponent`, `getProperties` and `getFieldType` each declared a non-nullable return and each returned `null`/`undefined` on a miss. Their three callers now handle it: the dynamic-property directive no longer calls `createComponent` with `null`, nor reads `.helpText` off a field type the endpoint does not know. - `getOrder` and `isDisabledInEditMode` declared `number`/`boolean` and returned `null`/`undefined`. They now return `0`/`false`, which is what the callers already treated the absence as — `null - n` was producing `NaN` in the property sort. Clearing the service's `any`-shaped returns exposed 22 further errors in the ten field-property components it feeds, which are also fixed here. Two model corrections came out of that: - `DotDynamicFieldComponent.helpText` is optional. Only one of the ten components declares it, while the host directive sets it on all of them. - `DotRelationshipsPropertyValue.velocityVar` is optional. It is absent until a content type is picked, and `validateRelationship` already treats that as invalid — which is the whole mechanism keeping a half-filled relationship from being saved. Three assertions pinned the old return values (`getOrder` → null, `isDisabledInEditMode` → undefined, `contentType` → undefined); each is updated with the reason. Verified: tsconfig.app.json and tsconfig.spec.json at 0/0, the strict consumers of dotcms-models (ui, edit-content, data-access, utils-testing, global-store) still at 0, tests at 222 suites / 2189 passed / 32 skipped, lint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…35933) Two ComponentStores and the trees around them. **dot-pages.** Both `setPages` and `setFavoritePages` are declared `updater<Partial<X>>` while annotating their callback parameter as the full `X` — a contravariance violation that made every call with an object literal resolve against the `Observable<ValueType>` overload instead. Three model statements came out of the rest: - `DotPagesInfo.items` holds `(DotCMSContentlet | undefined)[]`. `getPages` pads the array with `undefined` on purpose — there is a comment saying so, because the endless scroll needs the length kept — and every reader already goes through `page?.`. - `actionMenuDomId` is `string | null`; `actionMenuDomId$` had a `filter((i) => i !== null)` that never narrowed, now a type predicate. - The init pipeline's two tuples are named (`DotPagesSourceData`, `DotPagesInitData`). An array literal infers as a union array, so the `map` that built one and the callback that destructured it never agreed. The error branch of `setInitialStateData` set `languages: null`, `id: null` and four `null` permission flags against non-nullable declarations; it now says `[]`, `''` and `false`, which is what "nothing loaded" means and what the readers already treated those as. `getSessionStorageFilterParams` ran `JSON.parse` on `sessionStorage.getItem(...)` without checking for the first run, when there is nothing stored. **dot-templates.** `DotTemplatesService` swallows a failed request into `of(null)` — the same shape as the containers service — so `catchError` never fires and the `null` reaches the `tap`. Five effects in the store and five bulk handlers in the list read `.drawed` / `.identifier` / `.fails` off it. A failed save, publish, create, delete, archive or copy threw instead of reporting. `DotTemplateItem.type` was optional on both variants, so `template.type === 'design'` narrowed the design branch and left the other one as the whole union — which is why the advanced branch could not read `body`. It is required now; every producer sets it, and only the create payload strips it. Two things `cleanTemplateItem` was doing, both preserved rather than changed here, both now stated in a comment: it deleted `type` and *then* tested `template.type === 'design'`, so the branch stripping a design template's `containers` was dead and those containers have always been sent; and it deleted the key off its argument, which is the object the store holds, so every later `type === 'design'` in the store was reading a key a save had removed. The new version copies first. Also real: `DotTempFileUploadService` maps a failed upload to the HTTP status *string*, and the template thumbnail field destructured that as a temp-file list — taking the string's first character and carrying on. Model corrections, no strict consumer regressed (ui, edit-content, data-access, utils-testing, global-store, template-builder, edit-content-bridge all still 0): `DotLayout.width` is `string | null` (the template builder already read it as `width ?? ''`), and `DotTemplateItemDesign.theme` likewise, since the code falls back to null when a template carries neither `theme` nor `themeId`. 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>
Took main's side on all ten conflicts: they are its new asset-picker and the move of the folder-list-view and dropzone into `libs/ui`. Where that dropped narrowing this branch had added, the strict compile in the follow-up commit is what puts it back — it cannot be missed, since the projects involved (edit-content, new-block-editor, content-drive, ui) are already strict here.
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>
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>
What
Thirty-three steps of the strict-mode rollout (epic #35932), plus groundwork on one more (
dotcms-webcomponents, not closed):edit-contentdotcms-binary-field-builderinclude: []), so measuring that file reported a fake zeroportlets-dot-query-tool-portletnoImplicitReturns-without-strictcombination caught aTS7030I introduced inedit-contentedit-ema-uitemplate-builderdotcms-block-editorTS2688). Unmasking them surfaced a template defect that only a build can seeblock-editorOmitthat silently erased every member of PrimeNG'sMenuItemdotcdnswitchdefault. First app to go strict — surfaced template errors inlibs/uiportlets-dot-usagehtmldiff-jstype leakportlets-dot-tags-portletcatchbindingportlets-dot-locales-portletDynamicDialogRefannotations,of(null)forObservable<void>portlets-dot-es-search-portletjest.fn()assigned onto signal-typed membersportlets-dot-categories-portletedit-content-bridgeportlets-dot-analytics-data-accessglobal-store's barrelportlets-dot-experiments-data-accessglobal-storeexport typethat also closed #35954portlets-dot-locales-data-accesssdk-experimentsportlets-dot-plugins-portletmoduleResolution: node10broke 256 imports. Also corrects the portlets guide that recommended itportlets-dot-analytics@types/d3-*addedcontent-drive-uinew-block-editorEditorViewsourced from@tiptap/pm/view;@types/turndownaddeduidata-accessbuildtarget) — fixes the 36 lib + 47 spec errors hiding behind themsdk-angularnext/tsconfig refs that madetsconfig.spec.jsonunverifiable (TS6053)sdk-analyticsTS4111in source and 29 in specs (14 pre-existing)sdk-reactTS4111index-signature accessesutils-testingtypes: ["jasmine"]aborted all type checkingutilsstrict+ fix the 32 (+17 spec) resulting type errorsdotcms-jsstrict+ fix the 38 resulting type errorssdk-create-appstrict+ fix the 2 resulting type errorssdk-typesAll three add the standard six flags to the project's own
tsconfig.json, following the pattern established in #36879 (dotcms-models).tsconfig.base.jsonstays 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-uiadmin 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:
Auth.loginAsUserUsernullwhen nobody is impersonating, and every consumer already guards withauth.loginAsUser || auth.user. NowUser | null.StringUtils.getLinestringstring | null.HttpRequestUtils.getQueryStringParamstringRoutingService.getPortletURLstringMap.get(). Nowstring | undefined.SiteService.switchSiteByIdObservable<Site>of(null)when no site is found. NowObservable<Site | null>; its one consumer already handled null.ResponseView.bodyJsonObjectDotCMSResponse<T>HttpResponse.body, which is nullable. The surroundingtry/catchcould never throw and has been removed.LoginService.urlsmoved fromRecord<string, string>to inference-typed, which resolves all 8TS4111errors at once and gives each endpoint a named property.Two definite-assignment assertions were used, each with a
TODO:LoginService._authandSiteService.selectedSiteare assigned during init but not in the constructor. Modelling them as| undefinedis 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-erroranywhere 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-uitypechecks clean apart from a pre-existing missingdotcms-webcomponents/loaderdist.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_FIELDassignednullto 18 members thatDotCMSContentTypeFielddeclares non-nullable:nullstrictly — consumers use falsy checks such asisNewField's!field.id— so'',0andfalsebehave identically at runtime.clazzhas no zero value (DotCMSClazzis a union of concrete Java class names), soEMPTY_FIELDandEMPTY_SYSTEM_FIELDare nowOmit<DotCMSContentTypeField, 'clazz'>. They are partial templates, not valid fields, and the type now says so. The derivedCOLUMN_FIELD/ROW_FIELD/TAB_FIELDalready supply their ownclazz, so they remain complete.Other fixes:
getFieldsWithoutLayout.filter()did not narrow the optionalrow.columns. A type predicate clears theTS2532and bothTS2769without a cast.ellipsizeTextnull/undefinedat runtime — its own guard and its tests say so — but declaredstring/number. Widened to match, with an explicitlimit == nullcheck so later comparisons narrow.fallbackErrorMessages{ [key: number]: string }, mirroring the identical declaration already inlibs/data-access/.../dot-upload.service.ts.dot-utils.tsDotCMSContentletindex-signature reads ingetImageAssetUrl.dot-asset.service.tspromisesand the twofetchAssetparams.The nine
as unknown ascasts added are all in spec files, on inputs the tests deliberately pass as invalid, matching the idiom those files already used.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 — theOmitdid not break itsEMPTY_SYSTEM_FIELDspread.sdk-create-app(#35938)Two errors, both from flags beyond plain
strict:src/index.ts:393—process.env.DEBUGneeds bracket access undernoPropertyAccessFromIndexSignature(TS4111). It is the onlyprocess.env.*dot access in the project.src/utils/index.ts:41—fetchWithRetrytrippednoImplicitReturns(TS7030). The loop returns on success and throws on the last attempt, but withretries < 1the loop never runs and the function fell through returningundefined. Its only caller (isDotcmsRunning,src/index.ts:506) already guarded withif (res && …), so nothing broke in practice — but the signature was lying. Throwing after the loop closes the gap and narrows the return type toPromise<AxiosResponse>.No build or CI wiring was needed here. The
@nx/esbuild:esbuildexecutor type-checks before bundling (skipTypeCheckdefaults tofalseand is not overridden), and CI already builds this project vianx run-many -t build(build-testincore-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.sdk-types(#35935)libs/sdk/types/tsconfig.jsonhas carriedstrict: trueplus the four extra safety flags since the library was created (#31967), andtsc --noEmitpasses with zero errors. It is also already enforced:tsconfig.lib.jsonsets"declaration": true, so@rollup/plugin-typescriptsits 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:## TypeScript Strict Modesection 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 separatetypechecktarget)."strict": truein project tsconfigs. It sat under the Jest config guidance but read as a blanket ban, contradicteddocs/frontend/TYPESCRIPT_STANDARDS.md, and blocked the epic outright. The restriction now points attsconfig.spec.json, which is what it meant.utils-testing(#35944)The six strict flags were already in
tsconfig.json— but completely inert.tsconfig.lib.jsondeclared"types": ["jasmine"], that package is not installed, sotscemittedTS2688: 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/jestis installed. Switching to"types": ["jest"]removed the abort and 27 spuriousCannot find name 'jest'errors, leaving 5 real ones:clean-up-dialog.tsfixtureparam → typed structurally as{ nativeElement: unknown }, since only that property is touched (no need to pull in Angular'sComponentFixture)dot-page-state.service.mock.ts_lock: boolean = null→boolean | nulldot-page-tools.mock.ts×3tagsarray thatDotPageTooldoes not declare. Verified nothing in the repo reads.tagsoff a page tool, so the dead field was removed rather than added to the model indotcms-modelstsc -p libs/utils-testing/tsconfig.lib.json --noEmitnow exits 0 with no CLI overrides — the check is real rather than short-circuited.Verified across consumers of the touched mocks (
cleanUpDialogin 7 files, page-tools mock in 3):data-access751 tests passed,edit-ema-ui338 passed.dotcms-webcomponents(#35943) — groundwork only, not closedStrict 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:@Event!EventEmitter@Element!@State!@Prop?Using
!on@Propmade Stencil emit 28 props as required incomponents.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
tsc250 / 38. Size this work withtsc, not with build output.--skip-nx-cachedoes not clear Stencil's cache. Builds can report green against stale.stenciloutput. This bit me:0117273504annotated a prop, passed a "clean" build, and was actually broken — reverted inf22afce383after verifying twice with.stenciland the Nx cache cleared.That prop (
dot-binary-text-field'svalue) is genuinely contradictory:handleFilePasteassigns aFile, 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 aTODO(#35943)so it is not re-annotated in isolation.sdk-react(#35945)strict: truewas already present; the five companion flags were not. Adding them surfaced 14 errors, allTS4111— dot access on a type carrying an index signature — resolved with bracket notation. Two origins, same fix:node.attrs, declaredRecord<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.styles.rowinRow.tsx), whose generated type is also aRecord<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 noskip:tags, so CI builds, lints and tests it on every PR, and the same build runs in the SDK release pipeline.sdk-analytics(#35946)Same starting shape as
sdk-react:strict: truealready 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
TS4111fromnoPropertyAccessFromIndexSignature— dot access onHTMLElement.dataset(DOMStringMap) and on aRecord<string, unknown>of payload properties. Bracket notation throughout, reads and writes alike. Spread acrossdot-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
datasetfix, 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 inferredtypechecktarget runs onlytsconfig.lib.json, andjest.config.tstransforms viababel-jest, which strips types without checking them.ANALYTICS_CONTENTLET_CLASSno longer exported — renamed toCONTENTLET_CLASSdeviceinsidedataand omitted requiredlocale_idjest.fn()inferringneverformockResolvedValue/mockRejectedValueLocationmock missinghostjest.spyOn(...).mockImplementation()called with no argumentmockInitializeinferred as zero-argresult.custom— not onEnrichedTrackPayloadTS2589excessively deep instantiationThe pageview fixture was the instructive one: with
devicemisplaced andlocale_idmissing, thepageviewmember of theDotCMSEventunion stopped matching, so TypeScript fell through to the impression member and reported a misleading "doc_encodingdoes not exist onDotCMSContentImpressionPageData". One coherent fix cleared four errors. Fixtures were corrected rather than production types widened; no source bug hid behind any of them.So
sdk-analyticsjoinsdotcms-jsandutilsas strict but unenforced. Wiringnx affected -t typecheckintocore-web/pom.xmlwas 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(assdk-reactproved). It does not hold for Vite-built ones:sdk-analyticsinheritedstrictfrom the shared tsconfig lineage but neither the other five flags nor a type-checking build.0 internal dependents — the only references to
@dotcms/analyticsoutside the lib are doc comments inlibs/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,strictInjectionParametersandstrictInputAccessModifiers. Re-adding them would have been a cosmetic diff. The real defect was dead config.Both
tsconfig.lib.jsonandtsconfig.spec.jsonreferenced anext/directory that existed and was removed — the references landed on 2025-03-21 (09e879b2ac) and outlived the directory. One of them was fatal:tscaborts on that before semantic checking, sotsconfig.spec.jsonhad never completed a single semantic pass and any error count taken from it was meaningless. The asymmetry is the lesson: a non-matchingincludeglob is harmless, a missingfilesentry is fatal — which is whytsconfig.lib.json, whosenext/references were only ininclude/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-versionfromsdk-clientdocumented in thesdk-reactsection above.The spec config coming out clean was predicted, not lucky:
jest-preset-angular@17→ts-jest@29.4.6with diagnostics enabled and transpile-only unset already type-checked all 21 spec files against these exactcompilerOptions— just per-file, never as a whole program. That is the opposite ofsdk-analyticsbelow, wherebabel-jeststripped types and hid 14 errors. Same rollout, two projects, and the test transformer decided whether anything was checked at all.Production source is clean without escape hatches: 0
@ts-ignore/@ts-expect-error, 0 non-null assertions, and 2anys 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.mdnow documents theTS6053masking variant next to the existingTS2688one. Two of the fourteen projects triaged so far were masked this way — #35944 viaTS2688, #35947 viaTS6053— 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.jsonfor some time, and they were completely inert. The project has nobuildtarget, 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 thedotcms-jsandutilssections above.tsconfig.lib.jsontsconfig.spec.jsonProduction source (36)
paginator.service.ts(14) — 8 uninitialised fields given zero values; four header reads take?? ''(identicalNaNoutcome);private setLinks(linksString: string)widened tostring | nullsince its body already didlinksString?.split(',') || []; the file-localinterface Linksgained an index signature because theLink-header parser stores whateverrelthe server sends.dot-page-state.service.ts(12) — six declarations widened because they were simply wrong; the service really does emitnull. The interesting one ishandleSetPageStateFailed, declaredObservable<DotHttpErrorHandled>but ending inmap(() => undefined). Because it genuinely emitsundefined, the caller's= [null, null]destructuring default is reachable and load-bearing, not dead code. Declaring the honest type made the wholeswitchMaptypable; it now destructures explicitly instead of fighting an annotation.if (page)becameif (page && user)— whichforkJoinalready guaranteed.dot-router(5),dot-localstorage(3),dot-content-types-info(2) — nullable getters (previousUrl,storedRedirectUrl),localStoragereads, and a string index narrowed tokeyof.Specs (47)
25 came from three lines. The fake
Routerdeclarednavigate = jest.fn(() => ...), which infers zero parameters, so everytoHaveBeenCalledWith(...)was aTS2554.Two of the rest were real bugs hiding behind disabled suites:
dot-global-message.service.spec.tsimportedDotMessageServicefromdot-alert-confirm.service, which does not export it. The suite isxdescribed, so it never ran.dot-ai.service.ts— a production file — didexport { DotAiProviderConfig }on a type, invalid underisolatedModules. Only the spec config sets that flag, so only it surfaced the error.Also:
dot-page-layout.service.spec.tswas callingsave(id, mockDotLayout()), butsavetakes aDotTemplateDesignerand posts it verbatim — the spec was testing a payload shape production never sends (edit-ema-layout.component.ts:111sends the real one). And thedot-content-drivefixture used anoffsetfield removed fromDotContentDriveSearchRequest, copied from the model's own stale JSDoc example, which is fixed here too.dot-personasnow reuses the existingmockDotPersonafrom@dotcms/utils-testinginstead of hand-rolling 21 fields.No new
any, no@ts-ignore/@ts-expect-erroranywhere 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:
global-storeportlets-dot-analytics-data-accessportlets-dot-experiments-data-accessimage-editorportlets-dot-analyticsportlets-dot-locales-portletutils-testingThis is the "high leverage" the issue predicted, quantified.
A repo-wide finding:
testnever type-checks specsdata-accessusesjest-preset-angular→ts-jest@29.4.6againsttsconfig.spec.json, which looks like it type-checks. It does not, because that tsconfig setsisolatedModules: true:ts-jest/.../config/config-set.js:229reads TypeScript'sisolatedModulesinto ts-jest's own flag.ts-jest/.../compiler/ts-compiler.js:74builds the language-service host onlyif (!isolatedModules)._doTypeChecking()needs that host forgetSemanticDiagnostics.data-accessis the proof: 84tscerrors alongside 754 passing tests. Sincecore-web/CLAUDE.mdmandatesisolatedModules: truein everytsconfig.spec.json, no project'stesttarget 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 thesdk-angularsection above (that verdict was separately confirmed withtsc -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/uifirst, because it was the bottleneck (#35953 — still open)uihad 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:uiportlets-dot-usageportlets-dot-tags-portletportlets-dot-locales-portletportlets-dot-es-search-portletportlets-dot-categories-portletportlets-dot-analyticscontent-drive-uiui'stsconfig.lib.jsonis at 0 (from 122) and itstsconfig.spec.jsonat 90 (from 427), so #35953 stays open. Notable findings there:dot-icon'ssizebecamesize?: numberrather than= 0, because the template binds[style.font-size.px]="size"and0would have rendered invisible icons whereundefinedinherits.dot-sidebar,dot-dropdown,dot-site-selector,dot-container-optionsanddot-trim-inputall inject their host with{ optional: true }and then used it unguarded. They now guard.formElwas declaredHTMLFormElementwhile the template says#formEl="ngForm";getVariableIndexChangeddeclarednumberwhile its own JSDoc documentednumber | null.dot-add-to-bundleinvokedgetDefaultBundletwice for the same value.tsconfig.lib.jsonexcluded a non-existentsrc/test.tsbut nottest-setup.ts,**/*.test.tsor__mocks__/, so test files were being compiled into the library program — 15 errors by itself.selectlocal came from a single line.Already compliant, verified rather than assumed
#35950
portlets-dot-locales-data-accessand #35952portlets-dot-experiments-data-accessneeded no change: all six flags present, both configs at 0, and neither masked by aTS6053/TS2688config error. Both reported 36 errors before #35948 — purelydata-accessleaking.#35949
sdk-experimentswas 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-storere-exportedWebSocketStatus— a type — withexport {}, which isTS1205underisolatedModules. 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 #35954portlets-dot-analytics-data-access. Oneexport typeclosed both issues.Third and fourth instances of this shape followed in
dot-analytics's two barrels. A barrel can carry anisolatedModuleserror that only its consumers ever see — worth a repo-wide sweep, raised on #35932.An ambient declaration in the wrong place
The
htmldiff-jsdeclaration added foruilived underlibs/ui/src, so it was only inui's own program and every consumer still reportedTS7016. It is now registered throughtsconfig.base.jsonfrom a root-leveltypes/folder — insidelibs/uimade@nx/enforce-module-boundariesdemand a relative import. Same shape as the knownvirtual:sdk-versionleak fromlibs/sdk/client.Signal stores are the dominant spec pattern
#35968
dot-tags(18 of 22), #35963dot-es-search(all 28) and #35962dot-categories(15 of 31) were all the same theme: aSignal<T>does not structurally overlap ajest.Mock, so casts must route throughunknown, and assignments ofjest.fn()onto signal-typed members must state the type they stand in for.dot-categoriesalso had fixtures incomplete in two directions —DotCMSAPIResponseneeds four fields besideentity(now a sharedAPI_ENVELOPErather than repeated nine times) andDotCategoryDeleteResultneedsdeletedCount— plus four calls passingEventwhereopenRowMenutakes aMouseEvent.Wrong annotations, not loose ones
#35965
dot-locales: both dialog refs were annotatedDynamicDialogRef, butDialogService.open()is typed as possibly null in this PrimeNG version. Its store spec mockedObservable<void>methods withof(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,reconcileOnFormEventis assigned inside a nested callback, which control-flow analysis cannot see, so TypeScript narrowed it back tonulland called it not callable.#35969
portlets-dot-usage: one fixture missingUsageSummary.lastUpdated.The first app, and what it revealed about templates
#35970
dotcdnhad 19 errors, and 8 shared one cause:dispatchLoading'sswitchhad nodefault, so undernoImplicitReturnsthe updater's return type includedundefined, it stopped resolving as a one-argument updater, and all six call sites reportedTS2554: Expected 0 arguments.default: return statefixed all eight.More importantly, its build failed on
libs/ui's templates, not ondotcdn:libs/uihas nobuildtarget, so its templates had never been null-checked — they are only verified when a consuming app compiles them.dotcdnandedit-content-bridgeboth havebuildtargets, so their flags are genuinely enforced. The rest of this batch is not — nobuildtarget, and:testdoes not type-check.Dependencies
Added
@types/d3-scale,@types/d3-selectionand@types/d3-shape. All three d3 packages are direct dependencies with no bundled types, so those imports were implicitlyany. Maintained DefinitelyTyped packages, so installing beats hand-declaring the modules.Batch three:
libs/uifinished, plus four more (#35953 #35956 #35959 #35961 #35966)libs/ui(#35953) — 549 → 0, and it unblocked most of what followedBoth configs are now clean:
tsconfig.lib.json122 → 0,tsconfig.spec.json427 → 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-portlethad 1 error of its own, not 238.Highlights beyond the mechanical work:
DotLocaleTagPipeguards withif (!languageId || !languagesMap),DotRelativeDatePipewithconst time = date || Date.now(), andonAssignChange/onCommentChangewith?? ''— all three declared non-nullable parameters, making those guards unreachable and the specs that assert the null behaviour uncompilable.DotLanguageVariableEntrydeclared every language's value as always present; the API omits languages without a variable and the component already reads them with?.value.dot-browsing.service.specimportedSiteEntity, which no longer exists —dot-site.model.tssays to useDotSite, andcreateFakeSitealready returns it.dot-add-to-bundleinvokedgetDefaultBundletwice for the same value; five directives injected their host with{ optional: true }and used it unguarded;formElwas declaredHTMLFormElementwhile the template says#formEl="ngForm".tsconfig.lib.jsonexcluded a non-existentsrc/test.tsbut nottest-setup.ts,**/*.test.tsor__mocks__/— 15 errors by itself.Two techniques worth reusing: fix at the point of declaration (53 usages of one
selectlocal came from a single line; 95 declaration-level assertions cleared ~300 spec errors), and read each TS2564 site rather than applying the policy blindly —dot-icon'ssizebecamesize?: numberinstead of= 0, because the template binds[style.font-size.px]="size"and0renders invisible icons whereundefinedinherits.dot-plugins(#35966) — reported 733, had 63tsconfig.spec.jsonusedmodule: "commonjs"+moduleResolution: "node10", andtsconfig.jsonwas missingmoduleResolution: "bundler".node10cannot resolve the@dotcms/*subpath exports, so 256 imports failed and everything downstream collapsed tounknown(227TS2571, 115TS18046, 85TS2339). Aligning both withdot-tagstook it from 733 to 1.libs/portlets/CLAUDE.mdwas telling people to configure it that way. Its anti-patterns table said to omitstrict: true("causes issues with Angular compiler") and to use"module": "commonjs"intsconfig.spec.json— whiledot-tags, which the same guide calls the canonical reference, carries bothstrict: trueandmodule: "preserve"and compiles clean. Corrected, so the next portlet does not repeat it.dot-analytics(#35961) andcontent-drive-ui(#35959)Spectator's typed
propsdisagrees with Angular for aliased signal inputs. Components declare$tableState = input.required({ alias: 'tableState' }). Spectator'sInferInputSignalskeys off the field name; Angular'ssetInputrequires the alias. The specs passed the alias under anas unknowncast that made the props bagunknown, so removing it surfacedTS2561with 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'sonDropread the nullable$activeDropNode()and emitted it astargetFolder, 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-shapeadded — direct dependencies with no bundled types.new-block-editor(#35956)38 of 60 were
TS4111on TipTap node attrs, converted from the exact positionstscreports.EditorViewis annotated from@tiptap/pm/view, not top-levelprosemirror-view— the file's own comment explains that TipTap 3.x nests its own copy and mixing the two yieldsTS2322; the comment now names the correct source instead of saying the import is avoided entirely. Three plugin state fields hadinit: () => null, pinning the state type tonull.@types/turndownadded.On my own mistakes in this batch
Three self-inflicted breakages, all from over-broad regexes, all caught by running the suites:
contentlet?.asset→contentlet?['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 omittedAction.name, which is exactly the case its test asserts on.tscwas 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 implicitlyany; 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/uihad already removed ~300 of them without this project being touched. No inherited errors at all, so all 442 were local.block-editorhas nobuildtarget, sotsc -pontsconfig.lib.jsonandtsconfig.spec.jsonis 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 ofMenuItem. PrimeNG'sMenuItemcarries a[key: string]: anyindex signature, sokeyof MenuItemisstring | number, andExclude<string | number, 'icon'>removes nothing —Omitcollapsed the type to its index signatures alone.id,label,command,disabled: all silentlyany. I probed it rather than assuming:MenuItemalready declaresicon?: string, so the omission bought nothing. Extending it directly cleared 12TS4111and gave every consumer real types back.ImageNodereferenced itself.addCommandsusedImageNode.nameinsideImageNode's own initializer, so TypeScript typed the whole extensionany(TS7022).this.nameis the same value and breaks the cycle — which then exposed a realMapinference problem inDotBlockEditorComponent._customNodesthat theanyhad been hiding.loadCustomBlockshad the wrong element type. DeclaredPromiseSettledResult<AnyExtension>[], butimport(url)yields a module namespace — the element type isRecord<string, AnyExtension>.editor.storage.dotConfigwas optional althoughgetEditorExtensions()registersDotConfigExtensionunconditionally, first in the list. Existing readers were split between!and?.. Declared required, anddot-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.elementandAIImagePromptView.tippyOptions. Giving them an initializer to satisfyTS2564would have preserved dead code.Seven errors were already present without any strict flags, two of them broken references:
asset-form.component.spec.tstestedImageTabviewFormComponent, a component that does not exist onmaineither. Deleted.dot-upload-asset.component.spec.tsimportedDotUploadFileServicefrom block-editor'ssharedbarrel instead of@dotcms/data-access, so it provided a different token than the component injects.Flagged, not changed
FloatingActionsView.updatecallsthis.render().onExit(null), butActionsMenu'sonExitdestructureseditorfrom its argument. Preserved exactly, behind a cast and aFIXME(#35955)— changing runtime behaviour does not belong in a type-only pass.placeholder.pluginkeepstr.getMeta(this). It looks wrong, but ProseMirror binds a state field'sapply()to thePlugininstance, which is what makes it match thetr.setMeta(PlaceholderPlugin, …)calls. Annotated thethisparameter to record that.The test suite was already red — and stayed exactly as red
block-editor:testfails 16 suites / 37 tests onmain.libs/block-editoris byte-identical betweenmainand 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:
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-editorat82dbf4c9adand re-running each dependent'stsc -p, so the delta isolates this change from the rest of the branch.dotcms-block-editordotcms-uiedit-ema-uiedit-contentportlets-edit-ema-portletZero new errors; three cleared outright.
dotcms-block-editor(#35973) — and the template gateblock-editornever hadThree of this app's tsconfigs —
spec,editorand the sharedtsconfig.jsonthey extend — declared"types": ["jasmine", "node"].@types/jasmineis not installed, nor arekarma-jasmineorjasmine-core, so each aborted withTS2688before semantic checking. None had ever type-checked anything. The@angular/build:karmatest target cannot run for the same reason, and the app has no.spec.tsfiles at all.With that removed and the flags added,
tsc -pwas clean on all three — and the build failed:SuggestionsListItemComponent.datawas declared= null, which understrictinfers the typenull, sodata?.contentletnarrowed tonever. Typed properly now.The wider point:
libs/block-editorhas nobuildtarget, so its templates had never been type-checked by anything.tsc -pdoes 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.tsturned out to declare:Module augmentations are global to the program. This one gave TipTap's
Commandsa string index signature for every file compiled alongside it — which is howeditor.chain().focus(), a real declared command, became "Property 'focus' comes from an index signature".edit-content's program pulls in 249 files fromblock-editorplus all ofnew-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-contentreports 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) andTS5107(moduleResolution: node10) are deprecation errors under TypeScript 6 — and, likeTS2688andTS6053, they abort before semantic checking.libs/dotcms-webcomponentsreports 2 errors without--ignoreDeprecations 6.0and 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, andcore-web/CLAUDE.mdrecords the general rule: anyTS5xxx/TS6xxx/TS2688error is a configuration error, and the count after it means nothing.Batch five:
edit-ema-ui(#35971) andtemplate-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-builderreported 27 inherited errors, allCannot find name 'jest'inlibs/utils-testing. utils-testing was not the problem:tsconfig.lib.jsonexcludes*.spec.tsbut notsrc/**/utils/mocks.ts, so a fixtures file importing@dotcms/utils-testingwas in the lib program undertypes: []. Only specs import it and it is not in the public barrel, so it is now excluded from the lib build.edit-contentreports the same 27 from the same cause.sidebaris null when empty, and the model never said soDotLayout.sidebarandDotTemplateLayoutProperties.sidebarwere both declared non-nullable whileTemplateBuilderComponentdeliberately clears them: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
publishContentletAndWaitForIndextakes{ [key: string]: string | number }, anddot-favorite-page.storesendsinode: formData.inode || null. My first fix was?? ''. The spec caught it, because it asserts the payload containsinode: null— the endpoint distinguishes null from an empty string. The signature was wrong, not the call; widened it indata-access.Same shape in
DotContentCompareStore, which pipedhttpErrorManagerService.handle(err)out ofcatchErrorinto aswitchMaptyped for contentlets. A handled error is not a contentlet array; both blocks now complete withEMPTY.Two of my own mistakes, both caught by tests
A wrong zero value. The bulk TS2564 pass turned
@Input() showDiff: booleaninto= false. ThedotDiffpipe defaults totrueand the store seedsshowDiff: true, sofalsesilently disabled diffing and broke 4 specs. "Boolean means false" does not hold when the consumer's default is not the zero value.Spectator
propskeying. Renaming the key from the aliascontentletto the declared member$contentlettype-checks and then fails at runtime in 11 specs, because Spectator applies the alias whileInferInputSignalstypespropsby 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-contentand 9 intemplate-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 guardstemplate-builder: converted — twoDialogService.open()results, threesubGridOpts.childrenreads, onechild.containersfilter, and aresizestarthandler that asserted a four-link GridStack chain optional at every stepedit-ema-ui: five indot-favorite-pagereplaced withform.controls['x'], which the form always buildsBatch six:
edit-content(#35974) and the two it was blocking (#35976, #35967)edit-contentis the largest project closed so far afterui: 101 production errors and 208spec 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.jsoncarried explicit"strict": falseand"noPropertyAccessFromIndexSignature": falseopt-outs, which were hiding 666 of the 944 errorsthe specs really had.
The dominant pattern was not missing guards — it was declarations lagging behind code already
written for null. Six
signalMethodhandlers plus five utilities andBaseWrapperField.formControlall opened with a
!xguard while declaring the argument non-nullable.RelationshipFieldStore.initializehad three comments saying "contentlet is null in manualtranslation" over a type that said otherwise.
InputSignal<T>is not covariant. WideningBaseWrapperFieldtoInputSignal<T | null>tookthe 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
undefinedthrough anoutput<string>(); and the category fieldwrote
undefinedinto the form value for selected categories with no inode.Four fixtures could never have matched the code:
canLockmocked with the raw HTTP envelope when theservice maps
response.entity;getByInodemocked with the scheme-grouped shape;isDialogModemocked in three specs after being deleted from the store; and
MultiSelect.valuesAsString, which doesnot 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 isundefined", and the adjacent tests assert the opposite for the history handlers);
formValues: {}where a spec asserts
null; and acontentTypeguard that aborted the locales flow because thefixture 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, contentletlockedBy/lockedByName, content-type fielddefaultValue/values/rendered, categoriesdescription). Every reader already tolerated it andthe model was the outlier —
WorkflowTask.statuswas the only non-nullable member alongsidebelongsTo,descriptionanddueDate, all three already annotated. Blast radius re-measuredacross all 12 strict projects after every one: 0 errors.
Two things deliberately not done.
DotCMSContentlet.titleis genuinely nullable — two tests arenamed "without title" — but widening it lights up 10 errors across
block-editorandedit-ema-ui,both already closed, so it needs its own issue. And
onWorkflowActionFireddid not get aninodeguard: it carries a comment warning that one silently blocks saving new content.
Also recorded on #35974:
libs/edit-contenthas nobuildtarget, so its templates have neverbeen type-checked and its
strictTemplatesis inert — the same gap aslibs/block-editor.The two projects it was blocking land here too.
dotcms-binary-field-builderneeded no sourcechanges.
portlets-dot-query-tool-portletneeded three signal-mock casts, and its combination ofnoImplicitReturnswithoutstrictcaught aTS7030I had introduced inedit-content— a guardthat returned bare where the other path returns an Observable teardown.
Test plan
edit-ema-ui(#35971) andtemplate-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 unchangedtemplate-builder's 27 inherited errors traced tomocks.tsin the lib build, not to utils-testingDotLayout.sidebarand thedata-accesspayload signature:dotcms-models,data-access,ui,block-editor,edit-ema-ui,utils-testing,utils,global-storeall still 0dotcms-block-editor(#35973)tsc -p— 0 ontsconfig.app.json,tsconfig.spec.jsonandtsconfig.editor.json, all three of which previously aborted onTS2688nx run dotcms-block-editor:build— clean (it failed first, on a template defecttsc -pcannot see)libs/block-editorunaffected: 0/0, tests unchanged at 16 suites / 37 by namelibs/new-block-editorunaffected: 0edit-content:teststill green at 112 suites / 2218 tests after removing the poisoning augmentationblock-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 bothnx run block-editor:test— unchanged at 16 suites / 37 tests failing, verified by failing-test name: identical 38 entries before and afternx run block-editor:lint— unchanged at 11 errors, all in files this branch does not touch (git diff origin/mainconfirms)82dbf4c9ad: zero new errors, three go to 0buildtarget on this project, sotsc -pis the gate — stated on the issue rather than implieddotcms-jspnpm exec tsc -p libs/dotcms-js/tsconfig.lib.json --noEmit— 0 errors (from 38)data-access,global-store,portlets-dot-analytics,portlets-dot-analytics-data-access,portlets-dot-locales-portlet,utils-testingdata-accesstypecheck: 106 → 68 errors, zero newdotcms-uitypecheck clean (one pre-existing unrelated error)dotcms-jslint went from 42 to 41 problems (still tag-excluded)sdk-create-apptsc --noEmitclean on lib and specnx run sdk-create-app:build / :lint / :testgreennode dist/libs/sdk/create-app/index.js --helpworksDEBUGfix makesnx run sdk-create-app:buildfail with TS4111 — confirming the build gate is realsdk-analyticstsc --noEmitclean ontsconfig.lib.json(from 18) andtsconfig.spec.json(from 29)nx run sdk-analytics:typecheck / :lint / :build / :build:standalonegreennx 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 editsnx run sdk-analytics:build— this project's build is not a gatesdk-angulartsc -p tsconfig.spec.json --noEmitnow completes a semantic pass at all (previouslyTS6053), 0 own errorstsc -p tsconfig.lib.json --noEmit0 own errorsnx run sdk-angular:lintclean;:buildgreennx run sdk-angular:testunchanged at 21 suites / 234 tests — the guard that no file dropped out of the programnx run sdk-angular:build(TS2322, exit 1) — ngtsc gates this projectdata-accesstsc -p tsconfig.lib.json --noEmit36 → 0;tsc -p tsconfig.spec.json --noEmit84 → 0nx run data-access:lintclean;:testunchanged at 79 suites / 754 testsdotcms-ui820 tests andui2184 tests passnx affected -t buildgreen for all 6 affected projectsBatch two
tsc -pclean on lib and spec for [16/44] Enable TS strict mode in sdk-experiments #35949 [17/44] Enable TS strict mode in portlets-dot-locales-data-access #35950 [18/44] Enable TS strict mode in global-store #35951 [19/44] Enable TS strict mode in portlets-dot-experiments-data-access #35952 [21/44] Enable TS strict mode in portlets-dot-analytics-data-access #35954 [27/44] Enable TS strict mode in edit-content-bridge #35960 [29/44] Enable TS strict mode in portlets-dot-categories-portlet #35962 [30/44] Enable TS strict mode in portlets-dot-es-search-portlet #35963 [32/44] Enable TS strict mode in portlets-dot-locales-portlet #35965 [35/44] Enable TS strict mode in portlets-dot-tags-portlet #35968 [36/44] Enable TS strict mode in portlets-dot-usage #35969 [37/44] Enable TS strict mode in dotcdn #35970libs/uilibrary program 0; specs 90 and still open as [20/44] Enable TS strict mode in ui #35953dotcdn:buildandedit-content-bridge:buildpass — the two enforced projects in this batchsdk-experiments:build(Rollup TS2322)Batch three
libs/uiboth configs 0 (122 and 427 before); lint clean; 81 suites / 820 tests unchangeddot-plugins733 → 0;new-block-editor60 → 0;dot-analytics42 → 0;content-drive-ui59 → 0libs/uilanded, re-verified the ten already-closed projects plusdotcdnfor regressionsmoduleResolution;dot-pluginswas the only one misconfiguredBoth
pnpm exec nx format:check --base=origin/maingreenany/@ts-ignore/@ts-expect-error(verified by diff grep)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.jsondeclares"types": ["jasmine"], and that package is not installed.tsctherefore emitsTS2688: Cannot find type definition file for 'jasmine'and stops before semantic checking. Sotsc -p libs/utils-testing/tsconfig.lib.json --noEmitreports exactly one error no matter what the code does.The
utilssection originally reported "utils-testingunchanged 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 nodereveals 33 errors, including a genuineTS2741caused by retypingEMPTY_SYSTEM_FIELDtoOmit<DotCMSContentTypeField, 'clazz'>: the mock atdot-content-types.mock.ts:71spreads it and never suppliesclazz.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 —
clazzwentnull(pre-PR) → absent (this PR) →TEXT:FieldUtil.isRow/isColumn/isTabDividercompare for equality and returnfalsefor all three values.!field.clazzorfield.clazz === nullanywhere in the repo.default-value-property7/7;dot-content-types-edit545 passed across 48 suites;data-access751 passed across 79 suites.The
data-accessfigures reported elsewhere in this PR (106 → 68 fordotcms-js, 68 → 36 forutils) are not affected — that project has no unresolvedtypesentry, so those runs were doing real semantic checking.core-web/CLAUDE.mdnow 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", butretriesis the total attempt count (for (i = 0; i < retries; i++)), soretries = 1is one attempt and zero retries. Reworded to "attempt".CLAUDE.mdverify snippet — hard-codedlibs/<project>/tsconfig.lib.json, which resolves for neither nested projects (libs/sdk/create-app, which has notsconfig.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:
dotcms) and [04/44] Enable TS strict mode in dot-layout-grid #35937 (dot-layout-grid) — closed as not applicable. Both are dead libraries with zero consumers that do not compile today; removal is tracked in Remove dead core-web libraries (libs/dotcms, libs/dot-layout-grid) #36950.typescript-strict-plugin/tsc-strictapproach that was dropped — the bootstrap [00] Setup typescript-strict-plugin baseline + CI gate #35933 closed without the plugin ever landing. Sub-issues still referencingnpx tsc-strictor// @ts-strict-ignorecarry stale acceptance criteria; [06/44] Enable TS strict mode in dotcms-js #35939's were corrected on the issue.Batch seven: the last five projects (#35943 #35957 #35964 #35972 #35975 #35977)
This batch finishes the epic.
dotcms-uiwas the largest single project in it — 1005 errors across app and spec.dotcms-webcomponents(#35943) — now closedThe 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.textContentas an asymmetric accessor —get(): string,set(value: string | null)— soelement.textContent.replace(...)is clean under 6 andObject is possibly 'null'under 5.8. The project reached 0 ontsc -pand 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 deadincludeand a missingit-specexclude.content-drive: 270 errors.edit-ema/portlet: needed its test-onlymocks.tsexcluded from the lib config before the count meant anything.dot-rules: flags on, lib + spec to 0.dotcms-ui(#35977) — 1005 → 0Production source reached 0 first, then the specs. The findings worth a reviewer's time:
Real defects, fixed:
DotContentTypeComponentStore.saveCopyDialogassetSelected$(string | null) and passed that straight tosaveCopyContentType, so a submit with nothing selected sentnullas the content type to copy. The error-path test was reaching the effect without selecting — which is what surfaced itDotAutocompleteTagsComponent.addItemthis.value.unshift(this.value.pop())unshiftsundefinedback into the tag array when the list is empty, andgetStringifyLabelsthen reads.labeloff itshouldClearDropdown(): booleandropdown && options.length && …—options.lengthis a number, so a function declared: booleancould return0SearchableDropdownComponent.action@Input() action!: (event: Event) => voidclaimed 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 trueSignatures corrected at their source rather than at the call:
DotRouterService.currentPortlet(always setsid, three callers were paying for the optional);portletReload$(a bareSubject, sounknown);DotEventsService.listenandPaginatorService.getWithOffset/getCurrentPageleft unparameterised at four sites;LoginService.watchUsertyped(params?: unknown)while its body always calls with anAuth;DotNavLogoService.setLogo, whose ownnavLogo?.startsWithhad already assumed a nullable argument;ActionHeaderDeleteOptions.confirmHeader/confirmMessage, optional while feeding a confirm dialog that requires both — and nothing in the repo suppliesdeleteOptionsat all.DataTableColumn.icon?: (any) => stringwas not theanytype — it is a parameter namedanywith no type at all, which is why TypeScript reportedTS7051rather than an implicit-any. Nobody had ever read it.The app build found six errors
tsc -pcannot seetscdoes not check templates.strictTemplatesbeing off does not stop a project's ownstrictNullChecksfrom applying to template expressions, and the Angular compiler is the only thing that evaluates them. All six were real: two optional-sitesaccesses, a nullableform.get, an unnarrowed secondoptions()call, the@if (action)above, andrunningExperiment.scheduling.endDateinedit-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.jsonalso reached 0I expected to record this as a known limitation. It compiles what
tsconfig.app.jsonexcludes, and that is where things had been rotting unseen:index.tsbarrels andcomponents.ts— re-exporting NgModules and a component deleted in the standalone migration. Zero importers; none could have compiled. Removed.PrimeNGConfigis nowPrimeNGand itsrippleflag is a signal.libs/block-editor/NodeViewRenderer.ts, not this project's file. It declaresoverride decorations!: readonly DecorationWithType[]— clean under block-editor's es2015 target, where a class field is an assignment, andTS2612under dotcms-ui's ES2022, whereuseDefineForClassFieldsis on by default and the same field emits adefinePropertythat shadows the base value withundefined.declare(which TypeScript will not accept alongsideoverride) 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
aliasedPropsinapps/dotcms-ui/src/app/test/. Spectator keyspropsby the class property name ($field) while theComponentRef.setInputunderneath needs the alias (field), so a component following this repo's$name+{ alias }convention cannot express its inputs throughpropsat all. One spec already carried a comment saying exactly that.Verification for this batch
dotcms-uitsconfig.app.json/spec/editornx run dotcms-ui:build:productionnx run dotcms-ui:testnx run dotcms-ui:lintuidotcms-models,data-access,ui,block-editor,edit-ema/portlet,edit-contenttsc -pat 0data-access's fourPushPublishServicefailures andblock-editor's 16/37 reproduce identically atHEAD— confirmed against a stashed tree, not assumed.Correction: the commit trailers on the final batch cite the wrong issue
The commits for
dotcms-uiare 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 theClosesline below is right.I did not rewrite the trailers — the branch is pushed and shared, and the
Closeslines are what drive the automation. Flagging it so the trailer/issue mismatch is not a surprise in review or ingit log.Follow-ups filed rather than folded in
Four issues under epic #32713, so none of this rides along in an already-large PR:
strictTemplatesin the four apps. Measured: 430 errors across 373 files, and 231 of those files are inlibs/— it is a ~27-library project, not an app change. TheTODO(#35930)gating it points at an issue that is already closedtsconfig.base.jsonto"strict": true— which is also how five of those eleven came to be created non-strictas unknown as DotCMSContentTypeondotcmsContentTypeBasicMock, which lets ~20 fields of the most-used content-type fixture disagree with the model unchecked. Found because two specs overrodehostafter the spread, putting the assignment outside the cast.bind(this)on both add and remove), a drop guard that comparesundefined === nulland so has never fired, andcleanTemplateItemdeletingtypebefore testing itCloses #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