├── template
├── client
│ ├── static
│ │ ├── main.css
│ │ └── images
│ │ │ └── logo.png
│ ├── README.md
│ ├── components
│ │ └── HelloWorld
│ │ │ ├── HelloWorld.vue
│ │ │ └── HelloWorld.spec.js
│ ├── store
│ │ ├── modules
│ │ │ ├── auth
│ │ │ │ ├── mutations.js
│ │ │ │ ├── index.js
│ │ │ │ ├── mutations.spec.js
│ │ │ │ └── actions.js
│ │ │ └── async
│ │ │ │ ├── index.js
│ │ │ │ └── index.spec.js
│ │ └── index.js
│ ├── index.html
│ ├── view
│ │ ├── mixins
│ │ │ └── notification.js
│ │ ├── Dashboard.vue
│ │ ├── containers
│ │ │ └── HeaderContainer.vue
│ │ ├── Profile.vue
│ │ └── Login.vue
│ ├── style
│ │ ├── app.scss
│ │ ├── _spinner.scss
│ │ └── global.scss
│ ├── App.vue
│ ├── services
│ │ ├── loopback.spec.js
│ │ └── loopback.js
│ ├── main.js
│ ├── router.js
│ └── App.spec.js
├── .eslintignore
├── jest.plugins.js
├── gulp-tasks
│ ├── default.js
│ ├── tasks.js
│ ├── clear-cache.js
│ ├── config.js
│ ├── compilers.js
│ ├── copy.js
│ ├── serve.js
│ └── build.js
├── server
│ ├── config.production.json
│ ├── component-config.json
│ ├── initial-data
│ │ └── maintenance-account.json
│ ├── boot
│ │ ├── authentication.js
│ │ ├── add-initial-data.js
│ │ ├── root.js
│ │ ├── create-admin.js
│ │ └── __tests__
│ │ │ ├── create-admin.spec.js
│ │ │ └── boot.spec.js
│ ├── middleware.development.json
│ ├── datasources.production.json
│ ├── config.json
│ ├── datasources.json
│ ├── models
│ │ ├── account.js
│ │ ├── account.json
│ │ └── account.spec.js
│ ├── middleware.json
│ ├── server.js
│ ├── model-config.json
│ └── server.spec.js
├── gulpfile.babel.js
├── .gitignore
├── common
│ └── models
│ │ ├── message.js
│ │ └── message.json
├── .editorconfig
├── index.spec.js
├── test
│ └── utils
│ │ ├── create-loopback.js
│ │ └── create-loopback.spec.js
├── .babelrc
├── index.js
├── .eslintrc
├── README.md
├── jest.config.js
└── package.json
├── .eslintignore
├── .gitignore
├── .travis.yml
├── .editorconfig
├── .eslintrc
├── .release-it.json
├── package.json
├── LICENSE
├── meta.js
├── README.md
├── test.js
└── CHANGELOG.md
/template/client/static/main.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/template/.eslintignore:
--------------------------------------------------------------------------------
1 | /build/
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | test-project
2 | template
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | test-project
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/template/jest.plugins.js:
--------------------------------------------------------------------------------
1 | require('jest-plugins')([
2 | 'loopback-jest',
3 | ]);
4 |
--------------------------------------------------------------------------------
/template/gulp-tasks/default.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 |
3 | gulp.task('default', ['serve']);
4 |
--------------------------------------------------------------------------------
/template/client/README.md:
--------------------------------------------------------------------------------
1 | ## Client
2 |
3 | This is the place for your application front-end files.
4 |
--------------------------------------------------------------------------------
/template/server/config.production.json:
--------------------------------------------------------------------------------
1 | {
2 | "restApiHost": "123.123.123.123",
3 | "port": 80
4 | }
5 |
--------------------------------------------------------------------------------
/template/gulpfile.babel.js:
--------------------------------------------------------------------------------
1 | // Search for gulp tasks in gulp-tasks directory
2 | import './gulp-tasks/tasks';
3 |
--------------------------------------------------------------------------------
/template/client/static/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InCuca/vue-loopback/HEAD/template/client/static/images/logo.png
--------------------------------------------------------------------------------
/template/server/component-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "loopback-component-explorer": {
3 | "mountPath": "/explorer"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/template/server/initial-data/maintenance-account.json:
--------------------------------------------------------------------------------
1 | {
2 | "email": "admin@mydomain.com",
3 | "password": "h4ckme"
4 | }
5 |
--------------------------------------------------------------------------------
/template/server/boot/authentication.js:
--------------------------------------------------------------------------------
1 | export default function enableAuthentication(server) {
2 | // enable authentication
3 | server.enableAuth();
4 | }
5 |
--------------------------------------------------------------------------------
/template/gulp-tasks/tasks.js:
--------------------------------------------------------------------------------
1 | // This file just import the entire contents of this directory
2 | import './serve';
3 | import './default';
4 | import './build';
5 | import './copy';
6 |
--------------------------------------------------------------------------------
/template/server/boot/add-initial-data.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 |
3 | /**
4 | * Create the initial data of the system
5 | */
6 | export default function addInitialData(server) {
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/template/server/middleware.development.json:
--------------------------------------------------------------------------------
1 | {
2 | "final:after": {
3 | "strong-error-handler": {
4 | "params": {
5 | "debug": true,
6 | "log": true
7 | }
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/template/client/components/HelloWorld/HelloWorld.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hello World! This content is restricted.
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/template/server/boot/root.js:
--------------------------------------------------------------------------------
1 | export default function(server) {
2 | // Install a `/api` route that returns server status
3 | const router = server.loopback.Router();
4 | router.get('/api', server.loopback.status());
5 | server.use(router);
6 | }
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | sudo: required
3 | node_js:
4 | - "8"
5 | - "node"
6 | addons:
7 | chrome: stable
8 | before_script:
9 | - "export DISPLAY=:99.0"
10 | - "sh -e /etc/init.d/xvfb start"
11 | - sleep 3 # give xvfb some time to start
12 |
--------------------------------------------------------------------------------
/template/client/store/modules/auth/mutations.js:
--------------------------------------------------------------------------------
1 | export function setAccessToken(state, token) {
2 | // eslint-disable-next-line camelcase
3 | state.access_token = token;
4 | }
5 |
6 | export function setAccount(state, account) {
7 | state.account = account;
8 | }
9 |
--------------------------------------------------------------------------------
/template/.gitignore:
--------------------------------------------------------------------------------
1 | *.csv
2 | *.dat
3 | *.iml
4 | *.log
5 | *.out
6 | *.pid
7 | *.seed
8 | *.sublime-*
9 | *.swo
10 | *.swp
11 | *.tgz
12 | *.xml
13 | .DS_Store
14 | .idea
15 | .project
16 | .strong-pm
17 | coverage
18 | node_modules
19 | npm-debug.log
20 | build
21 |
--------------------------------------------------------------------------------
/template/gulp-tasks/clear-cache.js:
--------------------------------------------------------------------------------
1 | export default function(path) {
2 | const paths = Object.keys(require.cache);
3 | paths.forEach((p) => {
4 | if (p.includes(path)) {
5 | // console.log('clearing', p);
6 | delete require.cache[p];
7 | }
8 | });
9 | }
10 |
--------------------------------------------------------------------------------
/template/server/datasources.production.json:
--------------------------------------------------------------------------------
1 | {
2 | "db": {
3 | "name": "db",
4 | "host": "localhost",
5 | "port": 27017,
6 | "url": "",
7 | "database": "databaseName",
8 | "password": "",
9 | "user": "",
10 | "connector": "mongodb"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/template/common/models/message.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | export default function(Message) {
3 | Message.greet = (msg, cb) => {
4 | process.nextTick(() => {
5 | msg = msg || 'hello';
6 | cb(null, `Sender says ${msg} to receiver`);
7 | });
8 | };
9 | }
10 |
--------------------------------------------------------------------------------
/template/client/store/modules/auth/index.js:
--------------------------------------------------------------------------------
1 | import * as actions from './actions';
2 | import * as mutations from './mutations';
3 |
4 | export default {
5 | namespaced: true,
6 | state: {
7 | // eslint-disable-next-line camelcase
8 | access_token: null,
9 | account: null,
10 | },
11 | actions,
12 | mutations,
13 | };
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # http://editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | indent_style = space
9 | indent_size = 2
10 | end_of_line = lf
11 | charset = utf-8
12 | trim_trailing_whitespace = true
13 | insert_final_newline = true
14 |
--------------------------------------------------------------------------------
/template/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # http://editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | indent_style = space
9 | indent_size = 2
10 | end_of_line = lf
11 | charset = utf-8
12 | trim_trailing_whitespace = true
13 | insert_final_newline = true
14 |
--------------------------------------------------------------------------------
/template/client/components/HelloWorld/HelloWorld.spec.js:
--------------------------------------------------------------------------------
1 | import {mount} from 'vue-test-utils';
2 | import HelloWorld from './HelloWorld.vue';
3 |
4 | describe('HelloWorld.vue', () => {
5 | const wrapper = mount(HelloWorld);
6 |
7 | it('should render correct content', () => {
8 | expect(wrapper.html()).toContain(
9 | 'Hello World! This content is restricted.'
10 | );
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/template/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{name}}
6 | {{#extended}}
7 |
8 | {{/extended}}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/template/client/view/mixins/notification.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | export default {
3 | methods: {
4 | notifySuccess(msg) {
5 | // TODO: show success message
6 | },
7 | notifyError(error) {
8 | // TODO: show error message
9 | throw error;
10 | },
11 | notifyWhenSuccess(msg) {
12 | return () => {
13 | // TODO: show success message
14 | };
15 | },
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/template/index.spec.js:
--------------------------------------------------------------------------------
1 | import request from 'supertest';
2 | import server from './index';
3 |
4 | describe('Project Index', () => {
5 | beforeEach((done) => {
6 | server.once('started', done);
7 | server.start();
8 | });
9 |
10 | afterEach(() => server.close());
11 |
12 | it('should serve client files', async() => {
13 | const res = await request(server).get('/index.html');
14 | expect(res.statusCode).toBe(200);
15 | }, 10000);
16 | });
17 |
--------------------------------------------------------------------------------
/template/client/store/modules/auth/mutations.spec.js:
--------------------------------------------------------------------------------
1 | import * as mutations from './mutations';
2 |
3 | describe('auth mutations', () => {
4 | it('set access_token', () => {
5 | const state = {};
6 | mutations.setAccessToken(state, 'foo');
7 | expect(state.access_token).toEqual('foo');
8 | });
9 |
10 | it('set account', () => {
11 | const state = {};
12 | mutations.setAccount(state, 'foo');
13 | expect(state.account).toEqual('foo');
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parserOptions": {
4 | "parser": "babel-eslint"
5 | },
6 | "env": {
7 | "browser": true
8 | },
9 | "extends": [
10 | "plugin:vue/recommended",
11 | "airbnb-base",
12 | "loopback"
13 | ],
14 | "plugins": [
15 | "vue"
16 | ],
17 | "globals": {
18 | "expect": true,
19 | "assert": true,
20 | "require": true,
21 | "request": true
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/template/client/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuex from 'vuex';
3 |
4 | import async from './modules/async';
5 | import auth from './modules/auth';
6 |
7 | Vue.use(Vuex);
8 |
9 | export default new Vuex.Store({
10 | state: {
11 | breadcrumb: [{
12 | text: 'Home',
13 | to: '/dashboard',
14 | }, {
15 | text: 'Dashboard',
16 | active: true,
17 | }],
18 | },
19 | modules: {
20 | async, // async namespaced
21 | auth, // auth namespaced
22 | },
23 | });
24 |
--------------------------------------------------------------------------------
/template/server/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "restApiRoot": "/api",
3 | "restApiHost": "localhost",
4 | "host": "0.0.0.0",
5 | "port": 8000,
6 | "remoting": {
7 | "context": false,
8 | "rest": {
9 | "handleErrors": false,
10 | "normalizeHttpPath": false,
11 | "xml": false
12 | },
13 | "json": {
14 | "strict": false,
15 | "limit": "100kb"
16 | },
17 | "urlencoded": {
18 | "extended": true,
19 | "limit": "100kb"
20 | },
21 | "cors": false
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/template/common/models/message.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Message",
3 | "base": "Model",
4 | "properties": {},
5 | "methods": {
6 | "greet": {
7 | "isStatic": true,
8 | "accepts": [{
9 | "arg": "msg",
10 | "type": "string",
11 | "http": {
12 | "source": "query"
13 | }
14 | }],
15 | "returns": {
16 | "arg": "greeting",
17 | "type": "string"
18 | },
19 | "http": {
20 | "verb": "get"
21 | }
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/template/test/utils/create-loopback.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import loopback from 'loopback';
3 | import boot from 'loopback-boot';
4 |
5 | export default function(options) {
6 | const defaultOptions = {
7 | appRootDir: path.resolve(__dirname, '../../server'),
8 | scriptExtensions: ['.js', '.json', '.node', '.ejs'],
9 | };
10 |
11 | return new Promise((resolve) => {
12 | const server = loopback();
13 | boot(
14 | server,
15 | Object.assign(defaultOptions, options),
16 | () => resolve(server)
17 | );
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/template/client/view/Dashboard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Welcome!
6 |
7 |
8 |
9 |
10 |
11 |
23 |
24 |
27 |
--------------------------------------------------------------------------------
/template/client/style/app.scss:
--------------------------------------------------------------------------------
1 | @import "global";
2 | @import "bootstrap/scss/bootstrap";
3 |
4 | // Customize Bootstrap Components Style
5 | // (copy from node_modules/bootstrap/scss folder
6 | // @import "_breadcrumb";
7 | // @import "_buttons";
8 | // @import "_dropdown";
9 | // @import "_forms";
10 | // @import "_fullcalendar";
11 | // @import "_modal";
12 | @import "_spinner";
13 | // @import "_tables";
14 |
15 | $fa-font-path: "../static/fonts";
16 | @import "font-awesome/scss/font-awesome";
17 |
18 | #app {
19 | width: 100vw;
20 | height: 100vh;
21 | background-color: darken($light, 40%);
22 | }
23 |
--------------------------------------------------------------------------------
/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "pkgFiles": [
3 | "package.json"
4 | ],
5 | "src": {
6 | "tagName": "v%s",
7 | "tagAnnotation": "Release v%s",
8 | "commitMessage": "Release v%s"
9 | },
10 | "github": {
11 | "release": true,
12 | "releaseName": "Release v%s"
13 | },
14 | "buildCommand": "npx conventional-changelog -p angular -i CHANGELOG.md -s",
15 | "increment": "conventional:angular",
16 | "beforeChangelogCommand": "npx conventional-changelog -p angular -i CHANGELOG.md -s",
17 | "changelogCommand": "npx conventional-changelog -p angular -u | tail -n +3",
18 | "safeBump": false
19 | }
20 |
--------------------------------------------------------------------------------
/template/server/datasources.json:
--------------------------------------------------------------------------------
1 | {
2 | "db": {
3 | "name": "db",
4 | "connector": "memory"
5 | }{{#extended}},
6 | "transient": {
7 | "name": "transient",
8 | "connector": "transient"
9 | },
10 | "email": {
11 | "name": "email",
12 | "connector": "mail",
13 | "transports": [
14 | {
15 | "type": "smtp",
16 | "host": "smtp.mailtrap.io",
17 | "secure": false,
18 | "port": 2525,
19 | "auth": {
20 | "user": "bc6242e46c4abb",
21 | "pass": "6dcc9b55c677be"
22 | }
23 | }
24 | ]
25 | }
26 | {{/extended}}
27 | }
28 |
--------------------------------------------------------------------------------
/template/client/App.vue:
--------------------------------------------------------------------------------
1 |
2 | {{#extended}}
3 |
4 | {{else}}
5 | \{{ hello }}
6 | {{/extended}}
7 |
8 |
9 |
24 |
25 |
28 |
--------------------------------------------------------------------------------
/template/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "targets": {
5 | "browsers": [
6 | "> 1%",
7 | "last 2 versions",
8 | "not ie <= 8"
9 | ],
10 | "node": "8"
11 | }
12 | }],
13 | "stage-2"
14 | ],
15 | "plugins": [
16 | [
17 | "babel-plugin-root-import",
18 | [
19 | {
20 | "rootPathPrefix": "~",
21 | "rootPathSuffix": "."
22 | },
23 | {
24 | "rootPathPrefix": "@",
25 | "rootPathSuffix": "client"
26 | }
27 | ]
28 | ],
29 | [
30 | "transform-runtime",
31 | {
32 | "polyfill": false,
33 | "regenerator": true
34 | }
35 | ]
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/template/client/style/_spinner.scss:
--------------------------------------------------------------------------------
1 | // Loading Spinner
2 | .fa.fa-spinner {
3 | -webkit-animation-name: rotate;
4 | animation-name: rotate;
5 | -webkit-animation-duration: 2s;
6 | animation-duration: 2s;
7 | -webkit-animation-iteration-count: infinite;
8 | animation-iteration-count: infinite;
9 | -webkit-animation-timing-function: ease-in-out;
10 | animation-timing-function: ease-in-out;
11 | }
12 |
13 | @-webkit-keyframes rotate {
14 | from {-webkit-transform: rotate(0deg);transform: rotate(0deg);}
15 | to {-webkit-transform: rotate(360deg);transform: rotate(360deg);}
16 | }
17 |
18 | @keyframes rotate {
19 | from {-webkit-transform: rotate(0deg);transform: rotate(0deg);}
20 | to {-webkit-transform: rotate(360deg);transform: rotate(360deg);}
21 | }
22 |
--------------------------------------------------------------------------------
/template/server/models/account.js:
--------------------------------------------------------------------------------
1 | import {host} from '../config.json';
2 |
3 | export default function(Account) {
4 | Account.on('resetPasswordRequest', (info) => {
5 | const url = `https://${host}/#!/profile`;
6 | const html = `Hello!
Click here to create a new password.` +
8 | '
If you have not requested a password change,' +
9 | 'please ignore this email.
Webmaster';
10 | Account.app.models.Email.send({
11 | to: info.email,
12 | from: '{{name}} ',
13 | subject: '[{{name}}] Create a new password',
14 | html,
15 | }, (err) => {
16 | if (err) return console.log(err);
17 | console.log('> sending password reset email to:', info.email);
18 | return null;
19 | });
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/template/client/services/loopback.spec.js:
--------------------------------------------------------------------------------
1 | import {DateString} from 'loopback';
2 | import lb, {DateString as DS} from './loopback';
3 |
4 | describe('loopback', () => {
5 | it('exports DateString with parse', () => {
6 | expect(DS).toEqual(DateString);
7 | expect(DS.parse).toBeDefined();
8 | });
9 |
10 | it('parses a DateString object', () => {
11 | const date = {when: '2017-01-01'};
12 | const orignParsed = new DateString(date.when);
13 | expect(DS.parse(date)).toEqual(orignParsed);
14 | expect(DS.parse(date.when)).toEqual(orignParsed);
15 | });
16 |
17 | it('export extended axios', () => {
18 | expect(lb).toEqual(expect.any(Function));
19 | expect(lb.find).toEqual(expect.any(Function));
20 | expect(lb.removeToken).toEqual(expect.any(Function));
21 | expect(lb.setToken).toEqual(expect.any(Function));
22 | expect(lb.setLoadingFunction).toEqual(expect.any(Function));
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/template/index.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import fallback from 'express-history-api-fallback';
3 | import path from 'path';
4 | import app from './server/server';
5 |
6 | const client = path.resolve(__dirname, 'client');
7 |
8 | const unless = function(paths, middleware) {
9 | return function(req, res, next) {
10 | if (paths.some(p => req.path.indexOf(p) > -1)) {
11 | return next();
12 | }
13 |
14 | return middleware(req, res, next);
15 | };
16 | };
17 |
18 | // add static route for client
19 | app.use(express.static(client));
20 |
21 | // enable redirect urls to index
22 | app.use(unless(
23 | ['/api', '/explorer'],
24 | fallback('index.html', {root: client}),
25 | ));
26 |
27 | // start app
28 | app.on('started', () => {
29 | const baseUrl = app.get('url').replace(/\/$/, '');
30 | console.log('Browse your CLIENT files at %s', baseUrl);
31 | });
32 |
33 | if (require.main === module) { app.start(); }
34 |
35 | export default app;
36 |
--------------------------------------------------------------------------------
/template/client/main.js:
--------------------------------------------------------------------------------
1 | {{#extended}}
2 | /* Required by BootstrapVue */
3 | import 'babel-polyfill';
4 |
5 | {{/extended}}
6 | /* Global Components */
7 | import Vue from 'vue';
8 | {{#extended}}
9 | import {sync} from 'vuex-router-sync';
10 | import 'bootstrap-vue/dist/bootstrap-vue.css';
11 | import BootstrapVue from 'bootstrap-vue';
12 | // import 'vue-awesome/icons';
13 | import Icon from 'vue-awesome';
14 | {{/extended}}
15 |
16 | /* Local Components and modules */
17 | import App from './App.vue';
18 | {{#extended}}
19 | import router from './router';
20 | import store from './store';
21 |
22 | Vue.use(BootstrapVue);
23 | Vue.component('icon', Icon);
24 |
25 | // Add router state to store
26 | sync(store, router);
27 | {{/extended}}
28 | {{#unless extended}}
29 | import './static/main.css';
30 | {{/unless}}
31 |
32 | // Instance Application
33 | export default new Vue({
34 | el: '#app',
35 | render: r => r(App),
36 | {{#extended}}
37 | router,
38 | store,
39 | {{/extended}}
40 | });
41 |
--------------------------------------------------------------------------------
/template/client/store/modules/async/index.js:
--------------------------------------------------------------------------------
1 | import getUID from 'uid';
2 | import lb from '@/services/loopback';
3 |
4 | export default {
5 | namespaced: true,
6 | state: {
7 | ajaxCommands: [],
8 | },
9 | getters: {
10 | loadingAjax(state) {
11 | if (state.ajaxCommands.length > 0) return true;
12 | return false;
13 | },
14 | },
15 | actions: {
16 | syncLoopback({commit}) {
17 | lb.setLoadingFunction(
18 | (isLoading, uid = getUID()) => {
19 | if (isLoading) {
20 | commit('addAjaxCommand', uid);
21 | } else {
22 | commit('removeAjaxCommand', uid);
23 | }
24 | return uid;
25 | }
26 | );
27 | },
28 | },
29 | mutations: {
30 | addAjaxCommand(state, uid) {
31 | state.ajaxCommands.push(uid);
32 | },
33 | removeAjaxCommand(state, uid) {
34 | state.ajaxCommands.splice(
35 | state.ajaxCommands.indexOf(uid),
36 | 1
37 | );
38 | },
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/template/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parserOptions": {
4 | "parser": "babel-eslint"
5 | },
6 | "env": {
7 | "browser": true,
8 | "jest/globals": true
9 | },
10 | "extends": [
11 | "plugin:vue/recommended",
12 | "airbnb-base",
13 | "loopback"
14 | ],
15 | "plugins": [
16 | "vue",
17 | "jest"
18 | ],
19 | "rules": {
20 | "import/no-unresolved": "off",
21 | "import/no-extraneous-dependencies": [
22 | "error",
23 | {
24 | "devDependencies": [
25 | "**/*.spec.js",
26 | "__tests__/*.js",
27 | "__mocks__/*.js",
28 | "__snapshots__/*.js",
29 | "test/**/*.js",
30 | "gulp-tasks/**/*.js",
31 | "jest.config.js",
32 | "jest.plugins.js"
33 | ]
34 | }
35 | ],
36 | "import/extensions": "off",
37 | "import/prefer-default-export": "off",
38 | "no-param-reassign": "off",
39 | "one-var-declaration-per-line": "off"
40 | },
41 | "globals": {
42 | "require": true
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/template/gulp-tasks/config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/prefer-default-export */
2 | import path from 'path';
3 | import pkg from '../package.json';
4 |
5 | // true if this template was generated with extended option
6 | export const extended = pkg.extended || false;
7 |
8 | export const dirs = {};
9 | dirs.root = path.resolve(__dirname, '../');
10 | dirs.modules = path.resolve(dirs.root, 'node_modules');
11 | dirs.build = path.resolve(dirs.root, 'build');
12 | dirs.buildTest = path.resolve(dirs.root, dirs.build, 'test');
13 | dirs.buildClient = path.resolve(dirs.root, dirs.build, 'client');
14 | dirs.buildServer = path.resolve(dirs.root, dirs.build, 'server');
15 | dirs.buildCommon = path.resolve(dirs.root, dirs.build, 'common');
16 | dirs.test = path.resolve(dirs.root, 'test');
17 | dirs.testClient = path.resolve(dirs.test, 'client');
18 | dirs.testServer = path.resolve(dirs.test, 'server');
19 | dirs.srcClient = path.resolve(dirs.root, 'client');
20 | dirs.srcCommon = path.resolve(dirs.root, 'common');
21 | dirs.srcServer = path.resolve(dirs.root, 'server');
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-loopback",
3 | "version": "5.2.1",
4 | "description": "A Vue project template with Loopback framework featuring ES6, Gulp, and Mocha for unit tests",
5 | "main": "meta.js",
6 | "scripts": {
7 | "test": "node test.js",
8 | "lint": "npx eslint --ext vue,js ."
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/InCuca/vue-loopback.git"
13 | },
14 | "keywords": [
15 | "vuejs"
16 | ],
17 | "author": "Walker Leite",
18 | "license": "MIT",
19 | "bugs": {
20 | "url": "https://github.com/InCuca/vue-loopback/issues"
21 | },
22 | "homepage": "https://github.com/InCuca/vue-loopback#readme",
23 | "devDependencies": {
24 | "babel-eslint": "^8.2.2",
25 | "conventional-changelog-cli": "^1.3.22",
26 | "eslint": "^4.15.0",
27 | "eslint-config-airbnb-base": "^11.3.0",
28 | "eslint-config-loopback": "^10.0.0",
29 | "eslint-plugin-import": "^2.11.0",
30 | "eslint-plugin-vue": "^4.0.0",
31 | "release-it": "^7.4.3",
32 | "vue-cli": "^2.9.1"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/template/server/middleware.json:
--------------------------------------------------------------------------------
1 | {
2 | "initial:before": {
3 | "loopback#favicon": {}
4 | },
5 | "initial": {
6 | "compression": {},
7 | "cors": {
8 | "params": {
9 | "origin": true,
10 | "credentials": true,
11 | "maxAge": 86400
12 | }
13 | },
14 | "helmet#xssFilter": {},
15 | "helmet#frameguard": {
16 | "params": [
17 | "deny"
18 | ]
19 | },
20 | "helmet#hsts": {
21 | "params": {
22 | "maxAge": 0,
23 | "includeSubdomains": true
24 | }
25 | },
26 | "helmet#hidePoweredBy": {},
27 | "helmet#ieNoOpen": {},
28 | "helmet#noSniff": {},
29 | "helmet#noCache": {
30 | "enabled": false
31 | }
32 | },
33 | "session": {},
34 | "auth": {},
35 | "parse": {},
36 | "routes": {
37 | "loopback#rest": {
38 | "paths": [
39 | "${restApiRoot}"
40 | ]
41 | }
42 | },
43 | "files": {},
44 | "final": {
45 | "loopback#urlNotFound": {}
46 | },
47 | "final:after": {
48 | "strong-error-handler": {}
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/template/gulp-tasks/compilers.js:
--------------------------------------------------------------------------------
1 | /* eslint no-useless-escape: 0, import/prefer-default-export: 0 */
2 | import path from 'path';
3 | import sass from 'vueify/lib/compilers/sass';
4 | import {dirs} from './config';
5 |
6 | function replaceCuringas(content) {
7 | // FIXME: Not working
8 | const replacer = `@import "${dirs.srcClient}/$2";`;
9 |
10 | let replaced = content.replace(/@import.+(\@)(.*?)[\"\']/, replacer);
11 | replaced = content.replace(/@import.+(\~)(.*?)[\"\']/, replacer);
12 | return replaced;
13 | }
14 |
15 | export function customSass(content, callback, compiler, filePath) {
16 | let myContent = content;
17 | const relativePath = path.relative(
18 | path.dirname(filePath),
19 | path.resolve(dirs.srcClient, 'style/global.scss')
20 | ).replace(/\\/g, '/'); // Replace backslashes with forward slashes (windows)
21 |
22 | // Global SCSS
23 | //
24 | myContent = `@import "${relativePath}";${myContent}`;
25 |
26 | myContent = replaceCuringas(myContent);
27 |
28 | sass(
29 | myContent,
30 | callback,
31 | compiler,
32 | filePath
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/template/client/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import VueRouter from 'vue-router';
3 | import store from '@/store';
4 | import Login from './view/Login.vue';
5 | import Dashboard from './view/Dashboard.vue';
6 | import Profile from './view/Profile.vue';
7 |
8 | Vue.use(VueRouter);
9 |
10 | const router = new VueRouter({
11 | mode: 'history',
12 | routes: [
13 | {
14 | path: '/',
15 | name: 'home',
16 | redirect: {name: 'dashboard'},
17 | }, {
18 | path: '/dashboard',
19 | name: 'dashboard',
20 | component: Dashboard,
21 | meta: {requiresAuth: true},
22 | }, {
23 | path: '/login',
24 | name: 'login',
25 | component: Login,
26 | props: true,
27 | }, {
28 | path: '/profile',
29 | name: 'profile',
30 | component: Profile,
31 | // eslint-disable-next-line camelcase
32 | props: route => ({access_token: route.query.access_token}),
33 | meta: {requiresAuth: true},
34 | },
35 | ],
36 | });
37 |
38 | // Sync routes with auth module
39 | store.dispatch('auth/syncRouter', router);
40 |
41 | export default router;
42 |
--------------------------------------------------------------------------------
/template/client/store/modules/async/index.spec.js:
--------------------------------------------------------------------------------
1 | import {createLocalVue} from 'vue-test-utils';
2 | import Vuex from 'vuex';
3 | import async from './index';
4 |
5 | const Vue = createLocalVue();
6 | Vue.use(Vuex);
7 |
8 | describe('async module', () => {
9 | const initialState = JSON.stringify(async.state);
10 | const store = new Vuex.Store(async);
11 |
12 | beforeEach(() => {
13 | store.replaceState(JSON.parse(initialState));
14 | });
15 |
16 | it('has ajaxCommands state', () => {
17 | expect(store.state.ajaxCommands).toEqual([]);
18 | });
19 |
20 | it('adds command to ajaxCommands', () => {
21 | const cmd = 'foo';
22 | store.commit('addAjaxCommand', cmd);
23 | expect(store.state.ajaxCommands).toContain(cmd);
24 | expect(store.getters.loadingAjax).toBeTruthy();
25 | });
26 |
27 | it('remove command from ajaxCommands', () => {
28 | const cmd = 'foo';
29 | store.commit('addAjaxCommand', cmd);
30 | store.commit('removeAjaxCommand', cmd);
31 | expect(store.state.ajaxCommands).not.toContain(cmd);
32 | expect(store.getters.loadingAjax).not.toBeTruthy();
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 InCuca Tecnologia
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/template/README.md:
--------------------------------------------------------------------------------
1 | # {{ name }}
2 |
3 | {{ description }}
4 |
5 | ## Folder structure
6 |
7 | 1. `client`: Vue client files
8 | 2. `common`: Common client and server model files
9 | 3. `server`: Loopback server files
10 | 4. `test`: Testing helpers (do not place unit tests here, but as .spec.js files in client/common/server directories)
11 |
12 | ## Installation
13 |
14 | ```
15 | $ npm install
16 | ```
17 |
18 | ## Linting
19 |
20 | ```
21 | $ npm run lint
22 | ```
23 |
24 | ## Testing
25 |
26 | ```
27 | $ npm test
28 | ```
29 |
30 | ## Running the development server (API and Client)
31 |
32 | ```
33 | $ npm run dev
34 | ```
35 |
36 | ## Debug
37 |
38 | ```
39 | $ DEBUG=loopback npm run dev
40 | ```
41 |
42 | [More info...](https://loopback.io/doc/en/lb3/Setting-debug-strings.html)
43 |
44 | ## Build to ./build
45 |
46 | ```
47 | $ npm run build
48 | ```
49 |
50 | ## Executing built files
51 |
52 | Please remember to update `server/*.production.json` files to match your enviroment.
53 |
54 | ```bash
55 | $ cd build
56 | $ npm run start
57 | ```
58 |
59 | You can run only the server with:
60 |
61 | ```bash
62 | $ cd build/server
63 | $ node .
64 | ```
65 |
--------------------------------------------------------------------------------
/template/server/models/account.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Account",
3 | "plural": "Accounts",
4 | "base": "User",
5 | "idInjection": true,
6 | "options": {
7 | "validateUpsert": true
8 | },
9 | "mixins": {
10 | "TimeStamp": {
11 | "required" : false,
12 | "validateUpsert": true,
13 | "silenceWarnings": false
14 | }
15 | },
16 | "properties": {},
17 | "validations": [],
18 | "relations": {
19 | "roleMapping": {
20 | "type": "hasMany",
21 | "model": "RoleMapping",
22 | "foreignKey": "principalId"
23 | },
24 | "role": {
25 | "type": "hasMany",
26 | "model": "Role",
27 | "through": "RoleMapping",
28 | "foreignKey": "principalId"
29 | }
30 | },
31 | "acls": [{
32 | "accessType": "*",
33 | "principalType": "ROLE",
34 | "principalId": "admin",
35 | "permission": "ALLOW"
36 | },
37 | {
38 | "accessType": "EXECUTE",
39 | "principalType": "ROLE",
40 | "principalId": "$owner",
41 | "permission": "ALLOW",
42 | "property": "update"
43 | }
44 | ],
45 | "methods": {}
46 | }
47 |
--------------------------------------------------------------------------------
/template/server/server.js:
--------------------------------------------------------------------------------
1 | import loopback from 'loopback';
2 | import boot from 'loopback-boot';
3 |
4 | const app = loopback();
5 |
6 | const options = {
7 | appRootDir: __dirname,
8 | // File Extensions for jest (strongloop/loopback#3204)
9 | scriptExtensions: ['.js', '.json', '.node', '.ejs'],
10 | };
11 |
12 | let httpServer;
13 |
14 | app.start = function() {
15 | // start the web server
16 | httpServer = app.listen(() => {
17 | app.emit('started');
18 | const baseUrl = app.get('url').replace(/\/$/, '');
19 | console.log('API server listening at: %s', `${baseUrl}/api`);
20 | if (app.get('loopback-component-explorer')) {
21 | const explorerPath = app.get('loopback-component-explorer').mountPath;
22 | console.log('Browse your REST API at %s%s', baseUrl, explorerPath);
23 | }
24 | });
25 | return httpServer;
26 | };
27 |
28 | app.close = function() {
29 | httpServer.close();
30 | };
31 |
32 | // Bootstrap the application, configure models, datasources and middleware.
33 | // Sub-apps like REST API are mounted via boot scripts.
34 | boot(app, options, (err) => {
35 | if (err) throw err;
36 |
37 | // start the server if `$ node server.js`
38 | if (require.main === module) { app.start(); }
39 | });
40 |
41 | // export app
42 | export default app;
43 |
--------------------------------------------------------------------------------
/template/client/view/containers/HeaderContainer.vue:
--------------------------------------------------------------------------------
1 |
2 |
30 |
31 |
32 |
47 |
48 |
54 |
--------------------------------------------------------------------------------
/template/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | collectCoverage: true,
3 | collectCoverageFrom: [
4 | 'client/**/*.{js,vue}',
5 | 'server/**/*.js',
6 | 'common/**/*.js',
7 | 'test/**/*.js',
8 | '!**/*.spec.js',
9 | '!**/node_modules/**',
10 | ],
11 | projects: [
12 | {
13 | displayName: 'test helpers',
14 | testMatch: ['/test/**/*.spec.js'],
15 | },
16 | {
17 | displayName: 'server',
18 | testMatch: [
19 | '/server/**/*.spec.js',
20 | '/index.spec.js',
21 | ],
22 | preset: 'jest-preset-loopback',
23 | moduleFileExtensions: [
24 | 'js',
25 | 'json',
26 | ],
27 | transform: {
28 | '^.+\\.js$': 'babel-jest',
29 | },
30 | setupTestFrameworkScriptFile: './jest.plugins.js',
31 | },
32 | {
33 | displayName: 'client',
34 | testMatch: ['/client/**/*.spec.js'],
35 | moduleFileExtensions: [
36 | 'js',
37 | 'json',
38 | 'vue',
39 | ],
40 | moduleNameMapper: {
41 | '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
42 | },
43 | transform: {
44 | '^.+\\.js$': 'babel-jest',
45 | '^.+\\.vue$': 'vue-jest',
46 | },
47 | },
48 | {
49 | displayName: 'common',
50 | testMatch: ['/common/**/*.spec.js'],
51 | },
52 | ],
53 | };
54 |
--------------------------------------------------------------------------------
/template/client/App.spec.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | {{#extended}}
3 | import Vuex from 'vuex';
4 | {{/extended}}
5 | import App from '@/App.vue';
6 |
7 | describe('App.vue', () => {
8 | let Constructor, vm;
9 | {{#extended}}
10 | const routerView = {
11 | render: r => r('div', 'mocked component'),
12 | };
13 | {{/extended}}
14 |
15 | beforeEach((done) => {
16 | {{#extended}}
17 | Vue.use(Vuex);
18 | {{/extended}}
19 | Constructor = Vue.extend(App);
20 | vm = new Constructor({
21 | mounted: () => done(),
22 | {{#extended}}
23 | components: {routerView},
24 | store: new Vuex.Store({
25 | modules: {
26 | async: {
27 | namespaced: true,
28 | actions: {
29 | syncLoopback() {},
30 | },
31 | },
32 | },
33 | }),
34 | {{/extended}}
35 | });
36 | vm.$mount();
37 | });
38 |
39 | afterEach(() => vm.$destroy());
40 |
41 | {{#extended}}
42 | it('should render router component', () => {
43 | expect(vm.$el.innerHTML).toEqual('mocked component');
44 | expect(vm.$el.getAttribute('id')).toEqual('app');
45 | });
46 | {{else}}
47 | it('should render correct content', () => {
48 | const newVm = new Constructor().$mount();
49 | return Vue.nextTick().then(() => {
50 | expect(newVm.$el.innerHTML).toEqual('Hello World!');
51 | });
52 | });
53 | {{/extended}}
54 | });
55 |
--------------------------------------------------------------------------------
/meta.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | prompts: {
3 | name: {
4 | type: 'string',
5 | required: true,
6 | message: 'Project name',
7 | },
8 | description: {
9 | type: 'string',
10 | required: false,
11 | message: 'Project description',
12 | default: 'A Vue.js project',
13 | },
14 | author: {
15 | type: 'string',
16 | message: 'Author',
17 | },
18 | extended: {
19 | type: 'confirm',
20 | message: `Add basic Login and Admin views with Vuex,
21 | Vue-router and Bootstrap-vue?`,
22 | },
23 | },
24 | filters: {
25 | 'client/router.js': 'extended',
26 | 'client/static/main.css': 'extended === false',
27 | 'client/static/images/**/*': 'extended',
28 | 'client/components/**/*': 'extended',
29 | 'client/services/**/*': 'extended',
30 | 'client/store/**/*': 'extended',
31 | 'client/style/**/*': 'extended',
32 | 'client/view/**/*': 'extended',
33 | 'server/boot/add-initial-data.js': 'extended',
34 | 'server/boot/create-admin.js': 'extended',
35 | 'server/boot/__tests__/create-admin.spec.js': 'extended',
36 | 'server/initial-data/**/*': 'extended',
37 | 'server/models/**/*': 'extended',
38 | 'test/client/components/**/*': 'extended',
39 | 'test/server/account.spec.js': 'extended',
40 | },
41 | complete(data, {logger}) {
42 | logger.log('To get started:');
43 | logger.log('1. Install dependencies: npm install');
44 | logger.log('2. Build with: npm run build');
45 | },
46 | };
47 |
--------------------------------------------------------------------------------
/template/test/utils/create-loopback.spec.js:
--------------------------------------------------------------------------------
1 | import loopback from 'loopback';
2 | import boot from 'loopback-boot';
3 | import createLoopback from './create-loopback';
4 |
5 | jest.mock('loopback');
6 | jest.mock('loopback-boot');
7 |
8 | describe('createLoopback dev util', () => {
9 | it('exports a promise', () => {
10 | expect(createLoopback()).toBeInstanceOf(Promise);
11 | });
12 |
13 | it('calls loopback-boot with default options', () => {
14 | const mockServer = {};
15 | const optsMatcher = {
16 | appRootDir: expect.any(String),
17 | scriptExtensions: expect.any(Array),
18 | };
19 | loopback.mockReturnValue(mockServer);
20 | createLoopback();
21 | expect(boot).toBeCalledWith(
22 | mockServer,
23 | expect.objectContaining(optsMatcher),
24 | expect.any(Function),
25 | );
26 | });
27 |
28 | it('calls loopback-boot with custom options', () => {
29 | const mockServer = {};
30 | const myOpts = {myOpt: true};
31 | loopback.mockReturnValue(mockServer);
32 | createLoopback(myOpts);
33 | expect(boot).toBeCalledWith(
34 | mockServer,
35 | expect.objectContaining(myOpts),
36 | expect.any(Function),
37 | );
38 | });
39 |
40 | it('resolves with loopback server', async() => {
41 | const mockServer = {};
42 | loopback.mockReturnValue(mockServer);
43 | // just call callback fn
44 | boot.mockImplementation((s, o, r) => r());
45 | const server = await createLoopback();
46 | expect(server).toEqual(mockServer);
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/template/gulp-tasks/copy.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable arrow-body-style */
2 | import gulp from 'gulp';
3 | import path from 'path';
4 | import fs from 'fs';
5 | import {argv} from 'yargs';
6 | import {dirs} from './config';
7 | import production from '../server/config.production.json';
8 |
9 | gulp.task('copy:client:fa', () => {
10 | return gulp
11 | .src(path.resolve(dirs.modules, 'font-awesome/fonts/*'))
12 | .pipe(gulp.dest(path.resolve(dirs.buildClient, 'static/fonts')));
13 | });
14 |
15 | gulp.task('copy:client', ['copy:client:fa'], () => {
16 | return gulp
17 | .src(`${dirs.srcClient}/**/!(*.vue|*.js)`)
18 | .pipe(gulp.dest(path.resolve(dirs.buildClient)));
19 | });
20 |
21 | gulp.task('copy:package', () => {
22 | return gulp.src(`${dirs.root}/package.json`)
23 | .pipe(gulp.dest(path.resolve(dirs.build)));
24 | });
25 |
26 | gulp.task('copy:server', () => {
27 | return gulp.src(`${dirs.srcServer}/**/*.json`)
28 | .pipe(gulp.dest(path.resolve(dirs.buildServer)));
29 | });
30 |
31 | gulp.task('copy:config:server', ['copy:server'], (done) => {
32 | if (argv.production) {
33 | const configPath = path.resolve(
34 | dirs.srcServer,
35 | 'config.json'
36 | );
37 |
38 | fs.readFile(configPath, (err, data) => {
39 | if (err) done(err);
40 |
41 | fs.writeFile(
42 | path.resolve(dirs.buildServer, 'config.json'),
43 | JSON.stringify({
44 | ...JSON.parse(data),
45 | ...production,
46 | }),
47 | done
48 | );
49 | });
50 | } else {
51 | done();
52 | }
53 | });
54 |
--------------------------------------------------------------------------------
/template/gulp-tasks/serve.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import gutil from 'gulp-util';
3 | import connect from 'gulp-connect';
4 | import historyApiFallback from 'connect-history-api-fallback';
5 | import clearCache from './clear-cache';
6 | import {dirs} from './config';
7 |
8 | let server = require('../server/server').default;
9 |
10 | gulp.task('reload:server', ['build:server'], () => {
11 | gutil.log('Reloading server');
12 | server.close();
13 | clearCache(dirs.srcServer);
14 | /* eslint-disable-next-line global-require */
15 | server = require('../server/server').default;
16 | server.start();
17 | });
18 |
19 | gulp.task('watch:server', () => {
20 | gulp.watch([
21 | `${dirs.srcServer}/**/*.js`,
22 | `${dirs.srcServer}/**/*.json`,
23 | `${dirs.srcCommon}/**/*.js`,
24 | ], ['reload:server']);
25 | });
26 |
27 | gulp.task('reload:client', ['build:client'], () => {
28 | gutil.log('Reloading client');
29 | gulp.src(dirs.buildClient)
30 | .pipe(connect.reload());
31 | });
32 |
33 | gulp.task('watch:client', () => {
34 | gulp.watch([
35 | `${dirs.srcClient}/**/*`,
36 | `${dirs.srcCommon}/**/*.js`,
37 | ], ['reload:client']);
38 | });
39 |
40 | gulp.task('serve:server', ['build:server', 'watch:server'], () => {
41 | server.start();
42 | });
43 |
44 | gulp.task('serve:client', ['build:client', 'watch:client'], () => {
45 | connect.server({
46 | name: 'Client App',
47 | root: dirs.buildClient,
48 | livereload: true,
49 | middleware: () => [historyApiFallback()],
50 | });
51 | });
52 |
53 | gulp.task('serve', ['serve:server', 'serve:client']);
54 |
--------------------------------------------------------------------------------
/template/server/boot/create-admin.js:
--------------------------------------------------------------------------------
1 | import initialAccount from '../initial-data/maintenance-account.json';
2 |
3 | /**
4 | * Create the first admin user if there are not users in the system
5 | */
6 | export default function createAdmin(server) {
7 | const Account = server.models.Account;
8 | const Role = server.models.Role;
9 | const RoleMapping = server.models.RoleMapping;
10 |
11 | return Account
12 | .find()
13 | .then((accounts) => {
14 | if (accounts.length < 1) {
15 | return Account.create(initialAccount);
16 | }
17 | return null;
18 | })
19 | .then((account) => {
20 | if (account) {
21 | return Role
22 | .find({name: 'admin'})
23 | .then((roles) => {
24 | if (roles.length < 1) {
25 | return Role.create({
26 | name: 'admin',
27 | });
28 | }
29 |
30 | return roles[0];
31 | })
32 | .then(role =>
33 | // resolve with a payload
34 | ({account, role})
35 | );
36 | }
37 | return null;
38 | })
39 | .then((payload) => { // get account and role from payload
40 | if (payload && payload.account && payload.role) {
41 | const myPayload = {...payload};
42 | return myPayload.role.principals.create({
43 | principalType: RoleMapping.USER,
44 | principalId: myPayload.account.id,
45 | }).then((principal) => {
46 | myPayload.principal = principal;
47 | return myPayload;
48 | });
49 | }
50 | return null;
51 | });
52 | }
53 |
--------------------------------------------------------------------------------
/template/client/style/global.scss:
--------------------------------------------------------------------------------
1 | // CAUTION: This file will be duplicated in every componentscss style scope
2 | //; HINT: Use this for global variables and mixins
3 | //
4 | //
5 | // Color system
6 | //;
7 | // $primary: #5478c0;
8 | // $secondary: #7c8386;
9 | // $success: #44b6ae;
10 | // $info: #444b4e;
11 | // $warning: #ffc85f;
12 | // $danger: #fc5e66;
13 | // $light: #e9edef;
14 | // $light2: #a2a6a8;
15 | // $dark: #1e2123;
16 | // $blue: #5478c0;
17 | // $purple: #944fc0;
18 |
19 | // Fonts
20 | // $font-size-base: .875rem;
21 | // $font-weight-bold: 700;
22 | // $font-family-sans-serif: "OpenSans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
23 | // $font-family-monospace: "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
24 |
25 | // Links
26 | // $link-color: $secondary;
27 |
28 | // Components
29 | // $border-radius: 0px;
30 | // $border-radius-lg: 0px;
31 | // $border-radius-sm: 0px;
32 |
33 | // Buttons
34 | // $btn-border-radius: 1.250em;
35 | // $btn-border-radius-lg: 1.250em;
36 | // $btn-border-radius-sm: 1.250em;
37 | // $btn-font-weight: $font-weight-bold;
38 |
39 | // Forms
40 | // $input-color: $secondary;
41 | // $input-border-color: $light;
42 |
43 | // Breadcrumbs
44 | // $breadcrumb-bg: transparent;
45 | // $breadcrumb-divider-color: $light2;
46 | // $breadcrumb-active-color: $light2;
47 |
48 | // Body
49 | // $body-color: $secondary;
50 |
51 | // Import Global Bootstrap Variables
52 | // (the previous variables will not be overitten)
53 | @import "bootstrap/scss/_functions";
54 | @import "bootstrap/scss/_variables";
55 |
--------------------------------------------------------------------------------
/template/server/boot/__tests__/create-admin.spec.js:
--------------------------------------------------------------------------------
1 | import createLoopback from '~/test/utils/create-loopback';
2 | import {email} from '~/server/initial-data/maintenance-account';
3 | import createAdmin from '~/server/boot/create-admin';
4 |
5 | async function prepareLoopback(undoBoot = false) {
6 | const server = await createLoopback();
7 | const Account = server.models.Account;
8 | const Role = server.models.Role;
9 | const RoleMapping = server.models.RoleMapping;
10 | if (undoBoot) {
11 | let info = await Account.deleteAll({email});
12 | if (info.count < 1) throw Error('account not deleted');
13 | info = await Role.deleteAll({name: 'admin'});
14 | if (info.count < 1) throw Error('role not deleted');
15 | }
16 |
17 | return {server, Account, Role, RoleMapping};
18 | }
19 |
20 | describe('boot create-admin', () => {
21 | it('adds admin, role and roleMapping', async() => {
22 | const {server, RoleMapping} = await prepareLoopback(true);
23 | const result = await createAdmin(server);
24 |
25 | expect(result).not.toBe(null);
26 | expect(result.account).toMatchObject({email});
27 | expect(result.role).toMatchObject({name: 'admin'});
28 | expect(result.principal).toMatchObject({
29 | principalType: RoleMapping.USER,
30 | });
31 | });
32 |
33 | it('do not add account if already exists', async() => {
34 | const {server} = await prepareLoopback();
35 | const result = await createAdmin(server);
36 | expect(result).toBe(null);
37 | });
38 |
39 | it('do not add role if already exists', async() => {
40 | const {server, Role} = await prepareLoopback(true);
41 | const created = await Role.create({name: 'admin'});
42 | const result = await createAdmin(server);
43 |
44 | expect(result).not.toBe(null);
45 | expect(result.role).toMatchObject(created);
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue-loopback
2 | [](https://travis-ci.org/InCuca/vue-loopback/branches)
3 |
4 | A Vue project template with [Loopback](http://loopback.io/) framework featuring ES6, Gulp, and Jest for unit tests
5 |
6 | > This template is for Vue 2.x **only** with [vue-cli](https://github.com/vuejs/vue-cli).
7 |
8 | ## Features
9 |
10 | * Loopback service using [axios](https://github.com/axios/axios) at `client/services/loopback`;
11 | * Full authentication support, by default the account listed in `server/initial-data/maintenance-account.json` is created;
12 | * Ajax Async queue module in `client/modules/async` (useful to see if and how many requests are being made to the server);
13 | * [CSS Modules](https://github.com/css-modules/css-modules), [Sass](https://sass-lang.com/) and [Bootstrap Vue](https://bootstrap-vue.js.org).
14 | ## Usage
15 |
16 | ```
17 | $ npm install -g vue-cli
18 | $ vue init InCuca/vue-loopback project-name
19 | $ npm install
20 | ```
21 |
22 | ## Folder structure
23 |
24 | 1. `client`: Vue client files
25 | 2. `common`: Common client and server model files
26 | 3. `server`: Loopback server files
27 | 4. `test`: Unit test
28 |
29 | ## Linting
30 |
31 | ```
32 | $ npm run lint
33 | ```
34 |
35 | ## Testing
36 |
37 | ```
38 | $ npm test
39 | ```
40 |
41 | ## Running the development server (API and Client)
42 |
43 | ```
44 | $ npm run dev
45 | ```
46 |
47 | ## Debug
48 |
49 | ```
50 | $ DEBUG=loopback npm run dev
51 | ```
52 |
53 | [More info...](https://loopback.io/doc/en/lb3/Setting-debug-strings.html)
54 |
55 | ## Build to ./build
56 |
57 | ```
58 | $ npm run build
59 | ```
60 |
61 | ## Executing built files
62 |
63 | Please remember to update `server/*.production.json` files to match your enviroment.
64 |
65 | ```bash
66 | $ cd build
67 | $ npm run start
68 | ```
69 |
--------------------------------------------------------------------------------
/template/server/model-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "sources": [
4 | "loopback/common/models",
5 | "loopback/server/models",
6 | "../common/models",
7 | "./models"
8 | ],
9 | "mixins": [
10 | "loopback/common/mixins",
11 | "loopback/server/mixins",
12 | {{#extended}}
13 | "../node_modules/loopback-ds-timestamp-mixin",
14 | {{/extended}}
15 | "../common/mixins",
16 | "./mixins"
17 | ]
18 | },
19 | "User": {
20 | "dataSource": "db"{{#extended}},
21 | "public": false{{/extended}}
22 | },
23 | "AccessToken": {
24 | "dataSource": "db",
25 | "public": false{{#extended}},
26 | "relations": {
27 | "account": {
28 | "type": "belongsTo",
29 | "model": "Account",
30 | "foreignKey": "userId"
31 | }
32 | }{{/extended}}
33 | },
34 | "ACL": {
35 | "dataSource": "db",
36 | "public": false
37 | },
38 | "RoleMapping": {
39 | "dataSource": "db",
40 | "public": false,
41 | "options": {
42 | "strictObjectIDCoercion": true
43 | }{{#extended}},
44 | "relations": {
45 | "account": {
46 | "type": "belongsTo",
47 | "model": "Account",
48 | "foreignKey": "userId"
49 | },
50 | "role": {
51 | "type": "belongsTo",
52 | "model": "Role",
53 | "foreignKey": "roleId"
54 | }
55 | }{{/extended}}
56 | },
57 | "Role": {
58 | "dataSource": "db",
59 | "public": false{{#extended}},
60 | "relations": {
61 | "account": {
62 | "type": "hasMany",
63 | "model": "Account",
64 | "through": "RoleMapping",
65 | "foreignKey": "roleId"
66 | }
67 | }
68 | },
69 | "Account": {
70 | "dataSource": "db",
71 | "public": true
72 | },
73 | "Email": {
74 | "dataSource": "email"{{/extended}}
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/template/client/services/loopback.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import {restApiHost, restApiRoot, port} from '~/server/config.json';
3 | import {DateString} from 'loopback';
4 |
5 | DateString.parse = function(dateString) {
6 | const date = dateString.when || dateString;
7 | return new DateString(date);
8 | };
9 |
10 | export {DateString};
11 |
12 | const Storage = window.localStorage;
13 |
14 | /**
15 | * Add a token in the local storage
16 | * */
17 | function exportTokenToLocalStorage(token) {
18 | if (Storage) {
19 | Storage.setItem('loopback-token', JSON.stringify(token));
20 | }
21 | }
22 |
23 | /**
24 | * Remove token from local storage
25 | */
26 | function removeTokenFromLocalStorage() {
27 | if (Storage) {
28 | Storage.removeItem('loopback-token');
29 | }
30 | }
31 |
32 | function addTokenFromLocalStorage(http) {
33 | const token = Storage && Storage.getItem('loopback-token');
34 | if (token) http.setToken(JSON.parse(token), false);
35 | }
36 |
37 | const http = axios.create({
38 | baseURL: `http://${restApiHost}:${port}${restApiRoot}`,
39 | });
40 |
41 | // Current setLoading function
42 | let setLoading = () => {
43 | console.warn('[loopback service] setLoadingFunction is not defined');
44 | };
45 |
46 | http.setLoadingFunction = (fn) => {
47 | setLoading = fn;
48 | };
49 |
50 | http.setToken = (token, save = true) => {
51 | http.token = token;
52 | http.defaults.headers.common.Authorization = token.id;
53 | if (save) exportTokenToLocalStorage(token);
54 | };
55 |
56 | http.removeToken = () => {
57 | delete http.defaults.headers.common.Authorization;
58 | removeTokenFromLocalStorage();
59 | };
60 |
61 | http.find = (endpoint, filter) => http.get(endpoint, {params: {filter}});
62 |
63 | /* Response Interceptors */
64 | const interceptResErrors = (err) => {
65 | // console.log('error', err);
66 | try {
67 | setLoading(
68 | false,
69 | err.config.uid || err.response.config.uid
70 | );
71 | err = Object.assign(new Error(), err.response.data.error);
72 | } catch (e) {
73 | // Will return err if something goes wrong
74 | }
75 | return Promise.reject(err);
76 | };
77 | const interceptResponse = (res) => {
78 | // console.log('response', res.config);
79 | setLoading(false, res.config.uid);
80 | try {
81 | return res.data;
82 | } catch (e) {
83 | return res;
84 | }
85 | };
86 | http.interceptors.response.use(interceptResponse, interceptResErrors);
87 |
88 | // Set storage Token in http if exists
89 | addTokenFromLocalStorage(http);
90 |
91 | /* Request Interceptors */
92 | const interceptReqErrors = err => Promise.reject(err);
93 | const interceptRequest = (config) => {
94 | config.uid = setLoading(true, config.uid);
95 | // console.log('request', config);
96 | return config;
97 | };
98 | http.interceptors.request.use(interceptRequest, interceptReqErrors);
99 |
100 | export default http;
101 |
102 | // Documentation: https://github.com/axios/axios
103 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | const {spawn} = require('child_process');
2 |
3 | process.setMaxListeners(20); // increase listeners limit
4 |
5 | const initQuestions = [
6 | {search: /^\?.+/, response: '\n'},
7 | ];
8 |
9 | const initNoExtQuestions = [
10 | {search: /^\? Add basic Login and Admin.+/, response: 'No\n'},
11 | {search: /^\?.+/, response: '\n'},
12 | ];
13 |
14 | /* eslint-disable max-len */
15 | const commands = [
16 | {cmd: 'rm', args: ['-r', 'test-project'], ignoreErrors: true},
17 | {cmd: './node_modules/.bin/vue', args: ['init', '.', 'test-project'], responses: initQuestions},
18 | {cmd: 'npm', args: ['install'], cwd: 'test-project'},
19 | {cmd: 'npm', args: ['run', 'lint'], cwd: 'test-project'},
20 | {cmd: 'npm', args: ['run', 'test'], cwd: 'test-project'},
21 | {cmd: 'npm', args: ['run', 'build'], cwd: 'test-project'},
22 | {cmd: 'rm', args: ['-r', 'test-project'], ignoreErrors: true},
23 | {cmd: './node_modules/.bin/vue', args: ['init', '.', 'test-project'], responses: initNoExtQuestions},
24 | {cmd: 'npm', args: ['install'], cwd: 'test-project'},
25 | {cmd: 'npm', args: ['run', 'lint'], cwd: 'test-project'},
26 | {cmd: 'npm', args: ['run', 'test'], cwd: 'test-project'},
27 | {cmd: 'npm', args: ['run', 'build'], cwd: 'test-project'},
28 | ];
29 | /* eslint-enable max-len */
30 |
31 | function executeCommand(command, index) {
32 | return new Promise((resolve, reject) => {
33 | const cp = spawn(command.cmd, command.args, {cwd: command.cwd});
34 | process.on('exit', cp.kill);
35 | cp.stdout.setEncoding('utf-8');
36 | cp.stdin.setEncoding('utf-8');
37 | cp.stderr.setEncoding('utf-8');
38 |
39 | // Ignore pipe errors
40 | cp.stdin.on('error', () => {});
41 | cp.stdout.on('error', () => {});
42 |
43 | cp.stdout.pipe(process.stdout);
44 | cp.stderr.pipe(process.stderr);
45 |
46 | let rejected = false;
47 | if (!rejected && command.responses) {
48 | const registerResponse = (q) => {
49 | cp.stdout.on('data', (output) => {
50 | if (q.search.test(output)) {
51 | // console.log('sending', q);
52 | cp.stdin.write(q.response);
53 | }
54 | });
55 | };
56 |
57 | command.responses.forEach(registerResponse);
58 | }
59 | cp.once('error', (code) => {
60 | if (!rejected) {
61 | reject(code);
62 | rejected = true;
63 | }
64 | });
65 |
66 | cp.once('exit', (code) => {
67 | if (code) {
68 | reject(code);
69 | rejected = true;
70 | } else {
71 | console.log(
72 | '=> process',
73 | (index + 1),
74 | 'of',
75 | commands.length,
76 | 'exit with status',
77 | code
78 | );
79 | resolve(code);
80 | }
81 | });
82 | });
83 | }
84 |
85 | commands.reduce(
86 | (prev, next, index) =>
87 | prev.then(() =>
88 | executeCommand(next, index).catch((code) => {
89 | console.log('child process exit with', code);
90 | if (!next.ignoreErrors) process.exit(code);
91 | })),
92 | Promise.resolve()
93 | );
94 |
--------------------------------------------------------------------------------
/template/server/models/account.spec.js:
--------------------------------------------------------------------------------
1 | import request from 'supertest';
2 | import createLoopback from '~/test/utils/create-loopback';
3 | import AccountFactory from './account';
4 | import {host} from '../config.json';
5 |
6 | describe('Account unit', () => {
7 | const AccountMock = {
8 | on: jest.fn(),
9 | app: {models: {Email: {send: jest.fn()}}},
10 | };
11 |
12 | beforeEach(() => {
13 | jest.resetAllMocks();
14 | });
15 |
16 | it('add resetPasswordRequest remote method', () => {
17 | AccountFactory(AccountMock);
18 | expect(AccountMock.on).toBeCalledWith(
19 | 'resetPasswordRequest',
20 | expect.any(Function),
21 | );
22 | });
23 |
24 | it('calls send method from Email model', () => {
25 | const infoMock = {
26 | email: 'foo@bar.net',
27 | accessToken: {
28 | id: 'foobar',
29 | },
30 | };
31 | AccountMock.on.mockImplementation((_, fn) => {
32 | fn(infoMock);
33 | });
34 | console.log = jest.fn();
35 | AccountMock.app.models.Email.send.mockImplementation((_, errFn) => {
36 | errFn();
37 | });
38 | AccountFactory(AccountMock);
39 | expect(AccountMock.app.models.Email.send).toBeCalledWith(
40 | expect.objectContaining({
41 | to: infoMock.email,
42 | from: expect.stringContaining(
43 | 'noreply@mydomain.com'
44 | ),
45 | subject: expect.stringContaining(
46 | 'Create a new password'
47 | ),
48 | html: expect.stringMatching(
49 | new RegExp(`${host}.*${infoMock.accessToken.id}`)
50 | ),
51 | }),
52 | expect.any(Function),
53 | );
54 | expect(console.log).toBeCalledWith(
55 | expect.any(String),
56 | expect.stringContaining(infoMock.email)
57 | );
58 | });
59 |
60 | it('console log when Email send throws', () => {
61 | const infoMock = {
62 | email: 'foo@bar.net',
63 | accessToken: {
64 | id: 'foobar',
65 | },
66 | };
67 | AccountMock.on.mockImplementation((_, fn) => {
68 | fn(infoMock);
69 | });
70 | console.log = jest.fn();
71 | AccountMock.app.models.Email.send.mockImplementation((_, errFn) => {
72 | errFn('foo');
73 | });
74 | AccountFactory(AccountMock);
75 | expect(console.log).toBeCalledWith('foo');
76 | });
77 | });
78 |
79 | describe('Account e2e', () => {
80 | const email = '936ue5+4bnywbeje42pw@sharklasers.com';
81 | let server, testAccount, Account;
82 |
83 | beforeEach(async() => {
84 | server = await createLoopback();
85 | Account = server.models.Account;
86 | testAccount = await Account.create({
87 | email,
88 | password: 'IuhEW7HI#&HUH3',
89 | });
90 | });
91 |
92 | afterEach(() => Account.destroyById(testAccount.id));
93 |
94 | it('has been correctly declared', () => {
95 | expect(Account).toInherits(server.models.User);
96 | });
97 |
98 | it(
99 | 'should send reset email to test user',
100 | () => request(server)
101 | .post('/api/Accounts/reset')
102 | .send({email})
103 | .expect(204),
104 | 30000,
105 | );
106 | });
107 |
--------------------------------------------------------------------------------
/template/server/boot/__tests__/boot.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable arrow-body-style */
2 | import request from 'supertest';
3 | import createLoopback from '~/test/utils/create-loopback';
4 | {{#extended}}
5 | import initialAccount from '~/server/initial-data/maintenance-account.json';
6 | {{/extended}}
7 |
8 | describe('boot process', () => {
9 | let server;
10 |
11 | beforeEach(() => {
12 | return createLoopback().then((s) => {
13 | server = s;
14 | });
15 | });
16 |
17 | afterEach((done) => {
18 | // Clear memory database
19 | server.dataSources.db.automigrate(done);
20 | });
21 |
22 | describe('root.js', () => {
23 | it('should return server status by root.js', (done) => {
24 | const conn = server.listen(8000, () => {
25 | request(server).get('/api').then((res) => {
26 | expect(res.statusCode).toBe(200);
27 | expect(res.body).toHaveProperty('started');
28 | expect(res.body).toHaveProperty('uptime');
29 | conn.close(done);
30 | });
31 | });
32 | });
33 | });
34 | {{#extended}}
35 | describe('authentication.js', () => {
36 | it('should enable authentication by authentication.js', () => {
37 | expect(server.isAuthEnabled).toEqual(true);
38 | });
39 | });
40 |
41 | describe('email configuration', () => {
42 | it('should have Email model', () => {
43 | expect(server.models).toHaveProperty('Email');
44 | });
45 |
46 | it('Email model should send email', (done) => {
47 | server.models.Email.send({
48 | from: 'noreply@fakeserver.mailtrap.io',
49 | to: '92y0zm+7xhtk2ni75mas@grr.la',
50 | subject: 'Testing email',
51 | text: 'Testing email text',
52 | html: 'Testing email text',
53 | }, done);
54 | }, 30000);
55 | });
56 |
57 | describe('create-admin.js', () => {
58 | it('should have Account model', () => {
59 | expect(server.models).toHaveProperty('Account');
60 | });
61 |
62 | it('should create a default admin user', () => {
63 | return server.models.Account.find().then((res) => {
64 | expect(res).toHaveLength(1);
65 | expect(res[0]).toHaveProperty('createdAt');
66 | expect(res[0]).toHaveProperty('updatedAt');
67 | expect(res[0].id).toEqual(1);
68 | expect(res[0].email).toEqual(initialAccount.email);
69 | expect(res[0].password).toBeDefined();
70 | });
71 | });
72 |
73 | it('should create a default admin role', () => {
74 | return server.models.Role.find().then((res) => {
75 | expect(res).toHaveLength(1);
76 | expect(res[0]).toHaveProperty('created');
77 | expect(res[0]).toHaveProperty('modified');
78 | expect(res[0].id).toEqual(1);
79 | expect(res[0].name).toEqual('admin');
80 | });
81 | });
82 |
83 | it('should create RoleMapping entry for admin', () => {
84 | const RoleMapping = server.models.RoleMapping;
85 | return RoleMapping.find().then((res) => {
86 | expect(res).toHaveLength(1);
87 | expect(res[0].id).toEqual(1);
88 | expect(res[0].roleId).toEqual(1);
89 | expect(res[0].principalId).toEqual(1);
90 | expect(res[0].principalType).toEqual(RoleMapping.USER);
91 | });
92 | });
93 | });
94 | {{/extended}}
95 | });
96 |
--------------------------------------------------------------------------------
/template/server/server.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 | describe('server', () => {
3 | let loopback, boot;
4 | const mockApp = {};
5 | const getApp = () => require('./server').default;
6 |
7 | beforeEach(() => {
8 | jest.resetModules();
9 | jest.mock('loopback');
10 | jest.mock('loopback-boot');
11 | loopback = require('loopback');
12 | boot = require('loopback-boot');
13 | loopback.mockReturnValue(mockApp);
14 | });
15 |
16 | it('instances loopback', () => {
17 | getApp();
18 | expect(loopback).toBeCalled();
19 | });
20 |
21 | it('adds start function', () => {
22 | expect(getApp().start).toBeInstanceOf(Function);
23 | });
24 |
25 | it('adds close function', () => {
26 | expect(getApp().close).toBeInstanceOf(Function);
27 | });
28 |
29 | it('boot application', () => {
30 | getApp();
31 | expect(boot).toBeCalledWith(
32 | mockApp,
33 | expect.objectContaining({
34 | appRootDir: __dirname,
35 | scriptExtensions: expect.arrayContaining([
36 | '.js', '.json', '.node', '.ejs',
37 | ]),
38 | }),
39 | expect.any(Function),
40 | );
41 | });
42 |
43 | it('throws when application boot fails', () => {
44 | boot.mockImplementation((a, o, done) => done('foo'));
45 | expect(getApp).toThrowError('foo');
46 | });
47 |
48 | it('not throws when application boot success', () => {
49 | boot.mockImplementation((a, o, done) => done());
50 | expect(getApp).not.toThrow();
51 | });
52 |
53 | it('calls express listen when start is called', () => {
54 | const listen = jest.fn();
55 | loopback.mockReturnValue({listen});
56 | getApp().start();
57 | expect(listen).toBeCalledWith(expect.any(Function));
58 | });
59 |
60 | it('emit started when start is called', () => {
61 | const get = jest.fn(() => 'foo');
62 | const listen = jest.fn(cb => cb());
63 | const emit = jest.fn();
64 | loopback.mockReturnValue({listen, get, emit});
65 | getApp().start();
66 | expect(emit).toBeCalledWith('started');
67 | });
68 |
69 | it('log api endpoint when start is called', () => {
70 | const get = jest.fn(() => 'foo');
71 | const listen = jest.fn(cb => cb());
72 | const emit = jest.fn();
73 | console.log = jest.fn();
74 | loopback.mockReturnValue({listen, get, emit});
75 | getApp().start();
76 | expect(console.log).toBeCalledWith(
77 | expect.any(String),
78 | 'foo/api',
79 | );
80 | });
81 |
82 | it('log explorer url when start is called', () => {
83 | const get = jest.fn(
84 | term => (term === 'url' ? 'url' : {mountPath: 'foo'})
85 | );
86 | const listen = jest.fn(cb => cb());
87 | const emit = jest.fn();
88 | console.log = jest.fn();
89 | loopback.mockReturnValue({listen, get, emit});
90 | getApp().start();
91 | expect(console.log).toBeCalledWith(
92 | expect.any(String),
93 | 'url',
94 | 'foo',
95 | );
96 | });
97 |
98 | it('close server when close is called', () => {
99 | const close = jest.fn();
100 | const listen = jest.fn(() => ({close}));
101 | loopback.mockReturnValue({listen});
102 | getApp().start();
103 | getApp().close();
104 | expect(close).toBeCalled();
105 | });
106 | });
107 |
--------------------------------------------------------------------------------
/template/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "{{ name }}",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "engines": {
6 | "node": ">=8.9.4"
7 | },
8 | "scripts": {
9 | "lint": "npx eslint --ext vue,js .",
10 | "dev": "gulp serve",
11 | "build": "gulp build --production && npm run afterbuild",
12 | "afterbuild": "cd build && npm install --production",
13 | "test": "npx jest",
14 | "start": "NODE_ENV=production node ."
15 | },
16 | "dependencies": {
17 | "compression": "^1.0.3",
18 | "cors": "^2.5.2",
19 | "helmet": "^1.3.0",
20 | "http-server": "^0.11.1",
21 | "express": "^4.16.2",
22 | "express-history-api-fallback": "^2.2.1",
23 | "loopback": "^3.16.2",
24 | "loopback-boot": "github:klarkc/loopback-boot#master",
25 | "loopback-component-explorer": "^4.0.0",
26 | "serve-favicon": "^2.0.1",
27 | "strong-error-handler": "^2.0.0",
28 | "vue": "^2.5.3"{{#extended}},
29 | "axios": "^0.17.1",
30 | "babel-polyfill": "^6.26.0",
31 | "bootstrap": "^4.0.0-beta.2",
32 | "bootstrap-vue": "^1.2.0",
33 | "font-awesome": "^4.7.0",
34 | "install": "^0.10.1",
35 | "lodash.defaultsdeep": "^4.6.0",
36 | "loopback-ds-timestamp-mixin": "^3.4.1",
37 | "uid": "0.0.2",
38 | "vue-awesome": "^2.3.4",
39 | "vue-router": "^3.0.1",
40 | "vue-template-compiler": "^2.5.3",
41 | "vuex": "^3.0.1",
42 | "vuex-router-sync": "^5.0.0"
43 | {{/extended}}
44 | },
45 | "devDependencies": {
46 | "aliasify": "^2.1.0",
47 | "babel-core": "^6.26.3",
48 | "babel-eslint": "^8.2.2",
49 | "babel-jest": "^23.0.1",
50 | "regenerator-runtime": "^0.11.1",
51 | "supertest": "^3.1.0",
52 | "babel-plugin-transform-runtime": "^6.23.0",
53 | "babel-preset-env": "^1.6.1",
54 | "babel-preset-stage-2": "^6.24.1",
55 | "babelify": "^7.3.0",
56 | "browserify": "^14.4.0",
57 | "browserify-global-shim": "^1.0.3",
58 | "css-modulesify": "^0.28.0",
59 | "eslint": "^4.15.0",
60 | "eslint-config-airbnb-base": "^11.3.0",
61 | "eslint-plugin-import": "^2.9.0",
62 | "eslint-config-loopback": "^8.0.0",
63 | "eslint-plugin-vue": "^4.0.0",
64 | "eslint-plugin-jest": "^21.17.0",
65 | "gulp": "^3.9.1",
66 | "gulp-autoprefixer": "^4.0.0",
67 | "gulp-babel": "^6.1.2",
68 | "gulp-connect": "^5.0.0",
69 | "gulp-sourcemaps": "^2.6.0",
70 | "identity-obj-proxy": "^3.0.0",
71 | "jest": "^22.4.4",
72 | "jest-plugins": "^2.9.0",
73 | "jest-preset-loopback": "^1.0.0",
74 | "loopback-jest": "^1.3.0",
75 | "supertest": "^3.1.0",
76 | "tfilter": "^1.0.1",
77 | "through2": "^2.0.3",
78 | "tmp": "0.0.33",
79 | "vinyl-buffer": "^1.0.0",
80 | "vinyl-source-stream": "^1.1.0",
81 | "vue-jest": "^2.6.0",
82 | "vue-test-utils": "^1.0.0-beta.11",
83 | "vueify": "^9.4.1",
84 | "yargs": "^10.0.3",
85 | "babel-plugin-root-import": "^5.1.0",
86 | "connect-history-api-fallback": "^1.5.0",
87 | "node-sass": "^4.6.0",
88 | "gulp-util": "^3.0.8",
89 | "babel-register": "^6.26.0"
90 | },
91 | "repository": {
92 | "type": "",
93 | "url": ""
94 | },
95 | "license": "UNLICENSED",
96 | "description": "{{ description }}"{{#extended}},
97 | "extended": true
98 | {{/extended}}
99 | }
100 |
--------------------------------------------------------------------------------
/template/gulp-tasks/build.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable arrow-body-style */
2 | import gulp from 'gulp';
3 | import sourcemaps from 'gulp-sourcemaps';
4 | import babel from 'gulp-babel';
5 | import path from 'path';
6 | import browserify from 'browserify';
7 | import vueify from 'vueify';
8 | import babelify from 'babelify';
9 | import tfilter from 'tfilter';
10 | import modulesify from 'css-modulesify';
11 | import aliasify from 'aliasify';
12 | import source from 'vinyl-source-stream';
13 | import buffer from 'vinyl-buffer';
14 | import {argv} from 'yargs';
15 | import {dirs, extended} from './config';
16 | import {customSass} from './compilers';
17 |
18 | gulp.task('build:client', ['copy:client', 'copy:config:server'], () => {
19 | // Node modules to be included in babel transpilation
20 | // Use this with ES6 modules for example
21 | const bModules = [];
22 |
23 | if (extended) bModules.push('bootstrap-vue/es');
24 |
25 | vueify.compiler.applyConfig({
26 | sass: {
27 | includePaths: [
28 | dirs.modules,
29 | ],
30 | },
31 | customCompilers: {
32 | scss: customSass,
33 | },
34 | });
35 |
36 | const b = browserify({
37 | entries: [
38 | path.resolve(dirs.srcClient, 'main.js'),
39 | ...bModules.map(mod => path.resolve(dirs.modules, mod)),
40 | ],
41 | debug: true,
42 | });
43 |
44 | // Transpile .vue
45 | b.transform(vueify);
46 |
47 | // Babel Settings with module filter
48 | const bSettings = {
49 | plugins: ['transform-runtime'],
50 | };
51 |
52 | b.transform(tfilter(
53 | babelify,
54 | {
55 | filter: (filename) => {
56 | const exception = bModules.some(
57 | mod => filename.includes(
58 | path.resolve(dirs.modules, mod)
59 | )
60 | );
61 | if (exception) {
62 | // node module
63 | // console.log('Transpiling module', filename);
64 | return true;
65 | } else if (!filename.includes(dirs.modules)) {
66 | // filter not in node_modules
67 | // console.log('Transpiling src', filename);
68 | return true;
69 | }
70 | // console.log('NOT transpiling', filename);
71 | return false;
72 | },
73 | },
74 | {
75 | ...bSettings,
76 | global: true,
77 | presets: ['env'],
78 | }
79 | ));
80 |
81 | b.plugin(modulesify, {
82 | output: path.resolve(
83 | dirs.buildClient,
84 | 'bundle.css'
85 | ),
86 | global: true,
87 | generateScopedName(name, filename) {
88 | const matches = filename.match(/^\/node_modules/);
89 | if (matches) return name;
90 | if (process.env.NODE_ENV === 'production') {
91 | return modulesify.generateShortName(name, filename);
92 | }
93 | return modulesify.generateLongName(name, filename);
94 | },
95 | });
96 |
97 | if (argv.production) {
98 | b.transform(aliasify, {
99 | replacements: {
100 | '(.+)server/config.json$': path.resolve(
101 | dirs.buildServer,
102 | 'config.json',
103 | ),
104 | },
105 | verbose: true,
106 | });
107 | }
108 |
109 | return b.bundle()
110 | .pipe(source('bundle.js'))
111 | .pipe(buffer())
112 | .pipe(sourcemaps.init({loadMaps: true}))
113 | .pipe(sourcemaps.write('.'))
114 | .pipe(gulp.dest(dirs.buildClient));
115 | });
116 |
117 | gulp.task('build:common', () => {
118 | return gulp.src(path.resolve(dirs.srcCommon, '**/*.js'))
119 | .pipe(sourcemaps.init())
120 | .pipe(babel())
121 | .pipe(sourcemaps.write('.'))
122 | .pipe(gulp.dest(dirs.buildCommon));
123 | });
124 |
125 | gulp.task('build:server', ['copy:server', 'copy:config:server'], () => {
126 | return gulp.src(path.resolve(dirs.srcServer, '**/*.js'))
127 | .pipe(sourcemaps.init())
128 | .pipe(babel())
129 | .pipe(sourcemaps.write('.'))
130 | .pipe(gulp.dest(dirs.buildServer));
131 | });
132 |
133 | gulp.task('build:index', () => {
134 | return gulp.src(path.resolve(dirs.root, 'index.js'))
135 | .pipe(sourcemaps.init())
136 | .pipe(babel())
137 | .pipe(sourcemaps.write('.'))
138 | .pipe(gulp.dest(dirs.build));
139 | });
140 |
141 | gulp.task('build', [
142 | 'build:client',
143 | 'build:common',
144 | 'build:server',
145 | 'build:index',
146 | 'copy:package',
147 | ]);
148 |
--------------------------------------------------------------------------------
/template/client/view/Profile.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |

6 |
7 |
8 |
65 |
69 | The password has been updated!
70 |
71 |
72 |
73 |
74 |
75 |
76 |
132 |
133 |
160 |
--------------------------------------------------------------------------------
/template/client/store/modules/auth/actions.js:
--------------------------------------------------------------------------------
1 | import loopback from '@/services/loopback';
2 | import router from '@/router';
3 |
4 | /**
5 | * Sync loopback token with current state
6 | */
7 | export function syncToken({commit, dispatch}) {
8 | if (loopback.token) {
9 | commit('setAccessToken', loopback.token);
10 | return dispatch(
11 | 'loadAccount',
12 | loopback.token.userId,
13 | ).catch((err) => {
14 | commit('setAccessToken', null);
15 | return err;
16 | });
17 | }
18 | return Promise.resolve();
19 | }
20 |
21 | function evaluateRoute(state, to, from, next) {
22 | return (sessionError) => {
23 | if (to.matched.some(record => record.meta.requiresAuth)) {
24 | // this route requires auth, check if logged in
25 | // if not, redirect to login page (except when it's profile route and
26 | // there is an access_token).
27 | if (to.name === 'profile' && to.query.access_token) {
28 | next();
29 | } else if (!state.access_token) {
30 | next({
31 | name: 'login',
32 | params: {
33 | sessionError,
34 | },
35 | });
36 | } else {
37 | next();
38 | }
39 | } else {
40 | next(); // make sure to always call next()!
41 | }
42 | };
43 | }
44 |
45 | /**
46 | * Sync router for auth
47 | */
48 | export function syncRouter({state, dispatch}, myRouter) {
49 | myRouter.beforeEach((to, from, next) => {
50 | dispatch('syncToken').then(
51 | evaluateRoute(state, to, from, next)
52 | );
53 | });
54 | }
55 |
56 | /**
57 | * Sign-in account with email and password (stores in state.account)
58 | * @param {Function} commit commit mutations function
59 | * @param {String} email user email
60 | * @param {String} password user password
61 | * @return {Promise} promise of logged user
62 | */
63 | export function signIn({commit, dispatch, state}, {email, password}) {
64 | return loopback
65 | .post('/Accounts/login', {
66 | email,
67 | password,
68 | })
69 | .then((token) => {
70 | commit('setAccessToken', token);
71 |
72 | // Update Loopback Token
73 | if (state.access_token !== null) {
74 | loopback.setToken(state.access_token);
75 | } else {
76 | loopback.removeToken();
77 | }
78 |
79 | return dispatch('loadAccount', state.access_token.userId);
80 | });
81 | }
82 |
83 | /**
84 | * Sign-out current logged-in account
85 | * (assumes that current state.account is not null)
86 | * @param {Function} commit commit mutations function
87 | * @return {Promise} promise of the logout
88 | */
89 | export function signOut({commit, state}) {
90 | return loopback
91 | .post('/Accounts/logout', {
92 | // eslint-disable-next-line dot-notation
93 | accessToken: state['access_token'],
94 | })
95 | .then(() => {
96 | commit('setAccessToken', null);
97 | loopback.removeToken();
98 | router.push({name: 'login'});
99 | });
100 | }
101 |
102 | /**
103 | * Load an account by userId in state.account
104 | * @param {Function} commit commit mutations function
105 | * @param {Number} userId account id
106 | * @return {Promise} promise of the account
107 | */
108 | export function loadAccount({commit}, userId) {
109 | return loopback
110 | .get(`/Accounts/${userId}`)
111 | .then(acc => commit('setAccount', acc))
112 | .catch((err) => {
113 | loopback.removeToken();
114 | return Promise.reject(err);
115 | });
116 | }
117 |
118 | /**
119 | * Reset the password of the current logged-in account
120 | * (assumes that current state.account is not null)
121 | * (assumes that current state.access_token is not null)
122 | * @param {Function} commit commit mutations function
123 | * @param {Object} state Vuex state
124 | * @param {String} oldPassword old password
125 | * @param {String} newPassword new password
126 | * @return {Promise} promise of the password reset
127 | */
128 | export function resetPassword(ctx, {oldPassword, newPassword}) {
129 | return loopback
130 | .post(
131 | '/Accounts/change-password',
132 | {oldPassword, newPassword}
133 | );
134 | }
135 |
136 | /**
137 | * Send a email to the account user to reset the password
138 | * @param {Function} commit commit mutations function
139 | * @param {String} email user email
140 | * @return {Promise} promise of the sent email
141 | */
142 | export function rememberPassword(ctx, email) {
143 | return loopback
144 | .post('/Accounts/reset', {email});
145 | }
146 |
--------------------------------------------------------------------------------
/template/client/view/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |

6 |
7 |
8 |
9 |
55 |
56 |
57 |
63 |
84 |
85 |
86 |
89 | An email has been sent, please verify your mailbox
90 |
91 |
92 |
93 |
94 |
95 |
96 |
180 |
181 |
217 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | ## [5.2.1](https://github.com/InCuca/vue-loopback/compare/v5.2.0...v5.2.1) (2018-07-12)
3 |
4 |
5 | ### Features
6 |
7 | * **template:** fix deploy and add settings ([37fef59](https://github.com/InCuca/vue-loopback/commit/37fef59))
8 |
9 |
10 |
11 |
12 | # [5.2.0](https://github.com/InCuca/vue-loopback/compare/v5.1.0...v5.2.0) (2018-07-12)
13 |
14 |
15 | ### Bug Fixes
16 |
17 | * **lint:** add jest support ([bc62f54](https://github.com/InCuca/vue-loopback/commit/bc62f54))
18 | * **template:** filter only testable directories ([9664336](https://github.com/InCuca/vue-loopback/commit/9664336))
19 |
20 |
21 | ### Features
22 |
23 | * **eslint:** add more test folders ([82f4654](https://github.com/InCuca/vue-loopback/commit/82f4654))
24 |
25 |
26 |
27 |
28 | # [5.1.0](https://github.com/InCuca/vue-loopback/compare/v5.0.0...v5.1.0) (2018-06-28)
29 |
30 |
31 | ### Bug Fixes
32 |
33 | * **package:** fix test script ([1984f65](https://github.com/InCuca/vue-loopback/commit/1984f65))
34 |
35 |
36 |
37 |
38 | # [5.0.0](https://github.com/InCuca/vue-loopback/compare/v4.2.0...v5.0.0) (2018-06-28)
39 |
40 |
41 | ### Bug Fixes
42 |
43 | * **babel:** add stage2 support ([c95f47c](https://github.com/InCuca/vue-loopback/commit/c95f47c))
44 | * **gulp-tasks:** add exit in mocha to close mocha when test pass ([0963212](https://github.com/InCuca/vue-loopback/commit/0963212))
45 | * **gulp-tasks:** fix backslashs in win fixes [#8](https://github.com/InCuca/vue-loopback/issues/8) ([b721305](https://github.com/InCuca/vue-loopback/commit/b721305))
46 | * **jest:** fix jest plugins path ([6632ad6](https://github.com/InCuca/vue-loopback/commit/6632ad6))
47 | * **lint:** fix eslint errors ([dc2a6c7](https://github.com/InCuca/vue-loopback/commit/dc2a6c7))
48 | * **loopback:** pass previous uid to loading fn ([39093da](https://github.com/InCuca/vue-loopback/commit/39093da))
49 | * **loopback-boot:** add support for es6 modules in boot scripts ([2d813fe](https://github.com/InCuca/vue-loopback/commit/2d813fe)), closes [strongloop/loopback-boot#280](https://github.com/strongloop/loopback-boot/issues/280)
50 | * **package:** run server tests in band ([4d45501](https://github.com/InCuca/vue-loopback/commit/4d45501))
51 | * **package:** update loopback-boot to 3.1 to fix boot ([200b615](https://github.com/InCuca/vue-loopback/commit/200b615))
52 | * **server:** add option for compatibility with jest ([fbb68b2](https://github.com/InCuca/vue-loopback/commit/fbb68b2))
53 | * **tasks:** remove tests tasks ([e889a3c](https://github.com/InCuca/vue-loopback/commit/e889a3c))
54 | * **tests:** remove client and server separation ([cd4ac8c](https://github.com/InCuca/vue-loopback/commit/cd4ac8c))
55 |
56 |
57 | ### Features
58 |
59 | * **babel:** add node 8 add regenerator ([94cd1a4](https://github.com/InCuca/vue-loopback/commit/94cd1a4))
60 | * **jest:** add jest config file ([044a3eb](https://github.com/InCuca/vue-loopback/commit/044a3eb))
61 | * **jest:** add jest plugins config ([3021667](https://github.com/InCuca/vue-loopback/commit/3021667))
62 | * **jest:** add jest-preset-loopback ([575873c](https://github.com/InCuca/vue-loopback/commit/575873c))
63 | * **Login:** add sessionError with authorization error redirection ([a05a9eb](https://github.com/InCuca/vue-loopback/commit/a05a9eb))
64 | * **loopback:** add DateString type support ([5ba568d](https://github.com/InCuca/vue-loopback/commit/5ba568d))
65 | * **package:** add babel-jest ([29533ac](https://github.com/InCuca/vue-loopback/commit/29533ac))
66 | * **package:** add jest dependency ([1b82ba3](https://github.com/InCuca/vue-loopback/commit/1b82ba3))
67 | * **package:** add loopback-jest ([2bc6cc8](https://github.com/InCuca/vue-loopback/commit/2bc6cc8))
68 | * **package:** add vue-jest ([33c6d57](https://github.com/InCuca/vue-loopback/commit/33c6d57))
69 | * **package:** run afterbuild after build, fixes [#9](https://github.com/InCuca/vue-loopback/issues/9) ([77c6fcf](https://github.com/InCuca/vue-loopback/commit/77c6fcf))
70 |
71 |
72 |
73 |
74 | # [4.2.0](https://github.com/InCuca/vue-loopback/compare/v4.1.1...v4.2.0) (2018-05-17)
75 |
76 |
77 | ### Bug Fixes
78 |
79 | * **loopback:** fix lint errors ([ea23c48](https://github.com/InCuca/vue-loopback/commit/ea23c48))
80 |
81 |
82 | ### Features
83 |
84 | * **gulp-tasks:** add support to es6 modules, fixes [#7](https://github.com/InCuca/vue-loopback/issues/7) ([0c70326](https://github.com/InCuca/vue-loopback/commit/0c70326))
85 | * **loopback:** add aditional response check in interceptResErrors ([66d1b59](https://github.com/InCuca/vue-loopback/commit/66d1b59))
86 |
87 |
88 |
89 |
90 | ## [4.1.1](https://github.com/InCuca/vue-loopback/compare/v4.1.0...v4.1.1) (2018-05-15)
91 |
92 |
93 | ### Bug Fixes
94 |
95 | * **App:** fix lint error ([5cc2c97](https://github.com/InCuca/vue-loopback/commit/5cc2c97))
96 | * **project:** fix lint errors ([04d852a](https://github.com/InCuca/vue-loopback/commit/04d852a))
97 | * **template:** fix lint errors ([ea1c31e](https://github.com/InCuca/vue-loopback/commit/ea1c31e))
98 | * **template:** fix package lint command ([34b3a0b](https://github.com/InCuca/vue-loopback/commit/34b3a0b))
99 |
100 |
101 | ### Features
102 |
103 | * **App:** add some style to not extended App ([9f8d380](https://github.com/InCuca/vue-loopback/commit/9f8d380))
104 |
105 |
106 |
107 |
108 | # [4.1.0](https://github.com/InCuca/vue-loopback/compare/4.0.1...4.1.0) (2018-05-10)
109 |
110 |
111 | ### Bug Fixes
112 |
113 | * **App:** only dispatch on extended ([f45a002](https://github.com/InCuca/vue-loopback/commit/f45a002))
114 | * **karma:** fix App errors and consoleAppendfy ([1ef9df4](https://github.com/InCuca/vue-loopback/commit/1ef9df4))
115 | * **karma:** throws when vue warns ([7907f28](https://github.com/InCuca/vue-loopback/commit/7907f28))
116 | * **release-it:** fix buildCommand ([655954a](https://github.com/InCuca/vue-loopback/commit/655954a))
117 | * **template:** fix lint errors ([3ed71f8](https://github.com/InCuca/vue-loopback/commit/3ed71f8))
118 |
119 |
120 | ### Features
121 |
122 | * **Account:** add declaration tests and loopback-chai support ([903d1ef](https://github.com/InCuca/vue-loopback/commit/903d1ef))
123 | * **gulp:** add optional -t parameter to test files ([08e5ece](https://github.com/InCuca/vue-loopback/commit/08e5ece))
124 | * **project:** add release-it support ([eaf6f1c](https://github.com/InCuca/vue-loopback/commit/eaf6f1c))
125 |
126 |
127 |
128 |
129 | ## [4.0.1](https://github.com/InCuca/vue-loopback/compare/4.0.0...4.0.1) (2018-04-27)
130 |
131 |
132 | ### Bug Fixes
133 |
134 | * **gulp:** fix lint errors ([4545089](https://github.com/InCuca/vue-loopback/commit/4545089))
135 |
136 |
137 |
138 |
139 | # [4.0.0](https://github.com/InCuca/vue-loopback/compare/3.2.0...4.0.0) (2018-04-27)
140 |
141 |
142 | ### Bug Fixes
143 |
144 | * **auth:** fix eslint errors ([154897e](https://github.com/InCuca/vue-loopback/commit/154897e))
145 | * **gulp:** clear modules cache to enable hot reloading ([c1c8f80](https://github.com/InCuca/vue-loopback/commit/c1c8f80))
146 | * **gulp:** fix hot reload ([661639d](https://github.com/InCuca/vue-loopback/commit/661639d))
147 | * **gulp:** fix server hot reloading not restarting ([57609bb](https://github.com/InCuca/vue-loopback/commit/57609bb))
148 | * **loopback:** add accessToken to logout method ([4bac1b6](https://github.com/InCuca/vue-loopback/commit/4bac1b6))
149 | * **loopback:** change error to warning in console ([3413b45](https://github.com/InCuca/vue-loopback/commit/3413b45))
150 | * **loopback:** fix lint errors ([3086f12](https://github.com/InCuca/vue-loopback/commit/3086f12))
151 |
152 |
153 | ### Features
154 |
155 | * **async:** add async module ([7b17fc5](https://github.com/InCuca/vue-loopback/commit/7b17fc5))
156 |
157 |
158 |
159 |
160 | # [3.2.0](https://github.com/InCuca/vue-loopback/compare/3.1.1...3.2.0) (2018-03-25)
161 |
162 |
163 | ### Bug Fixes
164 |
165 | * **boot:** fix boot scripts ([33e3461](https://github.com/InCuca/vue-loopback/commit/33e3461))
166 | * **lint:** fix lint errors ([cb3dd31](https://github.com/InCuca/vue-loopback/commit/cb3dd31))
167 | * **loopback:** setLoading to false when errored ([49f48c5](https://github.com/InCuca/vue-loopback/commit/49f48c5))
168 | * **server:** fix index server ([0df8443](https://github.com/InCuca/vue-loopback/commit/0df8443))
169 |
170 |
171 | ### Features
172 |
173 | * **build:** add copy:config:server ([661ee6f](https://github.com/InCuca/vue-loopback/commit/661ee6f))
174 |
175 |
176 |
177 |
178 | ## [3.1.1](https://github.com/InCuca/vue-loopback/compare/3.1.0...3.1.1) (2018-03-07)
179 |
180 |
181 | ### Bug Fixes
182 |
183 | * **auth:** only change route after login ([19eb3e9](https://github.com/InCuca/vue-loopback/commit/19eb3e9))
184 | * **auth:** only change router after account exists ([624fff2](https://github.com/InCuca/vue-loopback/commit/624fff2))
185 | * **lint:** fix lint errors ([e1df64d](https://github.com/InCuca/vue-loopback/commit/e1df64d))
186 |
187 |
188 |
189 |
190 | # [3.1.0](https://github.com/InCuca/vue-loopback/compare/3.0.0...3.1.0) (2018-02-28)
191 |
192 |
193 | ### Bug Fixes
194 |
195 | * **auth:** redirect to login if token is expired ([fa3abbc](https://github.com/InCuca/vue-loopback/commit/fa3abbc))
196 |
197 |
198 | ### Features
199 |
200 | * **lint:** disable rule ([06108f4](https://github.com/InCuca/vue-loopback/commit/06108f4))
201 | * **loopback:** add setLoading fn ([92da75f](https://github.com/InCuca/vue-loopback/commit/92da75f))
202 |
203 |
204 |
205 |
206 | # [3.0.0](https://github.com/InCuca/vue-loopback/compare/2.1.0...3.0.0) (2018-02-28)
207 |
208 |
209 | ### Bug Fixes
210 |
211 | * **eslint:** disable resolver ([112395b](https://github.com/InCuca/vue-loopback/commit/112395b))
212 | * **eslint:** remove modules from ignore ([c9d0f1a](https://github.com/InCuca/vue-loopback/commit/c9d0f1a))
213 | * **eslintrc:** remove not existent plugin ([7534446](https://github.com/InCuca/vue-loopback/commit/7534446))
214 | * **gulp-tasks:** write sourcemaps to files instead of embed ([f4e2af3](https://github.com/InCuca/vue-loopback/commit/f4e2af3))
215 | * **lint:** fix auto-fixable errors ([ff283cc](https://github.com/InCuca/vue-loopback/commit/ff283cc))
216 | * **lint:** fix lint errors ([b7d673a](https://github.com/InCuca/vue-loopback/commit/b7d673a))
217 | * **lint:** fix lint errors ([b550d32](https://github.com/InCuca/vue-loopback/commit/b550d32))
218 | * **lint:** fix lint errors ([02dc21d](https://github.com/InCuca/vue-loopback/commit/02dc21d))
219 | * **lint:** fix lint errors ([982f0eb](https://github.com/InCuca/vue-loopback/commit/982f0eb))
220 | * **lint:** fix lint errors ([aa7df98](https://github.com/InCuca/vue-loopback/commit/aa7df98))
221 | * **lint:** fix lint errors ([348f1ad](https://github.com/InCuca/vue-loopback/commit/348f1ad))
222 | * **lint:** fix lint errors ([e8eb0b9](https://github.com/InCuca/vue-loopback/commit/e8eb0b9))
223 | * **lint:** fix lint errors and remove no-unresolved rule ([97a2062](https://github.com/InCuca/vue-loopback/commit/97a2062))
224 | * **lint:** fix linting errors ([d353c74](https://github.com/InCuca/vue-loopback/commit/d353c74))
225 | * **lint:** remove import settings ([ffd1574](https://github.com/InCuca/vue-loopback/commit/ffd1574))
226 | * **travis:** apply workaround travis-ci/travis-ci[#8836](https://github.com/InCuca/vue-loopback/issues/8836) ([9f5285b](https://github.com/InCuca/vue-loopback/commit/9f5285b))
227 |
228 |
229 | ### Features
230 |
231 | * **eslint:** add eslint to this repo itself ([3b20ee4](https://github.com/InCuca/vue-loopback/commit/3b20ee4))
232 | * **eslint:** ignore template directory ([8bcc688](https://github.com/InCuca/vue-loopback/commit/8bcc688))
233 | * **gulp:** add build:index ([31840ed](https://github.com/InCuca/vue-loopback/commit/31840ed))
234 | * **gulp:** build also index.js ([d23d144](https://github.com/InCuca/vue-loopback/commit/d23d144))
235 | * **lint:** add resolve script ([ed7ed68](https://github.com/InCuca/vue-loopback/commit/ed7ed68))
236 | * **lint:** update settings to airbnb - webpack template based ([df30173](https://github.com/InCuca/vue-loopback/commit/df30173))
237 | * **package:** update eslint ([3e350ae](https://github.com/InCuca/vue-loopback/commit/3e350ae))
238 | * **server:** add client server ([44bfb23](https://github.com/InCuca/vue-loopback/commit/44bfb23))
239 | * **travis:** remove node 6 and 7 ([90cc789](https://github.com/InCuca/vue-loopback/commit/90cc789))
240 |
241 |
242 |
243 |
244 | # [2.1.0](https://github.com/InCuca/vue-loopback/compare/2.0.1...2.1.0) (2018-02-20)
245 |
246 |
247 | ### Bug Fixes
248 |
249 | * **gulp:** fix gulp not reloading client ([f1aed3a](https://github.com/InCuca/vue-loopback/commit/f1aed3a))
250 | * **package:** fix font-awesome dependency ([8b21fc5](https://github.com/InCuca/vue-loopback/commit/8b21fc5))
251 | * **template:** add missing package copy ([5f1c62b](https://github.com/InCuca/vue-loopback/commit/5f1c62b))
252 | * **template:** fix lint errors ([f589041](https://github.com/InCuca/vue-loopback/commit/f589041))
253 |
254 |
255 | ### Features
256 |
257 | * **gulp:** add package to built file and add start script ([527e061](https://github.com/InCuca/vue-loopback/commit/527e061))
258 | * **loopback:** add token in local storage ([34d1613](https://github.com/InCuca/vue-loopback/commit/34d1613))
259 |
260 |
261 |
262 |
263 | ## [2.0.1](https://github.com/InCuca/vue-loopback/compare/2.0.0...2.0.1) (2017-12-15)
264 |
265 |
266 | ### Features
267 |
268 | * **babel:** change babel configuration ([81903ec](https://github.com/InCuca/vue-loopback/commit/81903ec))
269 |
270 |
271 |
272 |
273 | # [2.0.0](https://github.com/InCuca/vue-loopback/compare/1.0.2...2.0.0) (2017-12-13)
274 |
275 |
276 | ### Bug Fixes
277 |
278 | * **lint:** fix lint errors ([47475f1](https://github.com/InCuca/vue-loopback/commit/47475f1))
279 | * **meta:** fix wrong path name ([131f2ec](https://github.com/InCuca/vue-loopback/commit/131f2ec))
280 | * **readme:** add usage instructions ([8f5c4f5](https://github.com/InCuca/vue-loopback/commit/8f5c4f5))
281 | * **tempalte:** fix hello world component ([b6711ab](https://github.com/InCuca/vue-loopback/commit/b6711ab))
282 | * **template:** add missing dependencies and missing compilers file ([de88643](https://github.com/InCuca/vue-loopback/commit/de88643))
283 | * **template:** escape curly braces and translate strings to en ([e9cbf52](https://github.com/InCuca/vue-loopback/commit/e9cbf52))
284 | * **template:** fix css and translate strings to english ([0f31e93](https://github.com/InCuca/vue-loopback/commit/0f31e93))
285 | * **template:** fix misspeling ([8fa8ae3](https://github.com/InCuca/vue-loopback/commit/8fa8ae3))
286 | * **template:** fix template errors ([5d1e022](https://github.com/InCuca/vue-loopback/commit/5d1e022))
287 | * **template:** Fix wrong comonente filename ([b02518f](https://github.com/InCuca/vue-loopback/commit/b02518f))
288 | * **template:** fix wrong route and add create-admin test ([0942b36](https://github.com/InCuca/vue-loopback/commit/0942b36))
289 | * **template:** remove gitlab file ([92c2550](https://github.com/InCuca/vue-loopback/commit/92c2550))
290 | * **test:** add extended tests ([1897096](https://github.com/InCuca/vue-loopback/commit/1897096))
291 | * **test:** fix boot test ([f6b4a4a](https://github.com/InCuca/vue-loopback/commit/f6b4a4a))
292 | * **test:** fix lint error ([d865b79](https://github.com/InCuca/vue-loopback/commit/d865b79))
293 | * **test:** fix test commands ([1eb749c](https://github.com/InCuca/vue-loopback/commit/1eb749c))
294 | * **tests:** fix app test ([070b19e](https://github.com/InCuca/vue-loopback/commit/070b19e))
295 | * **tests:** add karma modulesify settings ([0ba8e18](https://github.com/InCuca/vue-loopback/commit/0ba8e18))
296 | * **tests:** add vuefy compiler to karma ([3e9e45e](https://github.com/InCuca/vue-loopback/commit/3e9e45e))
297 | * **tests:** fix test errors ([d8ae843](https://github.com/InCuca/vue-loopback/commit/d8ae843))
298 | * **tests:** fix test errors ([2f0a2e5](https://github.com/InCuca/vue-loopback/commit/2f0a2e5))
299 | * **tests:** fix tests for no extended build ([e9bb6de](https://github.com/InCuca/vue-loopback/commit/e9bb6de))
300 |
301 |
302 | ### Features
303 |
304 | * **template:** add extended option ([f08bb6f](https://github.com/InCuca/vue-loopback/commit/f08bb6f))
305 | * **tests:** rename and add more tests ([a71e8a6](https://github.com/InCuca/vue-loopback/commit/a71e8a6))
306 |
307 |
308 |
309 |
310 | ## [1.0.2](https://github.com/InCuca/vue-loopback/compare/1.0.1...1.0.2) (2017-10-29)
311 |
312 |
313 | ### Bug Fixes
314 |
315 | * **App:** fix databinding not working ([de7f8e6](https://github.com/InCuca/vue-loopback/commit/de7f8e6))
316 |
317 |
318 |
319 |
320 | ## [1.0.1](https://github.com/InCuca/vue-loopback/compare/722097d...1.0.1) (2017-10-29)
321 |
322 |
323 | ### Bug Fixes
324 |
325 | * **build:** add xvfb to travis config ([4442c6c](https://github.com/InCuca/vue-loopback/commit/4442c6c))
326 | * **build:** fix syntax errors ([2d4bb0f](https://github.com/InCuca/vue-loopback/commit/2d4bb0f))
327 | * **build:** remove start scripts ([8f9661e](https://github.com/InCuca/vue-loopback/commit/8f9661e))
328 | * **package:** add vue-cli as dependency ([722097d](https://github.com/InCuca/vue-loopback/commit/722097d))
329 | * **project:** add npm-debug to gitignore ([3043ea6](https://github.com/InCuca/vue-loopback/commit/3043ea6))
330 | * **test:** fix task finishing before test ([80d23fe](https://github.com/InCuca/vue-loopback/commit/80d23fe))
331 | * **test:** remove not used polyfill ([fa0eefd](https://github.com/InCuca/vue-loopback/commit/fa0eefd))
332 | * **test:** show tests output ([49573ef](https://github.com/InCuca/vue-loopback/commit/49573ef))
333 |
334 |
335 | ### Features
336 |
337 | * **App:** change prop to data ([825cc69](https://github.com/InCuca/vue-loopback/commit/825cc69))
338 | * **lint:** split eslint files ([7b1c31f](https://github.com/InCuca/vue-loopback/commit/7b1c31f))
339 | * **package:** add test scripts ([47c8f21](https://github.com/InCuca/vue-loopback/commit/47c8f21))
340 | * **template:** add hello prop ([5659f7e](https://github.com/InCuca/vue-loopback/commit/5659f7e))
341 | * **test:** add promise polyfill in client tests ([55afe8a](https://github.com/InCuca/vue-loopback/commit/55afe8a))
342 | * **test:** add support for client tests with karma ([a488810](https://github.com/InCuca/vue-loopback/commit/a488810))
343 | * **test:** change test platform to chrome ([b6af4f5](https://github.com/InCuca/vue-loopback/commit/b6af4f5))
344 | * **test:** split tests ([c079e2c](https://github.com/InCuca/vue-loopback/commit/c079e2c))
345 | * **travis:** remove travis from template and add chrome ([ea911fc](https://github.com/InCuca/vue-loopback/commit/ea911fc))
346 |
347 |
348 |
349 |
--------------------------------------------------------------------------------