diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 1f54f80914d4..f5ba754677aa 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -24,6 +24,8 @@ jobs: run: npm run markdownlint - name: format code run: npm run prettier-check + - name: tsc + run: npm run tsc coverage: runs-on: ubuntu-latest env: diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index abc62b7c734e..f3529e33d82b 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -20,6 +20,8 @@ jobs: run: npm run markdownlint - name: format code run: npm run prettier-check + - name: tsc + run: npm run tsc deploy: runs-on: ubuntu-latest env: diff --git a/Documentation/Contributors/CodingGuide/README.md b/Documentation/Contributors/CodingGuide/README.md index 56594f9dbe3d..bf939a40bfda 100644 --- a/Documentation/Contributors/CodingGuide/README.md +++ b/Documentation/Contributors/CodingGuide/README.md @@ -19,6 +19,7 @@ To some extent, this guide can be summarized as _make new code similar to existi - [Formatting](#formatting) - [Spelling](#spelling) - [Linting](#linting) + - [Type Checking](#type-checking) - [Units](#units) - [Basic Code Construction](#basic-code-construction) - [Functions](#functions) @@ -172,6 +173,34 @@ try { /*eslint-enable no-empty*/ ``` +## Type Checking + +We are incrementally adopting [type checking for JavaScript files](https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html). As of January 2026, type checks are enabled on a file-by-file basis, with `// @ts-check` annotations at the top of a file used to opt in. As adoption progresses, we expect this pattern will eventually be flipped to an opt-opt annotation instead. To see type system hints and errors in some editors or IDEs, you may need to configure or install TypeScript language server support. When in doubt, VSCode supports TypeScript language services [by default](https://code.visualstudio.com/docs/nodejs/working-with-javascript). + +**References:** + +For developers already familiar with TypeScript, writing JavaScript with JSDoc type annotations has some differences. For a summary of the annotation syntax, and supported features in JSDoc vs. TSDoc, see: + +- [Type Checking JavaScript Files \| TypeScript](https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html) +- [JSDoc Reference \| TypeScript](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html) +- [JSDoc Cheatsheet and Type Safety Tricks](https://docs.joshuatz.com/cheatsheets/js/jsdoc/) by Joshua Tzucker + +**Guidelines:** + +1. **Incremental adoption with `@ts-check` annotations.** Enabling type checks for existing and new JavaScript files, using `// @ts-check` annotations, is encouraged but not required in the course of ongoing feature development and maintenance. If type-related changes are noisy enough to obscure the functional changes your PR, consider splitting the type fixes into a separate PR. +2. **Common JavaScript/JSDoc obstacles.** JSDoc-based type checks have limited support for some coding patterns used in the CesiumJS codebase, such as classic prototype-based inheritance and `Object.defineProperties()`. In most cases these problems can be solved by refactoring with newer coding patterns, by hand or with tooling like [lebab](https://github.com/lebab/lebab). In other cases the solution may be less obvious, and a temporary skip-check annotation may be used to satisfy the type system. Example with lebab: + ```bash + npx lebab packages/engine/Source/Core/Cartesian2.js \ + -o packages/engine/Source/Core/Cartesian2.js --transform class + ``` +3. **Prefer `@ts-expect-error` over `@ts-ignore`.** Sometimes a source file's types are _mostly_ consistent, but a few stubborn errors remain unsolved. These may be blocked by out-of-scope work, or may simply be unjustifiably difficult to solve in JSDoc-based types. Use `@ts-expect-error` annotations to relax the type system for specific lines. Unlike `@ts-ignore`, `@ts-expect-error` will raise errors when the line no longer contains an error, making it easier to clean up annotations later on. +4. **Prefer `unknown` or `@ts-expect-error` over `any`.** Casting types to `any` forces them to be accepted everywhere, and effectively disables all type-checking dependent on that type. This tends to propagate throughout a type system, reducing the effectiveness of type checks, and should be avoided when possible. Prefer casting to `unknown`, or using a `@ts-expect-error` annotation. For example, when defining an object with string keys, with values whose types we don't care about, prefer `Record` to `Record`. +5. **Type-only imports.** When JSDoc annotations depend on types not otherwise imported in a source file, it will be necessary to tell TypeScript where to find them. To avoid otherwise-unused imports, use type-only imports in separate one-line JSDoc comments: + ```javascript + /** @import Cartesian3 from './Cartesian3.js'; */ + /** @import Cartesian4 from './Cartesian4.js'; */ + ``` + ## Units - Cesium uses SI units: diff --git a/Tools/jsdoc/cesiumTags.js b/Tools/jsdoc/cesiumTags.js index 85176b63654e..ba11e3ef8286 100644 --- a/Tools/jsdoc/cesiumTags.js +++ b/Tools/jsdoc/cesiumTags.js @@ -59,4 +59,13 @@ exports.defineTags = function (dictionary) { canHaveName: true, mustHaveValue: true, }); + + // Allow @import tags for type-only imports. Ignored by JSDoc, but resolved by tsc. + // https://github.com/microsoft/TypeScript/issues/22160#issuecomment-2021459033 + // https://devblogs.microsoft.com/typescript/announcing-typescript-5-5-beta/#type-imports-in-jsdoc + dictionary.defineTag("import", { + canHaveType: true, + canHaveName: true, + mustHaveValue: true, + }); }; diff --git a/eslint.config.js b/eslint.config.js index 70c1a4e81fd2..bd0ef652f691 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -81,6 +81,13 @@ export default [ "Avoid Array.push.apply(). Use addAllToArray() for arrays of unknown size, or the spread syntax for arrays that are known to be small", }, ], + // When ES6 class implementations refer to scratch variable instances of + // the same class, ESLint raises a use-before-define error. At runtime + // this is just fine, so configure ESLint to allow it in upper scopes. + "no-use-before-define": [ + "error", + { variables: false, functions: false, classes: false }, + ], }, }, { diff --git a/gulpfile.js b/gulpfile.js index e24d11e6b22d..fb6477b05e4a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -252,6 +252,28 @@ export async function buildTs() { await createTypeScriptDefinitions(); } +export async function tsc() { + let workspaces; + if (argv.workspace && !Array.isArray(argv.workspace)) { + workspaces = [argv.workspace]; + } else if (argv.workspace) { + workspaces = argv.workspace; + } else { + workspaces = getWorkspaces(true); + } + + for (const workspace of workspaces) { + const directory = workspace + .replace(`@${scope}/`, "") + .replace(`packages/`, ""); + + const tsconfigPath = `packages/${directory}/tsconfig.json`; + if (existsSync(tsconfigPath)) { + execSync(`npx tsc --project ${tsconfigPath}`, { stdio: "inherit" }); + } + } +} + const filesToClean = [ "Source/Cesium.js", "Source/Shaders/**/*.js", diff --git a/package.json b/package.json index 816d2bb589db..21f34314bd23 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "build-docs": "gulp buildDocs", "build-docs-watch": "gulp buildDocsWatch", "eslint": "eslint \"./**/*.*js\" \"./**/*.*ts*\" \"./**/*.html\" --cache --quiet", + "tsc": "gulp tsc", "make-zip": "gulp -f gulpfile.makezip.js makeZip", "markdownlint": "markdownlint \"**/*.md\"", "release": "gulp release", diff --git a/packages/engine/Source/Scene/Polyline.js b/packages/engine/Source/Scene/Polyline.js index f4a7a420b12c..0a7113ae4388 100644 --- a/packages/engine/Source/Scene/Polyline.js +++ b/packages/engine/Source/Scene/Polyline.js @@ -83,7 +83,6 @@ function Polyline(options, polylineCollection) { this._actualLength = undefined; - // eslint-disable-next-line no-use-before-define this._propertiesChanged = new Uint32Array(NUMBER_OF_PROPERTIES); this._polylineCollection = polylineCollection; this._dirty = false; diff --git a/packages/engine/lint-staged.config.js b/packages/engine/lint-staged.config.js new file mode 100644 index 000000000000..aecdcbc0e960 --- /dev/null +++ b/packages/engine/lint-staged.config.js @@ -0,0 +1,4 @@ +export default { + // https://github.com/lint-staged/lint-staged#how-can-i-resolve-typescript-tsc-ignoring-tsconfigjson-when-lint-staged-runs-via-husky-hooks + "*.{js,cjs,mjs,ts,cts,mts}": [() => "tsc"], +}; diff --git a/packages/engine/package.json b/packages/engine/package.json index 59ffad3f8713..83a54e7d1fdf 100644 --- a/packages/engine/package.json +++ b/packages/engine/package.json @@ -60,6 +60,7 @@ "build": "gulp build --workspace @cesium/engine", "build-ts": "gulp buildTs --workspace @cesium/engine", "coverage": "gulp coverage --workspace @cesium/engine", + "tsc": "gulp tsc --workspace @cesium/engine", "test": "gulp test --workspace @cesium/engine", "postversion": "gulp postversion --workspace @cesium/engine" }, diff --git a/packages/engine/tsconfig.json b/packages/engine/tsconfig.json new file mode 100644 index 000000000000..c08c48e8ff39 --- /dev/null +++ b/packages/engine/tsconfig.json @@ -0,0 +1,21 @@ +{ + "include": ["Source/**/*.js"], + "compilerOptions": { + // Module configuration. + "moduleResolution": "bundler", + "module": "ES2022", + "target": "ES2022", + + // I/O. + "noEmit": true, + "allowJs": true, + + // Disabled by default. Individual JS files may opt-in to type checking + // by including a `// @ts-check` comment at top of file. + "checkJs": false, + + // Checking declarations in dependencies is less important and less + // actionable than checking source JS. Skip until checkJS is on, at least. + "skipLibCheck": true + } +}