Introduce eslint and fix lint errors

This commit is contained in:
Junyoung Choi 2019-09-10 12:04:06 +09:00
parent 2758ad5381
commit d358daf9db
No known key found for this signature in database
GPG Key ID: C1B25CE98BF97FBF
16 changed files with 867 additions and 354 deletions

27
.eslintrc Normal file
View File

@ -0,0 +1,27 @@
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint/eslint-plugin", "react-hooks"],
"extends": [
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"plugin:prettier/recommended"
],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/camelcase": "off",
"@typescript-eslint/no-unused-vars": "off",
"react/prop-types": "off",
"react/display-name": "off"
},
"settings": {
"react": {
"version": "detect"
}
}
}

770
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@
"pack": "env-cmd ./.env build --dir",
"dist": "env-cmd ./.env build",
"shasum": "shasum -a 256 dist/mac/Inpad-$npm_package_version.dmg",
"lint": "tslint -p . src/**/*.ts{,x}",
"lint": "eslint src/**/*.ts{,x}",
"format": "prettier --write \"src/**/*\"",
"webpack": "NODE_ENV=development webpack-dev-server --config webpack.config.js",
"build": "NODE_ENV=production webpack --mode production",
@ -57,7 +57,14 @@
"@types/webpack": "^4.4.17",
"@types/webpack-dev-server": "^3.1.1",
"@types/webpack-env": "^1.13.6",
"@typescript-eslint/eslint-plugin": "^2.2.0",
"@typescript-eslint/parser": "^2.2.0",
"babel-jest": "^24.9.0",
"eslint": "^6.3.0",
"eslint-config-prettier": "^6.2.0",
"eslint-plugin-prettier": "^3.1.0",
"eslint-plugin-react": "^7.14.3",
"eslint-plugin-react-hooks": "^2.0.1",
"fork-ts-checker-webpack-plugin": "^0.4.14",
"html-webpack-plugin": "^3.2.0",
"jest": "^24.9.0",

View File

@ -9,11 +9,14 @@ import ContextMenu from './ContextMenu'
import Dialog from './Dialog/Dialog'
import { useDb } from '../lib/db'
export default () => {
const App = () => {
const db = useDb()
useEffect(() => {
db.initialize()
}, [])
useEffect(
() => {
db.initialize()
},
[db]
)
return (
<ThemeProvider theme={defaultTheme}>
<StyledAppContainer>
@ -32,3 +35,5 @@ export default () => {
</ThemeProvider>
)
}
export default App

View File

@ -1,4 +1,4 @@
import React, { useMemo, useCallback } from 'react'
import React, { useMemo } from 'react'
import StorageItem from './StorageItem/StorageItem'
import { useRouter } from '../../lib/router'
import SotrageCreateForm from './StorageCreateForm'
@ -6,49 +6,21 @@ import { StyledSideNavContainer, StyledStorageList } from './styled'
import { useDb } from '../../lib/db'
export default () => {
const db = useDb()
const {
createStorage,
renameStorage,
removeStorage,
createFolder,
removeFolder,
storageMap
} = useDb()
const router = useRouter()
const storageEntries = useMemo(
() => {
return Object.entries(db.storageMap)
return Object.entries(storageMap)
},
[db.storageMap]
)
const createStorage = useCallback(
async (storageName: string) => {
await db.createStorage(storageName)
},
[db.createStorage]
)
const renameStorage = useCallback(
async (storageId: string, name: string) => {
await db.renameStorage(storageId, name)
},
[db.renameStorage]
)
const removeStorage = useCallback(
async (storageId: string) => {
await db.removeStorage(storageId)
},
[db.removeStorage]
)
const createFolder = useCallback(
async (storageId: string, pathname: string) => {
await db.createFolder(storageId, pathname)
},
[db.createFolder]
)
const removeFolder = useCallback(
async (storageId: string, pathname: string) => {
await db.removeFolder(storageId, pathname)
},
[db.removeFolder]
[storageMap]
)
return (

View File

@ -1,7 +1,7 @@
import React from 'react'
type SotrageCreateFormProps = {
createStorage: (storageName: string) => Promise<void>
createStorage: (storageName: string) => Promise<any>
}
type SotrageCreateFormState = {
@ -36,12 +36,12 @@ export default class SotrageCreateForm extends React.Component<
<div>
<label>New storage</label>
<input
type="text"
type='text'
ref={this.nameInputRef}
value={this.state.name}
onChange={this.updateName}
/>
<button type="submit" onClick={this.createStorage}>
<button type='submit' onClick={this.createStorage}>
Add
</button>
</div>

View File

@ -15,7 +15,7 @@ type FolderItemProps = {
}
export default (props: FolderItemProps) => {
const dialog = useDialog()
const { prompt, messageBox } = useDialog()
const contextMenu = useContextMenu()
const { storageId, folder, active, removeFolder, createFolder } = props
const openContextMenu = useCallback(
@ -28,7 +28,7 @@ export default (props: FolderItemProps) => {
type: MenuTypes.Normal,
label: 'New Folder',
onClick: async () => {
dialog.prompt({
prompt({
title: 'Create a Folder',
message: 'Enter the path where do you want to create a folder',
iconType: DialogIconTypes.Question,
@ -46,7 +46,7 @@ export default (props: FolderItemProps) => {
label: 'Remove Folder',
enabled: !folderIsRootFolder,
onClick: () => {
dialog.messageBox({
messageBox({
title: `Remove "${folder.pathname}" folder`,
message: 'All notes and subfolders will be deleted.',
iconType: DialogIconTypes.Warning,
@ -63,7 +63,15 @@ export default (props: FolderItemProps) => {
}
])
},
[dialog.messageBox, contextMenu.popup, createFolder, removeFolder]
[
folder.pathname,
prompt,
messageBox,
contextMenu,
createFolder,
storageId,
removeFolder
]
)
return (

View File

@ -25,7 +25,7 @@ type StorageItemProps = {
}
export default (props: StorageItemProps) => {
const dialog = useDialog()
const { prompt, messageBox } = useDialog()
const contextMenu = useContextMenu()
const {
id,
@ -63,7 +63,7 @@ export default (props: StorageItemProps) => {
type: MenuTypes.Normal,
label: 'New Folder',
onClick: async () => {
dialog.prompt({
prompt({
title: 'Create a Folder',
message: 'Enter the path where do you want to create a folder',
iconType: DialogIconTypes.Question,
@ -80,7 +80,7 @@ export default (props: StorageItemProps) => {
type: MenuTypes.Normal,
label: 'Rename Storage',
onClick: async () => {
dialog.prompt({
prompt({
title: `Rename "${storageName}" storage`,
message: 'Enter new name for the storage',
iconType: DialogIconTypes.Question,
@ -97,7 +97,7 @@ export default (props: StorageItemProps) => {
type: MenuTypes.Normal,
label: 'Remove Storage',
onClick: async () => {
dialog.messageBox({
messageBox({
title: `Remove "${storageName}" storage`,
message: 'All notes and folders will be deleted.',
iconType: DialogIconTypes.Warning,
@ -114,7 +114,16 @@ export default (props: StorageItemProps) => {
}
])
},
[contextMenu.popup, dialog.prompt, dialog.messageBox, storageName, id]
[
contextMenu,
prompt,
messageBox,
createFolder,
id,
storageName,
renameStorage,
removeStorage
]
)
return (

View File

@ -8,7 +8,7 @@ export const menuMargin = 5
export const menuVerticalPadding = 4
export const menuZIndex = 9000
function createContextMenuStore(): ContextMenuContext {
function useContextMenuStore(): ContextMenuContext {
const [closed, setClosed] = useState(true)
const [position, setPosition] = useState({ x: 0, y: 0 })
const [menuItems, setMenuItems] = useState<MenuItem[]>([])
@ -55,4 +55,4 @@ function createContextMenuStore(): ContextMenuContext {
export const {
StoreProvider: ContextMenuProvider,
useStore: useContextMenu
} = createStoreContext(createContextMenuStore, 'context menu')
} = createStoreContext(useContextMenuStore, 'context menu')

View File

@ -4,7 +4,7 @@ import { getFolderId, getTagId, generateNoteId, getNow } from './utils'
import { NoteDoc, FolderDoc, ExceptRev } from './types'
let noteDbCount = 0
async function prepareNoteDb(shouldInit: boolean = true): Promise<NoteDb> {
async function prepareNoteDb(shouldInit = true): Promise<NoteDb> {
const id = `dummy${++noteDbCount}`
const pouchDb = new PouchDB(id, {
adapter: 'memory'

View File

@ -165,7 +165,7 @@ export function createDbStoreCreator(
})
)
},
[storageMap, router.pathname]
[storageMap, router]
)
return {

View File

@ -11,7 +11,7 @@ export * from './types'
let id = 0
function createDialog(): DialogContext {
function useDialogStore(): DialogContext {
const [data, setData] = useState<DialogData | null>(null)
const prompt = useCallback((options: PromptDialogOptions) => {
setData({
@ -42,4 +42,4 @@ function createDialog(): DialogContext {
export const {
StoreProvider: DialogProvider,
useStore: useDialog
} = createStoreContext(createDialog, 'dialog')
} = createStoreContext(useDialogStore, 'dialog')

View File

@ -1,155 +0,0 @@
import unified from 'unified'
import parse from 'remark-parse'
import frontmatter from 'remark-frontmatter'
import parseYaml from 'remark-parse-yaml'
import { getMetaData, getTitleFromNode, getTagsFromNode } from './markdown'
const processor = unified()
.use(parse)
.use(frontmatter)
.use(parseYaml)
function parseAndTransform(value: string) {
const parsedNode = processor.parse(value)
const transformedNode = processor.runSync(parsedNode)
return transformedNode
}
describe('markdown', () => {
describe('getMetaData', () => {
it('returns meta data from string', () => {
// Given
// prettier-ignore
const value = [
'---',
'title: yaml title',
'tags: [test, tags]',
'---',
'',
'# test',
''
].join('\n')
// When
const metaData = getMetaData(value)
// Then
expect(metaData).toEqual({
title: 'yaml title',
tags: ['test', 'tags']
})
})
})
describe('getTitleFromNode', () => {
it('returns title from content of the first heading node if there is no yaml title', () => {
// Given
// prettier-ignore
const node = parseAndTransform([
'---',
'otherValue: test',
'---',
'',
'# heading title',
'',
'## another heading',
''
].join('\n'))
// When
const title = getTitleFromNode(node)
// Then
expect(title).toBe('heading title')
})
it('returns title from content of the first line if there is not heading node nor yaml title', () => {
// Given
// prettier-ignore
const node = parseAndTransform([
'---',
'otherValue: test',
'---',
'',
'no title',
''
].join('\n'))
// When
const title = getTitleFromNode(node)
// Then
expect(title).toBe('no title')
})
})
describe('getTagsFromNode', () => {
it('parses tags as string', () => {
// Given
// prettier-ignore
const node = parseAndTransform([
'---',
'tags: test',
'---',
''
].join('\n'))
// When
const tags = getTagsFromNode(node)
// Then
expect(tags).toEqual(['test'])
})
it('parses tags as string array', () => {
// Given
// prettier-ignore
const node = parseAndTransform([
'---',
'tags: [test, tags, 123]',
'---',
''
].join('\n'))
// When
const tags = getTagsFromNode(node)
// Then
expect(tags).toEqual(['test', 'tags', '123'])
})
it('ignores non string value in array', () => {
// Given
// prettier-ignore
const node = parseAndTransform([
'---',
'tags: [test, [123]]',
'---',
''
].join('\n'))
// When
const tags = getTagsFromNode(node)
// Then
expect(tags).toEqual(['test'])
})
it('ignores duplicated entity', () => {
// Given
// prettier-ignore
const node = parseAndTransform([
'---',
'tags: [test, test]',
'---',
''
].join('\n'))
// When
const tags = getTagsFromNode(node)
// Then
expect(tags).toEqual(['test'])
})
})
})

View File

@ -1,93 +0,0 @@
import unified from 'unified'
import parse from 'remark-parse'
import frontmatter from 'remark-frontmatter'
import parseYaml from 'remark-parse-yaml'
import visit from 'unist-util-visit'
import convertMdastToString from 'mdast-util-to-string'
import { pipe, filter, map, uniq } from 'ramda'
const processor = unified()
.use(parse)
.use(frontmatter)
.use(parseYaml)
function hasYamlNode(node: any) {
if (node.children[0] == null) return false
return node.children[0].type === 'yaml'
}
function hasYamlTitle(node: any) {
if (!hasYamlNode(node)) return false
return node.children[0].data.parsedValue.title != null
}
function hasYamlTags(node: any) {
if (!hasYamlNode(node)) return false
return node.children[0].data.parsedValue.tags != null
}
interface MetaData {
title: string
tags: string[]
}
export function getMetaData(value: string): MetaData {
const parsedNode = processor.parse(value)
const transformedNode = processor.runSync(parsedNode)
const title = getTitleFromNode(transformedNode)
const tags = getTagsFromNode(transformedNode)
return {
title,
tags
}
}
export function getTitleFromNode(node: any): string {
if (hasYamlTitle(node)) {
return node.children[0]!.data.parsedValue.title
}
let title: string = ''
visit(node, 'heading', (headingNode: any) => {
title = convertMdastToString(headingNode)
if (title.length > 0) {
return visit.EXIT
} else {
return visit.CONTINUE
}
})
if (title.length > 0) return title
visit(node, 'text', (literalNode: any) => {
title = convertMdastToString(literalNode)
if (title.length > 0) {
return visit.EXIT
} else {
return visit.CONTINUE
}
})
return title
}
export function getTagsFromNode(node: any): string[] {
if (!hasYamlTags(node)) return []
const unknownTags: unknown = node.children[0].data.parsedValue.tags
if (isStringOrNumber(unknownTags)) return [unknownTags.toString()]
if (Array.isArray(unknownTags)) {
return filterTags(unknownTags)
}
return []
}
const filterTags = pipe(
filter(isStringOrNumber),
map(value => value.toString()),
uniq
)
function isStringOrNumber(value: any): value is string | number {
return typeof value === 'string' || typeof value === 'number'
}

View File

@ -7,6 +7,7 @@ import path from 'path'
import pathToRegexp from 'path-to-regexp'
import { createStoreContext } from '../utils/context'
import React, { useState, useEffect, useCallback, FC } from 'react'
import { omit } from 'ramda'
export const history = createBrowserHistory()
@ -43,10 +44,10 @@ function normalizePathname(pathname: string): string {
return normalizedPathname
}
function normalizeLocation({ pathname, key, ...otherProps }: Location) {
function normalizeLocation({ pathname, ...otherProps }: Location) {
return {
pathname: normalizePathname(pathname),
...otherProps
...omit(['key'], otherProps)
}
}
@ -63,7 +64,7 @@ export interface RouterStore {
const initialLocation = normalizeLocation(history.location)
function createRouteStore(): RouterStore {
function useRouteStore(): RouterStore {
const [location, setLocation] = useState(initialLocation)
useEffect(() => {
@ -83,8 +84,8 @@ function createRouteStore(): RouterStore {
const go = useCallback((count: number) => {
history.go(count)
}, [])
const goBack = useCallback(() => go(-1), [])
const goForward = useCallback(() => go(1), [])
const goBack = useCallback(() => go(-1), [go])
const goForward = useCallback(() => go(1), [go])
return {
pathname: location.pathname,
@ -101,7 +102,7 @@ function createRouteStore(): RouterStore {
export const {
StoreProvider: RouterProvider,
useStore: useRouter
} = createStoreContext(createRouteStore)
} = createStoreContext(useRouteStore)
export interface LinkProps {
href: string

View File

@ -1,38 +0,0 @@
export default class MemoryStorage implements Storage {
private map: Map<string, string>
get length() {
return this.map.size
}
[Symbol.iterator]() {
return this.map[Symbol.iterator]()
}
constructor() {
this.map = new Map()
}
setItem(key: string, value: any) {
this.map.set(key, value.toString())
}
getItem(key: string) {
if (this.map.has(key)) {
return this.map.get(key) as string
}
return null
}
clear() {
this.map = new Map()
}
removeItem(key: string) {
this.map.delete(key)
}
key(index: number) {
return this.map.keys()[index]
}
}