From 7cf33ea9e0035a7c42e3ee1627b22ddbd688cd91 Mon Sep 17 00:00:00 2001 From: Konstantin Tarkus Date: Thu, 3 Aug 2017 10:52:04 +0300 Subject: [PATCH] Integrate Prettier (#41) --- .eslintrc | 7 - .eslintrc.js | 29 ++++ migrations/201612010000_initial.js | 103 +++++++++++--- package.json | 3 + seeds/data-sample.js | 95 ++++++++----- src/DataLoaders.js | 109 +++++++++------ src/app.js | 114 +++++++++------- src/email.js | 23 ++-- src/passport.js | 169 +++++++++++++++-------- src/routes/account.js | 45 ++++-- src/schema/Comment.js | 19 ++- src/schema/CommentType.js | 8 +- src/schema/EmailType.js | 7 +- src/schema/Node.js | 2 +- src/schema/Story.js | 50 +++++-- src/schema/StoryType.js | 8 +- src/server.js | 8 +- test/query.me.spec.js | 11 +- tools/build.js | 211 ++++++++++++++++------------- tools/db.js | 6 +- tools/publish.js | 40 +++++- tools/run.js | 95 +++++++------ tools/task.js | 15 +- yarn.lock | 47 +++++-- 24 files changed, 816 insertions(+), 408 deletions(-) delete mode 100644 .eslintrc create mode 100644 .eslintrc.js diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index d7479d96..00000000 --- a/.eslintrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "parser": "babel-eslint", - "extends": "airbnb-base", - "plugins": [ - "flowtype" - ] -} diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..047e4ee4 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,29 @@ +/** + * Node.js API Starter Kit (https://reactstarter.com/nodejs) + * + * Copyright © 2016-present Kriasoft, LLC. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE.txt file in the root directory of this source tree. + */ + +module.exports = { + parser: 'babel-eslint', + extends: [ + 'airbnb-base', + 'prettier', + ], + plugins: [ + 'flowtype', + 'prettier' + ], + rules: { + 'prettier/prettier': [ + 'error', + { + singleQuote: true, + trailingComma: 'all', + }, + ] + } +} diff --git a/migrations/201612010000_initial.js b/migrations/201612010000_initial.js index 9944d094..28e026c1 100644 --- a/migrations/201612010000_initial.js +++ b/migrations/201612010000_initial.js @@ -9,13 +9,17 @@ // Create database schema for storing user accounts, logins and authentication claims/tokens // Source https://github.com/membership/membership.db -module.exports.up = async (db) => { +module.exports.up = async db => { // User accounts - await db.schema.createTable('users', (table) => { + await db.schema.createTable('users', table => { // UUID v1mc reduces the negative side effect of using random primary keys // with respect to keyspace fragmentation on disk for the tables because it's time based // https://www.postgresql.org/docs/current/static/uuid-ossp.html - table.uuid('id').notNullable().defaultTo(db.raw('uuid_generate_v1mc()')).primary(); + table + .uuid('id') + .notNullable() + .defaultTo(db.raw('uuid_generate_v1mc()')) + .primary(); table.string('display_name', 100); table.string('image_url', 200); table.jsonb('emails').notNullable().defaultTo('[]'); @@ -23,8 +27,14 @@ module.exports.up = async (db) => { }); // External logins with security tokens (e.g. Google, Facebook, Twitter) - await db.schema.createTable('logins', (table) => { - table.uuid('user_id').notNullable().references('id').inTable('users').onDelete('CASCADE').onUpdate('CASCADE'); + await db.schema.createTable('logins', table => { + table + .uuid('user_id') + .notNullable() + .references('id') + .inTable('users') + .onDelete('CASCADE') + .onUpdate('CASCADE'); table.string('provider', 16).notNullable(); table.string('id', 36).notNullable(); table.string('username', 100); @@ -34,38 +44,91 @@ module.exports.up = async (db) => { table.primary(['provider', 'id']); }); - await db.schema.createTable('stories', (table) => { - table.uuid('id').notNullable().defaultTo(db.raw('uuid_generate_v1mc()')).primary(); - table.uuid('author_id').notNullable().references('id').inTable('users').onDelete('CASCADE').onUpdate('CASCADE'); + await db.schema.createTable('stories', table => { + table + .uuid('id') + .notNullable() + .defaultTo(db.raw('uuid_generate_v1mc()')) + .primary(); + table + .uuid('author_id') + .notNullable() + .references('id') + .inTable('users') + .onDelete('CASCADE') + .onUpdate('CASCADE'); table.string('title', 80).notNullable(); table.string('url', 200); table.text('text'); table.timestamps(false, true); }); - await db.schema.createTable('story_points', (table) => { - table.uuid('story_id').references('id').inTable('stories').onDelete('CASCADE').onUpdate('CASCADE'); - table.uuid('user_id').notNullable().references('id').inTable('users').onDelete('CASCADE').onUpdate('CASCADE'); + await db.schema.createTable('story_points', table => { + table + .uuid('story_id') + .references('id') + .inTable('stories') + .onDelete('CASCADE') + .onUpdate('CASCADE'); + table + .uuid('user_id') + .notNullable() + .references('id') + .inTable('users') + .onDelete('CASCADE') + .onUpdate('CASCADE'); table.primary(['story_id', 'user_id']); }); - await db.schema.createTable('comments', (table) => { - table.uuid('id').notNullable().defaultTo(db.raw('uuid_generate_v1mc()')).primary(); - table.uuid('story_id').notNullable().references('id').inTable('stories').onDelete('CASCADE').onUpdate('CASCADE'); - table.uuid('parent_id').references('id').inTable('comments').onDelete('CASCADE').onUpdate('CASCADE'); - table.uuid('author_id').notNullable().references('id').inTable('users').onDelete('CASCADE').onUpdate('CASCADE'); + await db.schema.createTable('comments', table => { + table + .uuid('id') + .notNullable() + .defaultTo(db.raw('uuid_generate_v1mc()')) + .primary(); + table + .uuid('story_id') + .notNullable() + .references('id') + .inTable('stories') + .onDelete('CASCADE') + .onUpdate('CASCADE'); + table + .uuid('parent_id') + .references('id') + .inTable('comments') + .onDelete('CASCADE') + .onUpdate('CASCADE'); + table + .uuid('author_id') + .notNullable() + .references('id') + .inTable('users') + .onDelete('CASCADE') + .onUpdate('CASCADE'); table.text('text'); table.timestamps(false, true); }); - await db.schema.createTable('comment_points', (table) => { - table.uuid('comment_id').references('id').inTable('comments').onDelete('CASCADE').onUpdate('CASCADE'); - table.uuid('user_id').notNullable().references('id').inTable('users').onDelete('CASCADE').onUpdate('CASCADE'); + await db.schema.createTable('comment_points', table => { + table + .uuid('comment_id') + .references('id') + .inTable('comments') + .onDelete('CASCADE') + .onUpdate('CASCADE'); + table + .uuid('user_id') + .notNullable() + .references('id') + .inTable('users') + .onDelete('CASCADE') + .onUpdate('CASCADE'); table.primary(['comment_id', 'user_id']); }); }; -module.exports.down = async (db) => { +module.exports.down = async db => { await db.schema.dropTableIfExists('comment_points'); await db.schema.dropTableIfExists('comments'); await db.schema.dropTableIfExists('story_points'); diff --git a/package.json b/package.json index 6a1ace3b..c4fb0da0 100644 --- a/package.json +++ b/package.json @@ -51,10 +51,13 @@ "chokidar": "^1.7.0", "eslint": "^4.3.0", "eslint-config-airbnb-base": "^11.3.1", + "eslint-config-prettier": "^2.3.0", "eslint-plugin-flowtype": "^2.35.0", "eslint-plugin-import": "^2.7.0", + "eslint-plugin-prettier": "^2.1.2", "flow-bin": "^0.51.1", "mocha": "^3.5.0", + "prettier": "^1.5.3", "rimraf": "^2.6.1" }, "scripts": { diff --git a/seeds/data-sample.js b/seeds/data-sample.js index 986dc0a2..cb1826a1 100644 --- a/seeds/data-sample.js +++ b/seeds/data-sample.js @@ -11,45 +11,78 @@ const faker = require('faker'); -module.exports.seed = async (db) => { +module.exports.seed = async db => { // Create 10 random website users (as an example) const users = Array.from({ length: 10 }).map(() => ({ display_name: faker.name.findName(), image_url: faker.internet.avatar(), - emails: JSON.stringify([{ email: faker.internet.email().toLowerCase(), verified: false }]), + emails: JSON.stringify([ + { email: faker.internet.email().toLowerCase(), verified: false }, + ]), })); - await Promise.all(users.map(user => - db.table('users').insert(user).returning('id') - .then(rows => db.table('users').where('id', '=', rows[0]).first('*')) - .then(row => Object.assign(user, row)))); + await Promise.all( + users.map(user => + db + .table('users') + .insert(user) + .returning('id') + .then(rows => db.table('users').where('id', '=', rows[0]).first('*')) + .then(row => Object.assign(user, row)), + ), + ); // Create 500 stories - const stories = Array.from({ length: 500 }).map(() => Object.assign( - { - author_id: users[faker.random.number({ min: 0, max: users.length - 1 })].id, - title: faker.lorem.sentence(faker.random.number({ min: 4, max: 7 })) - .slice(0, -1).substr(0, 80), - }, - Math.random() > 0.3 ? { text: faker.lorem.text() } : { url: faker.internet.url() }, - (date => ({ created_at: date, updated_at: date }))(faker.date.past()))); - - await Promise.all(stories.map(story => - db.table('stories').insert(story).returning('id') - .then(rows => db.table('stories').where('id', '=', rows[0]).first('*')) - .then(row => Object.assign(story, row)))); + const stories = Array.from({ length: 500 }).map(() => + Object.assign( + { + author_id: + users[faker.random.number({ min: 0, max: users.length - 1 })].id, + title: faker.lorem + .sentence(faker.random.number({ min: 4, max: 7 })) + .slice(0, -1) + .substr(0, 80), + }, + Math.random() > 0.3 + ? { text: faker.lorem.text() } + : { url: faker.internet.url() }, + (date => ({ created_at: date, updated_at: date }))(faker.date.past()), + ), + ); + + await Promise.all( + stories.map(story => + db + .table('stories') + .insert(story) + .returning('id') + .then(rows => db.table('stories').where('id', '=', rows[0]).first('*')) + .then(row => Object.assign(story, row)), + ), + ); // Create some user comments - const comments = Array.from({ length: 2000 }).map(() => Object.assign( - { - story_id: stories[faker.random.number({ min: 0, max: stories.length - 1 })].id, - author_id: users[faker.random.number({ min: 0, max: users.length - 1 })].id, - text: faker.lorem.sentences(faker.random.number({ min: 1, max: 10 })), - }, - (date => ({ created_at: date, updated_at: date }))(faker.date.past()))); - - await Promise.all(comments.map(comment => - db.table('comments').insert(comment).returning('id') - .then(rows => db.table('comments').where('id', '=', rows[0]).first('*')) - .then(row => Object.assign(comment, row)))); + const comments = Array.from({ length: 2000 }).map(() => + Object.assign( + { + story_id: + stories[faker.random.number({ min: 0, max: stories.length - 1 })].id, + author_id: + users[faker.random.number({ min: 0, max: users.length - 1 })].id, + text: faker.lorem.sentences(faker.random.number({ min: 1, max: 10 })), + }, + (date => ({ created_at: date, updated_at: date }))(faker.date.past()), + ), + ); + + await Promise.all( + comments.map(comment => + db + .table('comments') + .insert(comment) + .returning('id') + .then(rows => db.table('comments').where('id', '=', rows[0]).first('*')) + .then(row => Object.assign(comment, row)), + ), + ); }; diff --git a/src/DataLoaders.js b/src/DataLoaders.js index e87050d4..21d14a87 100644 --- a/src/DataLoaders.js +++ b/src/DataLoaders.js @@ -53,45 +53,74 @@ function mapToValues(keys, keyFn, valueFn, rows) { export default { create: () => ({ - users: new DataLoader(keys => db.table('users') - .whereIn('id', keys).select() - .then(mapTo(keys, x => x.id, 'User'))), - - stories: new DataLoader(keys => db.table('stories') - .whereIn('id', keys).select() - .then(mapTo(keys, x => x.id, 'Story'))), - - storyCommentsCount: new DataLoader(keys => db.table('stories') - .leftJoin('comments', 'stories.id', 'comments.story_id') - .whereIn('stories.id', keys) - .groupBy('stories.id') - .select('stories.id', db.raw('count(comments.story_id)')) - .then(mapToValues(keys, x => x.id, x => x.count))), - - storyPointsCount: new DataLoader(keys => db.table('stories') - .leftJoin('story_points', 'stories.id', 'story_points.story_id') - .whereIn('stories.id', keys) - .groupBy('stories.id') - .select('stories.id', db.raw('count(story_points.story_id)')) - .then(mapToValues(keys, x => x.id, x => x.count))), - - comments: new DataLoader(keys => db.table('comments') - .whereIn('id', keys).select() - .then(mapTo(keys, x => x.id, 'Comment'))), - - commentsByStoryId: new DataLoader(keys => db.table('comments') - .whereIn('story_id', keys).select() - .then(mapToMany(keys, x => x.story_id, 'Comment'))), - - commentsByParentId: new DataLoader(keys => db.table('comments') - .whereIn('parent_id', keys).select() - .then(mapToMany(keys, x => x.parent_id, 'Comment'))), - - commentPointsCount: new DataLoader(keys => db.table('comments') - .leftJoin('comment_points', 'comments.id', 'comment_points.comment_id') - .whereIn('comments.id', keys) - .groupBy('comments.id') - .select('comments.id', db.raw('count(comment_points.comment_id)')) - .then(mapToValues(keys, x => x.id, x => x.count))), + users: new DataLoader(keys => + db + .table('users') + .whereIn('id', keys) + .select() + .then(mapTo(keys, x => x.id, 'User')), + ), + + stories: new DataLoader(keys => + db + .table('stories') + .whereIn('id', keys) + .select() + .then(mapTo(keys, x => x.id, 'Story')), + ), + + storyCommentsCount: new DataLoader(keys => + db + .table('stories') + .leftJoin('comments', 'stories.id', 'comments.story_id') + .whereIn('stories.id', keys) + .groupBy('stories.id') + .select('stories.id', db.raw('count(comments.story_id)')) + .then(mapToValues(keys, x => x.id, x => x.count)), + ), + + storyPointsCount: new DataLoader(keys => + db + .table('stories') + .leftJoin('story_points', 'stories.id', 'story_points.story_id') + .whereIn('stories.id', keys) + .groupBy('stories.id') + .select('stories.id', db.raw('count(story_points.story_id)')) + .then(mapToValues(keys, x => x.id, x => x.count)), + ), + + comments: new DataLoader(keys => + db + .table('comments') + .whereIn('id', keys) + .select() + .then(mapTo(keys, x => x.id, 'Comment')), + ), + + commentsByStoryId: new DataLoader(keys => + db + .table('comments') + .whereIn('story_id', keys) + .select() + .then(mapToMany(keys, x => x.story_id, 'Comment')), + ), + + commentsByParentId: new DataLoader(keys => + db + .table('comments') + .whereIn('parent_id', keys) + .select() + .then(mapToMany(keys, x => x.parent_id, 'Comment')), + ), + + commentPointsCount: new DataLoader(keys => + db + .table('comments') + .leftJoin('comment_points', 'comments.id', 'comment_points.comment_id') + .whereIn('comments.id', keys) + .groupBy('comments.id') + .select('comments.id', db.raw('count(comment_points.comment_id)')) + .then(mapToValues(keys, x => x.id, x => x.count)), + ), }), }; diff --git a/src/app.js b/src/app.js index e1c27216..002f6954 100644 --- a/src/app.js +++ b/src/app.js @@ -18,7 +18,9 @@ import session from 'express-session'; import connectRedis from 'connect-redis'; import flash from 'express-flash'; import i18next from 'i18next'; -import i18nextMiddleware, { LanguageDetector } from 'i18next-express-middleware'; +import i18nextMiddleware, { + LanguageDetector, +} from 'i18next-express-middleware'; import i18nextBackend from 'i18next-node-fs-backend'; import expressGraphQL from 'express-graphql'; import PrettyError from 'pretty-error'; @@ -30,44 +32,47 @@ import schema from './schema'; import DataLoaders from './DataLoaders'; import accountRoutes from './routes/account'; -i18next - .use(LanguageDetector) - .use(i18nextBackend) - .init({ - preload: ['en', 'de'], - ns: ['common', 'email'], - fallbackNS: 'common', - detection: { - lookupCookie: 'lng', - }, - backend: { - loadPath: path.resolve(__dirname, '../locales/{{lng}}/{{ns}}.json'), - addPath: path.resolve(__dirname, '../locales/{{lng}}/{{ns}}.missing.json'), - }, - }); +i18next.use(LanguageDetector).use(i18nextBackend).init({ + preload: ['en', 'de'], + ns: ['common', 'email'], + fallbackNS: 'common', + detection: { + lookupCookie: 'lng', + }, + backend: { + loadPath: path.resolve(__dirname, '../locales/{{lng}}/{{ns}}.json'), + addPath: path.resolve(__dirname, '../locales/{{lng}}/{{ns}}.missing.json'), + }, +}); const app = express(); app.set('trust proxy', 'loopback'); -app.use(cors({ - origin(origin, cb) { - const whitelist = process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(',') : []; - cb(null, whitelist.includes(origin)); - }, - credentials: true, -})); +app.use( + cors({ + origin(origin, cb) { + const whitelist = process.env.CORS_ORIGIN + ? process.env.CORS_ORIGIN.split(',') + : []; + cb(null, whitelist.includes(origin)); + }, + credentials: true, + }), +); app.use(cookieParser()); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); -app.use(session({ - store: new (connectRedis(session))({ client: redis }), - name: 'sid', - resave: true, - saveUninitialized: true, - secret: process.env.SESSION_SECRET, -})); +app.use( + session({ + store: new (connectRedis(session))({ client: redis }), + name: 'sid', + resave: true, + saveUninitialized: true, + secret: process.env.SESSION_SECRET, + }), +); app.use(i18nextMiddleware.handle(i18next)); app.use(passport.initialize()); app.use(passport.session()); @@ -79,22 +84,25 @@ app.get('/graphql/schema', (req, res) => { res.type('text/plain').send(printSchema(schema)); }); -app.use('/graphql', expressGraphQL(req => ({ - schema, - context: { - t: req.t, - user: req.user, - ...DataLoaders.create(), - }, - graphiql: process.env.NODE_ENV !== 'production', - pretty: process.env.NODE_ENV !== 'production', - formatError: error => ({ - message: error.message, - state: error.originalError && error.originalError.state, - locations: error.locations, - path: error.path, - }), -}))); +app.use( + '/graphql', + expressGraphQL(req => ({ + schema, + context: { + t: req.t, + user: req.user, + ...DataLoaders.create(), + }, + graphiql: process.env.NODE_ENV !== 'production', + pretty: process.env.NODE_ENV !== 'production', + formatError: error => ({ + message: error.message, + state: error.originalError && error.originalError.state, + locations: error.locations, + path: error.path, + }), + })), +); // The following routes are intended to be used in development mode only if (process.env.NODE_ENV !== 'production') { @@ -107,9 +115,19 @@ if (process.env.NODE_ENV !== 'production') { // A route for testing authentication/authorization app.get('/', (req, res) => { if (req.user) { - res.send(`

${req.t('Welcome, {{user}}!', { user: req.user.displayName })} (${req.t('log out')})

`); + res.send( + `

${req.t('Welcome, {{user}}!', { + user: req.user.displayName, + })} (${req.t( + 'log out', + )})

`, + ); } else { - res.send(`

${req.t('Welcome, guest!')} (${req.t('sign in')})

`); + res.send( + `

${req.t('Welcome, guest!')} (${req.t( + 'sign in', + )})

`, + ); } }); } diff --git a/src/email.js b/src/email.js index 82beaeb0..37623eb7 100644 --- a/src/email.js +++ b/src/email.js @@ -16,20 +16,25 @@ import handlebars from 'handlebars'; // TODO: Configure email transport for the production environment // https://nodemailer.com/smtp/ -const { from, ...config } = process.env.NODE_ENV === 'production' ? { - from: 'no-reply@example.com', - streamTransport: true, -} : { - from: 'no-reply@example.com', - streamTransport: true, -}; +const { from, ...config } = + process.env.NODE_ENV === 'production' + ? { + from: 'no-reply@example.com', + streamTransport: true, + } + : { + from: 'no-reply@example.com', + streamTransport: true, + }; const templates = new Map(); const baseDir = path.resolve(__dirname, 'emails'); const transporter = nodemailer.createTransport(config, { from }); // Register i18n translation helper, for example: {{t "Welcome, {{user}}" user="John"}} -handlebars.registerHelper('t', (key, options) => options.data.root.t(key, options.hash)); +handlebars.registerHelper('t', (key, options) => + options.data.root.t(key, options.hash), +); function loadTemplate(filename) { const m = new module.constructor(); @@ -56,7 +61,7 @@ export default { */ render(name: string, context: any = {}) { if (!templates.size) { - fs.readdirSync(baseDir).forEach((template) => { + fs.readdirSync(baseDir).forEach(template => { if (fs.statSync(`${baseDir}/${template}`).isDirectory()) { templates.set(template, { subject: loadTemplate(`${baseDir}/${template}/subject.js`), diff --git a/src/passport.js b/src/passport.js index 65e8d4b7..756a7d47 100644 --- a/src/passport.js +++ b/src/passport.js @@ -40,32 +40,53 @@ async function login(req, provider, profile, tokens) { } if (!user) { - user = await db.table('logins') + user = await db + .table('logins') .innerJoin('users', 'users.id', 'logins.user_id') .where({ 'logins.provider': provider, 'logins.id': profile.id }) .first('users.*'); - if (!user && profile.emails && profile.emails.length && profile.emails[0].verified === true) { - user = await db.table('users') - .where('emails', '@>', JSON.stringify([{ email: profile.emails[0].value, verified: true }])) + if ( + !user && + profile.emails && + profile.emails.length && + profile.emails[0].verified === true + ) { + user = await db + .table('users') + .where( + 'emails', + '@>', + JSON.stringify([{ email: profile.emails[0].value, verified: true }]), + ) .first(); } } if (!user) { - user = (await db.table('users') + user = (await db + .table('users') .insert({ display_name: profile.displayName, - emails: JSON.stringify((profile.emails || []).map(x => ({ - email: x.value, - verified: x.verified || false, - }))), - image_url: profile.photos && profile.photos.length ? profile.photos[0].value : null, + emails: JSON.stringify( + (profile.emails || []).map(x => ({ + email: x.value, + verified: x.verified || false, + })), + ), + image_url: + profile.photos && profile.photos.length + ? profile.photos[0].value + : null, }) .returning('*'))[0]; } const loginKeys = { user_id: user.id, provider, id: profile.id }; - const { count } = await db.table('logins').where(loginKeys).count('id').first(); + const { count } = await db + .table('logins') + .where(loginKeys) + .count('id') + .first(); if (count === '0') { await db.table('logins').insert({ @@ -92,54 +113,90 @@ async function login(req, provider, profile, tokens) { } // https://github.com/jaredhanson/passport-google-oauth2 -passport.use(new GoogleStrategy({ - clientID: process.env.GOOGLE_ID, - clientSecret: process.env.GOOGLE_SECRET, - callbackURL: '/login/google/return', - passReqToCallback: true, -}, async (req, accessToken, refreshToken, profile, done) => { - try { - const user = await login(req, 'google', profile, { accessToken, refreshToken }); - done(null, user); - } catch (err) { - done(err); - } -})); +passport.use( + new GoogleStrategy( + { + clientID: process.env.GOOGLE_ID, + clientSecret: process.env.GOOGLE_SECRET, + callbackURL: '/login/google/return', + passReqToCallback: true, + }, + async (req, accessToken, refreshToken, profile, done) => { + try { + const user = await login(req, 'google', profile, { + accessToken, + refreshToken, + }); + done(null, user); + } catch (err) { + done(err); + } + }, + ), +); // https://github.com/jaredhanson/passport-facebook -passport.use(new FacebookStrategy({ - clientID: process.env.FACEBOOK_ID, - clientSecret: process.env.FACEBOOK_SECRET, - profileFields: ['name', 'email', 'picture', 'link', 'locale', 'timezone', 'verified'], - callbackURL: '/login/facebook/return', - passReqToCallback: true, -}, async (req, accessToken, refreshToken, profile, done) => { - try { - if (profile.emails.length) profile.emails[0].verified = !!profile._json.verified; - profile.displayName = profile.displayName || `${profile.name.givenName} ${profile.name.familyName}`; - const user = await login(req, 'facebook', profile, { accessToken, refreshToken }); - done(null, user); - } catch (err) { - done(err); - } -})); +passport.use( + new FacebookStrategy( + { + clientID: process.env.FACEBOOK_ID, + clientSecret: process.env.FACEBOOK_SECRET, + profileFields: [ + 'name', + 'email', + 'picture', + 'link', + 'locale', + 'timezone', + 'verified', + ], + callbackURL: '/login/facebook/return', + passReqToCallback: true, + }, + async (req, accessToken, refreshToken, profile, done) => { + try { + if (profile.emails.length) + profile.emails[0].verified = !!profile._json.verified; + profile.displayName = + profile.displayName || + `${profile.name.givenName} ${profile.name.familyName}`; + const user = await login(req, 'facebook', profile, { + accessToken, + refreshToken, + }); + done(null, user); + } catch (err) { + done(err); + } + }, + ), +); // https://github.com/jaredhanson/passport-twitter -passport.use(new TwitterStrategy({ - consumerKey: process.env.TWITTER_KEY, - consumerSecret: process.env.TWITTER_SECRET, - callbackURL: '/login/twitter/return', - includeEmail: true, - includeStatus: false, - passReqToCallback: true, -}, async (req, token, tokenSecret, profile, done) => { - try { - if (profile.emails && profile.emails.length) profile.emails[0].verified = true; - const user = await login(req, 'twitter', profile, { token, tokenSecret }); - done(null, user); - } catch (err) { - done(err); - } -})); +passport.use( + new TwitterStrategy( + { + consumerKey: process.env.TWITTER_KEY, + consumerSecret: process.env.TWITTER_SECRET, + callbackURL: '/login/twitter/return', + includeEmail: true, + includeStatus: false, + passReqToCallback: true, + }, + async (req, token, tokenSecret, profile, done) => { + try { + if (profile.emails && profile.emails.length) + profile.emails[0].verified = true; + const user = await login(req, 'twitter', profile, { + token, + tokenSecret, + }); + done(null, user); + } catch (err) { + done(err); + } + }, + ), +); export default passport; diff --git a/src/routes/account.js b/src/routes/account.js index 582d14e9..90364a65 100644 --- a/src/routes/account.js +++ b/src/routes/account.js @@ -43,12 +43,16 @@ function getOrigin(url: string) { // 'http://localhost:3000/about' => `true` (but only if its origin is whitelisted) function isValidReturnURL(url: string) { if (url.startsWith('/')) return true; - const whitelist = process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(',') : []; - return validator.isURL(url, { - require_tld: false, - require_protocol: true, - protocols: ['http', 'https'], - }) && whitelist.includes(getOrigin(url)); + const whitelist = process.env.CORS_ORIGIN + ? process.env.CORS_ORIGIN.split(',') + : []; + return ( + validator.isURL(url, { + require_tld: false, + require_protocol: true, + protocols: ['http', 'https'], + }) && whitelist.includes(getOrigin(url)) + ); } // Generates a URL for redirecting a user to upon successfull authentication. @@ -63,30 +67,43 @@ function getSuccessRedirect(req) { const url = req.query.return || req.body.return || '/'; if (!isValidReturnURL(url)) return '/'; if (!getOrigin(url)) return url; - return `${url}${url.includes('?') ? '&' : '?'}sessionID=${req.cookies.sid}${ - req.session.cookie.originalMaxAge ? `&maxAge=${req.session.cookie.originalMaxAge}` : ''}`; + return `${url}${url.includes('?') ? '&' : '?'}sessionID=${req.cookies + .sid}${req.session.cookie.originalMaxAge + ? `&maxAge=${req.session.cookie.originalMaxAge}` + : ''}`; } // Registers route handlers for the external login providers loginProviders.forEach(({ provider, options }) => { - router.get(`/login/${provider}`, - (req, res, next) => { req.session.returnTo = getSuccessRedirect(req); next(); }, - passport.authenticate(provider, { failureFlash: true, ...options })); + router.get( + `/login/${provider}`, + (req, res, next) => { + req.session.returnTo = getSuccessRedirect(req); + next(); + }, + passport.authenticate(provider, { failureFlash: true, ...options }), + ); router.get(`/login/${provider}/return`, (req, res, next) => passport.authenticate(provider, { successReturnToOrRedirect: true, failureFlash: true, failureRedirect: `${getOrigin(req.session.returnTo)}/login`, - })(req, res, next)); + })(req, res, next), + ); }); // Remove the `user` object from the session. Example: // fetch('/login/clear', { method: 'POST', credentials: 'include' }) // .then(() => window.location = '/') -router.post('/login/clear', (req, res) => { req.logout(); res.status(200).send('OK'); }); +router.post('/login/clear', (req, res) => { + req.logout(); + res.status(200).send('OK'); +}); // Allows to fetch the last login error(s) (which is usefull for single-page apps) -router.post('/login/error', (req, res) => { res.send({ errors: req.flash('error') }); }); +router.post('/login/error', (req, res) => { + res.send({ errors: req.flash('error') }); +}); export default router; diff --git a/src/schema/Comment.js b/src/schema/Comment.js index 8f77be8d..ffea2b2c 100644 --- a/src/schema/Comment.js +++ b/src/schema/Comment.js @@ -28,13 +28,21 @@ function validate(input, { t, user }) { const data = {}; if (!user) { - throw new ValidationError([{ key: '', message: t('Only authenticated users can add comments.') }]); + throw new ValidationError([ + { key: '', message: t('Only authenticated users can add comments.') }, + ]); } if (typeof input.text === 'undefined' || input.text.trim() !== '') { - errors.push({ key: 'text', message: t('The comment field cannot be empty.') }); + errors.push({ + key: 'text', + message: t('The comment field cannot be empty.'), + }); } else if (!validator.isLength(input.text, { min: 20, max: 2000 })) { - errors.push({ key: 'text', message: t('The comment must be between 20 and 2000 characters long.') }); + errors.push({ + key: 'text', + message: t('The comment must be between 20 and 2000 characters long.'), + }); } else { data.text = input.text; } @@ -108,7 +116,10 @@ export const updateComment = mutationWithClientMutationId({ const comment = await db.table('comments').where('id', '=', id).first('*'); if (!comment) { - errors.push({ key: '', message: 'Failed to save the comment. Please make sure that it exists.' }); + errors.push({ + key: '', + message: 'Failed to save the comment. Please make sure that it exists.', + }); } else if (comment.author_id !== user.id) { errors.push({ key: '', message: 'You can only edit your own comments.' }); } diff --git a/src/schema/CommentType.js b/src/schema/CommentType.js index 7f71a039..5f50c53e 100644 --- a/src/schema/CommentType.js +++ b/src/schema/CommentType.js @@ -9,7 +9,13 @@ /* @flow */ -import { GraphQLObjectType, GraphQLList, GraphQLNonNull, GraphQLInt, GraphQLString } from 'graphql'; +import { + GraphQLObjectType, + GraphQLList, + GraphQLNonNull, + GraphQLInt, + GraphQLString, +} from 'graphql'; import { globalIdField } from 'graphql-relay'; import { nodeInterface } from './Node'; diff --git a/src/schema/EmailType.js b/src/schema/EmailType.js index b5c6a32f..3ebeb1a9 100644 --- a/src/schema/EmailType.js +++ b/src/schema/EmailType.js @@ -9,7 +9,12 @@ /* @flow */ -import { GraphQLObjectType, GraphQLNonNull, GraphQLString, GraphQLBoolean } from 'graphql'; +import { + GraphQLObjectType, + GraphQLNonNull, + GraphQLString, + GraphQLBoolean, +} from 'graphql'; export default new GraphQLObjectType({ name: 'Email', diff --git a/src/schema/Node.js b/src/schema/Node.js index bd23f06d..bf1e5b83 100644 --- a/src/schema/Node.js +++ b/src/schema/Node.js @@ -22,7 +22,7 @@ const { nodeInterface, nodeField: node, nodesField: nodes } = nodeDefinitions( return null; }, - (obj) => { + obj => { if (obj.__type === 'User') return require('./UserType').default; if (obj.__type === 'Story') return require('./StoryType').default; if (obj.__type === 'Comment') return require('./CommentType').default; diff --git a/src/schema/Story.js b/src/schema/Story.js index 3338d995..87a7c82b 100644 --- a/src/schema/Story.js +++ b/src/schema/Story.js @@ -38,12 +38,13 @@ export const stories = { const offset = args.after ? cursorToOffset(args.after) + 1 : 0; const [data, totalCount] = await Promise.all([ - db.table('stories') + db + .table('stories') .orderBy('created_at', 'desc') - .limit(limit).offset(offset) + .limit(limit) + .offset(offset) .then(rows => rows.map(x => Object.assign(x, { __type: 'Story' }))), - db.table('stories') - .count().then(x => x[0].count), + db.table('stories').count().then(x => x[0].count), ]); return { @@ -79,20 +80,31 @@ function validate(input, { t, user }) { const data = {}; if (!user) { - throw new ValidationError([{ key: '', message: t('Only authenticated users can create stories.') }]); + throw new ValidationError([ + { key: '', message: t('Only authenticated users can create stories.') }, + ]); } if (typeof input.title === 'undefined' || input.title.trim() === '') { - errors.push({ key: 'title', message: t('The title field cannot be empty.') }); + errors.push({ + key: 'title', + message: t('The title field cannot be empty.'), + }); } else if (!validator.isLength(input.title, { min: 3, max: 80 })) { - errors.push({ key: 'title', message: t('The title field must be between 3 and 80 characters long.') }); + errors.push({ + key: 'title', + message: t('The title field must be between 3 and 80 characters long.'), + }); } else { data.title = input.title; } if (typeof input.url !== 'undefined' && input.url.trim() !== '') { if (!validator.isLength(input.url, { max: 200 })) { - errors.push({ key: 'url', message: t('The URL field cannot be longer than 200 characters long.') }); + errors.push({ + key: 'url', + message: t('The URL field cannot be longer than 200 characters long.'), + }); } else if (!validator.isURL(input.url)) { errors.push({ key: 'url', message: t('The URL is invalid.') }); } else { @@ -102,16 +114,27 @@ function validate(input, { t, user }) { if (typeof input.text !== 'undefined' && input.text.trim() !== '') { if (!validator.isLength(input.text, { min: 20, max: 2000 })) { - errors.push({ key: 'text', message: t('The text field must be between 20 and 2000 characters long.') }); + errors.push({ + key: 'text', + message: t( + 'The text field must be between 20 and 2000 characters long.', + ), + }); } else { data.text = input.text; } } if (data.url && data.text) { - errors.push({ key: '', message: t('Please fill either the URL or the text field but not both.') }); + errors.push({ + key: '', + message: t('Please fill either the URL or the text field but not both.'), + }); } else if (!input.url && !input.text) { - errors.push({ key: '', message: t('Please fill either the URL or the text field.') }); + errors.push({ + key: '', + message: t('Please fill either the URL or the text field.'), + }); } data.author_id = user.id; @@ -153,7 +176,10 @@ export const updateStory = mutationWithClientMutationId({ const story = await db.table('stories').where('id', '=', id).first('*'); if (!story) { - errors.push({ key: '', message: 'Failed to save the story. Please make sure that it exists.' }); + errors.push({ + key: '', + message: 'Failed to save the story. Please make sure that it exists.', + }); } else if (story.author_id !== user.id) { errors.push({ key: '', message: 'You can only edit your own stories.' }); } diff --git a/src/schema/StoryType.js b/src/schema/StoryType.js index af5d9f13..bc92c925 100644 --- a/src/schema/StoryType.js +++ b/src/schema/StoryType.js @@ -9,7 +9,13 @@ /* @flow */ -import { GraphQLObjectType, GraphQLList, GraphQLNonNull, GraphQLInt, GraphQLString } from 'graphql'; +import { + GraphQLObjectType, + GraphQLList, + GraphQLNonNull, + GraphQLInt, + GraphQLString, +} from 'graphql'; import { globalIdField } from 'graphql-relay'; import { nodeInterface } from './Node'; diff --git a/src/server.js b/src/server.js index d8897a2a..17ce1a24 100644 --- a/src/server.js +++ b/src/server.js @@ -28,8 +28,12 @@ function handleExit(options, err) { const actions = [server.close, db.destroy, redis.quit]; actions.forEach((close, i) => { try { - close(() => { if (i === actions.length - 1) process.exit(); }); - } catch (err) { if (i === actions.length - 1) process.exit(); } + close(() => { + if (i === actions.length - 1) process.exit(); + }); + } catch (err) { + if (i === actions.length - 1) process.exit(); + } }); } if (err) console.log(err.stack); diff --git a/test/query.me.spec.js b/test/query.me.spec.js index f5deee49..54cee444 100644 --- a/test/query.me.spec.js +++ b/test/query.me.spec.js @@ -14,10 +14,12 @@ import app from '../src/app'; chai.use(chaiHttp); describe('query.me', () => { - it('.me must be null if user is not authenticated', (done) => { - chai.request(app) + it('.me must be null if user is not authenticated', done => { + chai + .request(app) .post('/graphql') - .send({ query: `query { + .send({ + query: `query { me { id displayName @@ -27,7 +29,8 @@ describe('query.me', () => { verified } } - }` }) + }`, + }) .end((err, res) => { expect(err).to.be.null; expect(res).to.have.status(200); diff --git a/tools/build.js b/tools/build.js index e3e9204e..f76ade37 100755 --- a/tools/build.js +++ b/tools/build.js @@ -20,114 +20,141 @@ const task = require('./task'); handlebarsLayouts.register(handlebars); -const delay100ms = (timeout => (callback) => { +const delay100ms = (timeout => callback => { if (timeout) clearTimeout(timeout); timeout = setTimeout(callback, 100); // eslint-disable-line no-param-reassign })(); // Pre-compile email templates to avoid unnecessary parsing at run time. See `src/emails`. -const compileEmail = (filename) => { - fs.readdirSync('src/emails').forEach((file) => { +const compileEmail = filename => { + fs.readdirSync('src/emails').forEach(file => { if (file.endsWith('.hbs')) { - const partial = fs.readFileSync(`src/emails/${file}`, 'utf8') + const partial = fs + .readFileSync(`src/emails/${file}`, 'utf8') .replace(/{{/g, '\\{{') .replace(/\\{{(#block|\/block)/g, '{{$1'); handlebars.registerPartial(file.substr(0, file.length - 4), partial); } }); - const template = fs.readFileSync(filename, 'utf8') + const template = fs + .readFileSync(filename, 'utf8') .replace(/{{/g, '\\{{') .replace(/\\{{(#extend|\/extend|#content|\/content)/g, '{{$1'); return handlebars.precompile(juice(handlebars.compile(template)({}))); }; -module.exports = task('build', ({ watch = false, onComplete } = {}) => new Promise((resolve) => { - let ready = false; - - // Clean up the output directory - rimraf.sync('build/*', { nosort: true, dot: true }); - - let watcher = chokidar.watch(['locales', 'src', 'package.json', 'yarn.lock']); - watcher.on('all', (event, src) => { - // Reload the app if package.json or yarn.lock files have changed (in watch mode) - if (src === 'package.json' || src === 'yarn.lock') { - if (ready && onComplete) delay100ms(onComplete); - return; - } - - // Skip files starting with a dot, e.g. .DS_Store, .eslintrc etc. - if (path.basename(src)[0] === '.') return; - - // Get destination file name, e.g. src/app.js (src) -> build/app.js (dest) - const dest = src.startsWith('src') ? `build/${path.relative('src', src)}` : `build/${src}`; - - try { - switch (event) { - // Create a directory if it doesn't exist - case 'addDir': - if (src.startsWith('src') && !fs.existsSync(dest)) fs.mkdirSync(dest); - if (ready && onComplete) onComplete(); - break; - - // Create or update a file inside the output (build) folder - case 'add': - case 'change': - if (src.startsWith('src') && src.endsWith('.js')) { - const { code, map } = babel.transformFileSync(src, { - sourceMaps: true, - sourceFileName: path.relative(path.dirname(`build${src.substr(3)}`), src), - }); - // Enable source maps - const data = (src === 'src/server.js' ? - 'require(\'source-map-support\').install(); ' : '') + code + - (map ? `\n//# sourceMappingURL=${path.basename(src)}.map\n` : ''); - fs.writeFileSync(dest, data, 'utf8'); - console.log(src, '->', dest); - if (map) fs.writeFileSync(`${dest}.map`, JSON.stringify(map), 'utf8'); - } else if (/^src\/emails\/.+/.test(src)) { - if (/^src\/emails\/.+\/.+\.hbs$/.test(src)) { - const template = compileEmail(src); - const destJs = dest.replace(/\.hbs$/, '.js'); - fs.writeFileSync(destJs, `module.exports = ${template};`, 'utf8'); - console.log(src, '->', destJs); - } - } else if (src.startsWith('src')) { - const data = fs.readFileSync(src, 'utf8'); - fs.writeFileSync(dest, data, 'utf8'); - console.log(src, '->', dest); - } +module.exports = task( + 'build', + ({ watch = false, onComplete } = {}) => + new Promise(resolve => { + let ready = false; + + // Clean up the output directory + rimraf.sync('build/*', { nosort: true, dot: true }); + + let watcher = chokidar.watch([ + 'locales', + 'src', + 'package.json', + 'yarn.lock', + ]); + watcher.on('all', (event, src) => { + // Reload the app if package.json or yarn.lock files have changed (in watch mode) + if (src === 'package.json' || src === 'yarn.lock') { if (ready && onComplete) delay100ms(onComplete); - break; - - // Remove directory if it was removed from the source folder - case 'unlinkDir': - if (fs.existsSync(dest)) fs.rmdirSync(dest); - if (ready && onComplete) onComplete(); - break; - - default: - // Skip + return; + } + + // Skip files starting with a dot, e.g. .DS_Store, .eslintrc etc. + if (path.basename(src)[0] === '.') return; + + // Get destination file name, e.g. src/app.js (src) -> build/app.js (dest) + const dest = src.startsWith('src') + ? `build/${path.relative('src', src)}` + : `build/${src}`; + + try { + switch (event) { + // Create a directory if it doesn't exist + case 'addDir': + if (src.startsWith('src') && !fs.existsSync(dest)) + fs.mkdirSync(dest); + if (ready && onComplete) onComplete(); + break; + + // Create or update a file inside the output (build) folder + case 'add': + case 'change': + if (src.startsWith('src') && src.endsWith('.js')) { + const { code, map } = babel.transformFileSync(src, { + sourceMaps: true, + sourceFileName: path.relative( + path.dirname(`build${src.substr(3)}`), + src, + ), + }); + // Enable source maps + const data = + (src === 'src/server.js' + ? "require('source-map-support').install(); " + : '') + + code + + (map + ? `\n//# sourceMappingURL=${path.basename(src)}.map\n` + : ''); + fs.writeFileSync(dest, data, 'utf8'); + console.log(src, '->', dest); + if (map) + fs.writeFileSync(`${dest}.map`, JSON.stringify(map), 'utf8'); + } else if (/^src\/emails\/.+/.test(src)) { + if (/^src\/emails\/.+\/.+\.hbs$/.test(src)) { + const template = compileEmail(src); + const destJs = dest.replace(/\.hbs$/, '.js'); + fs.writeFileSync( + destJs, + `module.exports = ${template};`, + 'utf8', + ); + console.log(src, '->', destJs); + } + } else if (src.startsWith('src')) { + const data = fs.readFileSync(src, 'utf8'); + fs.writeFileSync(dest, data, 'utf8'); + console.log(src, '->', dest); + } + if (ready && onComplete) delay100ms(onComplete); + break; + + // Remove directory if it was removed from the source folder + case 'unlinkDir': + if (fs.existsSync(dest)) fs.rmdirSync(dest); + if (ready && onComplete) onComplete(); + break; + + default: + // Skip + } + } catch (err) { + console.log(err.message); + } + }); + + watcher.on('ready', () => { + ready = true; + if (onComplete) onComplete(); + if (!watch) watcher.close(); + resolve(); + }); + + function cleanup() { + if (watcher) { + watcher.close(); + watcher = null; + } } - } catch (err) { - console.log(err.message); - } - }); - - watcher.on('ready', () => { - ready = true; - if (onComplete) onComplete(); - if (!watch) watcher.close(); - resolve(); - }); - - function cleanup() { - if (watcher) { - watcher.close(); - watcher = null; - } - } - process.on('SIGINT', cleanup); - process.on('SIGTERM', cleanup); - process.on('exit', cleanup); -})); + process.on('SIGINT', cleanup); + process.on('SIGTERM', cleanup); + process.on('exit', cleanup); + }), +); diff --git a/tools/db.js b/tools/db.js index 46f31fd8..b68a4da1 100644 --- a/tools/db.js +++ b/tools/db.js @@ -44,7 +44,11 @@ module.exports = task('db', async () => { await db.migrate.currentVersion(config).then(console.log); break; case 'migration': - fs.writeFileSync(`migrations/${version}_${process.argv[3] || 'new'}.js`, template, 'utf8'); + fs.writeFileSync( + `migrations/${version}_${process.argv[3] || 'new'}.js`, + template, + 'utf8', + ); break; case 'rollback': db = knex(config); diff --git a/tools/publish.js b/tools/publish.js index 6d7afad3..7a952bb6 100755 --- a/tools/publish.js +++ b/tools/publish.js @@ -27,14 +27,40 @@ Options: process.exit(); } -cp.spawnSync('docker-compose', ['run', '--rm', '--no-deps', 'api', '/bin/sh', '-c', 'yarn install; yarn run build'], { stdio: 'inherit' }); -cp.spawnSync('docker', ['build', '--no-cache', '--tag', pkg.name, '.'], { stdio: 'inherit' }); -const ssh = cp.spawn('ssh', ['-C', host, 'docker', 'load'], { stdio: ['pipe', 'inherit', 'inherit'] }); -const docker = cp.spawn('docker', ['save', pkg.name], { stdio: ['inherit', ssh.stdin, 'inherit'] }); -docker.on('exit', () => { ssh.stdin.end(); }); +cp.spawnSync( + 'docker-compose', + [ + 'run', + '--rm', + '--no-deps', + 'api', + '/bin/sh', + '-c', + 'yarn install; yarn run build', + ], + { stdio: 'inherit' }, +); +cp.spawnSync('docker', ['build', '--no-cache', '--tag', pkg.name, '.'], { + stdio: 'inherit', +}); +const ssh = cp.spawn('ssh', ['-C', host, 'docker', 'load'], { + stdio: ['pipe', 'inherit', 'inherit'], +}); +const docker = cp.spawn('docker', ['save', pkg.name], { + stdio: ['inherit', ssh.stdin, 'inherit'], +}); +docker.on('exit', () => { + ssh.stdin.end(); +}); ssh.on('exit', () => { if (process.argv.includes('--no-up')) return; - cp.spawnSync('ssh', ['-C', host, 'docker-compose', '-f', composeFile, 'up', '-d'], { stdio: 'inherit' }); + cp.spawnSync( + 'ssh', + ['-C', host, 'docker-compose', '-f', composeFile, 'up', '-d'], + { stdio: 'inherit' }, + ); if (process.argv.includes('--no-prune')) return; - cp.spawnSync('ssh', ['-C', host, 'docker', 'image', 'prune', '-a', '-f'], { stdio: 'inherit' }); + cp.spawnSync('ssh', ['-C', host, 'docker', 'image', 'prune', '-a', '-f'], { + stdio: 'inherit', + }); }); diff --git a/tools/run.js b/tools/run.js index 5392d0fa..e92a6b76 100644 --- a/tools/run.js +++ b/tools/run.js @@ -57,13 +57,17 @@ try { // Launch `node build/server.js` on a background thread function spawnServer() { - return cp.spawn('node', + return cp.spawn( + 'node', [ // Pre-load application dependencies to improve "hot reload" restart time - ...Object.keys(pkg.dependencies).reduce((requires, val) => requires.concat(['--require', val]), []), + ...Object.keys(pkg.dependencies).reduce( + (requires, val) => requires.concat(['--require', val]), + [], + ), // If the parent Node.js process is running in debug (inspect) mode, // launch a debugger for Express.js app on the next port - ...process.execArgv.map((arg) => { + ...process.execArgv.map(arg => { if (arg.startsWith('--inspect')) { const match = arg.match(/^--inspect=(\S+:|)(\d+)$/); if (match) debugPort = Number(match[2]) + 1; @@ -73,42 +77,55 @@ function spawnServer() { }), '--no-lazy', // Enable "hot reload", it only works when debugger is off - ...(isDebug ? ['./server.js'] : [ - '--eval', - 'process.stdin.on("data", data => { if (data.toString() === "load") require("./server.js"); });', - ]), + ...(isDebug + ? ['./server.js'] + : [ + '--eval', + 'process.stdin.on("data", data => { if (data.toString() === "load") require("./server.js"); });', + ]), ], - { cwd: './build', stdio: ['pipe', 'inherit', 'inherit'], timeout: 3000 }); + { cwd: './build', stdio: ['pipe', 'inherit', 'inherit'], timeout: 3000 }, + ); } -module.exports = task('run', () => Promise.resolve() - // Migrate database schema to the latest version - .then(() => { - cp.spawnSync('node', ['tools/db.js', 'migrate'], { stdio: 'inherit' }); - }) - // Compile and launch the app in watch mode, restart it after each rebuild - .then(() => build({ - watch: true, - onComplete() { - if (isDebug) { - if (server) { - server.on('exit', () => { server = spawnServer(); }); - server.kill('SIGTERM'); - } else { - server = spawnServer(); - } - } else { - if (server) server.kill('SIGTERM'); - server = serverQueue.splice(0, 1)[0] || spawnServer(); - server.stdin.write('load'); // this works faster than IPC - if (server) while (serverQueue.length < 3) serverQueue.push(spawnServer()); - } - }, - })) - // Resolve the promise on exit - .then(() => new Promise((resolve) => { - process.once('exit', () => { - if (server) server.kill(); - resolve(); - }); - }))); +module.exports = task('run', () => + Promise.resolve() + // Migrate database schema to the latest version + .then(() => { + cp.spawnSync('node', ['tools/db.js', 'migrate'], { stdio: 'inherit' }); + }) + // Compile and launch the app in watch mode, restart it after each rebuild + .then(() => + build({ + watch: true, + onComplete() { + if (isDebug) { + if (server) { + server.on('exit', () => { + server = spawnServer(); + }); + server.kill('SIGTERM'); + } else { + server = spawnServer(); + } + } else { + if (server) server.kill('SIGTERM'); + server = serverQueue.splice(0, 1)[0] || spawnServer(); + server.stdin.write('load'); // this works faster than IPC + if (server) + while (serverQueue.length < 3) serverQueue.push(spawnServer()); + } + }, + }), + ) + // Resolve the promise on exit + .then( + () => + new Promise(resolve => { + process.once('exit', () => { + if (server) server.kill(); + resolve(); + }); + }), + ), +); diff --git a/tools/task.js b/tools/task.js index 6c3f15a5..fbec22ca 100644 --- a/tools/task.js +++ b/tools/task.js @@ -17,12 +17,19 @@ function run(task, action, ...args) { const command = process.argv[2]; - const taskName = command && !command.startsWith('-') ? `${task}-${command}` : task; + const taskName = + command && !command.startsWith('-') ? `${task}-${command}` : task; const start = new Date(); process.stdout.write(`Starting '${taskName}'...\n`); - return Promise.resolve().then(() => action(...args)).then(() => { - process.stdout.write(`Finished '${taskName}' after ${new Date().getTime() - start.getTime()}ms\n`); - }, err => process.stderr.write(`${err.stack}\n`)); + return Promise.resolve().then(() => action(...args)).then( + () => { + process.stdout.write( + `Finished '${taskName}' after ${new Date().getTime() - + start.getTime()}ms\n`, + ); + }, + err => process.stderr.write(`${err.stack}\n`), + ); } process.nextTick(() => require.main.exports()); diff --git a/yarn.lock b/yarn.lock index 1a501118..8d0c6f7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1123,12 +1123,6 @@ datauri@^1.0.4: mimer "^0.2.1" semver "^5.0.3" -debug@2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b" - dependencies: - ms "0.7.2" - debug@2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e" @@ -1330,6 +1324,12 @@ eslint-config-airbnb-base@^11.3.1: dependencies: eslint-restricted-globals "^0.1.1" +eslint-config-prettier@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-2.3.0.tgz#b75b1eabea0c8b97b34403647ee25db349b9d8a0" + dependencies: + get-stdin "^5.0.1" + eslint-import-resolver-node@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.1.tgz#4422574cde66a9a7b099938ee4d508a199e0e3cc" @@ -1365,6 +1365,13 @@ eslint-plugin-import@^2.7.0: minimatch "^3.0.3" read-pkg-up "^2.0.0" +eslint-plugin-prettier@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-2.1.2.tgz#4b90f4ee7f92bfbe2e926017e1ca40eb628965ea" + dependencies: + fast-diff "^1.1.1" + jest-docblock "^20.0.1" + eslint-restricted-globals@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz#35f0d5cbc64c2e3ed62e93b4b1a7af05ba7ed4d7" @@ -1567,6 +1574,10 @@ fast-deep-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" +fast-diff@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.1.1.tgz#0aea0e4e605b6a2189f0e936d4b7fbaf1b7cfd9b" + fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" @@ -1756,6 +1767,10 @@ get-func-name@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" +get-stdin@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398" + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -2205,6 +2220,10 @@ iterall@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.1.1.tgz#f7f0af11e9a04ec6426260f5019d9fcca4d50214" +jest-docblock@^20.0.1: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-20.0.3.tgz#17bea984342cc33d83c50fbe1545ea0efaa44712" + js-tokens@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" @@ -2582,13 +2601,13 @@ mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1: dependencies: minimist "0.0.8" -mocha@^3.4.2: - version "3.4.2" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.4.2.tgz#d0ef4d332126dbf18d0d640c9b382dd48be97594" +mocha@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.0.tgz#1328567d2717f997030f8006234bce9b8cd72465" dependencies: browser-stdout "1.3.0" commander "2.9.0" - debug "2.6.0" + debug "2.6.8" diff "3.2.0" escape-string-regexp "1.0.5" glob "7.1.1" @@ -2598,10 +2617,6 @@ mocha@^3.4.2: mkdirp "0.5.1" supports-color "3.1.2" -ms@0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -2997,6 +3012,10 @@ preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" +prettier@^1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.5.3.tgz#59dadc683345ec6b88f88b94ed4ae7e1da394bfe" + pretty-error@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3"