Skip to content

Instantly share code, notes, and snippets.

@docwalter
Created June 25, 2018 18:51
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save docwalter/5360f90cdff7e9203ca0a0f164c68965 to your computer and use it in GitHub Desktop.
Save docwalter/5360f90cdff7e9203ca0a0f164c68965 to your computer and use it in GitHub Desktop.
create-react-app + TypeScript + MobX
#!/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 = "&lt;noch nicht geladen&gt;"
}
@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 = "&lt;laden...&gt;"
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