diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 80bcbed90c..0000000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -legacy-peer-deps = true diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 0d4efacdc9..cf3caf0895 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["dbaeumer.vscode-eslint", "oxc.oxc-vscode"] + "recommendations": ["dbaeumer.vscode-eslint", "oxc.oxc-vscode", "typescriptteam.native-preview"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index b76ddf9a99..762045fea0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,7 @@ "editor.formatOnSaveMode": "file", "js/ts.tsdk.promptToUseWorkspaceVersion": true, "js/ts.tsdk.path": "node_modules/typescript/lib", + "js/ts.experimental.useTsgo": true, "files.readonlyInclude": { "**/routeTree.gen.ts": true }, diff --git a/AGENTS.md b/AGENTS.md index b64555fe1a..c8d6953bd3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ ```shell npm install # setup (requires Node.js ≥ 22 for `node --run`) node --run build # library → lib/ -node --run typecheck # tsc --build +node --run typecheck # tsgo --build node --run eslint # eslint --max-warnings 0 node --run eslint:fix # eslint --fix node --run format # oxfmt @@ -48,12 +48,12 @@ website/ # demo site (Vite + TanStack Router) - **Light/dark mode** — handled via CSS `light-dark()` + `color-scheme`, not JS. - **Accessibility first** — ARIA attributes (e.g. `aria-colindex`, `aria-rowindex`, `aria-selected`, roles) are required. Tests query by role. - **Formatting** — oxfmt (not Prettier). **Linting** — ESLint (must pass with zero warnings). -- **Build** — Rolldown bundles library to `lib/`; `ecij` plugin prefixes classes with `rdg-{version}-` (dots→dashes) to avoid cross-version conflicts. +- **Build** — tsdown bundles library to `lib/`; `ecij` plugin prefixes classes with `rdg-{version}-` (dots→dashes) to avoid cross-version conflicts. ## Testing -- Browser tests use `vitest/browser` + Playwright. `test/setupBrowser.ts` configures `page.render()` via `vitest-browser-react` and registers custom locators via `locators.extend()` — prefer `page.getGrid()`, `page.getCell({ name })`, `page.getRow()`, `page.getHeaderCell()`, `page.getSelectedCell()`, etc. over raw `page.getByRole()`. -- Test helpers in `test/browser/utils.tsx`: `setup()`, `getRowWithCell()`, `getCellsAtRowIndex()`, `validateCellPosition()`, `scrollGrid()`, `tabIntoGrid()`, `testCount()`, `testRowCount()`. +- Browser tests use `vitest/browser` + Playwright. `test/setupBrowser.ts` configures `page.render()` via `vitest-browser-react` and registers custom locators via `locators.extend()` — prefer `page.getGrid()`, `page.getCell({ name })`, `page.getRow()`, `page.getHeaderCell()`, `page.getActiveCell()`, etc. over raw `page.getByRole()`. +- Test helpers in `test/browser/utils.tsx`: `setup()`, `getRowWithCell()`, `getCellsAtRowIndex()`, `validateCellPosition()`, `scrollGrid()`, `safeTab()`, `testCount()`, `testRowCount()`. - `test/failOnConsole.ts` fails tests on unexpected console warnings/errors. - **Never run visual regression tests** — screenshots are environment-dependent so visual regression tests must run in CI only. diff --git a/eslint.config.js b/eslint.config.js index 58699781c4..8ac5d142da 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,14 +1,8 @@ import eslintReact from '@eslint-react/eslint-plugin'; import markdown from '@eslint/markdown'; import vitest from '@vitest/eslint-plugin'; -import jestDom from 'eslint-plugin-jest-dom'; -import reactDom from 'eslint-plugin-react-dom'; import reactHooks from 'eslint-plugin-react-hooks'; -import reactNamingConvention from 'eslint-plugin-react-naming-convention'; -import reactRsc from 'eslint-plugin-react-rsc'; -import reactWebApi from 'eslint-plugin-react-web-api'; import sonarjs from 'eslint-plugin-sonarjs'; -import testingLibrary from 'eslint-plugin-testing-library'; import { defineConfig, globalIgnores } from 'eslint/config'; import tseslint from 'typescript-eslint'; @@ -29,10 +23,6 @@ export default defineConfig([ // @ts-expect-error 'react-hooks': reactHooks, '@eslint-react': eslintReact, - '@eslint-react/rsc': reactRsc, - '@eslint-react/dom': reactDom, - '@eslint-react/web-api': reactWebApi, - '@eslint-react/naming-convention': reactNamingConvention, sonarjs, '@typescript-eslint': tseslint.plugin }, @@ -288,7 +278,7 @@ export default defineConfig([ // https://www.eslint-react.xyz/docs/rules/overview /* // copy all the rules from the rules table for easy pasting -function getRules(id, prefix) { +function getRules(id) { return ( Iterator.from( document @@ -298,38 +288,33 @@ function getRules(id, prefix) { .querySelectorAll('tr a') ) // map link to rule declaration - .map((a) => `'@eslint-react/${prefix}${a.textContent}': 1,`) + .map((a) => `'@eslint-react/${a.getAttribute('href')}': 1,`) ); } copy( Iterator.from([ - getRules('x-rules', ''), - getRules('rsc-rules', 'rsc/'), - getRules('dom-rules', 'dom/'), - getRules('web-api-rules', 'web-api/'), - getRules('naming-convention-rules', 'naming-convention/'), + getRules('x-rules'), + getRules('jsx-rules'), + getRules('rsc-rules'), + getRules('dom-rules'), + getRules('web-api-rules'), + getRules('naming-convention-rules'), ]) .flatMap((x) => x) .toArray() .join('\n') ); */ - '@eslint-react/jsx-dollar': 1, - '@eslint-react/jsx-key-before-spread': 1, - '@eslint-react/jsx-no-comment-textnodes': 1, - '@eslint-react/jsx-shorthand-boolean': 1, - '@eslint-react/jsx-shorthand-fragment': 1, '@eslint-react/component-hook-factories': 1, '@eslint-react/error-boundaries': 1, '@eslint-react/exhaustive-deps': 1, - '@eslint-react/immutability': 0, + '@eslint-react/immutability': 1, '@eslint-react/no-access-state-in-setstate': 1, '@eslint-react/no-array-index-key': 0, '@eslint-react/no-children-count': 1, '@eslint-react/no-children-for-each': 1, '@eslint-react/no-children-map': 1, '@eslint-react/no-children-only': 1, - '@eslint-react/no-children-prop': 1, '@eslint-react/no-children-to-array': 1, '@eslint-react/no-class-component': 1, '@eslint-react/no-clone-element': 1, @@ -367,7 +352,6 @@ copy( '@eslint-react/no-unused-props': 1, '@eslint-react/no-unused-state': 1, '@eslint-react/no-use-context': 1, - '@eslint-react/no-useless-fragment': [1, { allowExpressions: false }], '@eslint-react/prefer-destructuring-assignment': 1, '@eslint-react/prefer-namespace-import': 1, '@eslint-react/purity': 1, @@ -378,32 +362,37 @@ copy( '@eslint-react/unsupported-syntax': 1, '@eslint-react/use-memo': 1, '@eslint-react/use-state': 1, - '@eslint-react/rsc/function-definition': 1, - '@eslint-react/dom/no-dangerously-set-innerhtml': 1, - '@eslint-react/dom/no-dangerously-set-innerhtml-with-children': 1, - '@eslint-react/dom/no-find-dom-node': 1, - '@eslint-react/dom/no-flush-sync': 0, - '@eslint-react/dom/no-hydrate': 1, - '@eslint-react/dom/no-missing-button-type': 1, - '@eslint-react/dom/no-missing-iframe-sandbox': 1, - '@eslint-react/dom/no-namespace': 1, - '@eslint-react/dom/no-render': 1, - '@eslint-react/dom/no-render-return-value': 1, - '@eslint-react/dom/no-script-url': 1, - '@eslint-react/dom/no-string-style-prop': 1, - '@eslint-react/dom/no-unknown-property': 0, - '@eslint-react/dom/no-unsafe-iframe-sandbox': 1, - '@eslint-react/dom/no-unsafe-target-blank': 1, - '@eslint-react/dom/no-use-form-state': 1, - '@eslint-react/dom/no-void-elements-with-children': 1, - '@eslint-react/dom/prefer-namespace-import': 1, - '@eslint-react/web-api/no-leaked-event-listener': 1, - '@eslint-react/web-api/no-leaked-interval': 1, - '@eslint-react/web-api/no-leaked-resize-observer': 1, - '@eslint-react/web-api/no-leaked-timeout': 1, - '@eslint-react/naming-convention/context-name': 1, - '@eslint-react/naming-convention/id-name': 1, - '@eslint-react/naming-convention/ref-name': 1, + '@eslint-react/jsx-no-children-prop': 1, + '@eslint-react/jsx-no-children-prop-with-children': 1, + '@eslint-react/jsx-no-comment-textnodes': 1, + '@eslint-react/jsx-no-useless-fragment': [1, { allowExpressions: false }], + '@eslint-react/jsx-no-key-after-spread': 1, + '@eslint-react/jsx-no-namespace': 1, + '@eslint-react/rsc-function-definition': 1, + '@eslint-react/dom-no-dangerously-set-innerhtml': 1, + '@eslint-react/dom-no-dangerously-set-innerhtml-with-children': 1, + '@eslint-react/dom-no-find-dom-node': 1, + '@eslint-react/dom-no-flush-sync': 0, + '@eslint-react/dom-no-hydrate': 1, + '@eslint-react/dom-no-missing-button-type': 1, + '@eslint-react/dom-no-missing-iframe-sandbox': 1, + '@eslint-react/dom-no-render': 1, + '@eslint-react/dom-no-render-return-value': 1, + '@eslint-react/dom-no-script-url': 1, + '@eslint-react/dom-no-string-style-prop': 1, + '@eslint-react/dom-no-unknown-property': 1, + '@eslint-react/dom-no-unsafe-iframe-sandbox': 1, + '@eslint-react/dom-no-unsafe-target-blank': 1, + '@eslint-react/dom-no-use-form-state': 1, + '@eslint-react/dom-no-void-elements-with-children': 1, + '@eslint-react/dom-prefer-namespace-import': 1, + '@eslint-react/web-api-no-leaked-event-listener': 1, + '@eslint-react/web-api-no-leaked-interval': 1, + '@eslint-react/web-api-no-leaked-resize-observer': 1, + '@eslint-react/web-api-no-leaked-timeout': 1, + '@eslint-react/naming-convention-context-name': 1, + '@eslint-react/naming-convention-id-name': 1, + '@eslint-react/naming-convention-ref-name': 1, // SonarJS rules // https://github.com/SonarSource/SonarJS/blob/master/packages/jsts/src/rules/README.md#rules @@ -893,7 +882,7 @@ copy( 1, { path: 'never', types: 'never', lib: 'never' } ], - '@typescript-eslint/unbound-method': 0, + '@typescript-eslint/unbound-method': 0, // replaced by vitest/unbound-method '@typescript-eslint/unified-signatures': 0, '@typescript-eslint/use-unknown-in-catch-callback-variable': 1 } @@ -905,9 +894,7 @@ copy( files: ['test/**/*'], plugins: { - vitest, - 'jest-dom': jestDom, - 'testing-library': testingLibrary + vitest }, rules: { @@ -1022,55 +1009,12 @@ copy( 'vitest/require-test-timeout': 0, 'vitest/require-to-throw-message': 1, 'vitest/require-top-level-describe': 0, + 'vitest/unbound-method': 0, 'vitest/valid-describe-callback': 1, 'vitest/valid-expect': [1, { alwaysAwait: true }], 'vitest/valid-expect-in-promise': 1, 'vitest/valid-title': 1, - 'vitest/warn-todo': 1, - - // https://github.com/testing-library/eslint-plugin-jest-dom#supported-rules - 'jest-dom/prefer-checked': 1, - 'jest-dom/prefer-empty': 1, - 'jest-dom/prefer-enabled-disabled': 1, - 'jest-dom/prefer-focus': 1, - 'jest-dom/prefer-in-document': 1, - 'jest-dom/prefer-required': 1, - 'jest-dom/prefer-to-have-attribute': 1, - 'jest-dom/prefer-to-have-class': 1, - 'jest-dom/prefer-to-have-style': 1, - 'jest-dom/prefer-to-have-text-content': 1, - 'jest-dom/prefer-to-have-value': 1, - - // eslint-plugin-testing-library Rules - // https://github.com/testing-library/eslint-plugin-testing-library#supported-rules - 'testing-library/await-async-events': 0, - 'testing-library/await-async-queries': 0, - 'testing-library/await-async-utils': 0, - 'testing-library/consistent-data-testid': 0, - 'testing-library/no-await-sync-events': 0, - 'testing-library/no-await-sync-queries': 0, - 'testing-library/no-container': 1, - 'testing-library/no-debugging-utils': 1, - 'testing-library/no-dom-import': 1, - 'testing-library/no-global-regexp-flag-in-query': 1, - 'testing-library/no-manual-cleanup': 0, - 'testing-library/no-node-access': 0, - 'testing-library/no-promise-in-fire-event': 0, - 'testing-library/no-render-in-lifecycle': 0, - 'testing-library/no-test-id-queries': 0, - 'testing-library/no-unnecessary-act': 1, - 'testing-library/no-wait-for-multiple-assertions': 1, - 'testing-library/no-wait-for-side-effects': 1, - 'testing-library/no-wait-for-snapshot': 0, - 'testing-library/prefer-explicit-assert': 1, - 'testing-library/prefer-find-by': 1, - 'testing-library/prefer-implicit-assert': 0, - 'testing-library/prefer-presence-queries': 0, - 'testing-library/prefer-query-by-disappearance': 1, - 'testing-library/prefer-query-matchers': 0, - 'testing-library/prefer-screen-queries': 0, - 'testing-library/prefer-user-event': 1, - 'testing-library/render-result-naming-convention': 0 + 'vitest/warn-todo': 1 } }, diff --git a/package.json b/package.json index da1256623f..90c1dc77ec 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "start": "vite serve --clearScreen false", "preview": "vite preview", "build:website": "vite build", - "build": "rolldown -c", + "build": "tsdown", "test": "vitest run --project browser --project node", "test:watch": "vitest watch --project browser --project node", "test:ci": "vitest run", @@ -44,28 +44,28 @@ "format:check": "oxfmt --check", "eslint": "eslint --max-warnings 0 --cache --cache-location .cache/eslint --cache-strategy content", "eslint:fix": "node --run eslint -- --fix", - "typecheck": "tsc --build" + "typecheck": "tsgo --build" }, "devDependencies": { - "@eslint-react/eslint-plugin": "^3.0.0", + "@eslint-react/eslint-plugin": "^4.2.1", "@eslint/markdown": "^8.0.1", "@faker-js/faker": "^10.3.0", "@tanstack/react-router": "^1.166.7", "@tanstack/router-plugin": "^1.166.7", + "@tsdown/css": "^0.21.7", "@types/node": "^25.5.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@typescript/native-preview": "^7.0.0-dev.20260401.1", "@vitejs/plugin-react": "^6.0.1", - "@vitest/browser-playwright": "^4.1.0", - "@vitest/coverage-istanbul": "^4.1.0", - "@vitest/eslint-plugin": "^1.6.12", + "@vitest/browser-playwright": "^4.1.2", + "@vitest/coverage-istanbul": "^4.1.2", + "@vitest/eslint-plugin": "^1.6.13", "clsx": "^2.1.1", "ecij": "^0.4.1", "eslint": "^10.0.3", - "eslint-plugin-jest-dom": "^5.5.0", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-sonarjs": "^4.0.2", - "eslint-plugin-testing-library": "^7.16.0", "jspdf": "^4.2.0", "jspdf-autotable": "^5.0.7", "oxfmt": "0.43.0", @@ -73,12 +73,11 @@ "postcss": "^8.5.2", "react": "^19.2.4", "react-dom": "^19.2.4", - "rolldown": "1.0.0-rc.5", - "rolldown-plugin-dts": "^0.22.5", - "typescript": "~6.0.1-rc", - "typescript-eslint": "^8.57.0", - "vite": "^8.0.0", - "vitest": "^4.1.0", + "tsdown": "^0.21.7", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.0", + "vite": "^8.0.3", + "vitest": "^4.1.2", "vitest-browser-react": "^2.1.0" }, "peerDependencies": { @@ -86,6 +85,6 @@ "react-dom": "^19.2" }, "overrides": { - "typescript": "$typescript" + "eslint": "$eslint" } } diff --git a/src/EditCell.tsx b/src/EditCell.tsx index 750aebf78f..78fd789230 100644 --- a/src/EditCell.tsx +++ b/src/EditCell.tsx @@ -21,7 +21,7 @@ import type { * The event can be `stopPropagation()`ed halfway through, so they may not always bubble back up to the window, * so an alternative check must be used. The check must happen after the event can reach the "inside" container, * and not before it run to completion. `postTask`/`requestAnimationFrame` are the best way we know to achieve this. - * Usually we want click event handlers from parent components to access the latest commited values, + * Usually we want click event handlers from parent components to access the latest committed values, * so `mousedown` is used instead of `click`. * * We must also rely on React's event capturing/bubbling to handle elements rendered in a portal. diff --git a/src/globals.d.ts b/src/globals.d.ts index ee06893ccf..b8e33ced84 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -5,5 +5,5 @@ declare module 'react' { } } -// somehow required to make types work +// required to make types work export {}; diff --git a/test/globals.d.ts b/test/globals.d.ts index ca127afd20..73ff9c63b1 100644 --- a/test/globals.d.ts +++ b/test/globals.d.ts @@ -13,5 +13,5 @@ declare module 'vitest/browser' { } } -// somehow required to make types work +// required to make types work export {}; diff --git a/tsconfig.base.json b/tsconfig.base.json index 413281e904..444fec02db 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -12,7 +12,6 @@ "noImplicitReturns": true, "noUnusedLocals": true, "outDir": "./.cache/ts", - "strictNullChecks": true, // TODO: remove once `typescript-eslint` supports TS 6 "target": "esnext", "verbatimModuleSyntax": true } diff --git a/tsconfig.test.json b/tsconfig.test.json index 0a8d0b1547..03cb7253c7 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -5,6 +5,6 @@ "skipLibCheck": true, "types": ["vitest/globals"] }, - "include": ["test/**/*"], + "include": ["src/css.d.ts", "test/**/*"], "references": [{ "path": "tsconfig.src.json" }] } diff --git a/tsconfig.vite.json b/tsconfig.vite.json index 54f86f05d6..57b2050a8e 100644 --- a/tsconfig.vite.json +++ b/tsconfig.vite.json @@ -4,5 +4,5 @@ "lib": ["ESNext", "DOM"], "skipLibCheck": true }, - "include": ["package.json", "rolldown.config.ts", "vite.config.ts"] + "include": ["package.json", "tsdown.config.ts", "vite.config.ts"] } diff --git a/rolldown.config.ts b/tsdown.config.ts similarity index 50% rename from rolldown.config.ts rename to tsdown.config.ts index 23394ef5af..ac943e1849 100644 --- a/rolldown.config.ts +++ b/tsdown.config.ts @@ -1,28 +1,27 @@ -import { isAbsolute } from 'node:path'; import { ecij } from 'ecij/plugin'; -import { defineConfig } from 'rolldown'; -import { dts } from 'rolldown-plugin-dts'; +import { defineConfig } from 'tsdown'; import pkg from './package.json' with { type: 'json' }; export default defineConfig({ - input: './src/index.ts', - output: { - dir: 'lib', - cssEntryFileNames: 'styles.css', - sourcemap: true, - cleanDir: true - }, + outDir: 'lib', platform: 'neutral', - external: (id) => !id.startsWith('.') && !isAbsolute(id), + sourcemap: true, + deps: { + skipNodeModulesBundle: true + }, + css: { + fileName: 'styles.css' + }, + dts: { + build: true, + tsconfig: './tsconfig.src.json' + }, plugins: [ ecij({ // We add the package version as prefix to avoid style conflicts // between multiple versions of RDG on the same page classPrefix: `rdg-${pkg.version.replaceAll('.', '-')}-` - }), - dts({ - tsconfig: './tsconfig.src.json' }) ] }); diff --git a/website/router.tsx b/website/router.tsx index ad95476652..d61fa5c8c7 100644 --- a/website/router.tsx +++ b/website/router.tsx @@ -15,7 +15,7 @@ export const router = createRouter({ }); // Register the router instance for type safety -declare module '@tanstack/react-router' { +declare module '@tanstack/router-core' { interface Register { router: typeof router; }