build: switch linterformatter to rome (#120)

* build: replace eslint with rome

* chore: run lint+formatter

* chore: more cleanup
This commit is contained in:
Marc 2023-07-01 00:26:28 -04:00 committed by GitHub
parent fc531cd84a
commit 19cb3dba19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 538 additions and 3044 deletions

View file

@ -1,6 +0,0 @@
module.exports = {
extends: '@tophat',
rules: {
"react/react-in-jsx-scope": "off"
}
}

View file

@ -10,8 +10,8 @@
"scripts": {
"start:app": "yarn workspace app start",
"start:api": "yarn workspace api start",
"lint": "eslint packages/**/*.ts",
"lint:fix": "eslint packages/**/*.ts netlify --fix",
"lint": "yarn rome format packages/**/src/*.ts && yarn rome check packages/**/src/*.ts",
"lint:fix": "yarn rome format packages/**/src/*.ts --write && yarn rome check packages/**/src/*.ts --apply",
"types": "tsc --noEmit",
"clean": "rm -rf dist/*",
"build:app": "yarn workspace app build",
@ -22,25 +22,12 @@
"devDependencies": {
"@parcel/reporter-bundle-analyzer": "^2.9.3",
"@parcel/validator-typescript": "^2.9.3",
"@tophat/eslint-config": "3.3.0",
"@tophat/eslint-import-resolver-require": "0.1.3",
"@typescript-eslint/eslint-plugin": "5.48.2",
"@typescript-eslint/parser": "5.48.2",
"esbuild": "^0.17.3",
"eslint": "8.32.0",
"eslint-config-prettier": "8.5.0",
"eslint-import-resolver-node": "0.3.6",
"eslint-import-resolver-typescript": "3.5.2",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-jest": "27.2.1",
"eslint-plugin-jsx-a11y": "6.6.1",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.31.10",
"eslint-plugin-react-hooks": "4.6.0",
"jest": "29.3.1",
"netlify-cli": "^15.7.0",
"parcel": "^2.9.3",
"prettier": "2.7.1",
"rome": "^12.1.3",
"typescript": "^4.9.4"
},
"resolutions": {

View file

@ -3,12 +3,12 @@ import {
type Request,
type Response,
default as express,
} from 'express'
} from "express";
const app: Express = express()
const app: Express = express();
app.get('/', (req: Request, res: Response) => {
res.send('ok')
})
app.get("/", (req: Request, res: Response) => {
res.send("ok");
});
app.listen(8081, () => {})
app.listen(8081, () => {});

View file

@ -1,12 +1,12 @@
import { FunctionComponent } from 'preact'
import { FunctionComponent } from "preact";
import FeedsPanel from './FeedsPanel'
import NavigationBar from './NavigationBar'
import SettingsPanel from './SettingsPanel'
import useNavigation, { routes } from './hooks/useNavigation'
import FeedsPanel from "./FeedsPanel";
import NavigationBar from "./NavigationBar";
import SettingsPanel from "./SettingsPanel";
import useNavigation, { routes } from "./hooks/useNavigation";
export default function App(): FunctionComponent {
const { location } = useNavigation()
const { location } = useNavigation();
return (
<>
@ -14,5 +14,5 @@ export default function App(): FunctionComponent {
{location === routes.FEEDS ? <FeedsPanel /> : null}
{location === routes.SETTINGS ? <SettingsPanel /> : null}
</>
)
);
}

View file

@ -1,42 +1,42 @@
import Box from '@material-ui/core/Box'
import Card from '@material-ui/core/Card'
import Typography from '@material-ui/core/Typography'
import { makeStyles } from '@material-ui/core/styles'
import { FunctionComponent } from 'preact'
import Box from "@material-ui/core/Box";
import Card from "@material-ui/core/Card";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/core/styles";
import { FunctionComponent } from "preact";
import useRSSFeeds from './hooks/useRSSFeeds'
import useSettings from './hooks/useSettings'
import sortFeedItemsByDate from './utils'
import useRSSFeeds from "./hooks/useRSSFeeds";
import useSettings from "./hooks/useSettings";
import sortFeedItemsByDate from "./utils";
interface CardProps {
title: string
url: string
published: Date
feedTitle: string
title: string;
url: string;
published: Date;
feedTitle: string;
}
const useStyles = makeStyles({
root: {
margin: '5px',
padding: '10px',
display: 'flex',
flexDirection: 'column',
margin: "5px",
padding: "10px",
display: "flex",
flexDirection: "column",
},
})
});
function ItemCard(props: CardProps) {
const { title, url, published, feedTitle } = props
const classes = useStyles()
const { title, url, published, feedTitle } = props;
const classes = useStyles();
const formattedDate = new Date(published).toLocaleString('en-GB', {
timeZone: 'UTC',
})
const formattedDate = new Date(published).toLocaleString("en-GB", {
timeZone: "UTC",
});
return (
<Card className={classes.root}>
<a href={url}>{title}</a>
<span>{`${feedTitle} - ${formattedDate}`}</span>
</Card>
)
);
}
function NoItemsNotice() {
@ -50,27 +50,26 @@ function NoItemsNotice() {
>
<Typography variant="h6">Nothing to see here!</Typography>
<Typography>
Add some feeds in the <strong>Settings</strong> panel to get
started!
Add some feeds in the <strong>Settings</strong> panel to get started!
</Typography>
</Box>
)
);
}
export default function FeedsPanel(): FunctionComponent {
const { getSettings } = useSettings()
const settings = getSettings()
const { feeds } = useRSSFeeds(settings.feedUrls)
const { getSettings } = useSettings();
const settings = getSettings();
const { feeds } = useRSSFeeds(settings.feedUrls);
const flattenedItems = sortFeedItemsByDate(feeds)
const flattenedItems = sortFeedItemsByDate(feeds);
const cardList = flattenedItems.map((item) => (
<ItemCard {...item} key={`feed_item_${item.title.replace(' ', '_')}`} />
))
<ItemCard {...item} key={`feed_item_${item.title.replace(" ", "_")}`} />
));
return (
<Box display="flex" flexDirection="column">
{cardList.length > 0 ? cardList : <NoItemsNotice />}
</Box>
)
);
}

View file

@ -1,47 +1,43 @@
import AppBar from '@material-ui/core/AppBar'
import Button from '@material-ui/core/Button'
import Toolbar from '@material-ui/core/Toolbar'
import Typography from '@material-ui/core/Typography'
import { makeStyles } from '@material-ui/core/styles'
import { FunctionComponent } from 'preact'
import AppBar from "@material-ui/core/AppBar";
import Button from "@material-ui/core/Button";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/core/styles";
import { FunctionComponent } from "preact";
import useNavigation, { routes } from './hooks/useNavigation'
import useNavigation, { routes } from "./hooks/useNavigation";
const useStyles = makeStyles((theme) => ({
offset: theme.mixins.toolbar,
title: { flexGrow: 1 },
}))
}));
const routePrettyNames = {
[routes.SETTINGS]: 'Settings',
[routes.FEEDS]: 'Your feeds',
}
[routes.SETTINGS]: "Settings",
[routes.FEEDS]: "Your feeds",
};
export default function NavigationBar(): FunctionComponent {
const { location, navigate } = useNavigation()
const { location, navigate } = useNavigation();
const classes = useStyles()
const classes = useStyles();
return (
<>
<AppBar position="fixed">
<Toolbar>
<Typography
edge="start"
variant="h6"
className={classes.title}
>
<Typography edge="start" variant="h6" className={classes.title}>
{routePrettyNames[location]}
</Typography>
<Button
key={'navigation-feeds'}
key={"navigation-feeds"}
color="inherit"
onClick={() => navigate(routes.FEEDS)}
>
{routePrettyNames[routes.FEEDS]}
</Button>
<Button
key={'navigation-settings'}
key={"navigation-settings"}
color="inherit"
onClick={() => navigate(routes.SETTINGS)}
>
@ -51,5 +47,5 @@ export default function NavigationBar(): FunctionComponent {
</AppBar>
<div className={classes.offset} />
</>
)
);
}

View file

@ -1,55 +1,53 @@
import Box from '@material-ui/core/Box'
import Button from '@material-ui/core/Button'
import Card from '@material-ui/core/Card'
import TextField from '@material-ui/core/TextField'
import Typography from '@material-ui/core/Typography'
import { makeStyles } from '@material-ui/core/styles'
import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline'
import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline'
import { FunctionComponent } from 'preact'
import { useState } from 'preact/hooks'
import Box from "@material-ui/core/Box";
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/core/styles";
import CheckCircleOutlineIcon from "@material-ui/icons/CheckCircleOutline";
import ErrorOutlineIcon from "@material-ui/icons/ErrorOutline";
import { FunctionComponent } from "preact";
import { useState } from "preact/hooks";
import useSettings from './hooks/useSettings'
import useSettings from "./hooks/useSettings";
const useStyles = makeStyles({
urlCard: {
margin: '5px',
padding: '5px',
display: 'flex',
alignItems: 'center',
gap: '5px',
margin: "5px",
padding: "5px",
display: "flex",
alignItems: "center",
gap: "5px",
},
})
});
function isValidUrl(url: string): boolean {
const urlPattern = /(https?:\/\/)?(www\.)?[\w.-_]+\.[a-zA-Z]{2,3}/
const urlPattern = /(https?:\/\/)?(www\.)?[\w.-_]+\.[a-zA-Z]{2,3}/;
return urlPattern.test(url)
return urlPattern.test(url);
}
export default function SettingsPanel(): FunctionComponent {
const { getSettings, setSettings } = useSettings()
const settings = getSettings()
const [feedUrlsForm, setFeedUrlsForm] = useState(settings.feedUrls)
const { getSettings, setSettings } = useSettings();
const settings = getSettings();
const [feedUrlsForm, setFeedUrlsForm] = useState(settings.feedUrls);
const classes = useStyles()
const classes = useStyles();
const urlCards = feedUrlsForm.map((url) => (
<Card key={`url_${url}`} variant="outlined" className={classes.urlCard}>
{isValidUrl(url) ? (
<CheckCircleOutlineIcon color="primary" />
) : (
<ErrorOutlineIcon color="error" />
)}{' '}
)}{" "}
{url}
</Card>
))
));
return (
<Box display="flex" flexDirection="column">
<Typography paragraph>
{
'Enter URLs to fetch feeds from. Invalid URLs are discarded on save.'
}
{"Enter URLs to fetch feeds from. Invalid URLs are discarded on save."}
</Typography>
<TextField
multiline
@ -57,8 +55,8 @@ export default function SettingsPanel(): FunctionComponent {
variant="outlined"
label="Feed URLs"
fullWidth
value={feedUrlsForm.join('\n')}
onChange={(v) => setFeedUrlsForm(v.target.value.split('\n'))}
value={feedUrlsForm.join("\n")}
onChange={(v) => setFeedUrlsForm(v.target.value.split("\n"))}
/>
<Box display="flex" flexDirection="column">
{urlCards}
@ -67,13 +65,13 @@ export default function SettingsPanel(): FunctionComponent {
variant="contained"
color="primary"
onClick={() => {
const validUrls = feedUrlsForm.filter(isValidUrl)
setSettings<string[]>('feedUrls', validUrls)
setFeedUrlsForm(validUrls)
const validUrls = feedUrlsForm.filter(isValidUrl);
setSettings<string[]>("feedUrls", validUrls);
setFeedUrlsForm(validUrls);
}}
>
Save
</Button>
</Box>
)
);
}

View file

@ -1,9 +1,9 @@
export const panelIdentifiers = {
FEEDS: 'feeds',
SETTINGS: 'settings',
}
FEEDS: "feeds",
SETTINGS: "settings",
};
export const readablePanelIdentifiers = {
[panelIdentifiers.FEEDS]: 'Feeds',
[panelIdentifiers.SETTINGS]: 'Settings',
}
[panelIdentifiers.FEEDS]: "Feeds",
[panelIdentifiers.SETTINGS]: "Settings",
};

View file

@ -1,6 +1,6 @@
interface LocalStorageHookReturnType {
getValue: <T>(k: string) => T
setValue: <T>(k: string, v: T) => void
getValue: <T>(k: string) => T;
setValue: <T>(k: string, v: T) => void;
}
/*
@ -9,18 +9,18 @@ interface LocalStorageHookReturnType {
export default function useLocalStorage(
{ isJSON }: { isJSON: boolean } = { isJSON: false },
): LocalStorageHookReturnType {
if (!window.localStorage) throw new Error('Local Storage is not available')
if (!window.localStorage) throw new Error("Local Storage is not available");
return {
getValue: <T>(key: string): T => {
const raw = window.localStorage.getItem(key)
return (isJSON ? JSON.parse(raw) : raw) as T
const raw = window.localStorage.getItem(key);
return (isJSON ? JSON.parse(raw) : raw) as T;
},
setValue: <T>(key: string, value: T) => {
window.localStorage.setItem(
key,
(isJSON ? JSON.stringify(value) : value) as string,
)
);
},
}
};
}

View file

@ -1,54 +1,53 @@
import { ComponentChildren, FunctionComponent, createContext } from 'preact'
import { useCallback, useContext, useState } from 'preact/hooks'
import { ComponentChildren, FunctionComponent, createContext } from "preact";
import { useCallback, useContext, useState } from "preact/hooks";
interface INavigationContext {
location: string
navigate: (url: string) => void
location: string;
navigate: (url: string) => void;
}
const NavigationContext = createContext(null)
const NavigationContext = createContext(null);
export default function useNavigation(): INavigationContext {
const context = useContext(NavigationContext)
const context = useContext(NavigationContext);
if (!context) throw Error('Invalid navigation context')
if (!context) throw Error("Invalid navigation context");
return context
return context;
}
export const routes = {
FEEDS: '/feeds/',
SETTINGS: '/settings/',
}
FEEDS: "/feeds/",
SETTINGS: "/settings/",
};
export function NavigationProvider({
children,
}: {
children: ComponentChildren
children: ComponentChildren;
}): FunctionComponent<{ children: ComponentChildren }> {
const [location, setLocation] = useState<string>(window.location.pathname)
const [location, setLocation] = useState<string>(window.location.pathname);
const navigate = useCallback(
(url: string) => {
window.history.pushState({}, '', url)
setLocation(url)
window.history.pushState({}, "", url);
setLocation(url);
},
[setLocation],
)
);
const suffixedLocation = location.endsWith('/') ? location : `${location}/`
const suffixedLocation = location.endsWith("/") ? location : `${location}/`;
if (suffixedLocation !== location) {
window.history.replaceState({}, '', suffixedLocation)
setLocation(suffixedLocation)
window.history.replaceState({}, "", suffixedLocation);
setLocation(suffixedLocation);
}
if (!Object.values(routes).includes(suffixedLocation))
navigate(routes.FEEDS)
if (!Object.values(routes).includes(suffixedLocation)) navigate(routes.FEEDS);
return (
<NavigationContext.Provider value={{ location, navigate }}>
{children}
</NavigationContext.Provider>
)
);
}

View file

@ -1,29 +1,29 @@
import { parseFeed } from 'htmlparser2'
import { useQueries } from 'react-query'
import { parseFeed } from "htmlparser2";
import { useQueries } from "react-query";
import { Feed } from '../types'
import { isDev } from '../utils'
import { Feed } from "../types";
import { isDev } from "../utils";
import useLocalStorage from './useLocalStorage'
import useLocalStorage from "./useLocalStorage";
function mergeFeeds(first, second) {
// Assuming `second` is newer.
const seen = new Set(first.items.map((item) => item.url))
const seen = new Set(first.items.map((item) => item.url));
const mergedItems = second.items.reduce(
(updatedItems, item) => {
if (!seen.has(item.url)) {
updatedItems.push(item)
seen.add(item.url)
updatedItems.push(item);
seen.add(item.url);
}
return updatedItems
return updatedItems;
},
[...first.items],
)
);
return {
...second,
items: mergedItems,
}
};
}
function processFeedXML(feed): Feed {
@ -35,66 +35,64 @@ function processFeedXML(feed): Feed {
title: feedItem.title,
url: feedItem.link,
published: new Date(feedItem.pubDate),
})
});
return items
return items;
}, []),
}
};
}
async function fetchFeed(
url: string,
persistedData: Feed | null,
): Promise<Feed> {
const response = await fetch(`/.netlify/functions/rss-proxy?url=${url}`)
const response = await fetch(`/.netlify/functions/rss-proxy?url=${url}`);
const responseData = await response.text()
const responseData = await response.text();
try {
const newFeedData = parseFeed(responseData)
const newFeed = processFeedXML(newFeedData)
const newFeedData = parseFeed(responseData);
const newFeed = processFeedXML(newFeedData);
const mergedFeeds = persistedData
? mergeFeeds(persistedData, newFeed)
: newFeed
: newFeed;
return mergedFeeds
return mergedFeeds;
} catch (e) {
if (isDev()) {
// eslint-disable-next-line no-console
console.error(e)
console.error(e);
}
}
return persistedData
return persistedData;
}
export default function useRSSFeeds(urls: string[]): { feeds: Feed[] } {
const { getValue, setValue } = useLocalStorage({ isJSON: true })
const { getValue, setValue } = useLocalStorage({ isJSON: true });
const queries = useQueries(
urls.map((feedUrl: string) => {
const localStorageKey = `feed_${feedUrl}`
const persistedData = getValue<Feed>(localStorageKey)
const localStorageKey = `feed_${feedUrl}`;
const persistedData = getValue<Feed>(localStorageKey);
return {
queryKey: ['feed', feedUrl],
queryKey: ["feed", feedUrl],
queryFn: () => fetchFeed(feedUrl, persistedData),
staleTime: 30000,
onSuccess: (data: Feed) => {
setValue(localStorageKey, data)
setValue(localStorageKey, data);
},
initialData: persistedData ?? undefined,
initialDataUpdatedAt:
Number(persistedData?.lastPull) ?? undefined,
}
initialDataUpdatedAt: Number(persistedData?.lastPull) ?? undefined,
};
}),
)
);
const fetchedFeeds = queries.reduce((fetchedFeeds: Feed[], current) => {
if (current.isSuccess && current.data) fetchedFeeds.push(current.data)
if (current.isSuccess && current.data) fetchedFeeds.push(current.data);
return fetchedFeeds
}, [])
return fetchedFeeds;
}, []);
return { feeds: fetchedFeeds }
return { feeds: fetchedFeeds };
}

View file

@ -1,25 +1,25 @@
import { Settings } from '../types'
import { Settings } from "../types";
import useLocalStorage from './useLocalStorage'
import useLocalStorage from "./useLocalStorage";
const defaultSettings = {
feedUrls: [],
}
};
export default function useSettings(): {
getSettings: () => Settings
setSettings: <T>(k: string, value: T) => void
getSettings: () => Settings;
setSettings: <T>(k: string, value: T) => void;
} {
const { getValue, setValue } = useLocalStorage({ isJSON: true })
const { getValue, setValue } = useLocalStorage({ isJSON: true });
const getSettings = (): Settings =>
getValue<Settings>('settings') ?? defaultSettings
getValue<Settings>("settings") ?? defaultSettings;
const setSettings = <T>(key: string, value: T) => {
const current = getSettings()
const current = getSettings();
setValue<Settings>('settings', { ...current, [key]: value })
}
setValue<Settings>("settings", { ...current, [key]: value });
};
return { getSettings, setSettings }
return { getSettings, setSettings };
}

View file

@ -1,14 +1,14 @@
if (process.env.NODE_ENV === 'development') {
require('preact/debug')
if (process.env.NODE_ENV === "development") {
require("preact/debug");
}
import { render } from 'preact'
import { QueryClient, QueryClientProvider } from 'react-query'
import { render } from "preact";
import { QueryClient, QueryClientProvider } from "react-query";
import App from './App'
import { NavigationProvider } from './hooks/useNavigation'
import App from "./App";
import { NavigationProvider } from "./hooks/useNavigation";
const queryClient = new QueryClient()
const queryClient = new QueryClient();
render(
<NavigationProvider>
@ -16,5 +16,5 @@ render(
<App />
</QueryClientProvider>
</NavigationProvider>,
document.getElementById('app'),
)
document.getElementById("app"),
);

View file

@ -1,28 +1,28 @@
export interface Feed {
title: string
lastPull: string
items: Item[]
title: string;
lastPull: string;
items: Item[];
}
export interface Item {
title: string
url: string
published: Date
source: string
title: string;
url: string;
published: Date;
source: string;
}
export interface State {
loaded: boolean
rssItems: Item[]
feedUrls: string[]
activePanel: string
loaded: boolean;
rssItems: Item[];
feedUrls: string[];
activePanel: string;
}
export interface RSSData {
items: Item[]
lastPushed: Date
items: Item[];
lastPushed: Date;
}
export interface Settings {
feedUrls: string[]
feedUrls: string[];
}

View file

@ -1,20 +1,20 @@
import type { Feed, Item } from './types'
import type { Feed, Item } from "./types";
export default function sortFeedItemsByDate(feeds: Feed[]): Item[] {
const flattened = feeds.reduce((flattenedFeeds, feed) => {
const items = feed.items.map((item) => ({
...item,
feedTitle: feed.title,
}))
flattenedFeeds.push(...items)
return flattenedFeeds
}, [])
}));
flattenedFeeds.push(...items);
return flattenedFeeds;
}, []);
return flattened.sort((first, second) =>
first.published > second.published ? -1 : 1,
)
);
}
export function isDev(): boolean {
return process.env.NODE_ENV === 'development'
return process.env.NODE_ENV === "development";
}

12
rome.json Normal file
View file

@ -0,0 +1,12 @@
{
"$schema": "https://docs.rome.tools/schemas/12.1.3/schema.json",
"organizeImports": {
"enabled": false
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
}
}

2803
yarn.lock

File diff suppressed because it is too large Load diff