Created
June 25, 2018 18:51
-
-
Save docwalter/5360f90cdff7e9203ca0a0f164c68965 to your computer and use it in GitHub Desktop.
create-react-app + TypeScript + MobX
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/env sh | |
set -e | |
if [ -z "$1" ] ; then | |
echo "Usage: `basename $0` appname" | |
exit 1 | |
fi | |
name="$1" | |
echo "Setting up $name..." | |
npm i -g npm | |
npm i -g create-react-app | |
create-react-app $name --scripts-version=react-scripts-ts --use-npm | |
cd $name | |
mkdir src/api src/components src/stores | |
npm i -S mobx mobx-react mobx-react-devtools react-router react-router-dom @types/react-router @types/react-router-dom mobx-react-router node-sass-chokidar bootstrap reactstrap @types/reactstrap | |
npm i -D jest-localstorage-mock | |
cat >.editorconfig <<EOF | |
root = true | |
[*] | |
end_of_line = lf | |
insert_final_newline = true | |
trim_trailing_whitespace = true | |
[*.{html,js,json,ts,tsx}] | |
indent_style = space | |
indent_size = 2 | |
EOF | |
sed -i -e '3 i\ "experimentalDecorators": true,' tsconfig.json | |
sed -i -e '/"extends":/a\ "rules": {\n "interface-name": false,\n "member-access": [true, "no-public"],\n "only-arrow-functions": false\n },' tslint.json | |
sed -i -e '/"eject":/i\ "build-css": "node-sass-chokidar src/ -o src/",\n "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive",' package.json | |
sed -i -e '/"dependencies": {/i\ "proxy": "http://localhost:8080/",' package.json | |
for f in src/*.css ; do mv "$f" src/$(basename "$f" .css).scss ; done | |
#### AB HIER BEISPIELINHALTE! | |
# --- API | |
cat >src/api/index.ts <<EOF | |
/** | |
* Ruft ein JSON-Objekt von einer URL ab. Standardmäßig wird GET verwendet, außer es ist ein POST-Body angegeben, dann wird POST benutzt. | |
* | |
* @param url die URL | |
* @param body optionaler POST-Body | |
*/ | |
export async function fetchJSON<T>(url: string, body?: any) { | |
const request: RequestInit = { headers: { Accept: "application/json", }, method: body ? "POST" : "GET", body } | |
const response = await fetch("http://api.icndb.com/jokes/random", request) | |
const result = await response.json() as T | |
return result | |
} | |
EOF | |
cat >src/api/index.test.ts <<EOF | |
import { fetchJSON } from "." | |
it("returns response on ok", async function () { | |
interface TestType { name: string, count: number } | |
window.fetch = jest.fn().mockImplementation(() => Promise.resolve({ json: () => ({ name: "Lurch", count: 4711 } as TestType) })) | |
const response = await fetchJSON("/url") | |
expect(response).toBeDefined() | |
}) | |
it("throws an error on error", async function () { | |
expect.assertions(1) | |
window.fetch = jest.fn().mockImplementation(() => { throw new Error("Jörgjörgjörgjörgjörgjörg...ULF!") }) | |
try { | |
await fetchJSON("/url") | |
} catch (reason) { | |
expect(reason).toEqual(new Error("Jörgjörgjörgjörgjörgjörg...ULF!")) | |
} | |
}) | |
EOF | |
# --- Stores | |
cat >src/stores/base.ts <<EOF | |
/** | |
* Basisklasse für alle Stores. | |
*/ | |
export abstract class Store { | |
/** Alle Store-Instanzen, die von dieser Klasse abgeleitet wurden. */ | |
static stores: Store[] = [] | |
/** Setzt alle Stores auf ihre Anfangszustände zurück. */ | |
static resetAllStores(): void { | |
for (const store of Store.stores) { | |
store.init() | |
} | |
} | |
/** Erzeugt einen neuen Store. */ | |
constructor() { | |
this.init() | |
Store.stores.push(this) | |
} | |
/** Setzt alle Werte im Store auf den Anfangszustand zurück. */ | |
abstract init(): void | |
} | |
EOF | |
cat >src/stores/chucknorris.ts <<EOF | |
import { action, observable } from "mobx" | |
import { fetchJSON } from "../api" | |
import { Store } from "./base" | |
/** | |
* Austauschformat, in dem der Webservice Chuck-Norris-Quotes liefert. Reverseengineered von folgender handgefetchedter Message: | |
* { | |
* "type": "success", | |
* "value": { | |
* "id": 267, | |
* "joke": "Ozzy Osbourne bites the heads off of bats. Chuck Norris bites the heads off of Siberian Tigers.", | |
* "categories": [] | |
* } | |
* } | |
*/ | |
export interface ChuckNorrisContent { | |
type: string | |
value: { | |
id: number | |
joke: string | |
categories: string[] | |
} | |
} | |
export class ChuckNorrisStore extends Store { | |
@observable quote: string | |
init() { | |
this.quote = "<noch nicht geladen>" | |
} | |
@action.bound setQuoteFromContent(content: ChuckNorrisContent) { | |
this.quote = content.value.joke | |
} | |
@action.bound setQuoteFromError(error: any) { | |
this.quote = "Fehler! " + error | |
} | |
@action.bound async nextQuote() { | |
this.quote = "<laden...>" | |
try { | |
const content = await fetchJSON<ChuckNorrisContent>("http://api.icndb.com/jokes/random") | |
this.setQuoteFromContent(content) | |
} catch (reason) { | |
this.setQuoteFromError(reason) | |
} | |
} | |
} | |
EOF | |
cat >src/stores/chucknorris.test.ts <<EOF | |
import { ChuckNorrisContent, ChuckNorrisStore } from "./chucknorris" | |
it("sets quote on ok", async function () { | |
const reply: ChuckNorrisContent = { | |
"type": "success", | |
"value": { | |
"categories": [], | |
"id": 267, | |
"joke": "Ozzy Osbourne bites the heads off of bats. Chuck Norris bites the heads off of Siberian Tigers." | |
} | |
} | |
window.fetch = jest.fn().mockImplementation(() => Promise.resolve({ json: () => reply, ok: true })) | |
const store = new ChuckNorrisStore() | |
await store.nextQuote() | |
expect(store.quote).toEqual(reply.value.joke) | |
}) | |
it("sets an error message on error", async function () { | |
expect.assertions(1) | |
window.fetch = jest.fn().mockImplementation(() => { throw new Error("Jörgjörgjörgjörgjörgjörg...ULF!") }) | |
const store = new ChuckNorrisStore() | |
await store.nextQuote() | |
expect(store.quote).toEqual("Fehler! Error: Jörgjörgjörgjörgjörgjörg...ULF!") | |
}) | |
EOF | |
cat >src/stores/index.ts <<EOF | |
import { RouterStore } from "mobx-react-router" | |
export { Store } from "./base" | |
import { ChuckNorrisStore } from "./chucknorris" | |
export const chuckNorrisStore = new ChuckNorrisStore() | |
export const routingStore = new RouterStore() | |
EOF | |
# --- Components | |
cat >src/components/chucknorris.tsx <<EOF | |
import { observer } from "mobx-react" | |
import * as React from "react" | |
import { chuckNorrisStore } from "../stores" | |
@observer | |
export class ChuckNorris extends React.Component { | |
render() { | |
return ( | |
<div> | |
<code dangerouslySetInnerHTML={{ __html: chuckNorrisStore.quote }} /> | |
<br /><button onClick={chuckNorrisStore.nextQuote}>Fetch</button> | |
</div> | |
) | |
} | |
} | |
EOF | |
cat >src/components/chucknorris.test.tsx <<EOF | |
import * as React from "react" | |
import * as ReactDOM from "react-dom" | |
import { chuckNorrisStore, Store } from "../stores" | |
import { ChuckNorris } from "./chucknorris" | |
beforeEach(() => Store.resetAllStores()) | |
it("renders without crashing", () => { | |
const div = document.createElement("div") | |
chuckNorrisStore.quote = "Bla" | |
ReactDOM.render(<ChuckNorris />, div) | |
}) | |
EOF | |
sed -i -e "/import logo/i\import { ChuckNorris } from './components/chucknorris';" src/App.tsx | |
sed -i -e 's/public render()/render()/' src/App.tsx | |
sed -i -e '/<\/div>/i\ <ChuckNorris />' src/App.tsx | |
### FERTIG! | |
npm run build-css | |
git init | |
git add . | |
git commit -m "Erster Commit. Projekt neu erzeugt." | |
echo "For SCSS live compiling and reloading: 'npm run watch-css &'" | |
echo "For tests run: 'npm run test'" | |
echo "Start with: 'npm start'" | |
echo "Build production deployment with: 'npm run build'" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment