diff --git a/packages/hydrogen-react/eslint.config.cjs b/packages/hydrogen-react/eslint.config.cjs index 0acfd6828..1ed713d8b 100644 --- a/packages/hydrogen-react/eslint.config.cjs +++ b/packages/hydrogen-react/eslint.config.cjs @@ -1,67 +1,81 @@ -const typescriptParser = require('@typescript-eslint/parser'); -const typescriptPlugin = require('@typescript-eslint/eslint-plugin'); -const eslintJs = require('@eslint/js'); -const eslintCommentsPlugin = require('@eslint-community/eslint-plugin-eslint-comments'); -const reactPlugin = require('eslint-plugin-react'); -const reactHooksPlugin = require('eslint-plugin-react-hooks'); -const jsxA11yPlugin = require('eslint-plugin-jsx-a11y'); -const prettierPlugin = require('eslint-plugin-prettier'); -const nodePlugin = require('eslint-plugin-node'); -const importPlugin = require('eslint-plugin-import'); -const jestPlugin = require('eslint-plugin-jest'); -const tsdocPlugin = require('eslint-plugin-tsdoc'); -const simpleImportSortPlugin = require('eslint-plugin-simple-import-sort'); -const {fixupPluginRules} = require('@eslint/compat'); +const {fixupConfigRules, fixupPluginRules} = require('@eslint/compat'); +const eslintComments = require('eslint-plugin-eslint-comments'); +const react = require('eslint-plugin-react'); +const reactHooks = require('eslint-plugin-react-hooks'); +const jsxA11Y = require('eslint-plugin-jsx-a11y'); +const tsdoc = require('eslint-plugin-tsdoc'); +const globals = require('globals'); +const tsParser = require('@typescript-eslint/parser'); +const jest = require('eslint-plugin-jest'); +const simpleImportSort = require('eslint-plugin-simple-import-sort'); +const js = require('@eslint/js'); +const {FlatCompat} = require('@eslint/eslintrc'); + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); -/** @type {import('eslint').Flat.Config[]} */ module.exports = [ - // Base configuration { ignores: [ - 'node_modules/', - 'build/', - '*.graphql.d.ts', - '*.graphql.ts', + '**/node_modules/', + '**/build/', + '**/*.graphql.d.ts', + '**/*.graphql.ts', '**/storefront-api-types.d.ts', '**/customer-account-api-types.d.ts', '**/codegen.ts', - '**/dist/**', - '**/coverage/**', - '**/docs/**', + '**/dist/**/*', + '**/coverage/**/*', + '**/docs/**/*', '**/.eslintrc.cjs', '**/src/*.example.tsx', '**/src/*.example.ts', '**/src/*.example.jsx', '**/src/*.example.js', + '**/eslint.config.cjs', + '**/scripts/**/*', ], }, - // Default configuration for all files + ...fixupConfigRules( + compat.extends( + 'plugin:node/recommended', + 'plugin:import/recommended', + 'plugin:import/typescript', + 'eslint:recommended', + 'plugin:eslint-comments/recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:jsx-a11y/recommended', + ), + ), { - files: ['**/*.{js,jsx,ts,tsx}'], - ...eslintJs.configs.recommended, plugins: { - 'eslint-comments': eslintCommentsPlugin, - react: reactPlugin, - 'react-hooks': reactHooksPlugin, - 'jsx-a11y': jsxA11yPlugin, - prettier: prettierPlugin, - node: fixupPluginRules(nodePlugin), - import: importPlugin, - '@typescript-eslint': typescriptPlugin, - tsdoc: tsdocPlugin, - 'simple-import-sort': simpleImportSortPlugin, + 'eslint-comments': fixupPluginRules(eslintComments), + react: fixupPluginRules(react), + 'react-hooks': fixupPluginRules(reactHooks), + 'jsx-a11y': fixupPluginRules(jsxA11Y), + tsdoc, }, languageOptions: { - parser: typescriptParser, + parser: tsParser, parserOptions: { - project: ['./tsconfig.json'], - tsconfigRootDir: '.', + projectService: { + allowDefaultProject: ['vite.config.ts', 'vitest.setup.ts'], + }, + tsconfigRootDir: __dirname, ecmaFeatures: { jsx: true, }, - sourceType: 'module', }, - ecmaVersion: 2021, + globals: { + ...globals.browser, + ...globals.node, + }, + ecmaVersion: 2020, + sourceType: 'module', }, linterOptions: { reportUnusedDisableDirectives: false, @@ -75,27 +89,29 @@ module.exports = [ }, }, rules: { - // TypeScript - '@typescript-eslint/naming-convention': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-empty-function': 'off', - '@typescript-eslint/no-empty-interface': 'off', - '@typescript-eslint/no-explicit-any': 'error', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-unused-vars': 'off', - - // React + '@shopify/jsx-no-complex-expressions': 'off', + '@shopify/jsx-no-hardcoded-content': 'off', + 'jsx-a11y/control-has-associated-label': 'off', + 'jsx-a11y/label-has-for': 'off', + 'no-use-before-define': 'off', + 'no-warning-comments': 'off', + 'object-shorthand': [ + 'error', + 'always', + { + avoidQuotes: true, + }, + ], 'react/display-name': 'off', 'react/no-array-index-key': 'warn', 'react/prop-types': 'off', 'react/react-in-jsx-scope': 'off', - 'react-hooks/exhaustive-deps': 'error', - - // A11y - 'jsx-a11y/control-has-associated-label': 'off', - 'jsx-a11y/label-has-for': 'off', - - // Node + 'eslint-comments/no-unused-disable': 'off', + 'jest/no-disabled-tests': 'off', + 'jest/no-export': 'off', + 'no-console': 'off', + 'no-constant-condition': 'off', + 'tsdoc/syntax': 'error', 'node/no-extraneous-import': [ 'error', { @@ -123,65 +139,106 @@ module.exports = [ ignores: [], }, ], - 'node/no-missing-import': 'off', - - // Import/Export + 'prefer-const': [ + 'warn', + { + destructuring: 'all', + }, + ], + '@typescript-eslint/naming-convention': 'off', 'import/extensions': ['error', 'ignorePackages'], 'import/no-unresolved': 'off', - 'simple-import-sort/exports': 'error', - - // General - 'no-console': 'off', - 'no-use-before-define': 'off', - 'no-warning-comments': 'off', - 'no-constant-condition': 'off', - 'object-shorthand': ['error', 'always', {avoidQuotes: true}], - 'prefer-const': ['warn', {destructuring: 'all'}], - - // Testing - 'jest/no-disabled-tests': 'off', - 'jest/no-export': 'off', - - // Other - 'eslint-comments/no-unused-disable': 'off', - 'tsdoc/syntax': 'error', + 'node/no-missing-import': 'off', + 'react-hooks/exhaustive-deps': 'error', }, }, - // Test files configuration + ...compat.extends('plugin:jest/recommended').map((config) => ({ + ...config, + files: ['**/*.test.*'], + })), { - files: ['*.test.*'], + files: ['**/*.test.*'], plugins: { - jest: jestPlugin, - }, - rules: { - ...jestPlugin.configs.recommended.rules, + jest, }, languageOptions: { globals: { - jest: true, - expect: true, - describe: true, - it: true, - beforeEach: true, - afterEach: true, + ...globals.node, + ...globals.jest, }, }, }, - // Server files configuration { - files: ['*.server.*'], + files: ['**/*.server.*'], rules: { 'react-hooks/rules-of-hooks': 'off', }, }, - // TypeScript files configuration + ...fixupConfigRules( + compat.extends( + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + ), + ).map((config) => ({ + ...config, + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parser: tsParser, + parserOptions: { + projectService: { + allowDefaultProject: ['vite.config.ts', 'vitest.setup.ts'], + }, + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + }, + }, + })), { - files: ['*.ts', '*.tsx'], - plugins: { - '@typescript-eslint': typescriptPlugin, + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parser: tsParser, + parserOptions: { + projectService: { + allowDefaultProject: ['vite.config.ts', 'vitest.setup.ts'], + }, + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + }, }, rules: { '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/prefer-promise-reject-errors': 'off', + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: 'default', + format: ['camelCase', 'PascalCase', 'UPPER_CASE'], + leadingUnderscore: 'allowSingleOrDouble', + trailingUnderscore: 'allowSingleOrDouble', + }, + { + selector: 'typeLike', + format: ['PascalCase'], + }, + { + selector: 'typeParameter', + format: ['PascalCase'], + leadingUnderscore: 'allow', + }, + { + selector: 'interface', + format: ['PascalCase'], + }, + { + selector: 'property', + format: null, + }, + ], '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-empty-interface': 'off', '@typescript-eslint/no-explicit-any': 'off', @@ -190,7 +247,6 @@ module.exports = [ 'react/prop-types': 'off', }, }, - // Example files configuration { files: ['src/*.example.?(ts|js|tsx|jsx)'], rules: { @@ -198,17 +254,15 @@ module.exports = [ 'node/no-extraneous-require': 'off', }, }, - // Index.ts configuration { files: ['src/index.ts'], plugins: { - 'simple-import-sort': simpleImportSortPlugin, + 'simple-import-sort': simpleImportSort, }, rules: { 'simple-import-sort/exports': 'error', }, }, - // Source files configuration (excluding tests, examples, etc.) { files: ['src/**/!(*.test|*.example|*.doc|*.stories).?(ts|js|tsx|jsx)'], rules: { diff --git a/packages/hydrogen-react/package.json b/packages/hydrogen-react/package.json index a2c62d527..987a417c8 100644 --- a/packages/hydrogen-react/package.json +++ b/packages/hydrogen-react/package.json @@ -133,6 +133,7 @@ "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^4.4.1", "@eslint/compat": "^1.2.5", + "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.18.0", "@faker-js/faker": "^7.6.0", "@graphql-codegen/add": "^5.0.1", @@ -161,6 +162,7 @@ "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-tsdoc": "^0.4.0", + "globals": "^15.14.0", "happy-dom": "8.7.2", "npm-run-all": "^4.1.5", "prettier": "^3.4.2", diff --git a/packages/hydrogen-react/src/CartCheckoutButton.tsx b/packages/hydrogen-react/src/CartCheckoutButton.tsx index c26f51252..41704e072 100644 --- a/packages/hydrogen-react/src/CartCheckoutButton.tsx +++ b/packages/hydrogen-react/src/CartCheckoutButton.tsx @@ -43,7 +43,7 @@ export function CartCheckoutButton( // This is only for documentation purposes, and it is not used in the code. // we ignore this issue because it makes the documentation look better than the equivalent `type` that it wants us to convert to -// eslint-disable-next-line @typescript-eslint/no-empty-interface +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface CartCheckoutButtonPropsForDocs< AsType extends React.ElementType = 'button', > extends Omit, 'onClick'> {} diff --git a/packages/hydrogen-react/src/Image.tsx b/packages/hydrogen-react/src/Image.tsx index 11ac0ae0c..6f356d10a 100644 --- a/packages/hydrogen-react/src/Image.tsx +++ b/packages/hydrogen-react/src/Image.tsx @@ -1,6 +1,5 @@ /* eslint-disable eslint-comments/disable-enable-pair */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable hydrogen/prefer-image-component */ import * as React from 'react'; import type {PartialDeep} from 'type-fest'; import type {Image as ImageType} from './storefront-api-types.js'; diff --git a/packages/hydrogen-react/src/ModelViewer.tsx b/packages/hydrogen-react/src/ModelViewer.tsx index b3c11c154..1e8bbc31d 100644 --- a/packages/hydrogen-react/src/ModelViewer.tsx +++ b/packages/hydrogen-react/src/ModelViewer.tsx @@ -105,7 +105,7 @@ export function ModelViewer(props: ModelViewerProps): JSX.Element | null { }, ); - return () => { + return (): void => { if (modelViewer == null) { return; } diff --git a/packages/hydrogen-react/src/analytics-schema-custom-storefront-customer-tracking.ts b/packages/hydrogen-react/src/analytics-schema-custom-storefront-customer-tracking.ts index e66e51245..a3f2665b7 100644 --- a/packages/hydrogen-react/src/analytics-schema-custom-storefront-customer-tracking.ts +++ b/packages/hydrogen-react/src/analytics-schema-custom-storefront-customer-tracking.ts @@ -200,7 +200,7 @@ export function addToCart( ): ShopifyMonorailEvent[] { const addToCartPayload = payload; const cartToken = parseGid(addToCartPayload.cartId); - const cart_token = cartToken?.id ? `${cartToken.id}` : null; + const cartTokenId = cartToken?.id ? `${cartToken.id}` : null; return [ schemaWrapper( SCHEMA_ID, @@ -208,7 +208,7 @@ export function addToCart( { event_name: PRODUCT_ADDED_TO_CART_EVENT_NAME, customerId: addToCartPayload.customerId, - cart_token, + cart_token: cartTokenId, total_value: addToCartPayload.totalValue, products: formatProductPayload(addToCartPayload.products), customer_id: parseInt( diff --git a/packages/hydrogen-react/src/analytics.test.ts b/packages/hydrogen-react/src/analytics.test.ts index a44ddb918..67186f542 100644 --- a/packages/hydrogen-react/src/analytics.test.ts +++ b/packages/hydrogen-react/src/analytics.test.ts @@ -26,6 +26,7 @@ const createFetchSpy = ({ getShopDomainMonorailEndpoint(shopDomain); if (input === MONORAIL_ENDPOINT || input === shopDomainMonorailEndpoint) { if (init?.body) { + // eslint-disable-next-line @typescript-eslint/no-base-to-string const reqData = init.body.toString(); const data = JSON.parse(reqData || '{}') as unknown; diff --git a/packages/hydrogen-react/src/cart-hooks.tsx b/packages/hydrogen-react/src/cart-hooks.tsx index 73853f008..4de3459d6 100644 --- a/packages/hydrogen-react/src/cart-hooks.tsx +++ b/packages/hydrogen-react/src/cart-hooks.tsx @@ -81,6 +81,7 @@ export function useInstantCheckout() { }); if (errors) { + // eslint-disable-next-line @typescript-eslint/no-base-to-string updateError(errors.toString()); updateCart(undefined); updateCheckoutUrl(undefined); diff --git a/packages/hydrogen-react/src/optionValueDecoder.ts b/packages/hydrogen-react/src/optionValueDecoder.ts index 65857d174..feb5eeb27 100644 --- a/packages/hydrogen-react/src/optionValueDecoder.ts +++ b/packages/hydrogen-react/src/optionValueDecoder.ts @@ -72,6 +72,7 @@ export const isOptionValueCombinationInEncodedVariant: IsOptionValueCombinationI type EncodedVariantField = | Product['encodedVariantAvailability'] + // eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents | Product['encodedVariantExistence']; type DecodedOptionValues = number[][]; diff --git a/packages/hydrogen-react/src/parse-metafield.test.ts b/packages/hydrogen-react/src/parse-metafield.test.ts index 735d8cf7d..10bbdaff7 100644 --- a/packages/hydrogen-react/src/parse-metafield.test.ts +++ b/packages/hydrogen-react/src/parse-metafield.test.ts @@ -122,7 +122,7 @@ describe(`parseMetafield`, () => { expect((parsed?.parsedValue as {test: string})?.test === 'testing').toBe( true, ); - expectType(parsed?.parsedValue); + expectType(parsed?.parsedValue); // with an extra generic, we can use that as the type instead const parsedOtherType = diff --git a/packages/hydrogen-react/src/storefront-api-response.types.ts b/packages/hydrogen-react/src/storefront-api-response.types.ts index 59527f16f..afea0e192 100644 --- a/packages/hydrogen-react/src/storefront-api-response.types.ts +++ b/packages/hydrogen-react/src/storefront-api-response.types.ts @@ -15,7 +15,6 @@ type StorefrontApiExtensions = { | 'ACCESS_DENIED' | 'SHOP_INACTIVE' | 'INTERNAL_SERVER_ERROR' - // eslint-disable-next-line @typescript-eslint/ban-types -- This enables autocomplete for the defined error codes we have, but also allows for any string to be used, too. https://github.com/ascorbic/unpic-img/blob/c5de1bf01abd85ef7e8ac07c40c19128a3097e69/packages/core/src/core.ts#L40 Thanks Matt Kane! | (string & {}); }; diff --git a/packages/hydrogen-react/src/useMoney.tsx b/packages/hydrogen-react/src/useMoney.tsx index 21d89567b..b2f002fa7 100644 --- a/packages/hydrogen-react/src/useMoney.tsx +++ b/packages/hydrogen-react/src/useMoney.tsx @@ -158,36 +158,37 @@ export function useMoney(money: MoneyV2): UseMoneyValue { // create formatters if they are going to be used. const lazyFormatters = useMemo( () => ({ - original: () => money, - currencyCode: () => money.currencyCode, + original: (): MoneyV2 => money, + currencyCode: (): CurrencyCode => money.currencyCode, - localizedString: () => defaultFormatter().format(amount), + localizedString: (): string => defaultFormatter().format(amount), - parts: () => defaultFormatter().formatToParts(amount), + parts: (): Intl.NumberFormatPart[] => + defaultFormatter().formatToParts(amount), - withoutTrailingZeros: () => + withoutTrailingZeros: (): string => amount % 1 === 0 ? withoutTrailingZerosFormatter().format(amount) : defaultFormatter().format(amount), - withoutTrailingZerosAndCurrency: () => + withoutTrailingZerosAndCurrency: (): string => amount % 1 === 0 ? withoutTrailingZerosOrCurrencyFormatter().format(amount) : withoutCurrencyFormatter().format(amount), - currencyName: () => + currencyName: (): string => nameFormatter().formatToParts(amount).find(isPartCurrency)?.value ?? money.currencyCode, // e.g. "US dollars" - currencySymbol: () => + currencySymbol: (): string => defaultFormatter().formatToParts(amount).find(isPartCurrency)?.value ?? money.currencyCode, // e.g. "USD" - currencyNarrowSymbol: () => + currencyNarrowSymbol: (): string => narrowSymbolFormatter().formatToParts(amount).find(isPartCurrency) ?.value ?? '', // e.g. "$" - amount: () => + amount: (): string => defaultFormatter() .formatToParts(amount) .filter((part) => diff --git a/packages/hydrogen-react/src/useSelectOptionInUrlParam.test.tsx b/packages/hydrogen-react/src/useSelectOptionInUrlParam.test.tsx index 7369c10bf..ee4512296 100644 --- a/packages/hydrogen-react/src/useSelectOptionInUrlParam.test.tsx +++ b/packages/hydrogen-react/src/useSelectOptionInUrlParam.test.tsx @@ -2,9 +2,9 @@ import {vi, afterEach, describe, expect, it} from 'vitest'; import {renderHook} from '@testing-library/react'; import {useSelectedOptionInUrlParam} from './useSelectedOptionInUrlParam.js'; -type mockOptions = {search?: string; pathname?: string}; +type MockOptions = {search?: string; pathname?: string}; -const globalMocks = ({search = '', pathname = ''}: mockOptions) => { +const globalMocks = ({search = '', pathname = ''}: MockOptions) => { let currentSearch = search; let currentPathname = pathname; @@ -33,7 +33,7 @@ const globalMocks = ({search = '', pathname = ''}: mockOptions) => { }; }; -const mockGlobals = (options?: mockOptions) => { +const mockGlobals = (options?: MockOptions) => { const mocks = globalMocks(options || {}); vi.stubGlobal('location', mocks.location); diff --git a/packages/hydrogen-react/vite.config.ts b/packages/hydrogen-react/vite.config.ts index faa642bf3..c12877d56 100644 --- a/packages/hydrogen-react/vite.config.ts +++ b/packages/hydrogen-react/vite.config.ts @@ -113,9 +113,14 @@ export default defineConfig(({mode, isSsrBuild}) => { }; }); +type PackageJson = { + dependencies: Record; + peerDependencies: Record; +}; + const externals = [ - ...Object.keys(packageJson.dependencies), - ...Object.keys(packageJson.peerDependencies), + ...Object.keys((packageJson as PackageJson).dependencies), + ...Object.keys((packageJson as PackageJson).peerDependencies), 'react/jsx-runtime', 'worktop/cookie', ]; diff --git a/templates/skeleton/eslint.config.js b/templates/skeleton/eslint.config.js index e95eb6709..d18d5d6c5 100644 --- a/templates/skeleton/eslint.config.js +++ b/templates/skeleton/eslint.config.js @@ -89,7 +89,6 @@ export default [ // Other plugin rules '@shopify/jsx-no-complex-expressions': 'off', '@shopify/jsx-no-hardcoded-content': 'off', - 'hydrogen/prefer-image-component': 'off', }, },