Use strict TypeScript, enable TSLint, update all types to work in strict mode.

This commit is contained in:
Jason Miller
2018-03-29 15:42:07 -04:00
parent 9977e5b8c6
commit 5e6500d196
16 changed files with 396 additions and 265 deletions

9
.editorconfig Normal file
View File

@@ -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

18
global.d.ts vendored
View File

@@ -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;
}

View File

@@ -11,6 +11,10 @@
"eslintConfig": { "eslintConfig": {
"extends": "eslint-config-developit", "extends": "eslint-config-developit",
"rules": { "rules": {
"indent": [
2,
2
],
"react/prefer-stateless-function": 0 "react/prefer-stateless-function": 0
} }
}, },
@@ -37,6 +41,8 @@
"eslint": "^4.18.2", "eslint": "^4.18.2",
"eslint-config-developit": "^1.1.1", "eslint-config-developit": "^1.1.1",
"extract-text-webpack-plugin": "^4.0.0-beta.0", "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", "html-webpack-plugin": "^3.0.6",
"if-env": "^1.0.4", "if-env": "^1.0.4",
"mini-css-extract-plugin": "^0.2.0", "mini-css-extract-plugin": "^0.2.0",
@@ -48,6 +54,9 @@
"script-ext-html-webpack-plugin": "^2.0.0", "script-ext-html-webpack-plugin": "^2.0.0",
"style-loader": "^0.20.3", "style-loader": "^0.20.3",
"ts-loader": "^4.0.1", "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": "^2.7.2",
"typescript-loader": "^1.1.3", "typescript-loader": "^1.1.3",
"typings-for-css-modules-loader": "^1.7.0", "typings-for-css-modules-loader": "^1.7.0",

View File

@@ -1,5 +1,5 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import { updater, toggle, When } from '../../lib/util'; import { When, bind } from '../../lib/util';
import Fab from '../fab'; import Fab from '../fab';
import Header from '../header'; import Header from '../header';
// import Drawer from 'async!../drawer'; // import Drawer from 'async!../drawer';
@@ -8,105 +8,113 @@ import Home from '../home';
import * as style from './style.scss'; import * as style from './style.scss';
type Props = { type Props = {
url?: String url?: string
} };
type FileObj = { export type FileObj = {
id: any, id: number,
data: any, data?: string,
error: Error | DOMError | String, error?: Error | DOMError | String,
file: File, file: File,
loading: Boolean loading: boolean
}; };
type State = { type State = {
showDrawer: Boolean, showDrawer: boolean,
showFab: Boolean, showFab: boolean,
files: FileObj[] files: FileObj[]
}; };
let counter = 0; let counter = 0;
export default class App extends Component<Props, State> { export default class App extends Component<Props, State> {
state: State = { state: State = {
showDrawer: false, showDrawer: false,
showFab: true, showFab: true,
files: [] files: []
}; };
loadFile = (file: File) => { enableDrawer = false;
let fileObj = {
id: ++counter,
file,
error: null,
loading: true,
data: null
};
this.setState({ @bind
files: [fileObj] openDrawer() {
}); this.setState({ showDrawer: true });
}
@bind
closeDrawer() {
this.setState({ showDrawer: false });
}
@bind
toggleDrawer() {
this.setState({ showDrawer: !this.state.showDrawer });
}
let fr = new FileReader(); @bind
// fr.readAsArrayBuffer(); openFab() {
fr.onerror = () => { this.setState({ showFab: true });
let files = this.state.files.slice(); }
files.splice(0, files.indexOf(fileObj), { @bind
...fileObj, closeFab() {
error: fr.error, this.setState({ showFab: false });
loading: false }
}); @bind
this.setState({ files }); toggleFab() {
}; this.setState({ showFab: !this.state.showFab });
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);
};
enableDrawer = false; @bind
loadFile(file: File) {
let fileObj: FileObj = {
id: ++counter,
file,
error: undefined,
loading: true,
data: undefined
};
openDrawer = updater(this, 'showDrawer', true); this.setState({
closeDrawer = updater(this, 'showDrawer', false); files: [fileObj]
toggleDrawer = updater(this, 'showDrawer', toggle); });
openFab = updater(this, 'showFab', true); let done = () => {
closeFab = updater(this, 'showFab', false); let files = this.state.files.slice();
toggleFab = updater(this, 'showFab', toggle); files[files.indexOf(fileObj)] = Object.assign({}, fileObj, {
error: fr.error,
loading: false,
data: fr.result
});
this.setState({ files });
};
render({ url }, { showDrawer, showFab, files }) { let fr = new FileReader();
if (showDrawer) this.enableDrawer = true; 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;
<div id="app" class={style.app}>
<Fab showing={showFab} />
<Header toggleDrawer={this.toggleDrawer} loadFile={this.loadFile} /> return (
<div id="app" class={style.app}>
<Fab showing={showFab} />
{/* Avoid loading & rendering the drawer until the first time it is shown. */} <Header class={style.header} toggleDrawer={this.toggleDrawer} loadFile={this.loadFile} />
<When value={showDrawer}>
<Drawer showing={showDrawer} openDrawer={this.openDrawer} closeDrawer={this.closeDrawer} />
</When>
{/* {/* Avoid loading & rendering the drawer until the first time it is shown. */}
Note: this is normally where a <Router> with auto code-splitting goes. <When value={showDrawer}>
Since we don't seem to need one (yet?), it's omitted. <Drawer showing={showDrawer} openDrawer={this.openDrawer} closeDrawer={this.closeDrawer} />
*/} </When>
<div class={style.content}>
<Home files={files} />
</div>
{/* This ends up in the body when prerendered, which makes it load async. */} {/*
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" /> Note: this is normally where a <Router> with auto code-splitting goes.
</div> Since we don't seem to need one (yet?), it's omitted.
); */}
} <div class={style.content}>
<Home files={files} />
</div>
</div>
);
}
} }

View File

@@ -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 (
<MdlDrawer open={showing} onOpen={openDrawer} onClose={closeDrawer}>
<MdlDrawer.Header class="mdc-theme--primary-bg">
<img class={style.logo} alt="logo" src="/assets/icon.png" />
</MdlDrawer.Header>
<MdlDrawer.Content class={style.list}>
<List>
<List.LinkItem href="/">
<List.ItemIcon>verified_user</List.ItemIcon>
<Text id="SIGN_IN">Sign In</Text>
</List.LinkItem>
<List.LinkItem href="/register">
<List.ItemIcon>account_circle</List.ItemIcon>
<Text id="REGISTER">Register</Text>
</List.LinkItem>
</List>
</MdlDrawer.Content>
<div class={style.bottom}>
<List.LinkItem href="/preferences">
<List.ItemIcon>settings</List.ItemIcon>
<Text id="PREFERENCES">Preferences</Text>
</List.LinkItem>
</div>
</MdlDrawer>
);
}
}

View File

@@ -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<Props, State> {
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 (
<MdlDrawer open={showing} onOpen={openDrawer} onClose={closeDrawer}>
<MdlDrawer.Header class="mdc-theme--primary-bg">
<img class={style.logo} alt="logo" src="/assets/icon.png" />
</MdlDrawer.Header>
<MdlDrawer.Content>
<List>
<List.LinkItem href="/">
<List.ItemIcon>verified_user</List.ItemIcon>
<Text id="SIGN_IN">Sign In</Text>
</List.LinkItem>
<List.LinkItem href="/register">
<List.ItemIcon>account_circle</List.ItemIcon>
<Text id="REGISTER">Register</Text>
</List.LinkItem>
</List>
</MdlDrawer.Content>
<div class={style.bottom}>
<List.LinkItem href="/preferences">
<List.ItemIcon>settings</List.ItemIcon>
<Text id="PREFERENCES">Preferences</Text>
</List.LinkItem>
</div>
</MdlDrawer>
);
}
}

View File

@@ -1,4 +1,5 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import { bind } from '../../lib/util';
import Icon from 'preact-material-components/Icon'; import Icon from 'preact-material-components/Icon';
import 'preact-material-components/Icon/style.css'; import 'preact-material-components/Icon/style.css';
import Fab from 'preact-material-components/Fab'; import Fab from 'preact-material-components/Fab';
@@ -6,34 +7,41 @@ import RadialProgress from 'material-radial-progress';
import * as style from './style.scss'; import * as style from './style.scss';
type Props = { type Props = {
showing: boolean showing: boolean
}; };
type State = { type State = {
loading: boolean loading: boolean
}; };
export default class AppFab extends Component<Props, State> { export default class AppFab extends Component<Props, State> {
state: State = { state: State = {
loading: false loading: false
}; };
handleClick = () => { @bind
console.log('TODO: Save the file to disk.'); setLoading(loading: boolean) {
this.setState({ loading: true }); this.setState({ loading });
setTimeout( () => { }
this.setState({ loading: false });
}, 1000);
};
render({ showing }, { loading }) { @bind
return ( handleClick() {
<Fab ripple secondary exited={showing===false} class={style.fab} onClick={this.handleClick}> console.log('TODO: Save the file to disk.');
{ loading ? ( this.setState({ loading: true });
<RadialProgress primary class={style.progress} /> setTimeout(() => {
) : ( this.setState({ loading: false });
<Icon>file_download</Icon> }, 1000);
) } }
</Fab>
); render({ showing }: Props, { loading }: State) {
} return (
<Fab ripple secondary exited={showing === false} class={style.fab} onClick={this.handleClick}>
{ loading ? (
<RadialProgress primary class={style.progress} />
) : (
<Icon>file_download</Icon>
) }
</Fab>
);
}
} }

View File

@@ -2,50 +2,52 @@ import { h, Component } from 'preact';
import Toolbar from 'preact-material-components/Toolbar'; import Toolbar from 'preact-material-components/Toolbar';
import cx from 'classnames'; import cx from 'classnames';
import * as style from './style.scss'; import * as style from './style.scss';
import { bind } from '../../lib/util';
type Props = { type Props = {
toggleDrawer?(), 'class'?: string,
showHeader?(), showHeader?: boolean,
showFab?(), toggleDrawer?(): void,
loadFile?(File) showFab?(): void,
loadFile(f: File): void
}; };
type State = { type State = {};
};
export default class Header extends Component<Props, State> { export default class Header extends Component<Props, State> {
input: HTMLInputElement; input?: HTMLInputElement;
setInputRef = c => { @bind
this.input = c; setInputRef(c?: Element) {
}; this.input = c as HTMLInputElement;
}
upload = () => { @bind
this.input.click(); upload() {
}; this.input!.click();
}
handleFiles = () => { @bind
let files = this.input.files; handleFiles() {
if (files.length) { let files = this.input!.files;
this.props.loadFile(files[0]); if (files && files.length) {
} this.props.loadFile(files[0]);
}; }
}
render({ toggleDrawer, showHeader, showFab }) { render({ class: c, toggleDrawer, showHeader = false, showFab }: Props) {
return ( return (
<Toolbar fixed class={cx(style.toolbar, 'inert', showHeader===false && style.minimal)}> <Toolbar fixed class={cx(c, style.toolbar, 'inert', !showHeader && style.minimal)}>
<Toolbar.Row> <Toolbar.Row>
<Toolbar.Title class={style.title}> <Toolbar.Title class={style.title}>
<img class={style.logo} src="/assets/icon.png" /> <Toolbar.Icon title="Upload" ripple onClick={this.upload}>file_upload</Toolbar.Icon>
<Toolbar.Icon ripple onClick={this.upload}>file_upload</Toolbar.Icon> </Toolbar.Title>
</Toolbar.Title> <Toolbar.Section align-end>
<Toolbar.Section align-end> <Toolbar.Icon ripple onClick={toggleDrawer}>menu</Toolbar.Icon>
<Toolbar.Icon ripple onClick={toggleDrawer}>menu</Toolbar.Icon> </Toolbar.Section>
</Toolbar.Section> </Toolbar.Row>
</Toolbar.Row> <input class={style.fileInput} ref={this.setInputRef} type="file" onChange={this.handleFiles} />
<input class={style.fileInput} ref={this.setInputRef} type="file" onChange={this.handleFiles} /> </Toolbar>
</Toolbar> );
); }
}
} }

View File

@@ -3,34 +3,34 @@ import { h, Component } from 'preact';
// import Switch from 'preact-material-components/Switch'; // import Switch from 'preact-material-components/Switch';
// import 'preact-material-components/Switch/style.css'; // import 'preact-material-components/Switch/style.css';
import * as style from './style.scss'; import * as style from './style.scss';
import { FileObj } from '../app';
type Props = { type Props = {
files: { files: FileObj[]
data: any
}[]
}; };
type State = { type State = {
active: boolean active: boolean
}; };
export default class Home extends Component<Props, State> { export default class Home extends Component<Props, State> {
state: State = { state: State = {
active: false active: false
}; };
componentDidMount() { componentDidMount() {
setTimeout( () => { setTimeout(() => {
this.setState({ active: true }); this.setState({ active: true });
}); });
} }
render({ files }, { active }) { render({ files }: Props, { active }: State) {
return ( return (
<div class={style.home+' '+(active ? style.active : '')}> <div class={style.home + ' ' + (active ? style.active : '')}>
{ files && files[0] && ( { files && files[0] && (
<img src={files[0].data} /> <img src={files[0].data} />
) } ) }
</div> </div>
); );
} }
} }

View File

@@ -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));
});
}

32
src/index.tsx Normal file
View File

@@ -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(<App />, 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(<App />, 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;
// }

View File

@@ -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;
}
}

39
src/lib/util.ts Normal file
View File

@@ -0,0 +1,39 @@
import { Component, ComponentProps } from 'preact';
type WhenProps = ComponentProps<When> & {
value: boolean,
children?: (JSX.Element | (() => JSX.Element))[]
};
type WhenState = {
ready: boolean
};
export class When extends Component<WhenProps, WhenState> {
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;
}

View File

@@ -1,16 +1,16 @@
{ {
"compileOnSave": false, "compileOnSave": false,
"compilerOptions": { "compilerOptions": {
"target": "es2017", "strict": true,
"module": "esnext", "target": "es2017",
"moduleResolution": "node", "module": "esnext",
"sourceMap": true, "moduleResolution": "node",
"jsx": "react", "experimentalDecorators": true,
"jsxFactory": "h", "noUnusedLocals": true,
"allowJs": true, "sourceMap": true,
"baseUrl": ".", "jsx": "react",
"paths": { "jsxFactory": "h",
"async!*": ["*"] "allowJs": false,
} "baseUrl": "."
} }
} }

16
tslint.json Normal file
View File

@@ -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
}
}

View File

@@ -1,6 +1,8 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const webpack = require('webpack'); 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 CleanWebpackPlugin = require('clean-webpack-plugin');
const ProgressBarPlugin = require('progress-bar-webpack-plugin'); const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
@@ -54,6 +56,10 @@ module.exports = function(_, env) {
loader: 'ts-loader', loader: 'ts-loader',
// Don't transpile anything in node_modules: // Don't transpile anything in node_modules:
exclude: nodeModules, exclude: nodeModules,
options: {
// Offload type checking to ForkTsCheckerWebpackPlugin for better performance:
transpileOnly: true
}
}, },
{ {
test: /\.(tsx?|jsx?)$/, test: /\.(tsx?|jsx?)$/,
@@ -112,6 +118,15 @@ module.exports = function(_, env) {
] ]
}, },
plugins: [ 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: // Pretty progressbar showing build progress:
new ProgressBarPlugin({ 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', 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',