diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9409b8e6c..81d5f77a9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,17 +44,28 @@ jobs: - name: Type check run: npm run check + # Samples are outside `astro check`. Their own tsconfigs cannot see the + # IG packages' types, so this checks them all with one shared config. + - name: Type check samples + run: npm run check:samples + + # A failed check above must not hide a broken build or smoke test, so + # the build runs regardless, and the smoke tests whenever it succeeds. - name: Build + id: build + if: ${{ !cancelled() }} run: npm run build # Browser binaries are cached per Playwright version; OS deps (apt) are # not cacheable and are installed on every run. - name: Get Playwright version id: playwright + if: ${{ !cancelled() && steps.build.outcome == 'success' }} run: echo "version=$(npx playwright --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" - name: Cache Playwright browser id: playwright-cache + if: ${{ !cancelled() && steps.build.outcome == 'success' }} uses: actions/cache@v6 with: path: ~/.cache/ms-playwright @@ -62,12 +73,13 @@ jobs: # Headless tests only need the headless shell, not full Chromium. - name: Install Playwright browser - if: steps.playwright-cache.outputs.cache-hit != 'true' + if: ${{ !cancelled() && steps.build.outcome == 'success' && steps.playwright-cache.outputs.cache-hit != 'true' }} run: npx playwright install --with-deps --only-shell chromium - name: Install Playwright OS deps - if: steps.playwright-cache.outputs.cache-hit == 'true' + if: ${{ !cancelled() && steps.build.outcome == 'success' && steps.playwright-cache.outputs.cache-hit == 'true' }} run: npx playwright install-deps chromium - name: Smoke tests + if: ${{ !cancelled() && steps.build.outcome == 'success' }} run: npm run test:smoke diff --git a/README.md b/README.md index afcb9f184a..05040b32b5 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,12 @@ Use the same `BASE_PATH` for the build and the tests. CI builds and tests with ` - the sample appears in the sidebar - it loads without errors in the browser console -4. Commit, push, and open a pull request targeting `vnext`. Include a screenshot of the running sample. +4. Type-check it along with every other sample (CI runs the same check): + ```bash + npm run check:samples + ``` + +5. Commit, push, and open a pull request targeting `vnext`. Include a screenshot of the running sample. ## Updating Ignite UI Package Versions @@ -156,6 +161,7 @@ Do **not** edit version strings in `package.json` files by hand; CI fails when a | `npm run preview` | Serve `dist/` on port 4200 | | `npm run test:smoke` | Run the Playwright smoke tests against the production build | | `npm run check` | Type-check `src/` and `tests/` with `astro check` | +| `npm run check:samples` | Type-check every published sample, except the generated ones listed in `scripts/lib/skip-type-check.json` | | `npm run add:sample` | Scaffold `samples/{group}/{component}/{name}/` | | `npm run update:ig` | Apply `scripts/lib/versions.js` to every `package.json` | | `npm run generate:code-viewer` | Regenerate `public/code-viewer/**/*.json` | diff --git a/package.json b/package.json index 87eeaf8a35..eb06a907d0 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "preview": "astro preview --port 4200", "build:preview": "npm run build && npm run preview", "check": "astro check", + "check:samples": "node scripts/check-samples.js", "generate:code-viewer": "node scripts/generate-code-viewer.js", "update:ig": "node scripts/update-ig.js", "test:smoke": "playwright test" diff --git a/samples/editors/x-date-picker/date-limits/src/index.tsx b/samples/editors/x-date-picker/date-limits/src/index.tsx index 52b5350e4f..5962ee993a 100644 --- a/samples/editors/x-date-picker/date-limits/src/index.tsx +++ b/samples/editors/x-date-picker/date-limits/src/index.tsx @@ -48,4 +48,6 @@ export default class DatePickerDateLimits extends React.Component { } } +// rendering above class to the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); root.render(); diff --git a/samples/editors/x-date-picker/editing/src/index.tsx b/samples/editors/x-date-picker/editing/src/index.tsx index 3623d05ea5..478e41b2ac 100644 --- a/samples/editors/x-date-picker/editing/src/index.tsx +++ b/samples/editors/x-date-picker/editing/src/index.tsx @@ -33,4 +33,6 @@ export default class DatePickerEditing extends React.Component { } } +// rendering above class to the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); root.render(); diff --git a/samples/editors/x-date-picker/format/src/index.tsx b/samples/editors/x-date-picker/format/src/index.tsx index 605638bf44..3739b700fb 100644 --- a/samples/editors/x-date-picker/format/src/index.tsx +++ b/samples/editors/x-date-picker/format/src/index.tsx @@ -47,4 +47,6 @@ export default class DatePickerFormat extends React.Component { } } +// rendering above class to the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); root.render(); diff --git a/samples/grids/grid-lite/sort-config-events/src/index.tsx b/samples/grids/grid-lite/sort-config-events/src/index.tsx index 45392a43b7..3067cf98e4 100644 --- a/samples/grids/grid-lite/sort-config-events/src/index.tsx +++ b/samples/grids/grid-lite/sort-config-events/src/index.tsx @@ -12,7 +12,7 @@ const getTimeStamp = (): string => `[${new Date().toLocaleTimeString()}]`; // Define cellTemplate function outside component const ratingCellTemplate = (ctx: any) => { return ( - + ); }; diff --git a/samples/grids/grid-lite/sort-config-pipeline/src/index.tsx b/samples/grids/grid-lite/sort-config-pipeline/src/index.tsx index 5cd62608f5..e9b2128d06 100644 --- a/samples/grids/grid-lite/sort-config-pipeline/src/index.tsx +++ b/samples/grids/grid-lite/sort-config-pipeline/src/index.tsx @@ -10,7 +10,7 @@ import "./index.css"; // Define cellTemplate function outside component const ratingCellTemplate = (ctx: any) => { return ( - + ); }; diff --git a/samples/grids/grid-lite/styling-config-themes/src/index.tsx b/samples/grids/grid-lite/styling-config-themes/src/index.tsx index 4ffdad21a2..f96cb8bf8a 100644 --- a/samples/grids/grid-lite/styling-config-themes/src/index.tsx +++ b/samples/grids/grid-lite/styling-config-themes/src/index.tsx @@ -12,7 +12,7 @@ import { html } from 'lit-html'; import "igniteui-webcomponents/themes/light/bootstrap.css"; import "./index.scss"; -declare global { +declare module 'react' { namespace JSX { interface IntrinsicElements { 'igc-select': React.DetailedHTMLProps, HTMLElement>; diff --git a/samples/grids/grid/editing-excel-style/src/index.tsx b/samples/grids/grid/editing-excel-style/src/index.tsx index 3f404fe3c8..b6f3b7d2a0 100644 --- a/samples/grids/grid/editing-excel-style/src/index.tsx +++ b/samples/grids/grid/editing-excel-style/src/index.tsx @@ -7,7 +7,7 @@ import NwindData from "./NwindData.json"; import "igniteui-react-grids/grids/themes/light/bootstrap.css"; function Sample() { - const gridRef = useRef(); + const gridRef = useRef(null); useEffect(() => { gridRef.current.addEventListener("keydown", handleKeyDown); return () => { diff --git a/samples/grids/grid/localization/src/index.tsx b/samples/grids/grid/localization/src/index.tsx index 518fdbd9c2..47220a78d4 100644 --- a/samples/grids/grid/localization/src/index.tsx +++ b/samples/grids/grid/localization/src/index.tsx @@ -87,7 +87,7 @@ const GridLocalizationSample: React.FC = () => { Grid with Localization ) => setLocale(e.target.value)} + onChange={(e) => setLocale(e.detail.value)} > HI BG diff --git a/samples/inputs/badge/dot/src/index.tsx b/samples/inputs/badge/dot/src/index.tsx index 551a61e7b7..8fd13d05aa 100644 --- a/samples/inputs/badge/dot/src/index.tsx +++ b/samples/inputs/badge/dot/src/index.tsx @@ -59,7 +59,6 @@ export default function BadgeDot(): JSX.Element { diff --git a/samples/inputs/badge/icon/src/index.tsx b/samples/inputs/badge/icon/src/index.tsx index bd4f68d39b..4de74975fd 100644 --- a/samples/inputs/badge/icon/src/index.tsx +++ b/samples/inputs/badge/icon/src/index.tsx @@ -49,7 +49,7 @@ export default function BadgeIcon(): JSX.Element { ))}
- + diff --git a/samples/inputs/badge/overview/src/index.tsx b/samples/inputs/badge/overview/src/index.tsx index 46b6333d63..ba0dcc902c 100644 --- a/samples/inputs/badge/overview/src/index.tsx +++ b/samples/inputs/badge/overview/src/index.tsx @@ -24,7 +24,6 @@ export default function BadgeOverview(): JSX.Element { diff --git a/samples/inputs/badge/tailwind-styling/src/index.tsx b/samples/inputs/badge/tailwind-styling/src/index.tsx index a34c17722e..1113aa0c66 100644 --- a/samples/inputs/badge/tailwind-styling/src/index.tsx +++ b/samples/inputs/badge/tailwind-styling/src/index.tsx @@ -27,13 +27,13 @@ export default function BadgeTailwindStyling(): JSX.Element { return (
- +
- + @@ -41,13 +41,13 @@ export default function BadgeTailwindStyling(): JSX.Element {
- +
- + diff --git a/samples/inputs/badge/type/src/index.tsx b/samples/inputs/badge/type/src/index.tsx index e5b881c715..f15df48766 100644 --- a/samples/inputs/badge/type/src/index.tsx +++ b/samples/inputs/badge/type/src/index.tsx @@ -19,7 +19,6 @@ export default function BadgeType(): JSX.Element {
@@ -30,7 +29,6 @@ export default function BadgeType(): JSX.Element { @@ -43,7 +41,6 @@ export default function BadgeType(): JSX.Element { 2 diff --git a/samples/inputs/button-group/custom-toggle/src/index.tsx b/samples/inputs/button-group/custom-toggle/src/index.tsx index f4ea4d7c1f..ff9b862789 100644 --- a/samples/inputs/button-group/custom-toggle/src/index.tsx +++ b/samples/inputs/button-group/custom-toggle/src/index.tsx @@ -20,7 +20,7 @@ icons.forEach(([name, text]) => registerIconFromText(name, text, 'material')); export default function ButtonGroupCustomToggle() { return ( - + diff --git a/samples/inputs/color-picker/styling/src/index.tsx b/samples/inputs/color-picker/styling/src/index.tsx index e8a0954cac..db6bc27e79 100644 --- a/samples/inputs/color-picker/styling/src/index.tsx +++ b/samples/inputs/color-picker/styling/src/index.tsx @@ -86,7 +86,7 @@ export default function ColorPickerStyling() {
Colors
- +
diff --git a/samples/inputs/color-picker/tailwind-styling/src/index.tsx b/samples/inputs/color-picker/tailwind-styling/src/index.tsx index 3b8519493d..a3649d38a8 100644 --- a/samples/inputs/color-picker/tailwind-styling/src/index.tsx +++ b/samples/inputs/color-picker/tailwind-styling/src/index.tsx @@ -84,7 +84,6 @@ export default function ColorPickerTailwindStyling() { className={pickerClassName} id="textPicker" value={text} - size="small" label="Text" onInput={(e: CustomEvent) => setText(e.detail)} > @@ -98,7 +97,6 @@ export default function ColorPickerTailwindStyling() { className={pickerClassName} id="accentPicker" value={accent} - size="small" label="Accent" onInput={(e: CustomEvent) => setAccent(e.detail)} > @@ -112,7 +110,6 @@ export default function ColorPickerTailwindStyling() { className={pickerClassName} id="borderPicker" value={border} - size="small" label="Border" onInput={(e: CustomEvent) => setBorder(e.detail)} > @@ -126,7 +123,6 @@ export default function ColorPickerTailwindStyling() { className={pickerClassName} disabled={true} value="#7d91ab" - size="small" label="Shadow (Pro)" > diff --git a/samples/interactions/chat/features/src/index.tsx b/samples/interactions/chat/features/src/index.tsx index 4015112390..3fd13cefe5 100644 --- a/samples/interactions/chat/features/src/index.tsx +++ b/samples/interactions/chat/features/src/index.tsx @@ -90,7 +90,7 @@ igc-avatar::part(base) { useEffect(() => { let mounted = true; - createChatMarkdownRenderer().then(renderer => { + createChatMarkdownRenderer({}).then(renderer => { if (!mounted) return; setOptions((prev: any) => ({ diff --git a/samples/scheduling/date-range-picker/slots/src/index.tsx b/samples/scheduling/date-range-picker/slots/src/index.tsx index 855d1e48d5..44f7a55def 100644 --- a/samples/scheduling/date-range-picker/slots/src/index.tsx +++ b/samples/scheduling/date-range-picker/slots/src/index.tsx @@ -15,7 +15,7 @@ const Down = ''; export default function DrpSlots() { - const dateRangeRef = useRef(); + const dateRangeRef = useRef(null); const today: Date = new Date(); let tomorrow: Date = new Date(); diff --git a/scripts/check-samples.js b/scripts/check-samples.js new file mode 100644 index 0000000000..f3e0324ae5 --- /dev/null +++ b/scripts/check-samples.js @@ -0,0 +1,83 @@ +#!/usr/bin/env node +/** + * check-samples.js: type-check every published sample in one TypeScript run. + * + * WHY not each sample's own tsconfig + * ────────────────────────────────── + * Most samples resolve modules with `moduleResolution: node`, which cannot + * see the types the IG packages publish only through `exports`. Every IG + * component then types as `any`, so a prop the component does not have, e.g. + * , passes unnoticed. tsconfig.samples.json + * checks all of them with bundler resolution instead; one program over every + * sample takes well under a minute, where ~1000 `tsc -p` runs take an hour. + * + * Any type error fails the check. Skipped: samples in lib/unpublished.json, + * as everywhere else, and in lib/skip-type-check.json, generated samples + * whose fix has to land in igniteui-xplat-examples. + */ +import path from 'node:path'; +import ts from 'typescript'; + +import { REPO_ROOT, UNPUBLISHED } from './lib/samples.js'; +import skipTypeCheck from './lib/skip-type-check.json' with { type: 'json' }; + +const CONFIG = path.join(REPO_ROOT, 'tsconfig.samples.json'); +const SKIPPED = new Set([...UNPUBLISHED, ...Object.keys(skipTypeCheck.samples)]); + +// samples////… → // +const SLUG_RE = /^samples\/([^/]+\/[^/]+\/[^/]+)\//; + +/** The sample slug a file belongs to, or undefined outside samples/. */ +function slugOf(fileName) { + const rel = path.relative(REPO_ROOT, fileName).replace(/\\/g, '/'); + return rel.match(SLUG_RE)?.[1]; +} + +const formatHost = { + getCanonicalFileName: f => f, + getCurrentDirectory: () => REPO_ROOT, + getNewLine: () => '\n', +}; +// Pretty output in a terminal, one line per error in CI logs (as tsc does). +const format = process.stdout.isTTY ? ts.formatDiagnosticsWithColorAndContext : ts.formatDiagnostics; + +function report(diagnostics) { + console.error(format(diagnostics, formatHost)); +} + +function run() { + const { config, error } = ts.readConfigFile(CONFIG, ts.sys.readFile); + if (error) { + report([error]); + process.exit(1); + } + + const parsed = ts.parseJsonConfigFileContent(config, ts.sys, REPO_ROOT, undefined, CONFIG); + if (parsed.errors.length) { + report(parsed.errors); + process.exit(1); + } + + const fileNames = parsed.fileNames.filter(f => !SKIPPED.has(slugOf(f))); + const checked = new Set(fileNames.map(slugOf).filter(Boolean)); + const program = ts.createProgram(fileNames, parsed.options); + + const diagnostics = ts.getPreEmitDiagnostics(program); + const failing = [...new Set(diagnostics.map(d => d.file && slugOf(d.file.fileName)).filter(Boolean))].sort(); + + if (diagnostics.length) { + report(diagnostics); + } + + console.log(`Type-checked ${checked.size} sample(s).`); + + if (failing.length) { + console.error(`\n${failing.length} sample(s) have type errors:`); + failing.forEach(slug => console.error(` ${slug}`)); + } + if (diagnostics.length) { + process.exit(1); + } +} + +run(); diff --git a/scripts/lib/sample-globals.d.ts b/scripts/lib/sample-globals.d.ts new file mode 100644 index 0000000000..084b97ef3a --- /dev/null +++ b/scripts/lib/sample-globals.d.ts @@ -0,0 +1,14 @@ +/** + * Globals for tsconfig.samples.json only. + * + * Samples generated by igniteui-xplat-examples annotate render() with the + * global JSX namespace, which @types/react 19 removed in favor of React.JSX. + * Restore the one member they use, so the check reports real errors only. + */ +import type { JSX as ReactJSX } from 'react'; + +declare global { + namespace JSX { + type Element = ReactJSX.Element; + } +} diff --git a/scripts/lib/skip-type-check.json b/scripts/lib/skip-type-check.json new file mode 100644 index 0000000000..55535cbee5 --- /dev/null +++ b/scripts/lib/skip-type-check.json @@ -0,0 +1,10 @@ +{ + "//": "Samples `npm run check:samples` skips, with the error each has. All are exported from igniteui-xplat-examples, so a fix made only here is lost at the next export: fix the source there, then remove the sample from this list.", + "samples": { + "grids/data-grid/column-resizing": "renders ; the component is GridColumnResizing", + "grids/data-grid/row-selection": "root.render() without `const root = ReactDOM.createRoot(...)`", + "grids/grid/row-pinning-extra-column": "rowList is an array; it has no toArray()", + "maps/geo-map/display-azure-imagery": "ref callback returns a value, which React 19 reads as a cleanup function", + "maps/geo-map/display-bing-imagery": "ref callback returns a value, which React 19 reads as a cleanup function" + } +} diff --git a/tsconfig.samples.json b/tsconfig.samples.json new file mode 100644 index 0000000000..5a40109e7d --- /dev/null +++ b/tsconfig.samples.json @@ -0,0 +1,26 @@ +{ + // Type-checks every sample in one program; run through `npm run check:samples`. + // The add:sample tsconfig settings (see scripts/lib/templates.js), but not + // strict: the generated samples are not written for strict mode. + "compilerOptions": { + "target": "es2020", + "lib": ["es2020", "dom", "dom.iterable"], + "module": "esnext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "types": ["vite/client"], + // TypeScript 6 is strict by default. + "strict": false, + // Off in TypeScript 5.9, which the samples build with. On, it rejects + // untyped side-effect entries such as igniteui-react-grids/grids/combined. + "noUncheckedSideEffectImports": false, + "resolveJsonModule": true, + "isolatedModules": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": [ + "samples/*/*/*/src/**/*", + "scripts/lib/sample-globals.d.ts" + ] +}