From 5e6500d1963fe63e7b33bf6be3bf0939861a3fd0 Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Thu, 29 Mar 2018 15:42:07 -0400 Subject: [PATCH] Use strict TypeScript, enable TSLint, update all types to work in strict mode. --- .editorconfig | 9 ++ global.d.ts | 18 ++++ package.json | 9 ++ src/components/app/index.tsx | 170 +++++++++++++++++--------------- src/components/drawer/index.js | 51 ---------- src/components/drawer/index.tsx | 63 ++++++++++++ src/components/fab/index.tsx | 56 ++++++----- src/components/header/index.tsx | 76 +++++++------- src/components/home/index.tsx | 42 ++++---- src/index.js | 18 ---- src/index.tsx | 32 ++++++ src/lib/util.js | 19 ---- src/lib/util.ts | 39 ++++++++ tsconfig.json | 28 +++--- tslint.json | 16 +++ webpack.config.js | 15 +++ 16 files changed, 396 insertions(+), 265 deletions(-) create mode 100644 .editorconfig delete mode 100644 src/components/drawer/index.js create mode 100644 src/components/drawer/index.tsx delete mode 100644 src/index.js create mode 100644 src/index.tsx delete mode 100644 src/lib/util.js create mode 100644 src/lib/util.ts create mode 100644 tslint.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..c6c8b362 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/global.d.ts b/global.d.ts index e69de29b..b04fcf0d 100644 --- a/global.d.ts +++ b/global.d.ts @@ -0,0 +1,18 @@ +declare const __webpack_public_path__: string; + +interface NodeModule { + hot: any; +} + +declare namespace JSX { + interface Element { } + interface IntrinsicElements { div: any; } +} + +declare module 'preact-i18n'; +declare module 'preact-material-components-drawer'; +declare module 'material-radial-progress'; + +declare module 'classnames' { + export default function classnames(...args: any[]): string; +} diff --git a/package.json b/package.json index 639ab1fc..57c4de1d 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,10 @@ "eslintConfig": { "extends": "eslint-config-developit", "rules": { + "indent": [ + 2, + 2 + ], "react/prefer-stateless-function": 0 } }, @@ -37,6 +41,8 @@ "eslint": "^4.18.2", "eslint-config-developit": "^1.1.1", "extract-text-webpack-plugin": "^4.0.0-beta.0", + "fork-ts-checker-notifier-webpack-plugin": "^0.4.0", + "fork-ts-checker-webpack-plugin": "^0.4.1", "html-webpack-plugin": "^3.0.6", "if-env": "^1.0.4", "mini-css-extract-plugin": "^0.2.0", @@ -48,6 +54,9 @@ "script-ext-html-webpack-plugin": "^2.0.0", "style-loader": "^0.20.3", "ts-loader": "^4.0.1", + "tslint": "^5.9.1", + "tslint-config-semistandard": "^7.0.0", + "tslint-react": "^3.5.1", "typescript": "^2.7.2", "typescript-loader": "^1.1.3", "typings-for-css-modules-loader": "^1.7.0", diff --git a/src/components/app/index.tsx b/src/components/app/index.tsx index b9441296..69e6781d 100644 --- a/src/components/app/index.tsx +++ b/src/components/app/index.tsx @@ -1,5 +1,5 @@ import { h, Component } from 'preact'; -import { updater, toggle, When } from '../../lib/util'; +import { When, bind } from '../../lib/util'; import Fab from '../fab'; import Header from '../header'; // import Drawer from 'async!../drawer'; @@ -8,105 +8,113 @@ import Home from '../home'; import * as style from './style.scss'; type Props = { - url?: String -} + url?: string +}; -type FileObj = { - id: any, - data: any, - error: Error | DOMError | String, - file: File, - loading: Boolean +export type FileObj = { + id: number, + data?: string, + error?: Error | DOMError | String, + file: File, + loading: boolean }; type State = { - showDrawer: Boolean, - showFab: Boolean, - files: FileObj[] + showDrawer: boolean, + showFab: boolean, + files: FileObj[] }; let counter = 0; export default class App extends Component { - state: State = { - showDrawer: false, - showFab: true, - files: [] - }; + state: State = { + showDrawer: false, + showFab: true, + files: [] + }; - loadFile = (file: File) => { - let fileObj = { - id: ++counter, - file, - error: null, - loading: true, - data: null - }; + enableDrawer = false; - this.setState({ - files: [fileObj] - }); + @bind + openDrawer() { + this.setState({ showDrawer: true }); + } + @bind + closeDrawer() { + this.setState({ showDrawer: false }); + } + @bind + toggleDrawer() { + this.setState({ showDrawer: !this.state.showDrawer }); + } - let fr = new FileReader(); - // fr.readAsArrayBuffer(); - fr.onerror = () => { - let files = this.state.files.slice(); - files.splice(0, files.indexOf(fileObj), { - ...fileObj, - error: fr.error, - loading: false - }); - this.setState({ files }); - }; - fr.onloadend = () => { - let files = this.state.files.slice(); - files.splice(0, files.indexOf(fileObj), { - ...fileObj, - data: fr.result, - loading: false - }); - this.setState({ files }); - }; - fr.readAsDataURL(file); - }; + @bind + openFab() { + this.setState({ showFab: true }); + } + @bind + closeFab() { + this.setState({ showFab: false }); + } + @bind + toggleFab() { + this.setState({ showFab: !this.state.showFab }); + } - enableDrawer = false; + @bind + loadFile(file: File) { + let fileObj: FileObj = { + id: ++counter, + file, + error: undefined, + loading: true, + data: undefined + }; - openDrawer = updater(this, 'showDrawer', true); - closeDrawer = updater(this, 'showDrawer', false); - toggleDrawer = updater(this, 'showDrawer', toggle); + this.setState({ + files: [fileObj] + }); - openFab = updater(this, 'showFab', true); - closeFab = updater(this, 'showFab', false); - toggleFab = updater(this, 'showFab', toggle); + let done = () => { + let files = this.state.files.slice(); + files[files.indexOf(fileObj)] = Object.assign({}, fileObj, { + error: fr.error, + loading: false, + data: fr.result + }); + this.setState({ files }); + }; - render({ url }, { showDrawer, showFab, files }) { - if (showDrawer) this.enableDrawer = true; + let fr = new FileReader(); + fr.onerror = fr.onloadend = done; + fr.readAsDataURL(file); + } - if (showFab===true) showFab = files.length>0; + render({ url }: Props, { showDrawer, showFab, files }: State) { + if (showDrawer) this.enableDrawer = true; - return ( -
- + if (showFab === true) showFab = files.length > 0; -
- - {/* Avoid loading & rendering the drawer until the first time it is shown. */} - - - + return ( +
+ - {/* - Note: this is normally where a with auto code-splitting goes. - Since we don't seem to need one (yet?), it's omitted. - */} -
- -
+
- {/* This ends up in the body when prerendered, which makes it load async. */} - -
- ); - } + {/* Avoid loading & rendering the drawer until the first time it is shown. */} + + + + + {/* + Note: this is normally where a with auto code-splitting goes. + Since we don't seem to need one (yet?), it's omitted. + */} +
+ +
+
+ ); + } } diff --git a/src/components/drawer/index.js b/src/components/drawer/index.js deleted file mode 100644 index 32666526..00000000 --- a/src/components/drawer/index.js +++ /dev/null @@ -1,51 +0,0 @@ -import { h, Component } from 'preact'; -import MdlDrawer from 'preact-material-components-drawer'; -import 'preact-material-components/Drawer/style.css'; -import List from 'preact-material-components/List'; -// import 'preact-material-components/List/style.css'; -import { Text } from 'preact-i18n'; -import style from './style'; - -export default class Drawer extends Component { - state = { - rendered: false - }; - - setRendered = () => { - this.setState({ rendered: true }); - }; - - render({ showing, openDrawer, closeDrawer }, { rendered }) { - if (showing && !rendered) { - setTimeout(this.setRendered, 20); - showing = false; - } - - return ( - - - logo - - - - - verified_user - Sign In - - - account_circle - Register - - - - -
- - settings - Preferences - -
-
- ); - } -} \ No newline at end of file diff --git a/src/components/drawer/index.tsx b/src/components/drawer/index.tsx new file mode 100644 index 00000000..d58ee657 --- /dev/null +++ b/src/components/drawer/index.tsx @@ -0,0 +1,63 @@ +import { h, Component } from 'preact'; +import MdlDrawer from 'preact-material-components-drawer'; +import 'preact-material-components/Drawer/style.css'; +import List from 'preact-material-components/List'; +// import 'preact-material-components/List/style.css'; +import { Text } from 'preact-i18n'; +import * as style from './style.scss'; +import { bind } from '../../lib/util'; + +type Props = { + showing: boolean, + openDrawer(): void, + closeDrawer(): void +}; + +type State = { + rendered: boolean +}; + +export default class Drawer extends Component { + state: State = { + rendered: false + }; + + @bind + setRendered() { + this.setState({ rendered: true }); + } + + render({ showing, openDrawer, closeDrawer }: Props, { rendered }: State) { + if (showing && !rendered) { + setTimeout(this.setRendered, 20); + showing = false; + } + + return ( + + + logo + + + + + verified_user + Sign In + + + account_circle + Register + + + + +
+ + settings + Preferences + +
+
+ ); + } +} diff --git a/src/components/fab/index.tsx b/src/components/fab/index.tsx index f17e18aa..a43feb27 100644 --- a/src/components/fab/index.tsx +++ b/src/components/fab/index.tsx @@ -1,4 +1,5 @@ import { h, Component } from 'preact'; +import { bind } from '../../lib/util'; import Icon from 'preact-material-components/Icon'; import 'preact-material-components/Icon/style.css'; import Fab from 'preact-material-components/Fab'; @@ -6,34 +7,41 @@ import RadialProgress from 'material-radial-progress'; import * as style from './style.scss'; type Props = { - showing: boolean + showing: boolean }; + type State = { - loading: boolean + loading: boolean }; export default class AppFab extends Component { - state: State = { - loading: false - }; + state: State = { + loading: false + }; - handleClick = () => { - console.log('TODO: Save the file to disk.'); - this.setState({ loading: true }); - setTimeout( () => { - this.setState({ loading: false }); - }, 1000); - }; + @bind + setLoading(loading: boolean) { + this.setState({ loading }); + } - render({ showing }, { loading }) { - return ( - - { loading ? ( - - ) : ( - file_download - ) } - - ); - } -} \ No newline at end of file + @bind + handleClick() { + console.log('TODO: Save the file to disk.'); + this.setState({ loading: true }); + setTimeout(() => { + this.setState({ loading: false }); + }, 1000); + } + + render({ showing }: Props, { loading }: State) { + return ( + + { loading ? ( + + ) : ( + file_download + ) } + + ); + } +} diff --git a/src/components/header/index.tsx b/src/components/header/index.tsx index bba18c1e..ab29de32 100644 --- a/src/components/header/index.tsx +++ b/src/components/header/index.tsx @@ -2,50 +2,52 @@ import { h, Component } from 'preact'; import Toolbar from 'preact-material-components/Toolbar'; import cx from 'classnames'; import * as style from './style.scss'; +import { bind } from '../../lib/util'; type Props = { - toggleDrawer?(), - showHeader?(), - showFab?(), - loadFile?(File) + 'class'?: string, + showHeader?: boolean, + toggleDrawer?(): void, + showFab?(): void, + loadFile(f: File): void }; -type State = { - -}; +type State = {}; export default class Header extends Component { - input: HTMLInputElement; + input?: HTMLInputElement; - setInputRef = c => { - this.input = c; - }; + @bind + setInputRef(c?: Element) { + this.input = c as HTMLInputElement; + } - upload = () => { - this.input.click(); - }; + @bind + upload() { + this.input!.click(); + } - handleFiles = () => { - let files = this.input.files; - if (files.length) { - this.props.loadFile(files[0]); - } - }; + @bind + handleFiles() { + let files = this.input!.files; + if (files && files.length) { + this.props.loadFile(files[0]); + } + } - render({ toggleDrawer, showHeader, showFab }) { - return ( - - - - - file_upload - - - menu - - - - - ); - } -} \ No newline at end of file + render({ class: c, toggleDrawer, showHeader = false, showFab }: Props) { + return ( + + + + file_upload + + + menu + + + + + ); + } +} diff --git a/src/components/home/index.tsx b/src/components/home/index.tsx index 1f2e9b75..1b0ddf0b 100644 --- a/src/components/home/index.tsx +++ b/src/components/home/index.tsx @@ -3,34 +3,34 @@ import { h, Component } from 'preact'; // import Switch from 'preact-material-components/Switch'; // import 'preact-material-components/Switch/style.css'; import * as style from './style.scss'; +import { FileObj } from '../app'; type Props = { - files: { - data: any - }[] + files: FileObj[] }; + type State = { - active: boolean + active: boolean }; export default class Home extends Component { - state: State = { - active: false - }; + state: State = { + active: false + }; - componentDidMount() { - setTimeout( () => { - this.setState({ active: true }); - }); - } + componentDidMount() { + setTimeout(() => { + this.setState({ active: true }); + }); + } - render({ files }, { active }) { - return ( -
- { files && files[0] && ( - - ) } -
- ); - } + render({ files }: Props, { active }: State) { + return ( +
+ { files && files[0] && ( + + ) } +
+ ); + } } diff --git a/src/index.js b/src/index.js deleted file mode 100644 index f6227aba..00000000 --- a/src/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import './style'; -import './lib/fix-pmc'; -import App from './components/app'; - -export default App; - -if (typeof window!=='undefined') { - addEventListener('click', e => { - let { target } = e; - do { - if (target.nodeName === 'A') { - history.pushState(null, null, target.pathname); - e.preventDefault(); - return false; - } - } while ((target = target.parentNode)); - }); -} diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 00000000..60f92c96 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,32 @@ +import { h, render } from 'preact'; +import './lib/fix-pmc'; +import './style'; +import App from './components/app'; + +// Find the outermost Element in our server-rendered HTML structure. +let root = document.querySelector('[prerender]') || undefined; + +// "attach" the client-side rendering to it, updating the DOM in-place instead of replacing: +root = render(, document.body, root); + +// In production, this entire condition is removed. +if (process.env.NODE_ENV === 'development') { + // Enable support for React DevTools and some helpful console warnings: + require('preact/debug'); + + // When an update to any module is received, re-import the app and trigger a full re-render: + module.hot.accept('./components/app', () => { + import('./components/app').then(({ default: App }) => { + root = render(, document.body, root); + }); + }); +} else if ('serviceWorker' in navigator && location.protocol === 'https:') { + addEventListener('load', () => { + navigator.serviceWorker.register(__webpack_public_path__ + 'sw.js'); + }); +} + +/** @todo SSR */ +// if (typeof module==='object') { +// module.exports = app; +// } diff --git a/src/lib/util.js b/src/lib/util.js deleted file mode 100644 index fd622c8c..00000000 --- a/src/lib/util.js +++ /dev/null @@ -1,19 +0,0 @@ -import { Component } from 'preact'; - -export function updater(obj, property, value) { - return e => { - let update = {}; - update[property] = typeof value === 'function' ? value(obj.state[property], e) : value; - obj.setState(update); - }; -} - -export const toggle = value => !value; - -export class When extends Component { - state = { ready: !!this.props.value }; - render({ value, children: [child] }, { ready }) { - if (value && !ready) this.setState({ ready: true }); - return ready ? (typeof child === 'function' ? child() : child) : null; - } -} diff --git a/src/lib/util.ts b/src/lib/util.ts new file mode 100644 index 00000000..d9765950 --- /dev/null +++ b/src/lib/util.ts @@ -0,0 +1,39 @@ +import { Component, ComponentProps } from 'preact'; + +type WhenProps = ComponentProps & { + value: boolean, + children?: (JSX.Element | (() => JSX.Element))[] +}; + +type WhenState = { + ready: boolean +}; + +export class When extends Component { + state: WhenState = { + ready: !!this.props.value + }; + + render({ value, children = [] }: WhenProps, { ready }: WhenState) { + let child = children[0]; + if (value && !ready) this.setState({ ready: true }); + return ready ? (typeof child === 'function' ? child() : child) : null; + } +} + +/** + * A decorator that binds values to their class instance. + * @example + * class C { + * @bind + * foo () { + * return this; + * } + * } + * let f = new C().foo; + * f() instanceof C; // true + */ +export function bind(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + descriptor.value = descriptor.value.bind(target); + return descriptor; +} diff --git a/tsconfig.json b/tsconfig.json index d9335c4e..8dbfb6a8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,16 @@ { - "compileOnSave": false, - "compilerOptions": { - "target": "es2017", - "module": "esnext", - "moduleResolution": "node", - "sourceMap": true, - "jsx": "react", - "jsxFactory": "h", - "allowJs": true, - "baseUrl": ".", - "paths": { - "async!*": ["*"] - } - } + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "target": "es2017", + "module": "esnext", + "moduleResolution": "node", + "experimentalDecorators": true, + "noUnusedLocals": true, + "sourceMap": true, + "jsx": "react", + "jsxFactory": "h", + "allowJs": false, + "baseUrl": "." + } } \ No newline at end of file diff --git a/tslint.json b/tslint.json new file mode 100644 index 00000000..7842ef4d --- /dev/null +++ b/tslint.json @@ -0,0 +1,16 @@ +{ + "extends": [ + "tslint-config-semistandard", + "tslint-react" + ], + "rules": { + "quotemark": [true, "single", "jsx-double", "avoid-escape"], + "no-use-before-declare": false, + "no-floating-promises": false, + "space-before-function-paren": [true, false], + "jsx-boolean-value": [true, "never"], + "jsx-no-multiline-js": false, + "jsx-no-bind": true, + "jsx-no-lambda": true + } +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 41e39a23..4382f78d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,6 +1,8 @@ const fs = require('fs'); const path = require('path'); const webpack = require('webpack'); +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); +const ForkTsCheckerNotifierWebpackPlugin = require('fork-ts-checker-notifier-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const ProgressBarPlugin = require('progress-bar-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); @@ -54,6 +56,10 @@ module.exports = function(_, env) { loader: 'ts-loader', // Don't transpile anything in node_modules: exclude: nodeModules, + options: { + // Offload type checking to ForkTsCheckerWebpackPlugin for better performance: + transpileOnly: true + } }, { test: /\.(tsx?|jsx?)$/, @@ -112,6 +118,15 @@ module.exports = function(_, env) { ] }, plugins: [ + // Runs tslint & type checking in a worker pool + new ForkTsCheckerWebpackPlugin({ + tslint: true, + // wait for type chec + async: !isProd, + formatter: 'codeframe' + }), + new ForkTsCheckerNotifierWebpackPlugin({ excludeWarnings: true }), + // Pretty progressbar showing build progress: new ProgressBarPlugin({ format: '\u001b[90m\u001b[44mBuild\u001b[49m\u001b[39m [:bar] \u001b[32m\u001b[1m:percent\u001b[22m\u001b[39m (:elapseds) \u001b[2m:msg\u001b[22m',