Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions core-web/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,46 @@ Always wrap form fields with this structure for consistent styling:
</form>
```

## TypeScript Strict Mode

Strict mode is being rolled out **one project at a time** (epic #35932), bottom-up through the dependency graph. `tsconfig.base.json` stays at `"strict": false` — never flip it globally.

To make a project strict:

1. Add the flags to the **project's own** `tsconfig.json` (not `tsconfig.spec.json`, not the base):

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

2. Fix every error. No new `any` — use explicit types. To silence something unavoidable, use `@ts-expect-error` with a `// TODO(#issue):` note, never a blanket `@ts-ignore`.

**What enforces this:** for Rollup libs that emit declarations (`"declaration": true`), `@rollup/plugin-typescript` is in the build chain and reports type errors, so the `build` target is the gate — CI runs `nx run-many -t build` (the `build-test` execution in `core-web/pom.xml`). Do **not** add a separate `typecheck` target to those projects; it is redundant. `lint` does not catch type errors — ESLint reports lint rules, not TS diagnostics.

Vite-based projects are the exception: their builds use esbuild and skip type checking, which is why the Nx Vite plugin infers a separate `typecheck` target for them.

Verify locally:

```bash
pnpm exec tsc -p <projectRoot>/tsconfig.lib.json --noEmit
pnpm exec nx run <project>:build
pnpm exec nx affected -t build,lint --base=origin/main # check you didn't break consumers
```

`<projectRoot>` is the path from `project.json`, which is often nested — e.g. `libs/sdk/create-app`, not `libs/create-app`. Two caveats on the tsconfig name:

- **Apps** use `tsconfig.app.json`.
- **Some projects have no `tsconfig.lib.json`** (`libs/sdk/create-app` is one); use their `tsconfig.json` instead.

Also check `tsconfig.spec.json` — the flags live in `tsconfig.json`, which the spec config extends, so specs go strict too and their errors are yours to fix.

> **Watch out for masked results.** If a tsconfig declares a `types` entry that is not installed, `tsc` reports `TS2688: Cannot find type definition file for '<name>'` and **stops before semantic checking** — you get one error and no type checking at all. A stable error count across a change proves nothing in that case. `libs/utils-testing` is affected today (`"types": ["jasmine"]`); check it with `--types node` to see real diagnostics.

## Portlet Development

New portlets go in `libs/portlets/`. For full patterns, architecture, testing, and Nx generator setup:
Expand All @@ -110,8 +150,8 @@ New portlets go in `libs/portlets/`. For full patterns, architecture, testing, a

- Use `dot-content-drive` portlet as reference for test config
- `tsconfig.spec.json` tsconfig.spec.json must have "isolatedModules": true in compilerOptions
- `tsconfig.json` — do NOT add `"strict": true` or `"module": "preserve"`
- `tsconfig.spec.json` — keep minimal (only `module`, `target`, `types`)
- `tsconfig.json` — do NOT add `"module": "preserve"`
- `tsconfig.spec.json` — keep minimal (only `module`, `target`, `types`); do NOT add `"strict": true` here, it belongs in the project's `tsconfig.json` (see [TypeScript Strict Mode](#typescript-strict-mode))
- Import `mockProvider` from `@openng/spectator/jest` (not `@openng/spectator`)

### SignalStore Tests
Expand Down
4 changes: 2 additions & 2 deletions core-web/libs/dotcms-js/src/lib/core/api-root.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ export class ApiRoot {
hideFireOn = false;
hideRulePushOptions = false;

static parseQueryParam(query: string, token: string): string {
static parseQueryParam(query: string, token: string): string | null {
let idx = -1;
let result = null;
let result: string | null = null;
token = token + '=';
if (query && query.length) {
idx = query.indexOf(token);
Expand Down
9 changes: 5 additions & 4 deletions core-web/libs/dotcms-js/src/lib/core/dotcms-config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ export class DotcmsConfigService {
private http = inject(HttpClient);
private loggerService = inject(LoggerService);

private configParamsSubject: BehaviorSubject<ConfigParams> = new BehaviorSubject(null);
private configParamsSubject: BehaviorSubject<ConfigParams | null> =
new BehaviorSubject<ConfigParams | null>(null);
private configUrl: string;

/**
Expand All @@ -138,7 +139,7 @@ export class DotcmsConfigService {
getConfig(): Observable<ConfigParams> {
return this.configParamsSubject
.asObservable()
.pipe(filter((config: ConfigParams) => !!config));
.pipe(filter((config): config is ConfigParams => !!config));
}

loadConfig(): void {
Expand All @@ -159,8 +160,8 @@ export class DotcmsConfigService {
paginatorLinks: res.config[DOTCMS_PAGINATOR_LINKS],
paginatorRows: res.config[DOTCMS_PAGINATOR_ROWS],
releaseInfo: {
buildDate: res.config.releaseInfo?.buildDate,
version: res.config.releaseInfo?.version
buildDate: res.config.releaseInfo?.buildDate ?? '',
version: res.config.releaseInfo?.version ?? ''
},
websocket: {
websocketReconnectTime:
Expand Down
7 changes: 4 additions & 3 deletions core-web/libs/dotcms-js/src/lib/core/logger.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class LoggerService {
* @returns boolean
*/
shouldShowLogs(): boolean {
const devMode: string = this.httpRequestUtils.getQueryStringParam(DEV_MODE_PARAM);
const devMode: string | null = this.httpRequestUtils.getQueryStringParam(DEV_MODE_PARAM);

return !environment.production || devMode === 'on';
} // isProduction.
Expand All @@ -77,13 +77,14 @@ export class LoggerService {
try {
throw new Error();
} catch (e) {
caller = this.cleanCaller(this.stringUtils.getLine(e.stack, 4));
const stack = e instanceof Error ? (e.stack ?? '') : '';
caller = this.cleanCaller(this.stringUtils.getLine(stack, 4));
}

return caller;
}

private cleanCaller(caller: string): string {
private cleanCaller(caller: string | null): string {
return caller ? caller.trim().substr(3) : 'unknown';
}
}
38 changes: 22 additions & 16 deletions core-web/libs/dotcms-js/src/lib/core/login.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,23 @@ export class LoginService {
currentUserLanguageId = '';
private country = '';
private lang = '';
private urls: Record<string, string>;
// Typed by inference rather than `Record<string, string>` so each endpoint is a named
// property; that keeps dot access valid under `noPropertyAccessFromIndexSignature`.
private readonly urls = {
changePassword: '/api/v1/changePassword',
getAuth: '/api/v1/authentication/logInUser',
loginAs: '/api/v1/users/loginas',
logout: '/api/v1/logout',
logoutAs: '/api/v1/users/logoutas',
recoverPassword: '/api/v1/forgotpassword',
serverInfo: '/api/v1/loginform',
userAuth: '/api/v1/authentication',
current: '/api/v1/users/current/'
};

constructor() {
this._loginAsUsersList$ = new Subject<User[]>();

this.urls = {
changePassword: '/api/v1/changePassword',
getAuth: '/api/v1/authentication/logInUser',
loginAs: '/api/v1/users/loginas',
logout: '/api/v1/logout',
logoutAs: '/api/v1/users/logoutas',
recoverPassword: '/api/v1/forgotpassword',
serverInfo: '/api/v1/loginform',
userAuth: '/api/v1/authentication',
current: '/api/v1/users/current/'
};

this.dotcmsEventsService.subscribeTo('SESSION_DESTROYED').subscribe(() => {
this.logOutUser();
this.clearExperimentPersistence();
Expand All @@ -77,7 +77,11 @@ export class LoginService {
return this._logout$.asObservable();
}

private _auth: Auth;
// TODO(#35939): assigned by `setAuth()` during the login flow, never in the constructor.
// Modelling it as `Auth | undefined` is the truthful type, but the public `auth` getter is
// consumed by already-strict projects (global-store, data-access), so widening it is a
// public-API change that belongs in its own issue.
private _auth!: Auth;

get auth(): Auth {
return this._auth;
Expand Down Expand Up @@ -291,7 +295,7 @@ export class LoginService {
this._auth = this.getFullAuth(auth);
this._auth$.next(this.getFullAuth(auth));

this.currentUserLanguageId = auth.user.languageId;
this.currentUserLanguageId = auth.user.languageId ?? '';

// When not logged user we need to fire the observable chain
if (!auth.user) {
Expand Down Expand Up @@ -373,6 +377,8 @@ export interface User {

export interface Auth {
user: User;
loginAsUser: User;
// Null whenever nobody is impersonating. Callers already guard with
// `auth.loginAsUser || auth.user`; the type just never said so.
loginAsUser: User | null;
isLoginAs?: boolean;
}
8 changes: 4 additions & 4 deletions core-web/libs/dotcms-js/src/lib/core/routing.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ export class RoutingService {
private http = inject(HttpClient);

private _menusChange$: Subject<Menu[]> = new Subject();
private menus: Menu[];
private menus: Menu[] = [];
private urlMenus: string;
private portlets: Map<string, string>;
private _currentPortletId: string;
private _currentPortletId = '';

private _portletUrlSource$ = new Subject<string>();
private _currentPortlet$ = new Subject<string>();
Expand Down Expand Up @@ -55,7 +55,7 @@ export class RoutingService {
return this._portletUrlSource$.asObservable();
}

get firstPortlet(): string {
get firstPortlet(): string | null {
const porlets = this.portlets.entries().next().value;

return porlets ? porlets[0] : null;
Expand All @@ -65,7 +65,7 @@ export class RoutingService {
this.portlets.set(portletId.replace(' ', '_'), url);
}

public getPortletURL(portletId: string): string {
public getPortletURL(portletId: string): string | undefined {
return this.portlets.get(portletId);
}

Expand Down
4 changes: 2 additions & 2 deletions core-web/libs/dotcms-js/src/lib/core/shared/user.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { LoggerService } from '../logger.service';
export class UserModel {
private loggerService = inject(LoggerService);

username: string;
password: string;
username = '';
password = '';
locale: string;
suppressAlerts = false;

Expand Down
2 changes: 1 addition & 1 deletion core-web/libs/dotcms-js/src/lib/core/site.service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const mockSites: Site[] = [
];

export class SiteServiceMock {
_currentSite: Site;
_currentSite: Site | undefined;
private _currentSite$: Subject<Site> = new Subject<Site>();

get currentSite(): Site {
Expand Down
7 changes: 5 additions & 2 deletions core-web/libs/dotcms-js/src/lib/core/site.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ export class SiteService {
private http = inject(HttpClient);
private loggerService = inject(LoggerService);

private selectedSite: Site;
// TODO(#35939): assigned by `setCurrentSite()` during init, never in the constructor.
// Same trade-off as `LoginService._auth`: the public `currentSite` getter is widely
// consumed, so widening it to `Site | undefined` belongs in its own issue.
private selectedSite!: Site;
private urls: { currentSiteUrl: string; sitesUrl: string; switchSiteUrl: string };
private events: string[] = [
'SAVE_SITE',
Expand Down Expand Up @@ -174,7 +177,7 @@ export class SiteService {
* @return {*} {Observable<Site>}
* @memberof SiteService
*/
switchSiteById(id: string): Observable<Site> {
switchSiteById(id: string): Observable<Site | null> {
this.loggerService.debug('Applying a Site Switch');

return this.getSiteById(id).pipe(
Expand Down
8 changes: 4 additions & 4 deletions core-web/libs/dotcms-js/src/lib/core/string-utils.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export class StringUtils {
* @param indexLine
* @returns string
*/
getLine(text: string, indexLine: number): string {
let line: string = null;
getLine(text: string, indexLine: number): string | null {
let line: string | null = null;

if (text) {
const lines = text.split('\n');
Expand All @@ -26,9 +26,9 @@ export class StringUtils {
* @param str
* @returns string
*/
camelize(str): string {
camelize(str: string): string {
return str
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => {
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter: string, index: number) => {
return index === 0 ? letter.toLowerCase() : letter.toUpperCase();
})
.replace(/\s+/g, '');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export class HttpRequestUtils {
* it is based on the window.location.href.
* @returns string
*/
getQueryStringParam(name: string): string {
let value = null;
getQueryStringParam(name: string): string | null {
let value: string | null = null;
const regex = new RegExp('[?&]' + name.replace(/[\[\]]/g, '\\$&') + '(=([^&#]*)|&|#|$)');
const results = regex.exec(window.location.href);

Expand Down
33 changes: 15 additions & 18 deletions core-web/libs/dotcms-js/src/lib/core/util/response-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,47 +15,44 @@ import { DotCMSResponse } from '@dotcms/dotcms-models';
* </code>
*/
export class ResponseView<T = any> {
private bodyJsonObject: DotCMSResponse<T>;
// `HttpResponse.body` is nullable, so the parsed body genuinely can be absent.
private bodyJsonObject: DotCMSResponse<T> | null;
private headers: HttpHeaders;

public constructor(private resp: HttpResponse<DotCMSResponse<T>>) {
try {
this.bodyJsonObject = resp.body;
this.headers = resp.headers;
} catch (e) {
this.bodyJsonObject = null;
}
this.bodyJsonObject = resp.body;
this.headers = resp.headers;
}

public header(headerName: string): string {
public header(headerName: string): string | null {
return this.headers.get(headerName);
}

get i18nMessagesMap(): { [key: string]: string } {
return this.bodyJsonObject.i18nMessagesMap;
return this.bodyJsonObject?.i18nMessagesMap ?? {};
}

get contentlets(): T {
return this.bodyJsonObject.contentlets;
get contentlets(): T | undefined {
return this.bodyJsonObject?.contentlets;
}

get entity(): T {
return this.bodyJsonObject.entity;
get entity(): T | undefined {
return this.bodyJsonObject?.entity;
}

get tempFiles(): T {
return this.bodyJsonObject.tempFiles;
get tempFiles(): T | undefined {
return this.bodyJsonObject?.tempFiles;
}

get errorsMessages(): string {
let errorMessages = '';

if (this.bodyJsonObject.errors) {
if (this.bodyJsonObject?.errors) {
this.bodyJsonObject.errors.forEach((e: any) => {
errorMessages += e.message;
});
} else {
errorMessages = this.bodyJsonObject.messages.toString();
errorMessages = this.bodyJsonObject?.messages.toString() ?? '';
}

return errorMessages;
Expand All @@ -71,7 +68,7 @@ export class ResponseView<T = any> {

public existError(errorCode: string): boolean {
return (
this.bodyJsonObject.errors &&
!!this.bodyJsonObject?.errors &&
this.bodyJsonObject.errors.filter((e: any) => e.errorCode === errorCode).length > 0
);
}
Expand Down
8 changes: 7 additions & 1 deletion core-web/libs/dotcms-js/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
"target": "es2020",
"module": "preserve",
"moduleResolution": "bundler",
"lib": ["dom", "dom.iterable", "es2022"]
"lib": ["dom", "dom.iterable", "es2022"],
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}
2 changes: 1 addition & 1 deletion core-web/libs/sdk/create-app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ program
if (error instanceof Error) {
console.error(error.message);
// Preserve stack trace for debugging when DEBUG mode is enabled
if (process.env.DEBUG) {
if (process.env['DEBUG']) {
console.error('\n' + chalk.gray('Stack trace:'));
console.error(chalk.gray(error.stack || 'No stack trace available'));
}
Expand Down
Loading
Loading