diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000000..e14bbdedd87 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +dist +node_modules +.github +.changeset diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 00000000000..ed729220b11 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,41 @@ +module.exports = { + root: true, + extends: ['eslint:recommended', 'prettier'], + plugins: ['no-only-tests'], + env: { + browser: true, + node: true, + }, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + tsconfigRootDir: __dirname, + project: ['./tsconfig.json', './docs/tsconfig.json'], + }, + rules: { + 'no-only-tests/no-only-tests': 'warn', + }, + overrides: [ + { + files: ['**/*.ts'], + parser: '@typescript-eslint/parser', + extends: ['plugin:@typescript-eslint/recommended-type-checked'], + rules: { + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + }, + }, + { + files: ['**/env.d.ts'], + rules: { + '@typescript-eslint/triple-slash-reference': 'off', + }, + }, + ], +}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fbde0027a03..fbca595eb4a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,23 @@ jobs: - name: Test packages run: pnpm -r test:coverage + lint: + name: Lint code + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v3 + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'pnpm' + - run: pnpm i + - name: Generate types + working-directory: ./docs + run: pnpm astro sync + - name: Run linter + run: pnpm lint + pa11y: name: Check for accessibility issues runs-on: ubuntu-20.04 diff --git a/package.json b/package.json index e0609e2ae0e..cdc5518a21d 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "description": "", "scripts": { "build:examples": "pnpm --no-bail --workspace-concurrency 1 --filter '@example/*' build", + "lint": "eslint . --max-warnings 0", "size": "size-limit", "version": "pnpm changeset version && pnpm i --no-frozen-lockfile", "format": "prettier -w --cache --plugin prettier-plugin-astro ." @@ -14,10 +15,16 @@ "@changesets/changelog-github": "^0.4.8", "@changesets/cli": "^2.26.1", "@size-limit/file": "^8.2.4", + "@typescript-eslint/eslint-plugin": "^7.3.1", + "@typescript-eslint/parser": "^7.3.1", "astro": "^4.3.5", + "eslint": "^8.53.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-no-only-tests": "^3.1.0", "prettier": "^3.0.0", "prettier-plugin-astro": "^0.13.0", - "size-limit": "^8.2.4" + "size-limit": "^8.2.4", + "typescript": "^5.4.2" }, "packageManager": "pnpm@8.7.4", "size-limit": [ diff --git a/packages/starlight/__tests__/basics/config-errors.test.ts b/packages/starlight/__tests__/basics/config-errors.test.ts index dae9b599b6b..fee5602148f 100644 --- a/packages/starlight/__tests__/basics/config-errors.test.ts +++ b/packages/starlight/__tests__/basics/config-errors.test.ts @@ -74,7 +74,8 @@ test('parses valid config successfully', () => { test('errors if title is missing', () => { expect(() => - parseStarlightConfigWithFriendlyErrors({} as any) + // @ts-expect-error Testing invalid config + parseStarlightConfigWithFriendlyErrors({}) ).toThrowErrorMatchingInlineSnapshot( ` "[AstroUserError]: @@ -87,7 +88,8 @@ test('errors if title is missing', () => { test('errors if title value is not a string', () => { expect(() => - parseStarlightConfigWithFriendlyErrors({ title: 5 } as any) + // @ts-expect-error Testing invalid config + parseStarlightConfigWithFriendlyErrors({ title: 5 }) ).toThrowErrorMatchingInlineSnapshot( ` "[AstroUserError]: @@ -100,7 +102,8 @@ test('errors if title value is not a string', () => { test('errors with bad social icon config', () => { expect(() => - parseStarlightConfigWithFriendlyErrors({ title: 'Test', social: { unknown: '' } as any }) + // @ts-expect-error Testing invalid config + parseStarlightConfigWithFriendlyErrors({ title: 'Test', social: { unknown: '' } }) ).toThrowErrorMatchingInlineSnapshot( ` "[AstroUserError]: @@ -114,7 +117,8 @@ test('errors with bad social icon config', () => { test('errors with bad logo config', () => { expect(() => - parseStarlightConfigWithFriendlyErrors({ title: 'Test', logo: { html: '' } as any }) + // @ts-expect-error Testing invalid config + parseStarlightConfigWithFriendlyErrors({ title: 'Test', logo: { html: '' } }) ).toThrowErrorMatchingInlineSnapshot( ` "[AstroUserError]: @@ -131,7 +135,8 @@ test('errors with bad head config', () => { expect(() => parseStarlightConfigWithFriendlyErrors({ title: 'Test', - head: [{ tag: 'unknown', attrs: { prop: null }, content: 20 } as any], + // @ts-expect-error Testing invalid config + head: [{ tag: 'unknown', attrs: { prop: null }, content: 20 }], }) ).toThrowErrorMatchingInlineSnapshot( ` @@ -150,7 +155,8 @@ test('errors with bad sidebar config', () => { expect(() => parseStarlightConfigWithFriendlyErrors({ title: 'Test', - sidebar: [{ label: 'Example', href: '/' } as any], + // @ts-expect-error Testing invalid config + sidebar: [{ label: 'Example', href: '/' }], }) ).toThrowErrorMatchingInlineSnapshot( ` @@ -173,9 +179,10 @@ test('errors with bad nested sidebar config', () => { label: 'Example', items: [ { label: 'Nested Example 1', link: '/' }, + // @ts-expect-error Testing invalid config { label: 'Nested Example 2', link: true }, ], - } as any, + }, ], }) ).toThrowErrorMatchingInlineSnapshot(` diff --git a/packages/starlight/__tests__/basics/git.test.ts b/packages/starlight/__tests__/basics/git.test.ts index 1f194f285c7..4847b7bd912 100644 --- a/packages/starlight/__tests__/basics/git.test.ts +++ b/packages/starlight/__tests__/basics/git.test.ts @@ -56,13 +56,13 @@ describe('getNewestCommitDate', () => { test('throws when failing to retrieve the git history for a file', () => { expect(() => getNewestCommitDate(getFilePath('../not-a-starlight-test-repo/test.md'))).toThrow( - /^Failed to retrieve the git history for file "[/\\-\w ]+\/test\.md"/ + /^Failed to retrieve the git history for file "[/\\:\w -]+[/\\]test\.md"/ ); }); test('throws when trying to get the history of a non-existing or untracked file', () => { const expectedError = - /^Failed to validate the timestamp for file "[/\\-\w ]+\/(?:unknown|untracked)\.md"$/; + /^Failed to validate the timestamp for file "[/\\:\w -]+[/\\](?:unknown|untracked)\.md"$/; writeFile('untracked.md', 'content'); expect(() => getNewestCommitDate(getFilePath('unknown.md'))).toThrow(expectedError); @@ -94,17 +94,17 @@ function makeTestRepo() { return { // The `dateStr` argument should be in the `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SSZ` format. - commitAllChanges(message: string, dateStr: ISODate) { + commitAllChanges: (message: string, dateStr: ISODate) => { const date = dateStr.endsWith('Z') ? dateStr : `${dateStr}T00:00:00Z`; runInRepo('git', ['add', '-A']); // This sets both the author and committer dates to the provided date. runInRepo('git', ['commit', '-m', message, '--date', date], { GIT_COMMITTER_DATE: date }); }, - getFilePath(name: string) { + getFilePath: (name: string) => { return join(repoPath, name); }, - writeFile(name: string, content: string) { + writeFile: (name: string, content: string) => { writeFileSync(join(repoPath, name), content); }, }; diff --git a/packages/starlight/__tests__/basics/i18n.test.ts b/packages/starlight/__tests__/basics/i18n.test.ts index 1ab6262fa6c..eee73cb0ca9 100644 --- a/packages/starlight/__tests__/basics/i18n.test.ts +++ b/packages/starlight/__tests__/basics/i18n.test.ts @@ -10,6 +10,7 @@ describe('pickLang', () => { }); test('returns undefined for unknown languages', () => { - expect(pickLang(dictionary, 'ar' as any)).toBeUndefined(); + // @ts-expect-error Testing invalid input + expect(pickLang(dictionary, 'ar')).toBeUndefined(); }); }); diff --git a/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts b/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts index 3ab5b8f5fcb..246163303c2 100644 --- a/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts +++ b/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts @@ -1,4 +1,4 @@ -import { assert, expect, test, vi } from 'vitest'; +import { expect, test, vi } from 'vitest'; import { generateStarlightPageRouteData, type StarlightPageProps, @@ -23,7 +23,7 @@ const starlightPageProps: StarlightPageProps = { test('throws a validation error if a built-in field required by the user schema is not passed down', async () => { // The first line should be a user-friendly error message describing the exact issue and the second line should be // the missing description field. - expect(() => + await expect(() => generateStarlightPageRouteData({ props: starlightPageProps, url: new URL('https://example.com/test-slug'), diff --git a/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts b/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts index 95f611e255c..e473f28ecb0 100644 --- a/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts +++ b/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts @@ -210,7 +210,7 @@ test('supports `items` field for sidebar groups', async () => { }); test('throws error if sidebar is malformated', async () => { - expect(() => + await expect(() => generateStarlightPageRouteData({ props: { ...starlightPageProps, @@ -236,7 +236,7 @@ test('throws error if sidebar is malformated', async () => { test('throws error if sidebar uses wrong literal for entry type', async () => { // This test also makes sure we show a helpful error for incorrect literals. - expect(() => + await expect(() => generateStarlightPageRouteData({ props: { ...starlightPageProps, @@ -441,7 +441,7 @@ test('disables table of contents for splash template', async () => { }); test('hides the sidebar if the `hasSidebar` option is not specified and the splash template is used', async () => { - const { hasSidebar, ...otherProps } = starlightPageProps; + const { hasSidebar: _, ...otherProps } = starlightPageProps; const data = await generateStarlightPageRouteData({ props: { ...otherProps, diff --git a/packages/starlight/__tests__/i18n-root-locale/routing.test.ts b/packages/starlight/__tests__/i18n-root-locale/routing.test.ts index c6824349091..d3015c64cbc 100644 --- a/packages/starlight/__tests__/i18n-root-locale/routing.test.ts +++ b/packages/starlight/__tests__/i18n-root-locale/routing.test.ts @@ -81,7 +81,7 @@ test('fallback routes use fallback entry last updated dates', () => { }); expect(getNewestCommitDate).toHaveBeenCalledOnce(); - expect(getNewestCommitDate.mock.lastCall?.[0]).toMatch( + expect(getNewestCommitDate.mock.lastCall?.[0]?.replace(/\\/g, '/')).toMatch( /src\/content\/docs\/guides\/authoring-content.md$/ // ^ no `en/` prefix ); diff --git a/packages/starlight/__tests__/plugins/config.test.ts b/packages/starlight/__tests__/plugins/config.test.ts index 0b866b1dacd..adbab659a37 100644 --- a/packages/starlight/__tests__/plugins/config.test.ts +++ b/packages/starlight/__tests__/plugins/config.test.ts @@ -52,7 +52,7 @@ test('receives the user provided configuration including the plugins list', asyn describe('validation', () => { test('validates starlight configuration before running plugins', async () => { - expect( + await expect( async () => await runPlugins( // @ts-expect-error - invalid sidebar config. @@ -64,7 +64,7 @@ describe('validation', () => { }); test('validates plugins configuration before running them', async () => { - expect( + await expect( async () => await runPlugins( { title: 'Test Docs' }, @@ -76,7 +76,7 @@ describe('validation', () => { }); test('validates configuration updates from plugins do not update the `plugins` config key', async () => { - expect( + await expect( async () => await runPlugins( { title: 'Test Docs' }, @@ -99,7 +99,7 @@ describe('validation', () => { }); test('validates configuration updates from plugins', async () => { - expect( + await expect( async () => await runPlugins( { title: 'Test Docs' }, diff --git a/packages/starlight/__tests__/remark-rehype/asides.test.ts b/packages/starlight/__tests__/remark-rehype/asides.test.ts index ba3548c9531..671c50eb22c 100644 --- a/packages/starlight/__tests__/remark-rehype/asides.test.ts +++ b/packages/starlight/__tests__/remark-rehype/asides.test.ts @@ -32,7 +32,7 @@ test('generates