├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .jscsrc
├── .scss-lint.yaml
├── Procfile
├── README.md
├── app
├── actions
│ ├── cars.js
│ ├── directors-actions.js
│ ├── films-actions.js
│ ├── login-actions.js
│ └── status-actions.js
├── components
│ ├── app.js
│ ├── director-profile.js
│ ├── directors
│ │ ├── director-form.js
│ │ ├── directors-table.js
│ │ └── directors-table.spec.js
│ ├── film-profile.js
│ ├── films
│ │ ├── film-form.js
│ │ ├── films-table.js
│ │ └── index.js
│ ├── login-new
│ │ ├── index.js
│ │ └── login-page.js
│ ├── login.js
│ ├── login.spec.js
│ └── shared
│ │ ├── action-bar.js
│ │ ├── bootstrap-input.js
│ │ ├── film-item.js
│ │ ├── footer.js
│ │ ├── navbar.js
│ │ ├── pikaday-input.js
│ │ ├── pikaday.js
│ │ └── select-input.js
├── data
│ ├── en.js
│ ├── fr.js
│ └── users.json
├── fonts
│ ├── Roboto-Medium-webfont.eot
│ ├── Roboto-Medium-webfont.svg
│ ├── Roboto-Medium-webfont.ttf
│ └── Roboto-Medium-webfont.woff
├── images
│ ├── favicon.ico
│ ├── react-logo.png
│ └── spinner.svg
├── index.js
├── main.js
├── pages
│ ├── not-found.jsx
│ └── server-error.jsx
├── router.js
├── routes.jsx
├── stores
│ ├── cars.js
│ ├── directors-store.js
│ ├── films-store.js
│ ├── login-store.js
│ ├── login-store.spec.js
│ ├── login-store.test.js
│ └── status-store.js
├── styles
│ ├── _app.scss
│ ├── _bootstrap-customizations.scss
│ ├── _cars.scss
│ ├── _fonts.scss
│ ├── _footer.scss
│ ├── _main.scss
│ ├── _navbar.scss
│ ├── _pre-bootstrap-customizations.scss
│ ├── _users.scss
│ ├── bootstrap-sass.config.js
│ └── bootstrap-sass.prod.config.js
└── utils
│ ├── action-utils.js
│ ├── alt-resolver.js
│ ├── alt.js
│ ├── api.js
│ ├── component-utils.js
│ ├── countries.js
│ ├── flux-old.js
│ ├── image-resolver.js
│ ├── intl-loader.js
│ ├── intl-polyfill.js
│ ├── promisify.js
│ ├── store-utils.js
│ └── test
│ ├── get-wrapped-component.js
│ ├── init-dom.js
│ ├── login-store-mock.js
│ ├── router-mock.jsx
│ ├── shallow-render-component.js
│ └── stub-router-context.js
├── package.json
├── processes.json
├── server
├── config
│ ├── all.json
│ ├── development.json
│ ├── init.js
│ └── production.json
├── index.js
├── koa.js
├── models
│ ├── car.js
│ ├── director.js
│ ├── film.js
│ └── user.js
├── rest.js
├── restable
│ ├── .npmignore
│ ├── .travis.yml
│ ├── Makefile
│ ├── README.md
│ ├── docs
│ │ ├── coverage.html
│ │ ├── docs.md
│ │ ├── head.html
│ │ ├── tail.html
│ │ └── test.html
│ ├── example
│ │ ├── app.js
│ │ └── coffee
│ │ │ └── app.coffee
│ ├── gulpfile.coffee
│ ├── gulpfile.js
│ ├── lib
│ │ ├── actions.js
│ │ ├── index.js
│ │ ├── model.js
│ │ ├── mount-router.js
│ │ └── routes.js
│ ├── package.json
│ └── test
│ │ ├── index.coffee
│ │ ├── mocha.opts
│ │ └── server.js
├── router.jsx
└── views
│ ├── layouts
│ └── index.hbs
│ └── main.hbs
├── test
├── mocha-runner.js
└── tape-runner.js
├── tests.webpack.js
└── webpack
├── dev-server.js
├── dev.config.js
├── prod.config.js
└── utils
├── clean-dist.js
├── start-koa.js
└── write-stats.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "stage": 0
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 |
9 | # Change these settings to your own preference
10 | indent_style = tab
11 | indent_size = 2
12 |
13 | # We recommend you to keep these unchanged
14 | end_of_line = lf
15 | charset = utf-8
16 | trim_trailing_whitespace = true
17 | insert_final_newline = true
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
21 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/**
2 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "plugins": ["react"],
4 | "env": {
5 | "es6": true,
6 | "browser": true,
7 | "node": true,
8 | "mocha": true
9 | },
10 | "rules": {
11 | "quotes": [0],
12 | "eol-last": [0],
13 | "no-mixed-requires": [0],
14 | "no-underscore-dangle": [0],
15 | "camelcase": 1,
16 | "curly": 0,
17 | "no-unused-expressions": [1],
18 | "react/display-name": 0,
19 | "react/jsx-boolean-value": 1,
20 | "react/jsx-quotes": 1,
21 | "react/jsx-no-undef": 1,
22 | "react/jsx-uses-react": 1,
23 | "react/jsx-uses-vars": 1,
24 | "react/no-did-mount-set-state": 1,
25 | "react/no-did-update-set-state": 1,
26 | "react/no-multi-comp": 1,
27 | "react/no-unknown-property": 1,
28 | "react/prop-types": 1,
29 | "react/react-in-jsx-scope": 1,
30 | "react/self-closing-comp": 1,
31 | "react/wrap-multilines": 1
32 | },
33 | "ecmaFeatures": {
34 | "jsx": true,
35 | "modules": true,
36 | "blockBindings": true
37 | },
38 | "globals": {
39 | "sinon": true,
40 | "chai": true,
41 | "assert": true,
42 | "expect": true,
43 | "should": true
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/webpack-stats.json
2 | logs
3 | coverage
4 | node_modules
5 | npm-debug.log
6 | dist
7 | .tmp
8 | .DS_Store
9 | .sass-cache
10 | .env
11 | .c9
--------------------------------------------------------------------------------
/.jscsrc:
--------------------------------------------------------------------------------
1 | {
2 | "preset": "airbnb",
3 | "esprima": "./node_modules/esprima-fb",
4 | "requireSemicolons": null,
5 | "validateQuoteMarks": "\"",
6 | "validateIndentation": "\t",
7 | "excludeFiles": ["node_modules/**"]
8 | }
--------------------------------------------------------------------------------
/.scss-lint.yaml:
--------------------------------------------------------------------------------
1 | # Default application configuration that all configurations inherit from.
2 |
3 | scss_files: "**/*.scss"
4 |
5 | linters:
6 | BangFormat:
7 | enabled: true
8 | space_before_bang: true
9 | space_after_bang: false
10 |
11 | BorderZero:
12 | enabled: true
13 | convention: zero # or `none`
14 |
15 | ColorKeyword:
16 | enabled: true
17 |
18 | ColorVariable:
19 | enabled: true
20 |
21 | Comment:
22 | enabled: true
23 |
24 | DebugStatement:
25 | enabled: true
26 |
27 | DeclarationOrder:
28 | enabled: true
29 |
30 | DuplicateProperty:
31 | enabled: true
32 |
33 | ElsePlacement:
34 | enabled: true
35 | style: same_line # or 'new_line'
36 |
37 | EmptyLineBetweenBlocks:
38 | enabled: true
39 | ignore_single_line_blocks: true
40 |
41 | EmptyRule:
42 | enabled: true
43 |
44 | FinalNewline:
45 | enabled: true
46 | present: true
47 |
48 | HexLength:
49 | enabled: true
50 | style: short # or 'long'
51 |
52 | HexNotation:
53 | enabled: true
54 | style: uppercase # or 'lowercase'
55 |
56 | HexValidation:
57 | enabled: true
58 |
59 | IdSelector:
60 | enabled: false
61 |
62 | ImportantRule:
63 | enabled: true
64 |
65 | ImportPath:
66 | enabled: true
67 | leading_underscore: false
68 | filename_extension: false
69 |
70 | Indentation:
71 | enabled: true
72 | allow_non_nested_indentation: true
73 | character: space # or 'tab'
74 | width: 2
75 |
76 | LeadingZero:
77 | enabled: true
78 | style: exclude_zero # or 'include_zero'
79 |
80 | MergeableSelector:
81 | enabled: true
82 | force_nesting: true
83 |
84 | NameFormat:
85 | enabled: false
86 | allow_leading_underscore: true
87 | convention: BEM # or 'BEM', or a regex pattern
88 |
89 | NestingDepth:
90 | enabled: true
91 | max_depth: 3
92 |
93 | PlaceholderInExtend:
94 | enabled: true
95 |
96 | PropertyCount:
97 | enabled: false
98 | include_nested: false
99 | max_properties: 10
100 |
101 | PropertySortOrder:
102 | enabled: true
103 | order: ["position", "top", "right", "bottom", "left", "z-index", "display", "flex-direction", "flex-flow", "flex-wrap", "justify-content", "align-items", "align-content", "order", "flex", "flex-grow", "flex-shrink", "flex-basis", "align-self", "visibility", "overflow", "overflow-x", "overflow-y", "float", "clear", "table-layout", "border-collapse", "empty-cells", "box-sizing", "width", "min-width", "max-width", "height", "min-height", "max-height", "margin", "margin-top", "margin-right", "margin-bottom", "margin-left", "padding", "padding-top", "padding-right", "padding-bottom", "padding-left", "border", "border-width", "border-style", "border-color", "border-radius", "border-top", "border-top-width", "border-top-style", "border-top-color", "border-right", "border-right-width", "border-right-style", "border-right-color", "border-bottom", "border-bottom-width", "border-bottom-style", "border-bottom-color", "border-left", "border-left-width", "border-left-style", "border-left-color", "border-top-left-radius", "border-top-right-radius", "border-bottom-right-radius", "border-bottom-left-radius", "white-space", "content", "color", "background", "background-color", "background-image", "background-repeat", "background-attachment", "background-position", "background-size", "opacity", "font", "font-weight", "font-style", "font-variant", "font-size", "font-family", "letter-spacing", "line-height", "list-style", "list-style-type", "list-style-position", "list-style-image", "outline", "outline-width", "outline-style", "outline-color", "text-align", "text-decoration", "text-indent", "text-transform", "text-shadow", "animation", "transform", "transition", "box-shadow"]
104 | ignore_unspecified: true
105 |
106 | PropertySpelling:
107 | enabled: true
108 | extra_properties: []
109 |
110 | QualifyingElement:
111 | enabled: false
112 | allow_element_with_attribute: false
113 | allow_element_with_class: false
114 | allow_element_with_id: false
115 |
116 | SelectorDepth:
117 | enabled: true
118 | max_depth: 3
119 |
120 | SelectorFormat:
121 | enabled: false
122 | convention: BEM # or 'BEM', or 'hyphenated_BEM', or 'snake_case', or 'camel_case', or a regex pattern
123 |
124 | Shorthand:
125 | enabled: true
126 |
127 | SingleLinePerProperty:
128 | enabled: true
129 | allow_single_line_rule_sets: true
130 |
131 | SingleLinePerSelector:
132 | enabled: true
133 |
134 | SpaceAfterComma:
135 | enabled: true
136 |
137 | SpaceAfterPropertyColon:
138 | enabled: true
139 | style: one_space # or 'no_space', or 'at_least_one_space', or 'aligned'
140 |
141 | SpaceAfterPropertyName:
142 | enabled: true
143 |
144 | SpaceBeforeBrace:
145 | enabled: true
146 | style: space # or 'new_line'
147 | allow_single_line_padding: false
148 |
149 | SpaceBetweenParens:
150 | enabled: true
151 | spaces: 0
152 |
153 | StringQuotes:
154 | enabled: true
155 | style: single_quotes # or double_quotes
156 |
157 | TrailingSemicolon:
158 | enabled: true
159 |
160 | TrailingZero:
161 | enabled: false
162 |
163 | UnnecessaryMantissa:
164 | enabled: true
165 |
166 | UnnecessaryParentReference:
167 | enabled: true
168 |
169 | UrlFormat:
170 | enabled: true
171 |
172 | UrlQuotes:
173 | enabled: true
174 |
175 | VariableForProperty:
176 | enabled: false
177 | properties: []
178 |
179 | VendorPrefixes:
180 | enabled: false
181 | identifier_list: base
182 | additional_identifiers: []
183 | excluded_identifiers: []
184 |
185 | ZeroUnit:
186 | enabled: true
187 |
188 | Compass::*:
189 | enabled: false
190 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: npm run prod
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Isomorphic React + Flux film database example
2 |
3 | > Complete application example including user authentication using koa on server side, and React/Flux(Alt) on frontend. Also uses koa-mongo-rest to generate REST API, and restful.js to consume them. With a healthy dose of Bootstrap, using react-bootstrap.
4 |
5 | **Demo:** https://react-example-filmdb.herokuapp.com
6 |
7 |
8 | ## Libraries Included
9 |
10 | * [react](https://facebook.github.io/react/)
11 | * [react-router](https://github.com/rackt/react-router)
12 | * [react-hot-loader](https://github.com/gaearon/react-hot-loader)
13 | * [react-bootstrap](http://react-bootstrap.github.io/)
14 | * [alt](https://github.com/goatslacker/alt)
15 | * [iso](https://github.com/goatslacker/iso)
16 | * [koa](http://koajs.com/)
17 | * [webpack](http://webpack.github.io/)
18 | * [babeljs](https://babeljs.io/)
19 | * [koa-mongo-rest](https://github.com/t3chnoboy/koa-mongo-rest)
20 | * [restful.js](https://github.com/marmelab/restful.js)
21 | * [formsy-react](https://github.com/christianalfoni/formsy-react)
22 |
23 | ## TL;DR
24 |
25 | Use with `iojs^1.8.0` or `nodejs^0.12.0`, clone the repo, `npm install` and `npm run dev`.
26 |
27 | Learn React ([react-prime-draft](https://github.com/mikechau/react-primer-draft)), learn Flux and Alt ([alt guide](http://alt.js.org/guide/)).
28 |
29 | Build for production with `npm run build`.
30 |
31 | ## Concepts
32 |
33 | **Koa** will be our server for the server side rendering, we use **alt** for our Flux architecture and **react-router** for routing in our app.
34 |
35 | With **iso** as helper we can populate **alt** flux stores before the first rendering and have a complete async isomorphic React application.
36 |
37 | **For a more comprehensive guide to the source code, see [wiki](https://github.com/tomaash/react-example-filmdb/wiki)**
38 |
39 | ## Flux
40 |
41 | We use [alt](alt.js.org) singleton as [Flux](http://facebook.github.io/react/blog/2014/05/06/flux.html) implementation.
42 |
43 | On the server, it's similar but Flux is initialized using `alt.bootstrap` and for next rendering, the data are emptied using `alt.flush`
44 |
45 | ## Installation / How-to
46 |
47 | I recommend to use [io.js](https://iojs.org/) to take advantages of `ES6` without `--harmony` flag on `NodeJS`.
48 |
49 | It's super easy to do with [nvm](https://github.com/creationix/nvm):
50 |
51 | * `$ nvm install iojs`
52 | * `$ nvm use iojs`
53 | * `$ nvm alias default iojs` (to make `node` default to `iojs`)
54 |
55 | But it works well with `nodejs^0.12.0` as well :)
56 |
57 | ### Run the project in development:
58 |
59 | * `$ npm run dev`
60 |
61 | ### Run tests
62 |
63 | * `$ npm test` will run the tests once
64 | * `$ ./node_modules/.bin/karma start` will watch for changes and run the tests on change
65 |
66 | ### Build project:
67 |
68 | Just run `$ npm run build`, it will produce these tasks:
69 |
70 | * Run tests from `test/spec/**/*.jsx`
71 | * Concat & minify styles to `/dist/app-[hash].css`
72 | * Concat & minify scripts to `/dist/js/app-[hash].js`
73 |
74 | ### Run in production
75 |
76 | Build the project first:
77 |
78 | * `$ npm run build`
79 |
80 | Then start the koa server:
81 |
82 | * `$ NODE_ENV=production node server/index.js` (iojs)
83 | * `$ NODE_ENV=production node --harmony server/index.js` (nodejs 0.12.x)
84 |
85 | You can also use `processes.json` to run the application with [PM2 Monitor](https://github.com/Unitech/pm2) on your production server (customize it for your use):
86 |
87 | * `$ pm2 start processes.json`
88 |
89 |
--------------------------------------------------------------------------------
/app/actions/cars.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import api from 'utils/api';
3 |
4 | class CarsActions {
5 | constructor() {
6 | this.generateActions(
7 | 'addSuccess', 'fetchSuccess', 'updateSuccess', 'deleteSuccess'
8 | );
9 | }
10 | async fetch() {
11 | try {
12 | console.log('will fetch');
13 | const response = await api.cars.getAll();
14 | if (response) {
15 | this.actions.fetchSuccess(response().data);
16 | }
17 | } catch (err) {
18 | console.log(err.stack);
19 | }
20 | }
21 | async add(car) {
22 | this.dispatch(car);
23 | const response = await api.cars.post(car);
24 | this.actions.addSuccess(response().data);
25 | }
26 | async update(data, item) {
27 | const response = await api.cars.put(item._id, data);
28 | this.actions.updateSuccess({data: response().data, item: item});
29 | }
30 | async delete(car, index) {
31 | await api.cars.delete(car._id);
32 | this.actions.deleteSuccess(index);
33 | }
34 | }
35 |
36 | export default CarsActions;
37 |
--------------------------------------------------------------------------------
/app/actions/directors-actions.js:
--------------------------------------------------------------------------------
1 | import alt from 'utils/alt';
2 | import api from 'utils/api';
3 | import {clone} from 'lodash';
4 | import {networkAction} from 'utils/action-utils';
5 |
6 | class DirectorsActions {
7 | constructor() {
8 | this.generateActions('removeCurrent');
9 | }
10 | fetch() {
11 | networkAction(this, api.directors.getAll);
12 | }
13 | get(id) {
14 | networkAction(this, api.directors.get, id);
15 | }
16 | add(data) {
17 | networkAction(this, api.directors.post, clone(data));
18 | }
19 | update(id, data) {
20 | networkAction(this, api.directors.put, id, clone(data));
21 | }
22 | delete(id) {
23 | networkAction(this, api.directors.delete, id);
24 | }
25 | }
26 |
27 | module.exports = (alt.createActions(DirectorsActions));
28 |
--------------------------------------------------------------------------------
/app/actions/films-actions.js:
--------------------------------------------------------------------------------
1 | import alt from 'utils/alt';
2 | import api from 'utils/api';
3 | import {clone} from 'lodash';
4 | import {networkAction} from 'utils/action-utils';
5 |
6 | class FilmsActions {
7 | fetch() {
8 | networkAction(this, api.films.getAll);
9 | }
10 | get(id) {
11 | networkAction(this, api.films.get, id);
12 | }
13 | findByDirectorId(id) {
14 | networkAction(this, api.films.getAll, {
15 | conditions: JSON.stringify({
16 | director: id
17 | })
18 | });
19 | }
20 | add(data) {
21 | networkAction(this, api.films.post, clone(data));
22 | }
23 | update(id, data) {
24 | networkAction(this, api.films.put, id, clone(data));
25 | }
26 | delete(id) {
27 | networkAction(this, api.films.delete, id);
28 | }
29 | }
30 |
31 | module.exports = (alt.createActions(FilmsActions));
32 |
--------------------------------------------------------------------------------
/app/actions/login-actions.js:
--------------------------------------------------------------------------------
1 | import alt from 'utils/alt';
2 | import axios from 'axios';
3 |
4 | class LoginActions {
5 | constructor() {
6 | this.generateActions('logout', 'loadLocalUser');
7 | }
8 | async login(data) {
9 | try {
10 | const response = await axios.post('/auth/login', data);
11 | this.dispatch({ok: true, user: response.data});
12 | } catch (err) {
13 | console.error(err);
14 | this.dispatch({ok: false, error: err.data});
15 | }
16 | }
17 | async register(data) {
18 | try {
19 | const response = await axios.post('/auth/register', data);
20 | this.dispatch({ok: true, user: response.data});
21 | } catch (err) {
22 | console.error(err);
23 | this.dispatch({ok: false, error: err.data});
24 | }
25 | }
26 | }
27 |
28 | module.exports = (alt.createActions(LoginActions));
29 |
--------------------------------------------------------------------------------
/app/actions/status-actions.js:
--------------------------------------------------------------------------------
1 | import alt from 'utils/alt';
2 |
3 | class StatusActions {
4 | constructor() {
5 | this.generateActions('started', 'done', 'failed', 'retry');
6 | }
7 | }
8 |
9 | module.exports = (alt.createActions(StatusActions));
10 |
--------------------------------------------------------------------------------
/app/components/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from 'react';
4 | import {RouteHandler} from 'react-router';
5 |
6 | import Navbar from 'components/shared/navbar';
7 | import Footer from 'components/shared/footer';
8 | import Router from 'react-router';
9 | import reactMixin from 'react-mixin';
10 | import LoginActions from 'actions/login-actions';
11 |
12 | @reactMixin.decorate(Router.State)
13 | export default class App extends React.Component {
14 | constructor(props) {
15 | super(props);
16 | LoginActions.loadLocalUser();
17 | }
18 | render() {
19 | var navbar;
20 | if (this.getPathname() !== '/login') {
21 | navbar =
{this.props.director.biography}
54 |Name | 57 |Nationality | 58 |Birthday | 59 |Biography | 60 |Action | 61 |
---|---|---|---|---|
{item.name} | 67 |{item.nationality} | 68 |{moment(item.birthday).format('D MMMM YYYY')} | 69 |{item.biography} | 70 |
71 | |
77 |
Name | 49 |Director | 50 |Year | 51 |Description | 52 |Action | 53 |
---|---|---|---|---|
{item.name} | 59 |{this.directorName(item.director)} | 60 |{item.year} | 61 |{item.description} | 62 |
63 | |
69 |
Watch This™
53 |Watch This™
54 |{film && film.description}
24 |` block so the code 5 | // flows with our right column layout. 6 | $pre-border-color: $pre-bg; // hide the border. 7 | 8 | // This makes the font on the buttons and the text BIG 9 | $font-size-base: 18px; 10 | 11 | -------------------------------------------------------------------------------- /app/styles/_cars.scss: -------------------------------------------------------------------------------- 1 | div.action { 2 | min-width: 140px; 3 | min-height: 23px; 4 | // text-align: right; 5 | button { 6 | display: inline-block; 7 | margin-right: 5px; 8 | } 9 | } 10 | table.car-table tr td span.action-buttons { display:none;} 11 | table.car-table tr:hover td span.action-buttons { display:inline-block;} 12 | 13 | table.car-table { 14 | color: gray; 15 | } -------------------------------------------------------------------------------- /app/styles/_fonts.scss: -------------------------------------------------------------------------------- 1 | $font-0: 'Roboto'; 2 | 3 | $url-0: url('../fonts/Roboto-Medium-webfont.eot'); 4 | $url-1: url('../fonts/Roboto-Medium-webfont.eot?#iefix'); 5 | $url-2: url('../fonts/Roboto-Medium-webfont.woff'); 6 | $url-3: url('../fonts/Roboto-Medium-webfont.ttf'); 7 | $url-4: url('../fonts/Roboto-Medium-webfont.svg#robotomedium'); 8 | 9 | @font-face { 10 | font-family: $font-0; 11 | src: $url-0; 12 | src: $url-1 format('embedded-opentype'), $url-2 format('woff'), $url-3 format('truetype'), $url-4 format('svg'); 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /app/styles/_footer.scss: -------------------------------------------------------------------------------- 1 | .app-footer { 2 | margin-top: 50px; 3 | 4 | &-content { 5 | font-size: 10px; 6 | padding-top: 10px; 7 | text-align: center; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/styles/_main.scss: -------------------------------------------------------------------------------- 1 | // This file is used as part of the bootstrap-sass-loader customization 2 | // Compare this to main.scss (no underscore) which is loaded by itself 3 | // ## Baseline 4 | 5 | // Included from bootstrap.config.less. 6 | 7 | $fa-font-path: '../../node_modules/font-awesome/fonts'; 8 | @import '../../node_modules/font-awesome/scss/font-awesome'; 9 | 10 | @import 'fonts'; 11 | 12 | @import 'app'; 13 | 14 | @import 'cars'; 15 | @import 'users'; 16 | 17 | @import 'footer'; 18 | 19 | @import 'navbar'; 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/styles/_navbar.scss: -------------------------------------------------------------------------------- 1 | .busy-indicator { 2 | line-height: 1.7em; 3 | font-size: 1.6em; 4 | } -------------------------------------------------------------------------------- /app/styles/_pre-bootstrap-customizations.scss: -------------------------------------------------------------------------------- 1 | // Customize Variables that get imported before the _variables file. Thus the _variables file 2 | // can depend on values here. 3 | 4 | $brand-primary: purple; 5 | 6 | -------------------------------------------------------------------------------- /app/styles/_users.scss: -------------------------------------------------------------------------------- 1 | $black: #000; 2 | 3 | .app-users { 4 | margin: 0 auto; 5 | 6 | td, 7 | th { 8 | padding: 5px 10px; 9 | } 10 | 11 | thead { 12 | border-bottom: 1px solid $black; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/styles/bootstrap-sass.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Use preBootstrapCustomizations to change $brand-primary. Ensure this preBootstrapCustomizations does not 3 | // depend on other bootstrap variables. 4 | 5 | preBootstrapCustomizations: 'app/styles/_pre-bootstrap-customizations.scss', 6 | 7 | // Use bootstrapCustomizations to utilize other sass variables defined in preBootstrapCustomizations or the 8 | // _variables.scss file. This is useful to set one customization value based on another value. 9 | bootstrapCustomizations: 'app/styles/_bootstrap-customizations.scss', 10 | 11 | mainSass: 'app/styles/_main.scss', 12 | verbose: true, 13 | debug: false, 14 | // Default for the style loading is to put in your js files 15 | // styleLoader: "style-loader!css-loader!sass-loader"; 16 | 17 | // If you want to use the ExtractTextPlugin 18 | // and you want compressed 19 | styleLoader: 'style-loader!css-loader!sass-loader', 20 | // or if you want expanded CSS 21 | // styleLoader: ExtractTextPlugin.extract("style-loader", "css-loader!sass?outputStyle=expanded"), 22 | 23 | // ### Scripts 24 | // Any scripts here set to false will never 25 | // make it to the client, it's not packaged 26 | // by webpack. 27 | scripts: { 28 | // 'transition': true, 29 | // 'alert': true, 30 | // 'button': true, 31 | // 'carousel': true, 32 | // 'collapse': true, 33 | // 'dropdown': true, 34 | // 'modal': true, 35 | // 'tooltip': true, 36 | // 'popover': true, 37 | // 'scrollspy': true, 38 | // 'tab': true, 39 | // 'affix': true 40 | }, 41 | // ### Styles 42 | // Enable or disable certain less components and thus remove 43 | // the css for them from the build. 44 | styles: { 45 | 'mixins': true, 46 | 47 | 'normalize': true, 48 | 'print': true, 49 | 50 | 'scaffolding': true, 51 | 'type': true, 52 | 'code': true, 53 | 'grid': true, 54 | 'tables': true, 55 | 'forms': true, 56 | 'buttons': true, 57 | 58 | // 'component-animations': true, 59 | // 'glyphicons': true, 60 | 'dropdowns': true, 61 | // 'button-groups': true, 62 | 'input-groups': true, 63 | 'navs': true, 64 | 'navbar': true, 65 | // 'breadcrumbs': true, 66 | // 'pagination': true, 67 | // 'pager': true, 68 | 'labels': true, 69 | // 'badges': true, 70 | 'jumbotron': true, 71 | 'thumbnails': true, 72 | 'alerts': true, 73 | // 'progress-bars': true, 74 | 'media': true, 75 | 'list-group': true, 76 | // 'panels': true, 77 | 'wells': true, 78 | 'close': true, 79 | 80 | 'modals': true, 81 | 'tooltip': true, 82 | 'popovers': true, 83 | // 'carousel': true, 84 | 85 | 'utilities': true, 86 | 'responsive-utilities': true 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /app/styles/bootstrap-sass.prod.config.js: -------------------------------------------------------------------------------- 1 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 2 | var config = require('./bootstrap-sass.config'); 3 | 4 | config.styleLoader = ExtractTextPlugin.extract('style-loader', 'css-loader!sass-loader?sourceMap'); 5 | 6 | module.exports = config; 7 | -------------------------------------------------------------------------------- /app/utils/action-utils.js: -------------------------------------------------------------------------------- 1 | // import {isFunction} from 'lodash'; 2 | import StatusActions from 'actions/status-actions'; 3 | import LoginActions from 'actions/login-actions'; 4 | 5 | export default { 6 | networkAction: async function(context, method, ...params) { 7 | // try { 8 | StatusActions.started(); 9 | const response = await method.apply(context, params); 10 | // const data = isFunction(response) ? response().data : response.data; 11 | context.dispatch(response().data); 12 | StatusActions.done(); 13 | // } catch (err) { 14 | // console.error(err); 15 | // if (err.status === 401) { 16 | // LoginActions.logout(); 17 | // } 18 | // else { 19 | // StatusActions.failed({config: err.config, action: context.actionDetails}); 20 | // } 21 | // } 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /app/utils/alt-resolver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import Iso from 'iso'; 5 | import debug from 'debug'; 6 | 7 | import ErrorPage from 'pages/server-error'; 8 | 9 | export default class AltResolver { 10 | constructor() { 11 | this._toResolve = []; 12 | } 13 | resolve(promise: Function, later = false) { 14 | if (process.env.BROWSER && !later) { 15 | return new Promise(promise); 16 | } 17 | else { 18 | this._toResolve.push(promise); 19 | } 20 | } 21 | mapPromises() { 22 | return this._toResolve.map((promise) => new Promise(promise)); 23 | } 24 | async render(Handler: object, flux: object, force: ?boolean = false) { 25 | if (process.env.BROWSER && !force) { 26 | debug('dev')('`altResolver.render` should not be used in browser, something went wrong'); 27 | return null; 28 | } 29 | else { 30 | let content: string; 31 | try { 32 | // Fire first render to collect XHR promises 33 | 34 | // debug('dev')('first render'); 35 | // React.renderToString(React.createElement(Handler, {flux})); 36 | 37 | // // Get the promises collected from the first rendering 38 | // const promises: Array = this.mapPromises(); 39 | 40 | // // Resolve all promises collected 41 | // await Promise.all(promises); 42 | 43 | // debug('dev')('second render'); 44 | 45 | // Get the new content with promises resolved 46 | const app: string = React.renderToString(React.createElement(Handler, {flux})); 47 | 48 | // Render the html with state in it 49 | content = Iso.render(app, flux.flush()); 50 | } 51 | catch (error) { 52 | // catch script error, render 500 page 53 | debug('koa')('`rendering error`'); 54 | debug('koa')(error); 55 | const app: string = React.renderToString(React.createElement(ErrorPage)); 56 | content = Iso.render(app, flux.flush()); 57 | } 58 | 59 | // return the content 60 | return content; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/utils/alt.js: -------------------------------------------------------------------------------- 1 | import Alt from 'alt'; 2 | var alt = new Alt(); 3 | export default alt; 4 | -------------------------------------------------------------------------------- /app/utils/api.js: -------------------------------------------------------------------------------- 1 | import restful from 'restful.js'; 2 | 3 | const server = restful().fullUrl('/api'); 4 | const api = {}; 5 | 6 | api.server = server; 7 | const endpoints = ['cars', 'films', 'directors']; 8 | 9 | endpoints.forEach((endpoint)=> { 10 | api[endpoint] = server.all(endpoint); 11 | }); 12 | 13 | api.updateToken = function (token) { 14 | server.header('auth-token', token); 15 | }; 16 | 17 | export default api; 18 | -------------------------------------------------------------------------------- /app/utils/component-utils.js: -------------------------------------------------------------------------------- 1 | export default { 2 | changeHandler: function(target) { 3 | target.prototype.changeHandler = function(key, attr, event) { 4 | var state = {}; 5 | state[key] = this.state[key] || {}; 6 | state[key][attr] = event.currentTarget.value; 7 | this.setState(state); 8 | }; 9 | return target; 10 | }, 11 | authDecorator: function(target) { 12 | target.willTransitionTo = function(transition) { 13 | if (process.env.BROWSER) { 14 | if (!localStorage.filmdbUser) { 15 | transition.redirect('/login'); 16 | } 17 | } 18 | }; 19 | return target; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /app/utils/countries.js: -------------------------------------------------------------------------------- 1 | //jscs:disable 2 | /*eslint quotes:0*/ 3 | const list = ["Afghanistan", "Åland Islands", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla", "Antarctica", "Antigua and Barbuda", "Argentina", "Armenia", "Aruba", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia, Plurinational State of", "Bonaire, Sint Eustatius and Saba", "Bosnia and Herzegovina", "Botswana", "Bouvet Island", "Brazil", "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cambodia", "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China", "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo", "Congo, the Democratic Republic of the", "Cook Islands", "Costa Rica", "Côte d'Ivoire", "Croatia", "Cuba", "Curaçao", "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Ethiopia", "Falkland Islands (Malvinas)", "Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia", "French Southern Territories", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guernsey", "Guinea", "Guinea-Bissau", "Guyana", "Haiti", "Heard Island and McDonald Islands", "Holy See (Vatican City State)", "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran, Islamic Republic of", "Iraq", "Ireland", "Isle of Man", "Israel", "Italy", "Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait", "Kyrgyzstan", "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg", "Macao", "Macedonia, the former Yugoslav Republic of", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique", "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of", "Monaco", "Mongolia", "Montenegro", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal", "Netherlands", "New Caledonia", "New Zealand", "Nicaragua", "Niger", "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", "Palestinian Territory, Occupied", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Réunion", "Romania", "Russian Federation", "Rwanda", "Saint Barthélemy", "Saint Helena, Ascension and Tristan da Cunha", "Saint Kitts and Nevis", "Saint Lucia", "Saint Martin (French part)", "Saint Pierre and Miquelon", "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", "Sint Maarten (Dutch part)", "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", "South Georgia and the South Sandwich Islands", "South Sudan", "Spain", "Sri Lanka", "Sudan", "Suriname", "Svalbard and Jan Mayen", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic", "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of", "Thailand", "Timor-Leste", "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela, Bolivarian Republic of", "Viet Nam", "Virgin Islands, British", "Virgin Islands, U.S.", "Wallis and Futuna", "Western Sahara", "Yemen", "Zambia", "Zimbabwe"]; 4 | 5 | const options = list.map((x) => { 6 | return { 7 | value: x, 8 | label: x 9 | }; 10 | }); 11 | 12 | export default {list, options}; 13 | -------------------------------------------------------------------------------- /app/utils/flux-old.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import alt from 'utils/alt'; 4 | import AltResolver from './alt-resolver.js'; 5 | 6 | import CarsActions from 'actions/cars'; 7 | import FilmsActions from 'actions/films-actions'; 8 | import DirectorsActions from 'actions/directors-actions'; 9 | import StatusActions from 'actions/status-actions'; 10 | import LoginActions from 'actions/login-actions'; 11 | 12 | import CarsStore from 'stores/cars'; 13 | import FilmsStore from 'stores/films-store'; 14 | import DirectorsStore from 'stores/directors-store'; 15 | import StatusStore from 'stores/status-store'; 16 | import LoginStore from 'stores/login-store'; 17 | 18 | class Flux extends Alt { 19 | 20 | constructor(config = {}) { 21 | super(config); 22 | 23 | this._resolver = new AltResolver(); 24 | 25 | // Register Actions 26 | this.addActions('cars', CarsActions); 27 | this.addActions('films', FilmsActions); 28 | this.addActions('directors', DirectorsActions); 29 | this.addActions('status', StatusActions); 30 | this.addActions('login', LoginActions); 31 | 32 | // Register Stores 33 | this.addStore('cars', CarsStore); 34 | this.addStore('films', FilmsStore); 35 | this.addStore('directors', DirectorsStore); 36 | this.addStore('status', StatusStore); 37 | this.addStore('login', LoginStore); 38 | } 39 | 40 | resolve(result) { 41 | this._resolver.resolve(result); 42 | } 43 | 44 | render(handler) { 45 | return this._resolver.render(handler, this); 46 | } 47 | } 48 | 49 | export default Flux; 50 | -------------------------------------------------------------------------------- /app/utils/image-resolver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import debug from 'debug'; 4 | 5 | export default (imagePath) => { 6 | if (process.env.BROWSER) { 7 | debug('dev')('`image-resolver` should not be used in browser, something went wrong'); 8 | throw new Error('image-resolver called on browser'); 9 | } 10 | else { 11 | // Load images compiled from `webpack-stats` 12 | // don't cache the `webpack-stats.json` on dev 13 | // so we gonna read the file on each request 14 | let images; 15 | if (process.env.NODE_ENV === 'development') { 16 | const fs = require('fs'); 17 | const path = require('path'); 18 | images = fs.readFileSync(path.resolve(__dirname, '../../server/webpack-stats.json')); 19 | images = JSON.parse(images).images; 20 | } 21 | // on production, use simple `require` to cache the file 22 | else { 23 | images = require('../../server/webpack-stats.json').images; 24 | } 25 | 26 | // Find the correct image 27 | const regex = new RegExp(`${imagePath}$`); 28 | const image = images.find(img => regex.test(img.original)); 29 | 30 | if (image) { 31 | return image.compiled; 32 | } 33 | else { 34 | // Serve a not-found asset maybe? 35 | return ''; 36 | } 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /app/utils/intl-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import debug from 'debug'; 4 | 5 | const loaders = { 6 | en(callback: Function, force = false) { 7 | if (!window.Intl || force) { 8 | require.ensure([ 9 | 'intl/Intl', 10 | 'intl/locale-data/jsonp/en.js', 11 | 'data/en' 12 | ], (require) => { 13 | require('intl/Intl'); 14 | require('intl/locale-data/jsonp/en.js'); 15 | const lang = require('data/en'); 16 | return callback(lang); 17 | }); 18 | } 19 | else { 20 | require.ensure(['data/en'], (require) => { 21 | const lang = require('data/en'); 22 | return callback(lang); 23 | }); 24 | } 25 | }, 26 | fr(callback: Function, force = false) { 27 | if (!window.Intl || force) { 28 | require.ensure([ 29 | 'intl/Intl', 30 | 'intl/locale-data/jsonp/fr.js', 31 | 'data/fr' 32 | ], (require) => { 33 | require('intl/Intl'); 34 | require('intl/locale-data/jsonp/fr.js'); 35 | const lang = require('data/fr'); 36 | return callback(lang); 37 | }); 38 | } 39 | else { 40 | require.ensure(['data/fr'], (require) => { 41 | const lang = require('data/fr'); 42 | return callback(lang); 43 | }); 44 | } 45 | } 46 | }; 47 | 48 | export default (locale, force) => { 49 | debug('dev')(`loading lang ${locale}`); 50 | const promise: Promise = new Promise((resolve) => loaders[locale](resolve, force)); 51 | return promise; 52 | }; 53 | -------------------------------------------------------------------------------- /app/utils/intl-polyfill.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import debug from 'debug'; 4 | 5 | const hasBuiltInLocaleData = (locale) => { 6 | return Intl.NumberFormat.supportedLocalesOf(locale)[0] === locale && Intl.DateTimeFormat.supportedLocalesOf(locale)[0] === locale; 7 | }; 8 | 9 | export default (locales) => { 10 | if (!process.env.BROWSER) { 11 | if (global.Intl) { 12 | if (!locales.every(hasBuiltInLocaleData)) { 13 | // `Intl` exists, but it doesn't have the data we need, so load the 14 | // polyfill and replace the constructors with need with the polyfill's. 15 | const IntlPolyfill = require('intl'); 16 | Intl.NumberFormat = IntlPolyfill.NumberFormat; 17 | Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat; 18 | } 19 | } 20 | else { 21 | // No `Intl`: use and load polyfill 22 | global.Intl = require('intl'); 23 | debug('koa')('Intl is not supported, so the polyfill has been loaded'); 24 | } 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /app/utils/promisify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default (fn) => new Promise((resolve) => fn((result) => resolve(result))); 4 | -------------------------------------------------------------------------------- /app/utils/store-utils.js: -------------------------------------------------------------------------------- 1 | export default { 2 | findItemById: function(collection, id) { 3 | return collection.find(x => x._id === id); 4 | }, 5 | findIndexById: function(collection, id) { 6 | var index; 7 | collection.find((x, i) => { 8 | index = i; 9 | return x._id === id; 10 | }); 11 | return index; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /app/utils/test/get-wrapped-component.js: -------------------------------------------------------------------------------- 1 | export default function(instance) { 2 | return instance._reactInternalInstance._renderedComponent._instance; 3 | } 4 | -------------------------------------------------------------------------------- /app/utils/test/init-dom.js: -------------------------------------------------------------------------------- 1 | var jsdom = require("jsdom"); 2 | var sinon = require("sinon"); 3 | 4 | module.exports = { 5 | run: function() { 6 | delete global.document; 7 | delete global.window; 8 | delete global.navigator; 9 | global.document = jsdom.jsdom(); 10 | global.window = document.defaultView; 11 | global.navigator = global.window.navigator; 12 | }, 13 | 14 | stashWindow: function() { 15 | global._window = global.window; 16 | global.window = undefined; 17 | }, 18 | 19 | restoreWindow: function() { 20 | global.window = global._window; 21 | global._window = undefined; 22 | }, 23 | 24 | fakeLocalStorage: function() { 25 | global.localStorage = { 26 | getItem: sinon.spy(), 27 | setItem: sinon.spy(), 28 | removeItem: sinon.spy() 29 | }; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /app/utils/test/login-store-mock.js: -------------------------------------------------------------------------------- 1 | console.log("in login store mock"); 2 | import alt from "utils/alt"; 3 | import LoginActions from "actions/login-actions"; 4 | 5 | class LoginStoreMock { 6 | constructor() { 7 | this.bindActions(LoginActions); 8 | this.user = null; 9 | this.error = null; 10 | } 11 | 12 | onLogin(data) { 13 | console.log(data); 14 | this.user = true; 15 | } 16 | 17 | onRegister(data) { 18 | console.log(data); 19 | this.user = true; 20 | } 21 | 22 | onLogout() { 23 | this.user = false; 24 | } 25 | } 26 | 27 | module.exports = (alt.createStore(LoginStoreMock)); 28 | -------------------------------------------------------------------------------- /app/utils/test/router-mock.jsx: -------------------------------------------------------------------------------- 1 | import sinon from "sinon"; 2 | 3 | module.exports = { 4 | makePath: sinon.spy(), 5 | makeHref: sinon.spy(), 6 | transitionTo: sinon.spy(), 7 | replaceWith: sinon.spy(), 8 | goBack: sinon.spy(), 9 | getCurrentPath: sinon.spy(), 10 | getCurrentRoutes: sinon.spy(), 11 | getCurrentPathname: sinon.spy(), 12 | getCurrentParams: sinon.spy(), 13 | getCurrentQuery: sinon.spy(), 14 | isActive: sinon.spy(), 15 | getRouteAtDepth: sinon.spy(), 16 | setRouteComponentAtDepth: sinon.spy() 17 | }; 18 | -------------------------------------------------------------------------------- /app/utils/test/shallow-render-component.js: -------------------------------------------------------------------------------- 1 | import React from "react/addons"; 2 | 3 | export default function(component, props, ...children) { 4 | const shallowRenderer = React.addons.TestUtils.createRenderer(); 5 | shallowRenderer.render(React.createElement(component, props, children.length > 1 ? children : children[0])); 6 | return shallowRenderer.getRenderOutput(); 7 | } 8 | -------------------------------------------------------------------------------- /app/utils/test/stub-router-context.js: -------------------------------------------------------------------------------- 1 | /** 2 | * From https://github.com/rackt/react-router/blob/master/docs/guides/testing.md 3 | * 4 | * var stubRouterContext = require('./stubRouterContext'); 5 | * var IndividualComponent = require('./IndividualComponent'); 6 | * var Subject = stubRouterContext(IndividualComponent, {someProp: 'foo'}); 7 | * React.render(, testElement); 8 | */ 9 | 10 | var React = require('react'); 11 | var _ = require('lodash'); 12 | 13 | var func = React.PropTypes.func; 14 | 15 | var stubRouterContext = function(Component, props, stubs) { 16 | return React.createClass({ 17 | childContextTypes: { 18 | makePath: func, 19 | makeHref: func, 20 | transitionTo: func, 21 | replaceWith: func, 22 | goBack: func, 23 | getCurrentPath: func, 24 | getCurrentRoutes: func, 25 | getCurrentPathname: func, 26 | getCurrentParams: func, 27 | getCurrentQuery: func, 28 | isActive: func, 29 | }, 30 | 31 | getChildContext: function() { 32 | return _.merge({}, { 33 | makePath: function() {}, 34 | makeHref: function() {}, 35 | transitionTo: function() {}, 36 | replaceWith: function() {}, 37 | goBack: function() {}, 38 | getCurrentPath: function() {}, 39 | getCurrentRoutes: function() {}, 40 | getCurrentPathname: function() {}, 41 | getCurrentParams: function() {}, 42 | getCurrentQuery: function() {}, 43 | isActive: function() {}, 44 | }, stubs); 45 | }, 46 | 47 | render: function() { 48 | return 49 | } 50 | }); 51 | }; 52 | 53 | module.exports = stubRouterContext -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-example-filmdb", 3 | "version": "0.0.2", 4 | "description": "Isomorphic React + Flux film database example", 5 | "main": "server/index.js", 6 | "scripts": { 7 | "build": "./node_modules/.bin/webpack --progress --stats --config ./webpack/prod.config.js", 8 | "dev": "node --harmony ./webpack/dev-server.js", 9 | "prod": "NODE_ENV=production node server/index.js", 10 | "test": "node --harmony ./test/mocha-runner", 11 | "testwatch": "node --harmony ./test/mocha-runner -w", 12 | "postinstall": "npm run build", 13 | "coverage": "./node_modules/.bin/babel-istanbul --include-all-sources cover ./test/mocha-runner.js;open ./coverage/lcov-report/index.html " 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/tomaash/react-example-filmdb.git" 18 | }, 19 | "author": "tomaash", 20 | "license": "MIT", 21 | "dependencies": { 22 | "alt": "^0.16.10", 23 | "autoprefixer-loader": "^2.0.0", 24 | "axios": "^0.5.4", 25 | "babel": "^5.6.14", 26 | "babel-core": "^5.6.15", 27 | "babel-eslint": "^3.1.23", 28 | "babel-istanbul": "^0.2.10", 29 | "babel-loader": "^5.3.1", 30 | "bcrypt": "^0.8.3", 31 | "blocked": "^1.1.0", 32 | "bootstrap": "^3.3.5", 33 | "browser-sync": "^2.7.13", 34 | "chai": "^3.0.0", 35 | "chai-spies": "^0.6.0", 36 | "chokidar": "^1.0.3", 37 | "classnames": "^2.1.3", 38 | "co-body": "^2.0.0", 39 | "cookies-js": "^1.2.1", 40 | "css-loader": "^0.15.1", 41 | "debug": "^2.2.0", 42 | "del": "^1.2.0", 43 | "eslint": "^0.24.0", 44 | "eslint-loader": "^0.14.1", 45 | "eslint-plugin-react": "^2.6.4", 46 | "esprima-fb": "^15001.1.0-dev-harmony-fb", 47 | "extract-text-webpack-plugin": "^0.8.2", 48 | "faucet": "0.0.1", 49 | "file-loader": "^0.8.4", 50 | "flowcheck": "^0.2.7", 51 | "flowcheck-loader": "^1.0.0", 52 | "font-awesome": "^4.3.0", 53 | "formsy-react": "^0.14.1", 54 | "glob": "^5.0.13", 55 | "image-webpack-loader": "^1.6.1", 56 | "imagemin": "^3.2.0", 57 | "inject-loader": "^2.0.0", 58 | "intl": "^1.0.0-rc-4", 59 | "iso": "^4.1.0", 60 | "isparta-loader": "^0.2.0", 61 | "jscs": "^1.13.1", 62 | "jscs-loader": "0.1.0", 63 | "jsdom": "^5.6.0", 64 | "json-loader": "^0.5.2", 65 | "karma": "^0.12.37", 66 | "karma-chrome-launcher": "^0.2.0", 67 | "karma-cli": "0.1.0", 68 | "karma-coverage": "^0.4.2", 69 | "karma-coveralls": "^1.1.0", 70 | "karma-firefox-launcher": "^0.1.6", 71 | "karma-mocha": "^0.2.0", 72 | "karma-sourcemap-loader": "^0.3.5", 73 | "karma-webpack": "^1.5.1", 74 | "koa": "^0.21.0", 75 | "koa-body-parser": "git://github.com/tomaash/koa-body-parser.git", 76 | "koa-bodyparser": "^2.0.1", 77 | "koa-cash": "^1.0.1", 78 | "koa-compressor": "^1.0.3", 79 | "koa-conditional-get": "^1.0.3", 80 | "koa-etag": "^2.0.0", 81 | "koa-favicon": "^1.2.0", 82 | "koa-hbs": "^0.7.0", 83 | "koa-helmet": "0.1.0", 84 | "koa-logger": "^1.2.2", 85 | "koa-mongo-rest": "^0.3.4", 86 | "koa-mount": "^1.3.0", 87 | "koa-proxy": "^0.3.0", 88 | "koa-response-time": "^1.0.2", 89 | "koa-router": "^5.1.2", 90 | "koa-static-cache": "^3.1.1", 91 | "lodash": "^3.10.0", 92 | "lru-cache": "^2.6.5", 93 | "mocha": "^2.2.5", 94 | "mockery": "^1.4.0", 95 | "modularized-normalize-scss": "^3.0.3", 96 | "moment": "^2.10.3", 97 | "mongoose": "^4.0.6", 98 | "mongoose-json-select": "^0.2.1", 99 | "nock": "^2.7.0", 100 | "node-sass": "^3.2.0", 101 | "node-uuid": "^1.4.3", 102 | "node-watch": "^0.3.4", 103 | "nodemon": "^1.3.7", 104 | "pikaday": "^1.3.3", 105 | "pluralize": "^1.1.4", 106 | "react": "^0.13.3", 107 | "react-bootstrap": "^0.22.3", 108 | "react-hot-loader": "^1.2.7", 109 | "react-intl": "^1.2.0", 110 | "react-mixin": "^1.5.0", 111 | "react-pikaday": "^0.2.0", 112 | "react-router": "^0.13.3", 113 | "react-router-bootstrap": "^0.16.0", 114 | "react-select": "^0.5.4", 115 | "require-uncache": "0.0.2", 116 | "restful.js": "git://github.com/tomaash/restful.js.git", 117 | "rewire": "^2.3.4", 118 | "rewire-webpack": "^1.0.0", 119 | "sass-loader": "^1.0.2", 120 | "sinon": "^1.15.4", 121 | "sinon-chai": "^2.8.0", 122 | "style-loader": "^0.12.3", 123 | "superagent": "^1.2.0", 124 | "tap-spec": "^4.0.2", 125 | "tape": "^4.0.0", 126 | "tape-catch": "^1.0.4", 127 | "url-loader": "^0.5.6", 128 | "webpack": "^1.10.1", 129 | "webpack-dev-server": "^1.10.1", 130 | "yargs": "^3.15.0" 131 | }, 132 | "engines": { 133 | "iojs": "^1.7.0" 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /processes.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "app", 5 | "script": "./server/index.js", 6 | "instances": 2, 7 | "log_date_format": "YYYY-MM-DD HH:mm Z", 8 | "log_file": "./logs/app.log", 9 | "error_file": "./logs/app-err.log", 10 | "out_file": "./logs/app-out.log", 11 | "exec_mode": "cluster_mode", 12 | "max_memory_restart": "500M", 13 | "watch": true, 14 | "env": { 15 | "NODE_ENV": "production", 16 | "DEBUG": "koa" 17 | }, 18 | "node_args": "--max_old_space_size=2048 --harmony" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /server/config/all.json: -------------------------------------------------------------------------------- 1 | { 2 | "locales": ["en", "fr"] 3 | } 4 | -------------------------------------------------------------------------------- /server/config/development.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3000 3 | } 4 | -------------------------------------------------------------------------------- /server/config/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import debug from 'debug'; 4 | import objectAssign from 'react/lib/Object.assign'; 5 | 6 | import baseConfig from './all.json'; 7 | const env = process.env.NODE_ENV || 'development'; 8 | let config; 9 | 10 | try { 11 | config = require(`./${env}.json`); 12 | } 13 | catch (error) { 14 | debug('dev')(`No specific configuration for env ${env}`); 15 | } 16 | 17 | export default objectAssign(baseConfig, config); 18 | -------------------------------------------------------------------------------- /server/config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3010 3 | } 4 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | // Delete the `BROWSER` env variable if it's present 2 | // https://github.com/iam4x/isomorphic-flux-boilerplate/issues/16 3 | delete process.env.BROWSER; 4 | 5 | // Tell `require` calls to look into `/app` also 6 | // it will avoid `../../../../../` require strings 7 | process.env.NODE_PATH = "app"; 8 | require("module").Module._initPaths(); 9 | 10 | // Install `babel` hook for ES6 11 | require("babel/register"); 12 | 13 | // Load Intl polyfill 14 | require("utils/intl-polyfill")(require("./config/init").locales); 15 | 16 | // Start the server 17 | require("./koa.js"); 18 | -------------------------------------------------------------------------------- /server/koa.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import debug from "debug"; 3 | 4 | import koa from "koa"; 5 | import hbs from "koa-hbs"; 6 | import mount from "koa-mount"; 7 | import helmet from "koa-helmet"; 8 | import logger from "koa-logger"; 9 | import favicon from "koa-favicon"; 10 | import staticCache from "koa-static-cache"; 11 | import responseTime from "koa-response-time"; 12 | import bodyParser from "koa-bodyparser"; 13 | // import koaRouter from "koa-router"; 14 | 15 | import router from "./router"; 16 | import config from "./config/init"; 17 | 18 | import rest from "./rest"; 19 | import {clone} from "lodash"; 20 | 21 | const app = koa(); 22 | const env = process.env.NODE_ENV || "development"; 23 | // const appRouter = koaRouter(); 24 | 25 | app.use(responseTime()); 26 | app.use(logger()); 27 | 28 | // various security headers 29 | app.use(helmet.defaults()); 30 | 31 | if (env === "production") { 32 | app.use(require("koa-conditional-get")()); 33 | app.use(require("koa-etag")()); 34 | app.use(require("koa-compressor")()); 35 | 36 | // Cache pages 37 | const cache = require("lru-cache")({maxAge: 3000}); 38 | app.use(require("koa-cash")({ 39 | get: function*(key) { 40 | return cache.get(key); 41 | }, 42 | 43 | set: function*(key, value) { 44 | cache.set(key, value); 45 | } 46 | })); 47 | } 48 | 49 | if (env === "development") { 50 | // set debug env, must be programmaticaly for windows 51 | debug.enable("dev,koa"); 52 | 53 | // log when process is blocked 54 | require("blocked")((ms) => debug("koa")(`blocked for ${ms}ms`)); 55 | } 56 | 57 | app.use(favicon(path.join(__dirname, "../app/images/favicon.ico"))); 58 | app.use(hbs.middleware({ 59 | defaultLayout: "index", 60 | layoutsPath: path.join(__dirname, "/views/layouts"), 61 | viewPath: path.join(__dirname, "/views") 62 | })); 63 | 64 | const cacheOpts = {maxAge: 86400000, gzip: true}; 65 | 66 | // Proxy asset folder to webpack development server in development mode 67 | if (env === "development") { 68 | // console.log(__dirname); 69 | var webpackConfig = require("../webpack/dev.config"); 70 | // console.log(webpackConfig); 71 | // if (!webpackConfig.server) { 72 | // webpackConfig = { 73 | // server: { 74 | // port: 3001 75 | // } 76 | // }; 77 | // } 78 | 79 | app.use(mount("/assets", require("koa-proxy")({ host: `http://localhost:${webpackConfig.server.port}` }))); 80 | } 81 | else { 82 | app.use(mount("/assets", staticCache(path.join(__dirname, "../dist"), cacheOpts))); 83 | } 84 | 85 | // Parse body 86 | app.use(bodyParser()); 87 | 88 | import User from "./models/user"; 89 | 90 | // Authenticate 91 | app.use(function*(next) { 92 | const token = this.req.headers["auth-token"]; 93 | const isApi = !!this.request.url.match(/^\/api/); 94 | const user = token && (yield User.findOne({token})); 95 | if (isApi && !user) { 96 | this.status = 401; 97 | this.body = "401 Unauthorized"; 98 | return; 99 | } 100 | 101 | this.request.user = user; 102 | // if (user) { 103 | // // Add user to get condition for API 104 | // if (this.request.method === "GET") { 105 | // var conditions; 106 | // var query = clone(this.request.query); 107 | // try { 108 | // conditions = (query.conditions && JSON.parse(query.conditions)) || {}; 109 | // } catch (err) { 110 | // console.error(err); 111 | // conditions = {}; 112 | // } 113 | 114 | // conditions.user = user._id; 115 | // query.conditions = JSON.stringify(conditions); 116 | // this.request.query = query; 117 | // console.log("has query"); 118 | // console.log(this.request.query); 119 | // } 120 | 121 | // // Add user to post data for API 122 | // else if (this.request.body) { 123 | // console.log("has body"); 124 | // this.request.body.user = user._id; 125 | // console.log(this.request.body); 126 | // } 127 | // } 128 | 129 | yield next; 130 | }); 131 | 132 | // Connect REST API 133 | rest(app); 134 | // rest(appRouter); 135 | 136 | // app 137 | // .use(appRouter.routes()) 138 | // .use(appRouter.allowedMethods()); 139 | 140 | app.use(router); 141 | var port = process.env.PORT || config.port || 3000; 142 | app.listen(port); 143 | 144 | console.log(`Application started on port ${config.port}`); 145 | if (process.send) { 146 | process.send("online"); 147 | } 148 | -------------------------------------------------------------------------------- /server/models/car.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | const CarSchema = new mongoose.Schema({ 3 | brand: String, 4 | model: String, 5 | year: Number 6 | }); 7 | 8 | export default mongoose.model('Car', CarSchema); 9 | -------------------------------------------------------------------------------- /server/models/director.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | const DirectorSchema = new mongoose.Schema({ 3 | name: String, 4 | nationality: String, 5 | birthday: Date, 6 | biography: String, 7 | user: { 8 | type: mongoose.Schema.Types.ObjectId, 9 | ref: 'user' 10 | }, 11 | createdAt: { 12 | type: Date, 13 | default: Date.now 14 | } 15 | }); 16 | 17 | export default mongoose.model('director', DirectorSchema); 18 | 19 | -------------------------------------------------------------------------------- /server/models/film.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | const FilmSchema = new mongoose.Schema({ 3 | name: String, 4 | director: { 5 | type: mongoose.Schema.Types.ObjectId, 6 | ref: 'director' 7 | }, 8 | description: String, 9 | year: Number, 10 | user: { 11 | type: mongoose.Schema.Types.ObjectId, 12 | ref: 'user' 13 | }, 14 | createdAt: { 15 | type: Date, 16 | default: Date.now 17 | } 18 | }); 19 | 20 | FilmSchema.methods.fromSameDirector = function fromSameDirector () { 21 | return mongoose.model('film').find({ director: this.director }).exec(); 22 | }; 23 | 24 | FilmSchema.statics.search = function search (query) { 25 | return this.where('name', new RegExp(query.q, 'i')).exec(); 26 | }; 27 | 28 | export default mongoose.model('film', FilmSchema); 29 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | // import bcrypt from 'bcrypt'; 3 | import jsonSelect from 'mongoose-json-select'; 4 | 5 | // const SALT_WORK_FACTOR = 10; 6 | 7 | const UserSchema = new mongoose.Schema({ 8 | username: { 9 | type: String, 10 | required: true, 11 | index: { 12 | unique: true 13 | } 14 | }, 15 | password: { 16 | type: String, 17 | required: true 18 | }, 19 | token: String 20 | }); 21 | 22 | // UserSchema.pre('save', function *(next) { 23 | // // only hash the password if it has been modified (or is new) 24 | // if (!this.isModified('password')) { 25 | // yield next; 26 | // } 27 | // console.log(this); 28 | // // generate a salt 29 | // const salt = yield bcrypt.genSalt(SALT_WORK_FACTOR); 30 | // console.log(salt); 31 | // const hash = yield bcrypt.hash(this.password, salt); 32 | // console.log(hash); 33 | // this.password = hash; 34 | // console.log(this); 35 | // yield next; 36 | // }); 37 | 38 | // UserSchema.methods.comparePassword = function(candidatePassword) { 39 | // var def = Q.defer(); 40 | // if (!candidatePassword) { 41 | // console.log('compare called without password'); 42 | // def.resolve(false); 43 | // } 44 | // bcrypt.compare(candidatePassword, this.password, function(err, isMatch) { 45 | // if (err) { 46 | // def.reject(err); 47 | // } else { 48 | // def.resolve(isMatch); 49 | // } 50 | // }); 51 | // return def.promise; 52 | // }; 53 | 54 | UserSchema.plugin(jsonSelect, '-__v -password'); 55 | 56 | export default mongoose.model('user', UserSchema); 57 | 58 | -------------------------------------------------------------------------------- /server/rest.js: -------------------------------------------------------------------------------- 1 | import generateApi from "./restable/lib"; 2 | 3 | import Car from "./models/car"; 4 | import Director from "./models/director"; 5 | import Film from "./models/film"; 6 | import User from "./models/user"; 7 | import bcrypt from "bcrypt"; 8 | import uuid from "node-uuid"; 9 | 10 | import koaRouter from "koa-router"; 11 | 12 | export default function(app) { 13 | const mongoUrl = process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || "127.0.0.1:27017/webpackexample"; 14 | const mongoose = require("mongoose"); 15 | 16 | mongoose.connect(mongoUrl); 17 | 18 | // app.use(koaRouter(app)); 19 | 20 | // var CarsController = generateApi(app, Car, "/api"); 21 | var FilmsController = generateApi(app, Film, "/api"); 22 | FilmsController.use(function*(next) { 23 | console.log('fimscontroller middleware'); 24 | console.log(this.request.url); 25 | yield next; 26 | }); 27 | 28 | FilmsController.conditionsInterceptor(function(ctx, conditions){ 29 | if (ctx.request.user) { 30 | conditions.user = ctx.request.user._id; 31 | } 32 | return conditions; 33 | }); 34 | 35 | FilmsController.collectionHandler("get", "foo", function*(next) { 36 | yield next; 37 | console.log('foo handler'); 38 | this.body = 'foo handler'; 39 | }); 40 | 41 | FilmsController.mount(); 42 | var DirectorsController = generateApi(app, Director, "/api"); 43 | DirectorsController.mount(); 44 | 45 | var authRouter = koaRouter(); 46 | 47 | authRouter.post("/auth/register", function*(next) { 48 | yield next; 49 | const SALT_WORK_FACTOR = 10; 50 | const error = {message: "Username already exists"}; 51 | try { 52 | const body = this.request.body; 53 | const salt = yield bcrypt.genSalt.bind(this, SALT_WORK_FACTOR); 54 | const hash = yield bcrypt.hash.bind(this, body.password, salt); 55 | body.password = hash; 56 | body.token = uuid.v1(); 57 | const result = yield User.create(body); 58 | this.status = 201; 59 | this.body = result; 60 | } catch (err) { 61 | this.status = 409; 62 | this.body = error; 63 | } 64 | }); 65 | 66 | authRouter.post("/auth/login", function*(next) { 67 | yield next; 68 | try { 69 | const body = this.request.body; 70 | const error = { message: "Username and password doesn't match" }; 71 | const user = yield User.findOne({ 72 | username: body.username 73 | }); 74 | if (!user) throw error; 75 | const match = yield bcrypt.compare.bind(this, body.password, user.password); 76 | if (!match) throw error; 77 | user.token = uuid.v1(); 78 | this.status = 201; 79 | this.body = yield user.save(); 80 | } catch (err) { 81 | this.status = 401; 82 | this.body = err; 83 | } 84 | }); 85 | 86 | app 87 | .use(authRouter.routes()) 88 | .use(authRouter.allowedMethods()); 89 | 90 | // generateApi(app, User, "/api"); 91 | } 92 | 93 | -------------------------------------------------------------------------------- /server/restable/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | *.swp 4 | lib-cov/ 5 | *.log -------------------------------------------------------------------------------- /server/restable/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | services: 5 | - mongodb 6 | -------------------------------------------------------------------------------- /server/restable/Makefile: -------------------------------------------------------------------------------- 1 | TESTS = test/**/*.coffee 2 | REPORTER = dot 3 | 4 | test: 5 | @NODE_ENV=test NODE_TLS_REJECT_UNAUTHORIZED=0 ./node_modules/.bin/mocha \ 6 | --harmony \ 7 | --require should \ 8 | --require co-mocha \ 9 | --compilers coffee:coffee-script/register \ 10 | --reporter $(REPORTER) \ 11 | --timeout 5000 \ 12 | --growl \ 13 | $(TESTS) 14 | 15 | test-cov: lib-cov 16 | SUPERAGENT_COV=1 $(MAKE) test REPORTER=html-cov > docs/coverage.html 17 | 18 | lib-cov: 19 | jscoverage lib lib-cov 20 | 21 | test-server: 22 | @node --harmony example/app 23 | 24 | docs: test-docs 25 | 26 | md: test-md 27 | 28 | test-docs: 29 | make test REPORTER=doc \ 30 | | cat docs/head.html - docs/tail.html \ 31 | > docs/test.html 32 | 33 | test-md: 34 | make test REPORTER=markdown > docs/docs.md 35 | .PHONY: test-cov test docs test-docs 36 | -------------------------------------------------------------------------------- /server/restable/README.md: -------------------------------------------------------------------------------- 1 | # Koa mongo REST [](http://badge.fury.io/js/koa-mongo-rest) [](https://gemnasium.com/t3chnoboy/koa-mongo-rest) [](https://travis-ci.org/t3chnoboy/koa-mongo-rest) 2 | 3 | Easy REST api for [koa](http://koajs.com) server 4 | 5 | [](https://nodei.co/npm/koa-mongo-rest/) 6 | 7 | 8 | 9 | ## Installation 10 | Install using npm: 11 | ```sh 12 | npm install koa-mongo-rest 13 | ``` 14 | 15 | ## Usage 16 | 17 | Require library 18 | ```javascript 19 | generateApi = require('koa-mongo-rest'); 20 | ``` 21 | 22 | Create mongoose model 23 | ```javascript 24 | mongoUrl = '127.0.0.1:27017'; 25 | mongoose = require('mongoose'); 26 | mongoose.connect(mongoUrl); 27 | 28 | schema = new mongoose.Schema({ 29 | email: String, 30 | name: String, 31 | password: String, 32 | address: String, 33 | zipcode: Number, 34 | lists: Array 35 | }); 36 | 37 | model = mongoose.model('users', schema); 38 | ``` 39 | 40 | Create server 41 | ```javascript 42 | var koa = require('koa'); 43 | var router = require('koa-router'); 44 | 45 | var app = koa(); 46 | 47 | //router is required 48 | app.use(router(app)); 49 | 50 | 51 | //add REST routes to your app. Prefix is optional 52 | generateApi(app, model, '/api'); 53 | 54 | app.listen(process.env.PORT || 5000); 55 | ``` 56 | 57 | Following REST API is now created for you: 58 | 59 | | HTTP Verb | /users | /users/:id | 60 | | ------------- | ------------- | --------------- | 61 | | GET | Get all documents, or documents that match the query.
You can use [mongoose find conditions] (http://mongoosejs.com/docs/queries.html), limit, skip and sort.
For example:
**/api/users?conditions={"name":"john"}&limit=10&skip=1&sort=-zipcode** | Get the addressed document. | 62 | | POST | Create a new document and send it back. | Update the addressed document with specified attributes. | 63 | | PUT | Create a new document and send it back. | Replace the addressed document. | 64 | | DELETE | n/a | Delete the addressed document. | 65 | | PATCH | n/a | Update the addressed document with specified attributes. | 66 | -------------------------------------------------------------------------------- /server/restable/docs/coverage.html: -------------------------------------------------------------------------------- 1 |Coverage 35 |-------------------------------------------------------------------------------- /server/restable/docs/docs.md: -------------------------------------------------------------------------------- 1 | # TOC 2 | - [REST API](#rest-api) 3 | - [Routes](#rest-api-routes) 4 | - [GET](#rest-api-routes-get) 5 | - [GET /:model](#rest-api-routes-get-get-model) 6 | - [GET /:model/:id](#rest-api-routes-get-get-modelid) 7 | - [POST](#rest-api-routes-post) 8 | - [POST /:model](#rest-api-routes-post-post-model) 9 | - [POST /:model/:id](#rest-api-routes-post-post-modelid) 10 | - [DELETE](#rest-api-routes-delete) 11 | - [DELETE /:model/:id](#rest-api-routes-delete-delete-modelid) 12 | - [PUT](#rest-api-routes-put) 13 | - [PUT /:model](#rest-api-routes-put-put-model) 14 | - [PUT /:model/:id](#rest-api-routes-put-put-modelid) 15 | - [PATCH](#rest-api-routes-patch) 16 | - [PATCH /:model/:id](#rest-api-routes-patch-patch-modelid) 17 | 18 | 19 | 20 | # REST API 21 | 22 | ## Routes 23 | 24 | ### GET 25 | 26 | #### GET /:model 27 | should respond with JSON for all records. 28 | 29 | ```js 30 | return request.get('/user').expect(200).expect('Content-Type', /json/).expect(users).end(done); 31 | ``` 32 | 33 | 34 | #### GET /:model/:id 35 | should respond with JSON for the record with the specified id. 36 | 37 | ```js 38 | return request.get('/user/2').expect(200).expect('Content-Type', /json/).expect({ 39 | name: 'Joff', 40 | age: 27, 41 | _id: 2 42 | }).end(done); 43 | ``` 44 | 45 | 46 | ### POST 47 | 48 | #### POST /:model 49 | should respond with JSON for the created record. 50 | 51 | ```js 52 | return request.post('/user').send({ 53 | name: 'James', 54 | age: 40, 55 | _id: 4 56 | }).expect(201).expect({ 57 | name: 'James', 58 | age: 40, 59 | _id: 4 60 | }).end(done); 61 | ``` 62 | 63 | 64 | #### POST /:model/:id 65 | should respond with JSON for the updated record. 66 | 67 | ```js 68 | return request.post('/user/2').send({ 69 | age: 28 70 | }).expect(200).expect({ 71 | name: 'Joff', 72 | age: 28, 73 | _id: 2 74 | }).end(done); 75 | ``` 76 | 77 | 78 | ### DELETE 79 | 80 | #### DELETE /:model/:id 81 | should respond with JSON for the destroyed record. 82 | 83 | ```js 84 | return request.del('/user/2').expect(200).expect({ 85 | name: 'Joff', 86 | age: 27, 87 | _id: 2 88 | }).end(done); 89 | ``` 90 | 91 | 92 | ### PUT 93 | 94 | #### PUT /:model 95 | should respond with JSON for the created record. 96 | 97 | ```js 98 | return request.put('/user').send({ 99 | name: 'John', 100 | age: 26, 101 | _id: 5 102 | }).expect(201).expect({ 103 | name: 'John', 104 | age: 26, 105 | _id: 5 106 | }).end(done); 107 | ``` 108 | 109 | 110 | #### PUT /:model/:id 111 | should return JSON for the replaced record. 112 | 113 | ```js 114 | return request.put('/user/2').send({ 115 | name: 'Joseph', 116 | age: 37 117 | }).expect(200).expect({ 118 | name: 'Joseph', 119 | age: 37, 120 | _id: 2 121 | }).end(done); 122 | ``` 123 | 124 | 125 | ### PATCH 126 | 127 | #### PATCH /:model/:id 128 | should respond with JSON for the updated record. 129 | 130 | ```js 131 | return request.patch('/user/2').send({ 132 | age: 28 133 | }).expect(200).expect({ 134 | name: 'Joff', 135 | age: 28, 136 | _id: 2 137 | }).end(done); 138 | ``` 139 | 140 | -------------------------------------------------------------------------------- /server/restable/docs/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |Coverage
100%34340index.js
100%11110
Line Hits Source 1 1 var createModel, generateActions, generateRoutes; 2 3 1 generateRoutes = require('./routes'); 4 5 1 generateActions = require('./actions'); 6 7 1 createModel = require('./model'); 8 9 1 module.exports = function(schema, mongoUrl) { 10 1 var actions, model; 11 1 model = createModel(schema, mongoUrl); 12 1 actions = generateActions(model); 13 1 model.generateApi = function(app) { 14 1 return generateRoutes(app, schema, actions); 15 }; 16 1 return model; 17 }; model.js
100%770
Line Hits Source 1 1 var createModel, mongoose; 2 3 1 mongoose = require('mongoose'); 4 5 1 module.exports = createModel = function(schema, mongoUrl) { 6 1 var DocumentSchema; 7 1 mongoose.connect(mongoUrl); 8 1 DocumentSchema = new mongoose.Schema(schema.schema, { 9 collection: schema.collectionName, 10 versionKey: schema.versionKey 11 }); 12 1 return mongoose.model(schema.collectionName, DocumentSchema); 13 }; routes.js
100%16160
Line Hits Source 1 1 var generateRoutes; 2 3 1 module.exports = generateRoutes = function(app, schema, actions) { 4 1 app.get("/" + schema.collectionName + "/find", actions.findAll); 5 1 app.get("/" + schema.collectionName + "/find/:id", actions.findById); 6 1 app.get("/" + schema.collectionName + "/create", actions.createWithQuery); 7 1 app.get("/" + schema.collectionName + "/destroy/:id", actions.deleteById); 8 1 app.get("/" + schema.collectionName + "/update/:id", actions.updateByIdWithQuery); 9 1 app.get("/" + schema.collectionName + "/replace/:id", actions.replaceByIdWithQuery); 10 1 app.get("/" + schema.collectionName, actions.findAll); 11 1 app.get("/" + schema.collectionName + "/:id", actions.findById); 12 1 app.post("/" + schema.collectionName, actions.create); 13 1 app.post("/" + schema.collectionName + "/:id", actions.updateById); 14 1 app["delete"]("/" + schema.collectionName + "/:id", actions.deleteById); 15 1 app.put("/" + schema.collectionName + "/", actions.create); 16 1 app.put("/" + schema.collectionName + "/:id", actions.replaceById); 17 1 app.patch("/" + schema.collectionName + "/:id", actions.updateById); 18 }; Koa mongo REST 5 | 6 | 17 | 36 | 37 | 38 |Koa mongo REST
39 |Rest api generation for koa server.
40 | -------------------------------------------------------------------------------- /server/restable/docs/tail.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /server/restable/docs/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |Koa mongo REST 5 | 6 | 17 | 36 | 37 | 38 |Koa mongo REST
39 |Rest api generation for koa server.
40 |41 | 174 | 175 | -------------------------------------------------------------------------------- /server/restable/example/app.js: -------------------------------------------------------------------------------- 1 | koa = require('koa'); 2 | router = require('koa-router'); 3 | generateApi = require('../lib/index'); 4 | 5 | mongoUrl = '127.0.0.1:27017'; 6 | mongoose = require('mongoose'); 7 | mongoose.connect(mongoUrl); 8 | 9 | schema = new mongoose.Schema({ 10 | email: String, 11 | name: String, 12 | password: String, 13 | address: String, 14 | zipcode: Number, 15 | lists: Array 16 | }); 17 | 18 | app = koa(); 19 | app.use(router(app)); 20 | 21 | model = mongoose.model('user', schema); 22 | generateApi(app, model, '/api'); 23 | 24 | app.listen(process.env.PORT || 5000); 25 | -------------------------------------------------------------------------------- /server/restable/example/coffee/app.coffee: -------------------------------------------------------------------------------- 1 | koa = require 'koa' 2 | router = require 'koa-router' 3 | generateApi = require '../lib/index' 4 | 5 | mongoUrl = '127.0.0.1:27017' 6 | mongoose = require 'mongoose' 7 | mongoose.connect mongoUrl 8 | 9 | schema = new mongoose.Schema 10 | email: String 11 | name: String 12 | password: String 13 | address: String 14 | zipcode: Number 15 | lists: Array 16 | 17 | app = koa() 18 | app.use router(app) 19 | 20 | model = mongoose.model 'user', schema 21 | generateApi app, model, '/api' 22 | 23 | app.listen(process.env.PORT || 5000) 24 | -------------------------------------------------------------------------------- /server/restable/gulpfile.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | nodemon = require 'gulp-nodemon' 3 | coffeeES6 = require 'gulp-coffee-es6' 4 | 5 | paths = 6 | src : 'src/**/*.coffee' 7 | dest : 'lib' 8 | 9 | gulp.task 'compile', -> 10 | gulp.src paths.src 11 | .pipe coffeeES6 bare: yes 12 | .pipe gulp.dest paths.dest 13 | 14 | gulp.src 'example/coffee/**/*.coffee' 15 | .pipe coffeeES6 bare: yes 16 | .pipe gulp.dest 'example' 17 | 18 | gulp.task 'example-server', -> 19 | nodemon 20 | script: 'example/app.js' 21 | nodeArgs: ['--harmony'] 22 | ignore: [ 23 | './src/**' 24 | './test/**' 25 | './node_modules/**' 26 | ] 27 | 28 | gulp.task 'watch', -> 29 | gulp.watch paths.src, ['compile'] 30 | 31 | gulp.task 'default', ['compile', 'watch'] 32 | -------------------------------------------------------------------------------- /server/restable/gulpfile.js: -------------------------------------------------------------------------------- 1 | require('coffee-script/register'); 2 | require('./gulpfile.coffee'); 3 | -------------------------------------------------------------------------------- /server/restable/lib/actions.js: -------------------------------------------------------------------------------- 1 | module.exports = function(model) { 2 | return { 3 | findAll: function*(next) { 4 | yield next; 5 | var error, result; 6 | try { 7 | var conditions = {}; 8 | var query = this.request.query; 9 | if (query.conditions) { 10 | conditions = JSON.parse(query.conditions); 11 | } 12 | var builder = model.find(conditions); 13 | ['limit', 'skip', 'sort'].forEach(function(key){ 14 | if (query[key]) { 15 | builder[key](query[key]); 16 | } 17 | }) 18 | result = yield builder.exec(); 19 | return this.body = result; 20 | } catch (_error) { 21 | error = _error; 22 | return this.body = error; 23 | } 24 | }, 25 | findById: function*(next) { 26 | yield next; 27 | var error, result; 28 | try { 29 | result = yield model.findById(this.params.id).exec(); 30 | return this.body = result; 31 | } catch (_error) { 32 | error = _error; 33 | return this.body = error; 34 | } 35 | }, 36 | deleteById: function*(next) { 37 | yield next; 38 | var error, result; 39 | try { 40 | result = yield model.findByIdAndRemove(this.params.id).exec(); 41 | return this.body = result; 42 | } catch (_error) { 43 | error = _error; 44 | return this.body = error; 45 | } 46 | }, 47 | replaceById: function*(next) { 48 | yield next; 49 | var error, newDocument, result; 50 | try { 51 | yield model.findByIdAndRemove(this.params.id).exec(); 52 | newDocument = this.request.body; 53 | newDocument._id = this.params.id; 54 | result = yield model.create(newDocument); 55 | return this.body = result; 56 | } catch (_error) { 57 | error = _error; 58 | return this.body = error; 59 | } 60 | }, 61 | updateById: function*(next) { 62 | yield next; 63 | var error, result; 64 | try { 65 | result = yield model.findByIdAndUpdate(this.params.id, this.request.body, {new: true}).exec(); 66 | return this.body = result; 67 | } catch (_error) { 68 | error = _error; 69 | return this.body = error; 70 | } 71 | }, 72 | create: function*(next) { 73 | yield next; 74 | var error, result; 75 | try { 76 | result = yield model.create(this.request.body); 77 | this.status = 201; 78 | return this.body = result; 79 | } catch (_error) { 80 | error = _error; 81 | return this.body = error; 82 | } 83 | } 84 | }; 85 | }; 86 | -------------------------------------------------------------------------------- /server/restable/lib/index.js: -------------------------------------------------------------------------------- 1 | var generateActions, generateApi, generateRoutes; 2 | 3 | generateRoutes = require('./routes'); 4 | 5 | generateActions = require('./actions'); 6 | 7 | module.exports = generateApi = function(app, model, prefix) { 8 | var actions; 9 | if (prefix == null) { 10 | prefix = ''; 11 | } 12 | actions = generateActions(model); 13 | return generateRoutes(app, model, actions, prefix); 14 | }; 15 | -------------------------------------------------------------------------------- /server/restable/lib/model.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /server/restable/lib/mount-router.js: -------------------------------------------------------------------------------- 1 | export default function(app, router) { 2 | return app 3 | .use(router.routes()) 4 | .use(router.allowedMethods()); 5 | } -------------------------------------------------------------------------------- /server/restable/lib/routes.js: -------------------------------------------------------------------------------- 1 | var generateRoutes; 2 | var pluralize = require('pluralize'); 3 | var koaRouter = require('koa-router'); 4 | var _ = require('lodash'); 5 | 6 | // bodyParser = require('koa-body-parser'); 7 | 8 | module.exports = generateRoutes = function(app, model, actions, prefix) { 9 | if (prefix == null) { 10 | prefix = ''; 11 | } 12 | 13 | const modelName = pluralize(model.modelName); 14 | 15 | const collectionPrefix = prefix + ("/" + modelName); 16 | const itemPrefix = prefix + ("/" + modelName + "/:id"); 17 | 18 | 19 | // app.use(bodyParser()); 20 | var router = koaRouter(); 21 | 22 | router.queryInterceptor = function(cb) { 23 | router.use(function*(next){ 24 | if (this.request.method === "GET") { 25 | this.request.query = cb(this, this.request.query) 26 | console.log("query interceptor query:"); 27 | console.log(this.request.query); 28 | } 29 | yield next; 30 | }) 31 | } 32 | 33 | router.conditionsInterceptor = function(cb) { 34 | router.use(function*(next){ 35 | if (this.request.method === "GET") { 36 | var conditions; 37 | var query = _.clone(this.request.query); 38 | try { 39 | conditions = (query.conditions && JSON.parse(query.conditions)) || {}; 40 | } catch (err) { 41 | console.error(err); 42 | conditions = {}; 43 | } 44 | 45 | conditions = cb(this, conditions) 46 | 47 | query.conditions = JSON.stringify(conditions); 48 | this.request.query = query; 49 | console.log("conditions interceptor query:"); 50 | console.log(this.request.query); 51 | } 52 | yield next; 53 | }) 54 | } 55 | 56 | router.collectionHandler = function(method, name, cb) { 57 | router[method](collectionPrefix + "/" + name, cb); 58 | } 59 | 60 | router.itemHandler = function(method, name, cb) { 61 | router[method](itemPrefix + "/" + name, cb); 62 | } 63 | 64 | router.mount = function() { 65 | console.log('methods:'); 66 | console.log(model.schema.methods); 67 | 68 | // Mount schema methods 69 | _.keys(model.schema.methods).forEach(function(method){ 70 | //TODO: All schema methods are bound to get http verb. Do we need to be able to override this? 71 | router.itemHandler("get", method, function*(next){ 72 | yield next; 73 | var item = yield model.findById(this.params.id).exec(); 74 | var result = item[method](this.request.query); 75 | //TODO: This way, 2 database calls are made. Is it possible to optimize? 76 | this.body = yield result;///item; 77 | }); 78 | }); 79 | 80 | // Mount static methods 81 | _.keys(model.schema.statics).forEach(function(method){ 82 | //TODO: All schema methods are bound to get http verb. Do we need to be able to override this? 83 | router.collectionHandler("get", method, function*(next){ 84 | yield next; 85 | var result = model[method](this.request.query); 86 | this.body = yield result;///item; 87 | }); 88 | }); 89 | 90 | router.get(collectionPrefix, actions.findAll); 91 | router.get(itemPrefix, actions.findById); 92 | router.post(collectionPrefix, actions.create); 93 | router.post(itemPrefix, actions.updateById); 94 | router.del(itemPrefix, actions.deleteById); 95 | router.put(collectionPrefix, actions.create); 96 | router.put(itemPrefix, actions.replaceById); 97 | router.patch(itemPrefix, actions.updateById); 98 | app 99 | .use(router.routes()) 100 | .use(router.allowedMethods()); 101 | } 102 | 103 | return router; 104 | }; -------------------------------------------------------------------------------- /server/restable/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-mongo-rest", 3 | "version": "0.3.4", 4 | "description": "generate REST API with koa and mongo", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "mocha --harmony" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+ssh://git@github.com/t3chnoboy/koa-mongo-rest.git" 12 | }, 13 | "keywords": [ 14 | "koa", 15 | "REST", 16 | "API", 17 | "mongo", 18 | "mongodb", 19 | "mongoose", 20 | "es6", 21 | "generators", 22 | "database", 23 | "db" 24 | ], 25 | "author": { 26 | "name": "Dmitry Mazuro", 27 | "email": "dmitry.mazuro@icloud.com" 28 | }, 29 | "license": "BSD-2-Clause", 30 | "bugs": { 31 | "url": "https://github.com/t3chnoboy/koa-mongo-rest/issues" 32 | }, 33 | "homepage": "https://github.com/t3chnoboy/koa-mongo-rest", 34 | "dependencies": { 35 | "koa-body-parser": "git://github.com/tomaash/koa-body-parser.git", 36 | "mongoose": "^4.0.2", 37 | "pluralize": "1.1.2" 38 | }, 39 | "devDependencies": { 40 | "co-mocha": "0.0.2", 41 | "coffee-script": "git://github.com/xixixao/coffee-script.git", 42 | "gulp": "^3.6.0", 43 | "gulp-coffee-es6": "git://github.com/t3chnoboy/gulp-coffee-es6.git", 44 | "gulp-nodemon": "^1.0.2", 45 | "koa": "^0.5.2", 46 | "koa-logger": "^1.2.0", 47 | "koa-router": "^3.1.2", 48 | "should": "^3.2.0-beta1", 49 | "supertest": "^0.10.0" 50 | }, 51 | "readme": "# Koa mongo REST [](http://badge.fury.io/js/koa-mongo-rest) [](https://gemnasium.com/t3chnoboy/koa-mongo-rest) [](https://travis-ci.org/t3chnoboy/koa-mongo-rest)\n\nEasy REST api for [koa](http://koajs.com) server \n\n[](https://nodei.co/npm/koa-mongo-rest/)\n\n\n\n## Installation\nInstall using npm:\n```sh\nnpm install koa-mongo-rest\n```\n\n## Usage\n\nRequire library\n```javascript\ngenerateApi = require('koa-mongo-rest');\n```\n\nCreate mongoose model\n```javascript\nmongoUrl = '127.0.0.1:27017';\nmongoose = require('mongoose');\nmongoose.connect(mongoUrl);\n\nschema = new mongoose.Schema({\n email: String,\n name: String,\n password: String,\n address: String,\n zipcode: Number,\n lists: Array\n});\n\nmodel = mongoose.model('users', schema);\n```\n\nCreate server\n```javascript\nvar koa = require('koa');\nvar router = require('koa-router');\n\nvar app = koa();\n\n//router is required\napp.use(router(app));\n\n\n//add REST routes to your app. Prefix is optional\ngenerateApi(app, model, '/api');\n\napp.listen(process.env.PORT || 5000);\n```\n\nFollowing REST API is now created for you:\n\n| HTTP Verb | /users | /users/:id |\n| ------------- | ------------- | --------------- |\n| GET | Get all documents, or documents that match the query.REST API
42 |43 |
173 |44 | 172 |Routes
45 |46 |
171 |47 | 69 |GET
48 |49 |
68 |50 | 56 |GET /:model
51 |52 |
55 |- should respond with JSON for all records
53 |- 54 |
return request.get('/user').expect(200).expect('Content-Type', /json/).expect(users).end(done);
57 | 67 |GET /:model/:id
58 |59 |
66 |- should respond with JSON for the record with the specified id
60 |- 65 |
return request.get('/user/2').expect(200).expect('Content-Type', /json/).expect({ 61 | name: 'Joff', 62 | age: 27, 63 | _id: 2 64 | }).end(done);
70 | 102 |POST
71 |72 |
101 |73 | 87 |POST /:model
74 |75 |
86 |- should respond with JSON for the created record
76 |- 85 |
return request.post('/user').send({ 77 | name: 'James', 78 | age: 40, 79 | _id: 4 80 | }).expect(201).expect({ 81 | name: 'James', 82 | age: 40, 83 | _id: 4 84 | }).end(done);
88 | 100 |POST /:model/:id
89 |90 |
99 |- should respond with JSON for the updated record
91 |- 98 |
return request.post('/user/2').send({ 92 | age: 28 93 | }).expect(200).expect({ 94 | name: 'Joff', 95 | age: 28, 96 | _id: 2 97 | }).end(done);
103 | 118 |DELETE
104 |105 |
117 |106 | 116 |DELETE /:model/:id
107 |108 |
115 |- should respond with JSON for the destroyed record
109 |- 114 |
return request.del('/user/2').expect(200).expect({ 110 | name: 'Joff', 111 | age: 27, 112 | _id: 2 113 | }).end(done);
119 | 152 |PUT
120 |121 |
151 |122 | 136 |PUT /:model
123 |124 |
135 |- should respond with JSON for the created record
125 |- 134 |
return request.put('/user').send({ 126 | name: 'John', 127 | age: 26, 128 | _id: 5 129 | }).expect(201).expect({ 130 | name: 'John', 131 | age: 26, 132 | _id: 5 133 | }).end(done);
137 | 150 |PUT /:model/:id
138 |139 |
149 |- should return JSON for the replaced record
140 |- 148 |
return request.put('/user/2').send({ 141 | name: 'Joseph', 142 | age: 37 143 | }).expect(200).expect({ 144 | name: 'Joseph', 145 | age: 37, 146 | _id: 2 147 | }).end(done);
153 | 170 |PATCH
154 |155 |
169 |156 | 168 |PATCH /:model/:id
157 |158 |
167 |- should respond with JSON for the updated record
159 |- 166 |
return request.patch('/user/2').send({ 160 | age: 28 161 | }).expect(200).expect({ 162 | name: 'Joff', 163 | age: 28, 164 | _id: 2 165 | }).end(done);
You can use [mongoose find conditions] (http://mongoosejs.com/docs/queries.html), limit, skip and sort.
For example:
**/api/users?conditions={\"name\":\"john\"}&limit=10&skip=1&sort=-zipcode** | Get the addressed document. |\n| POST | Create a new document and send it back. | Update the addressed document with specified attributes. |\n| PUT | Create a new document and send it back. | Replace the addressed document. |\n| DELETE | n/a | Delete the addressed document. |\n| PATCH | n/a | Update the addressed document with specified attributes. |\n", 52 | "readmeFilename": "README.md", 53 | "gitHead": "55724c658d6c1f294968637596924bd71f09e811", 54 | "_id": "koa-mongo-rest@0.3.4", 55 | "_shasum": "387174b012f50eccef4745c9c8edaae5ac9e7629", 56 | "_from": "koa-mongo-rest@>=0.3.4 <0.4.0" 57 | } 58 | -------------------------------------------------------------------------------- /server/restable/test/index.coffee: -------------------------------------------------------------------------------- 1 | server = require './server' 2 | model = server.model 3 | request = require('supertest').agent server.listen() 4 | 5 | users = [ 6 | name: 'Fronk' 7 | age : 28 8 | _id : 1 9 | , 10 | name: 'Joff' 11 | age : 27 12 | _id : 2 13 | , 14 | name: 'Scoobert' 15 | age : 54 16 | _id : 3 17 | ] 18 | 19 | describe 'REST API', -> 20 | 21 | describe 'Routes', -> 22 | 23 | beforeEach --> 24 | for user in users 25 | yield model.create user 26 | 27 | afterEach (done) -> 28 | model.remove {}, -> done() 29 | 30 | describe 'GET', -> 31 | 32 | describe 'GET /:model', -> 33 | it 'should respond with JSON for all records', (done) -> 34 | request 35 | .get '/users' 36 | .expect 200 37 | .expect 'Content-Type', /json/ 38 | .expect users 39 | .end done 40 | 41 | describe 'GET /:model/:id', -> 42 | it 'should respond with JSON for the record with the specified id', (done) -> 43 | request 44 | .get '/users/2' 45 | .expect 200 46 | .expect 'Content-Type', /json/ 47 | .expect 48 | name : 'Joff' 49 | age : 27 50 | _id : 2 51 | .end done 52 | 53 | describe 'POST', -> 54 | 55 | describe 'POST /:model', -> 56 | it 'should respond with JSON for the created record', (done) -> 57 | request 58 | .post '/users' 59 | .send 60 | name : 'James' 61 | age : 40 62 | _id : 4 63 | .expect 201 64 | .expect 65 | name : 'James' 66 | age : 40 67 | _id : 4 68 | .end done 69 | 70 | describe 'POST /:model/:id', -> 71 | it 'should respond with JSON for the updated record', (done) -> 72 | request 73 | .post '/users/2' 74 | .send 75 | age : 28 76 | .expect 200 77 | .expect 78 | name : 'Joff' 79 | age : 28 80 | _id : 2 81 | .end done 82 | 83 | 84 | describe 'DELETE', -> 85 | 86 | describe 'DELETE /:model/:id', -> 87 | it 'should respond with JSON for the destroyed record', (done) -> 88 | request 89 | .del '/users/2' 90 | .expect 200 91 | .expect 92 | name : 'Joff' 93 | age : 27 94 | _id : 2 95 | .end done 96 | 97 | describe 'PUT', -> 98 | 99 | describe 'PUT /:model', -> 100 | it 'should respond with JSON for the created record', (done) -> 101 | request 102 | .put '/users' 103 | .send 104 | name : 'John' 105 | age : 26 106 | _id : 5 107 | .expect 201 108 | .expect 109 | name : 'John' 110 | age : 26 111 | _id : 5 112 | .end done 113 | 114 | 115 | describe 'PUT /:model/:id', -> 116 | it 'should return JSON for the replaced record', (done) -> 117 | request 118 | .put '/users/2' 119 | .send 120 | name : 'Joseph' 121 | age : 37 122 | .expect 200 123 | .expect 124 | name : 'Joseph' 125 | age : 37 126 | _id : 2 127 | .end done 128 | 129 | describe 'PATCH', -> 130 | 131 | describe 'PATCH /:model/:id', -> 132 | it 'should respond with JSON for the updated record', (done) -> 133 | request 134 | .patch '/users/2' 135 | .send 136 | age : 28 137 | .expect 200 138 | .expect 139 | name : 'Joff' 140 | age : 28 141 | _id : 2 142 | .end done 143 | 144 | after (done) -> 145 | model.db.close(done) 146 | -------------------------------------------------------------------------------- /server/restable/test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should 2 | --reporter spec 3 | --compilers coffee:coffee-script/register 4 | --require co-mocha 5 | -------------------------------------------------------------------------------- /server/restable/test/server.js: -------------------------------------------------------------------------------- 1 | var koa = require('koa'); 2 | var router = require('koa-router'); 3 | var mongoose = require('mongoose'); 4 | var generateApi = require('../lib/index'); 5 | 6 | var app = koa(); 7 | app.use(router(app)); 8 | 9 | var mongoUrl = '127.0.0.1:27017'; 10 | mongoose.connect(mongoUrl); 11 | 12 | var schema = new mongoose.Schema({ 13 | name: String, 14 | age : Number, 15 | _id : Number 16 | }, {versionKey: false}); 17 | 18 | model = app.model = mongoose.model('user', schema); 19 | generateApi(app, model); 20 | 21 | module.exports = app; 22 | -------------------------------------------------------------------------------- /server/router.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import debug from 'debug'; 6 | 7 | import Router from 'react-router'; 8 | import React from 'react'; 9 | 10 | // Paths are relative to `app` directory 11 | import routes from 'routes'; 12 | 13 | import Iso from 'iso'; 14 | import alt from 'utils/alt'; 15 | 16 | export default function *() { 17 | const isCashed = this.cashed ? yield *this.cashed() : false; 18 | if (!isCashed) { 19 | 20 | var getHandler = function(routes, url) { 21 | return new Promise(function(resolve) { 22 | Router.run(routes, url, function (Handler) { 23 | resolve(Handler); 24 | }); 25 | }); 26 | }; 27 | 28 | // We seed our stores with data 29 | alt.bootstrap(JSON.stringify({})); 30 | var iso = new Iso(); 31 | 32 | // We use react-router to run the URL that is provided in routes.jsx 33 | const handler = yield getHandler(routes, this.request.url); 34 | const node = React.renderToString(React.createElement(handler)); 35 | 36 | iso.add(node, alt.flush()); 37 | var content = iso.render(); 38 | let assets; 39 | if (process.env.NODE_ENV === 'development') { 40 | assets = fs.readFileSync(path.resolve(__dirname, './webpack-stats.json')); 41 | assets = JSON.parse(assets); 42 | } 43 | else { 44 | assets = require('./webpack-stats.json'); 45 | } 46 | 47 | debug('dev')('return html content'); 48 | yield this.render('main', {content, assets}); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /server/views/layouts/index.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | {{#each assets.style}} 13 | 14 | {{/each}} 15 |ISO-ReactJS 16 | 17 | 18 | {{{body}}} 19 | {{#each assets.script}} 20 | 21 | {{/each}} 22 | 23 | 24 | -------------------------------------------------------------------------------- /server/views/main.hbs: -------------------------------------------------------------------------------- 1 |{{{content}}}2 | -------------------------------------------------------------------------------- /test/mocha-runner.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_PATH = "app"; 2 | require("module").Module._initPaths(); 3 | require("babel/register"); 4 | 5 | var _ = require("lodash"); 6 | var uncache = require("require-uncache"); 7 | var glob = require("glob").sync; 8 | var Mocha = require("mocha"); 9 | var jsdom = require("jsdom"); 10 | 11 | global.sinon = require("sinon"); 12 | global.chai = require("chai"); 13 | global.chai.use(require("chai-spies")); 14 | global.chai.use(require("sinon-chai")); 15 | global.assert = global.chai.assert; 16 | global.expect = global.chai.expect; 17 | global.should = global.chai.should(); 18 | 19 | var filePatterns = _([ 20 | "test/**/*spec.js", 21 | "app/**/*spec.js" 22 | ]); 23 | 24 | function runTests() { 25 | // globals 26 | global.document = jsdom.jsdom(); 27 | global.window = document.defaultView; 28 | global.navigator = global.window.navigator; 29 | global.location = global.window.location; 30 | global.localStorage = { 31 | getItem: _.noop, 32 | setItem: _.noop, 33 | removeItem: _.noop 34 | }; 35 | 36 | DEBUG = false; 37 | global.navigator.userAgent = "NodeJs JsDom"; 38 | global.navigator.appVersion = ""; 39 | 40 | var mocha = new Mocha(); 41 | mocha.reporter("spec").ui("bdd"); 42 | var testFiles = filePatterns.reduce(function(sum, item) { 43 | return sum.concat(glob(item)); 44 | }, []); 45 | 46 | mocha.suite.on("pre-require", function(context, file) { 47 | uncache(file); 48 | }); 49 | 50 | testFiles.forEach(function(file) { 51 | mocha.addFile(file); 52 | }); 53 | 54 | mocha.run(); 55 | }; 56 | 57 | runTests(); 58 | 59 | var argv = require("yargs").argv; 60 | if (argv.w) { 61 | process.on("uncaughtException", function(err) { 62 | console.error(err.stack); 63 | }); 64 | 65 | var chokidar = require("chokidar"); 66 | chokidar.watch(["./app", "./test"], {ignored: /[\/\\]\./}).on("all", function(event, path) { 67 | if (event === "change") { 68 | console.log(event, path); 69 | runTests(); 70 | } 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /test/tape-runner.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_PATH = "app"; 2 | require("module").Module._initPaths(); 3 | require("babel/register"); 4 | 5 | var _ = require("lodash"); 6 | var uncache = require("require-uncache"); 7 | var glob = require("glob").sync; 8 | var jsdom = require("jsdom"); 9 | var pathUtils = require("path"); 10 | 11 | var filePatterns = _([ 12 | "test/**/*test.js", 13 | "app/**/*test.js" 14 | ]); 15 | 16 | function runTests() { 17 | // globals 18 | global.document = jsdom.jsdom(); 19 | global.window = document.defaultView; 20 | global.navigator = global.window.navigator; 21 | global.location = global.window.location; 22 | global.localStorage = { 23 | getItem: _.noop, 24 | setItem: _.noop, 25 | removeItem: _.noop 26 | }; 27 | 28 | DEBUG = false; 29 | global.navigator.userAgent = "NodeJs JsDom"; 30 | global.navigator.appVersion = ""; 31 | 32 | var testFiles = filePatterns.reduce(function(sum, item) { 33 | return sum.concat(glob(item)); 34 | }, []); 35 | 36 | uncache("tape"); 37 | var test = require("tape"); 38 | var faucet = require("faucet"); 39 | // var tapSpec = require("tap-spec"); 40 | 41 | test.createStream() 42 | .pipe(faucet()) 43 | .pipe(process.stdout); 44 | 45 | testFiles.forEach(function(file) { 46 | const absPath = pathUtils.join(process.cwd(), file); 47 | uncache(absPath); 48 | require(absPath); 49 | }); 50 | } 51 | 52 | runTests(); 53 | 54 | var argv = require("yargs").argv; 55 | if (argv.w) { 56 | process.on("uncaughtException", function(err) { 57 | console.error(err.stack); 58 | }); 59 | 60 | var chokidar = require("chokidar"); 61 | chokidar.watch(["./app", "./test"], {ignored: /[\/\\]\./}).on("all", function(event, path) { 62 | if (event === "change") { 63 | console.log(event, path); 64 | runTests(); 65 | } 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /tests.webpack.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Browser ES6 Polyfill 4 | require('babel/polyfill'); 5 | 6 | var context = require.context('./test/spec', true, /\.test\.jsx$|\.test\.js$/); 7 | context.keys().forEach(context); 8 | -------------------------------------------------------------------------------- /webpack/dev-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('babel/register'); 4 | 5 | var debug = require('debug')('dev'); 6 | var webpack = require('webpack'); 7 | var WebpackDevServer = require('webpack-dev-server'); 8 | 9 | var config = require('./dev.config'); 10 | 11 | var compiler = webpack(config.webpack); 12 | var devServer = new WebpackDevServer(compiler, config.server.options); 13 | 14 | devServer.listen(config.server.port, 'localhost', function () { 15 | debug('webpack-dev-server listen on port %s', config.server.port); 16 | }); 17 | -------------------------------------------------------------------------------- /webpack/dev.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import webpack from "webpack"; 3 | 4 | import writeStats from "./utils/write-stats"; 5 | import startKoa from "./utils/start-koa"; 6 | 7 | const PROTOCOL = (process.env.C9_HOSTNAME) ? "https" : "http"; 8 | const HOST = process.env.C9_HOSTNAME || "localhost"; 9 | const PORT = (process.env.C9_HOSTNAME) ? "443" : "3001"; 10 | const PUBLIC_PATH = `${PROTOCOL}://${HOST}:${PORT}/assets/`; 11 | 12 | const WEBPACK_PORT = parseInt(process.env.PORT) + 1 || 3001; 13 | 14 | export default { 15 | server: { 16 | port: WEBPACK_PORT, 17 | options: { 18 | publicPath: (process.env.C9_HOSTNAME) ? "/" : PUBLIC_PATH, 19 | hot: true, 20 | stats: { 21 | assets: true, 22 | colors: true, 23 | version: false, 24 | hash: false, 25 | timings: true, 26 | chunks: false, 27 | chunkModules: false 28 | } 29 | } 30 | }, 31 | webpack: { 32 | devtool: "eval-source-map", 33 | entry: { 34 | app: [ 35 | `webpack-dev-server/client?http://localhost:${WEBPACK_PORT}`, 36 | "webpack/hot/only-dev-server", 37 | // "bootstrap-sass!./app/styles/bootstrap-sass.config.js", 38 | "./app/index.js" 39 | ] 40 | }, 41 | publicPath: PUBLIC_PATH, 42 | output: { 43 | path: path.join(__dirname, "../dist"), 44 | filename: "[name]-[hash].js", 45 | chunkFilename: "[name]-[hash].js", 46 | publicPath: PUBLIC_PATH 47 | }, 48 | module: { 49 | // preLoaders: [ 50 | // { 51 | // test: /\.js$|.jsx$/, 52 | // exclude: /node_modules|styles/, 53 | // loaders: ["eslint-loader", "jscs-loader"] 54 | // } 55 | // ], 56 | loaders: [ 57 | { 58 | test: /\.json$/, 59 | loader: "json" 60 | }, 61 | { 62 | // |svg|woff|eot|ttf 63 | test: /\.(jpe?g|png|gif)$/, 64 | loader: "url?limit=10000&name=[sha512:hash:base64:7].[ext]" 65 | }, 66 | { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "url-loader?limit=10000&minetype=application/font-woff" 67 | }, 68 | { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file-loader" 69 | }, 70 | { 71 | test: /\.js$|.jsx$/, 72 | exclude: /node_modules/, 73 | loaders: ["react-hot", "babel"] 74 | }, 75 | { 76 | test: /\.scss$/, 77 | loader: "style!css?sourceMap!autoprefixer?browsers=last 2 version!sass?outputStyle=expanded&sourceMap" 78 | }, 79 | { 80 | test: /\.less$/, 81 | loader: "style!css?sourceMap!autoprefixer?browsers=last 2 version!less?outputStyle=expanded&sourceMap" 82 | }, 83 | { test: /\.css$/, 84 | loader: "style-loader!css-loader" 85 | } 86 | ] 87 | }, 88 | 89 | // jscs: { 90 | // emitErrors: false, 91 | // failOnHint: false 92 | // }, 93 | // eslint: { 94 | // emitWarning: true, 95 | // failOnError: false, 96 | // failOnWarning: false 97 | // }, 98 | plugins: [ 99 | 100 | // hot reload 101 | new webpack.HotModuleReplacementPlugin(), 102 | new webpack.DefinePlugin({ 103 | "process.env": { 104 | BROWSER: JSON.stringify(true), 105 | NODE_ENV: JSON.stringify("development") 106 | } 107 | }), 108 | 109 | new webpack.optimize.DedupePlugin(), 110 | new webpack.optimize.OccurenceOrderPlugin(), 111 | 112 | function() { this.plugin("done", writeStats); }, 113 | 114 | function() { this.plugin("done", startKoa); } 115 | 116 | ], 117 | resolve: { 118 | extensions: ["", ".js", ".json", ".jsx"], 119 | modulesDirectories: ["node_modules", "app"] 120 | } 121 | } 122 | }; -------------------------------------------------------------------------------- /webpack/prod.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */ 4 | /* eslint camelcase: 0 */ 5 | 6 | require('babel/register'); 7 | 8 | var path = require('path'); 9 | var webpack = require('webpack'); 10 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 11 | 12 | var writeStats = require('./utils/write-stats'); 13 | 14 | // clean `.tmp` && `dist` 15 | require('./utils/clean-dist')(); 16 | 17 | module.exports = { 18 | devtool: 'source-map', 19 | entry: { 20 | app: [ 21 | './app/index.js', 22 | // 'bootstrap-sass!./app/styles/bootstrap-sass.prod.config.js' 23 | ] 24 | }, 25 | output: { 26 | path: path.join(__dirname, '../dist'), 27 | filename: '[name]-[hash].js', 28 | chunkFilename: '[name]-[hash].js', 29 | publicPath: '/assets/' 30 | }, 31 | module: { 32 | // preLoaders: [ 33 | // { 34 | // test: /\.js$|.jsx$/, 35 | // exclude: /node_modules|styles/, 36 | // loaders: ['eslint', 'jscs'] 37 | // } 38 | // ], 39 | loaders: [ 40 | { 41 | test: /\.json$/, 42 | loader: 'json' 43 | }, 44 | // { 45 | // test: /\.(eot|ttf)$/, 46 | // loader: 'url?limit=10000&name=[sha512:hash:base64:7].[ext]' 47 | // }, 48 | // { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'url-loader?limit=10000&minetype=application/font-woff' 49 | // }, 50 | // { 51 | // test: /\.(jpe?g|png|gif|svg)$/, 52 | // loader: 'url?limit=10000&name=[sha512:hash:base64:7].[ext]!image?optimizationLevel=7&progressive&interlaced' 53 | // }, 54 | { 55 | // |svg|woff|eot|ttf 56 | test: /\.(jpe?g|png|gif)$/, 57 | loader: 'url?limit=10000&name=[sha512:hash:base64:7].[ext]' 58 | }, 59 | { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'url-loader?limit=10000&minetype=application/font-woff' 60 | }, 61 | { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file-loader' 62 | }, 63 | { 64 | test: /\.js$|.jsx$/, 65 | exclude: /node_modules/, 66 | loader: 'babel' 67 | }, 68 | { 69 | test: /\.scss$/, 70 | loader: ExtractTextPlugin.extract('style', 'css!autoprefixer?browsers=last 2 version!sass') 71 | }, 72 | { test: /\.css$/, 73 | loader: 'style-loader!css-loader' 74 | } 75 | ] 76 | }, 77 | plugins: [ 78 | 79 | // extract css 80 | new ExtractTextPlugin('[name]-[hash].css'), 81 | 82 | // set env 83 | new webpack.DefinePlugin({ 84 | 'process.env': { 85 | BROWSER: JSON.stringify(true), 86 | NODE_ENV: JSON.stringify('production') 87 | } 88 | }), 89 | 90 | // optimizations 91 | new webpack.optimize.DedupePlugin(), 92 | new webpack.optimize.OccurenceOrderPlugin(), 93 | new webpack.optimize.UglifyJsPlugin({ 94 | compress: { 95 | warnings: false, 96 | screw_ie8: true, 97 | sequences: true, 98 | dead_code: true, 99 | drop_debugger: true, 100 | comparisons: true, 101 | conditionals: true, 102 | evaluate: true, 103 | booleans: true, 104 | loops: true, 105 | unused: true, 106 | hoist_funs: true, 107 | if_return: true, 108 | join_vars: true, 109 | cascade: true, 110 | drop_console: true 111 | }, 112 | output: { 113 | comments: false 114 | } 115 | }), 116 | 117 | // write webpack stats 118 | function () { this.plugin('done', writeStats); } 119 | 120 | ], 121 | resolve: { 122 | extensions: ['', '.js', '.json', '.jsx'], 123 | modulesDirectories: ['node_modules', 'app'] 124 | } 125 | }; 126 | -------------------------------------------------------------------------------- /webpack/utils/clean-dist.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import del from 'del'; 4 | import path from 'path'; 5 | import debug from 'debug'; 6 | 7 | export default () => { 8 | const DIST_PATH = path.resolve(__dirname, '../../dist/*'); 9 | del.sync([DIST_PATH]); 10 | debug('dev')('cleaned `dist` directory'); 11 | }; 12 | -------------------------------------------------------------------------------- /webpack/utils/start-koa.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import cp from 'child_process'; 4 | import path from 'path'; 5 | import debug from 'debug'; 6 | import browserSync from 'browser-sync'; 7 | import watch from 'node-watch'; 8 | 9 | import assign from 'react/lib/Object.assign'; 10 | 11 | let server; 12 | let started; 13 | let serverReload; 14 | const KOA_PATH = path.join(__dirname, '../../server/index'); 15 | 16 | const startServer = () => { 17 | // merge env for the new process 18 | const env = assign({NODE_ENV: 'development'}, process.env); 19 | // start the server procress 20 | server = cp.fork(KOA_PATH, {env}); 21 | // when server is `online` 22 | server.once('message', (message) => { 23 | if (message.match(/^online$/)) { 24 | if (serverReload) { 25 | serverReload = false; 26 | browserSync.reload(); 27 | } 28 | if (!started) { 29 | started = true; 30 | // Start browserSync 31 | browserSync({ 32 | port: 8080, 33 | proxy: 'http://localhost:3000' 34 | }); 35 | // Start watcher on server files 36 | // and reload browser on change 37 | watch( 38 | path.join(__dirname, '../../server'), 39 | (file) => { 40 | if (!file.match('webpack-stats.json')) { 41 | debug('dev')('restarting koa application'); 42 | serverReload = true; 43 | server.kill('SIGTERM'); 44 | return startServer(); 45 | } 46 | } 47 | ); 48 | } 49 | } 50 | }); 51 | }; 52 | 53 | // kill server on exit 54 | process.on('exit', () => server.kill('SIGTERM')); 55 | 56 | export default function () { 57 | if (!server) { 58 | return startServer(); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /webpack/utils/write-stats.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // borrowed from https://github.com/gpbl/isomorphic500/blob/master/webpack%2Futils%2Fwrite-stats.js 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | import debug from 'debug'; 7 | 8 | const filepath = path.resolve(__dirname, '../../server/webpack-stats.json'); 9 | 10 | export default function (stats) { 11 | 12 | const publicPath = this.options.output.publicPath; 13 | const json = stats.toJson(); 14 | 15 | // get chunks by name and extensions 16 | const getChunks = function (name, ext) { 17 | ext = ext || /.js$/; 18 | let chunks = json.assetsByChunkName[name]; 19 | 20 | // a chunk could be a string or an array, so make sure it is an array 21 | if (!(Array.isArray(chunks))) { 22 | chunks = [chunks]; 23 | } 24 | 25 | return chunks 26 | .filter(chunk => ext.test(path.extname(chunk))) // filter by extension 27 | .map(chunk => `${publicPath}${chunk}`); // add public path to it 28 | }; 29 | 30 | const script = getChunks('app', /js/); 31 | const style = getChunks('app', /css/); 32 | 33 | // Find compiled images in modules 34 | // it will be used to map original filename to the compiled one 35 | // for server side rendering 36 | const imagesRegex = /\.(jpe?g|png|gif|svg)$/; 37 | const images = json.modules 38 | .filter(module => imagesRegex.test(module.name)) 39 | .map(image => { 40 | return { 41 | original: image.name, 42 | compiled: `${publicPath}${image.assets[0]}` 43 | }; 44 | }); 45 | 46 | const content = {script, style, images}; 47 | 48 | fs.writeFileSync(filepath, JSON.stringify(content)); 49 | debug('dev')('`webpack-stats.json` updated'); 50 | } 51 | --------------------------------------------------------------------------------