Set up eslint for wardrobe-2020

Ok cool, I have just not been running any of this since moving out of
impress-2020, but now that we're doing serious JS work in here it's time
to turn it back on!!

1. Install eslint and the plugins we use
2. Set up a `yarn lint` command
3. Set up a git hook via husky to lint on pre-commit
4. Fix/disable all the lint errors!
This commit is contained in:
Emi Matchu 2023-11-02 18:03:15 -07:00
parent 629706a182
commit 494f82601f
15 changed files with 1760 additions and 86 deletions

45
.eslintrc.json Normal file
View file

@ -0,0 +1,45 @@
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:jsx-a11y/recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "react", "react-hooks", "jsx-a11y"],
"env": {
"browser": true,
"es2021": true
},
"globals": {
"process": true // For process.env["NODE_ENV"]
},
"rules": {
"no-console": [
"warn",
{
"allow": ["debug", "info", "warn", "error"]
}
],
"import/first": "off",
"import/no-webpack-loader-syntax": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{
"varsIgnorePattern": "^unused",
"argsIgnorePattern": "^_+$|^e$"
}
],
"react/no-unescaped-entities": ["error", { "forbid": [">", "}"] }],
// We have some React.forwardRefs that trigger this, not sure how to improve
"react/display-name": "off",
"react/prop-types": "off"
},
"settings": {
"react": {
"version": "detect"
}
}
}

4
.husky/pre-commit Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn lint --max-warnings=0 --fix

View file

@ -5,6 +5,8 @@ import { AppProvider, ItemPageOutfitPreview } from "./wardrobe-2020";
const rootNode = document.querySelector("#outfit-preview-root"); const rootNode = document.querySelector("#outfit-preview-root");
const itemId = rootNode.getAttribute("data-item-id"); const itemId = rootNode.getAttribute("data-item-id");
// TODO: Use the new React 18 APIs instead!
// eslint-disable-next-line react/no-deprecated
ReactDOM.render( ReactDOM.render(
<AppProvider> <AppProvider>
<ItemPageOutfitPreview itemId={itemId} /> <ItemPageOutfitPreview itemId={itemId} />

View file

@ -4,6 +4,8 @@ import ReactDOM from "react-dom";
import { AppProvider, WardrobePage } from "./wardrobe-2020"; import { AppProvider, WardrobePage } from "./wardrobe-2020";
const rootNode = document.querySelector("#wardrobe-2020-root"); const rootNode = document.querySelector("#wardrobe-2020-root");
// TODO: Use the new React 18 APIs instead!
// eslint-disable-next-line react/no-deprecated
ReactDOM.render( ReactDOM.render(
<AppProvider> <AppProvider>
<WardrobePage /> <WardrobePage />

View file

@ -285,7 +285,7 @@ function ItemActionButton({ icon, label, to, onClick }) {
); );
} }
function LinkOrButton({ href, component = Button, ...props }) { function LinkOrButton({ href, component, ...props }) {
const ButtonComponent = component; const ButtonComponent = component;
if (href != null) { if (href != null) {
return <ButtonComponent as="a" href={href} {...props} />; return <ButtonComponent as="a" href={href} {...props} />;

View file

@ -227,6 +227,8 @@ function SearchResultItem({
); );
return ( return (
// We're wrapping the control inside the label, which works just fine!
// eslint-disable-next-line jsx-a11y/label-has-associated-control
<label> <label>
<VisuallyHidden <VisuallyHidden
as="input" as="input"

View file

@ -242,6 +242,8 @@ function SearchToolbar({
)} )}
<Input <Input
background={background} background={background}
// TODO: How to improve a11y here?
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={autoFocus} autoFocus={autoFocus}
{...inputProps} {...inputProps}
/> />

View file

@ -4,7 +4,6 @@ import { useToast } from "@chakra-ui/react";
import { emptySearchQuery } from "./SearchToolbar"; import { emptySearchQuery } from "./SearchToolbar";
import ItemsAndSearchPanels from "./ItemsAndSearchPanels"; import ItemsAndSearchPanels from "./ItemsAndSearchPanels";
import SearchFooter from "./SearchFooter"; import SearchFooter from "./SearchFooter";
import SupportOnly from "./support/SupportOnly";
import useOutfitSaving from "./useOutfitSaving"; import useOutfitSaving from "./useOutfitSaving";
import useOutfitState, { OutfitStateContext } from "./useOutfitState"; import useOutfitState, { OutfitStateContext } from "./useOutfitState";
import WardrobePageLayout from "./WardrobePageLayout"; import WardrobePageLayout from "./WardrobePageLayout";
@ -111,36 +110,4 @@ function WardrobePage() {
); );
} }
/**
* SavedOutfitMetaTags renders the meta tags that we use to render pretty
* share cards for social media for saved outfits!
*/
function SavedOutfitMetaTags({ outfitState }) {
const updatedAtTimestamp = Math.floor(
new Date(outfitState.updatedAt).getTime() / 1000,
);
const imageUrl =
`https://impress-outfit-images.openneo.net/outfits` +
`/${encodeURIComponent(outfitState.id)}` +
`/v/${encodeURIComponent(updatedAtTimestamp)}` +
`/600.png`;
return (
<>
<meta
property="og:title"
content={outfitState.name || "Untitled outfit"}
/>
<meta property="og:type" content="website" />
<meta property="og:image" content={imageUrl} />
<meta property="og:url" content={outfitState.url} />
<meta property="og:site_name" content="Dress to Impress" />
<meta
property="og:description"
content="A custom Neopets outfit, designed on Dress to Impress!"
/>
</>
);
}
export default WardrobePage; export default WardrobePage;

View file

@ -96,7 +96,7 @@ function useOutfitSaving(outfitState, dispatchToOutfit) {
// It's important that this callback _doesn't_ change when the outfit // It's important that this callback _doesn't_ change when the outfit
// changes, so that the auto-save effect is only responding to the // changes, so that the auto-save effect is only responding to the
// debounced state! // debounced state!
[saveOutfitMutation.mutateAsync, pathname, navigate, toast], [saveOutfitMutation, pathname, navigate, toast],
); );
const saveOutfit = React.useCallback( const saveOutfit = React.useCallback(

View file

@ -250,13 +250,9 @@ function useOutfitState() {
// Keep the URL up-to-date. // Keep the URL up-to-date.
const path = buildOutfitPath(outfitState); const path = buildOutfitPath(outfitState);
React.useEffect(() => { React.useEffect(() => {
console.debug( console.debug(`[useOutfitState] Navigating to latest outfit path:`, path);
`[useOutfitState] Navigating to latest outfit path:`,
path,
outfitState,
);
navigate(path, { replace: true }); navigate(path, { replace: true });
}, [path]); }, [path, navigate]);
return { return {
loading: outfitLoading || itemsLoading, loading: outfitLoading || itemsLoading,
@ -393,6 +389,9 @@ function useParseOutfitUrl() {
// stable object! // stable object!
const memoizedOutfitState = React.useMemo( const memoizedOutfitState = React.useMemo(
() => readOutfitStateFromSearchParams(location.pathname, mergedParams), () => readOutfitStateFromSearchParams(location.pathname, mergedParams),
// TODO: This hook is reliable as-is, I think… but is there a simpler way
// to make it obvious that it is?
// eslint-disable-next-line react-hooks/exhaustive-deps
[location.pathname, mergedParams.toString()], [location.pathname, mergedParams.toString()],
); );

View file

@ -1,6 +1,5 @@
import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client"; import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
import { loadErrorMessages, loadDevMessages } from "@apollo/client/dev"; import { loadErrorMessages, loadDevMessages } from "@apollo/client/dev";
import { setContext } from "@apollo/client/link/context";
import { createPersistedQueryLink } from "apollo-link-persisted-queries"; import { createPersistedQueryLink } from "apollo-link-persisted-queries";
// Use Apollo's error messages in development. // Use Apollo's error messages in development.

View file

@ -1,6 +1,5 @@
import React from "react"; import React from "react";
import { Box, Button, Flex, Select } from "@chakra-ui/react"; import { Box, Button, Flex, Select } from "@chakra-ui/react";
import { useSearchParams } from "react-router-dom";
function PaginationToolbar({ function PaginationToolbar({
isLoading, isLoading,
@ -71,42 +70,6 @@ function PaginationToolbar({
); );
} }
export function useRouterPagination(totalCount, numPerPage) {
const [searchParams, setSearchParams] = useSearchParams();
const currentOffset = parseInt(searchParams.get("offset")) || 0;
const currentPageIndex = Math.floor(currentOffset / numPerPage);
const currentPageNumber = currentPageIndex + 1;
const numTotalPages = totalCount ? Math.ceil(totalCount / numPerPage) : null;
const buildPageUrl = React.useCallback(
(newPageNumber) => {
setSearchParams((newParams) => {
const newPageIndex = newPageNumber - 1;
const newOffset = newPageIndex * numPerPage;
newParams.set("offset", newOffset);
return newParams;
});
},
[query, numPerPage],
);
const goToPageNumber = React.useCallback(
(newPageNumber) => {
pushHistory(buildPageUrl(newPageNumber));
},
[buildPageUrl, pushHistory],
);
return {
numTotalPages,
currentPageNumber,
goToPageNumber,
buildPageUrl,
};
}
function LinkOrButton({ href, ...props }) { function LinkOrButton({ href, ...props }) {
if (href != null) { if (href != null) {
return <Button as="a" href={href} {...props} />; return <Button as="a" href={href} {...props} />;

View file

@ -454,11 +454,10 @@ export function MajorErrorMessage({ error = null, variant = "unexpected" }) {
> >
<img <img
src={ErrorGrundoImg} src={ErrorGrundoImg}
srcset={`${ErrorGrundoImg}, ${ErrorGrundoImg2x} 2x`} srcSet={`${ErrorGrundoImg}, ${ErrorGrundoImg2x} 2x`}
alt="Distressed Grundo programmer" alt="Distressed Grundo programmer"
width={100} width={100}
height={100} height={100}
layout="fixed"
/> />
</Box> </Box>
</Box> </Box>

View file

@ -1,5 +1,6 @@
{ {
"name": "app", "name": "impress",
"private": true,
"dependencies": { "dependencies": {
"@apollo/client": "^3.6.9", "@apollo/client": "^3.6.9",
"@chakra-ui/icons": "^1.0.4", "@chakra-ui/icons": "^1.0.4",
@ -27,11 +28,21 @@
"tweenjs": "^1.0.2" "tweenjs": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"prettier": "^3.0.3" "@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"eslint": "^8.52.0",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"husky": "^8.0.3",
"prettier": "^3.0.3",
"typescript": "^5.2.2"
}, },
"scripts": { "scripts": {
"build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets --asset-names=[name]-[hash].digested --loader:.js=jsx --loader:.png=file --loader:.svg=file --loader:.min.js=text", "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets --asset-names=[name]-[hash].digested --loader:.js=jsx --loader:.png=file --loader:.svg=file --loader:.min.js=text",
"build:dev": "yarn build --public-path=/dev-assets", "build:dev": "yarn build --public-path=/dev-assets",
"dev": "yarn build:dev --watch" "dev": "yarn build:dev --watch",
"lint": "eslint app/javascript",
"prepare": "husky install"
} }
} }

1683
yarn.lock

File diff suppressed because it is too large Load diff