diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..b3ddb93817 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +core/server/public/**/*.js +core/server/lib/members/static/auth/**/*.js diff --git a/Gruntfile.js b/Gruntfile.js index eaf7aace53..398f496938 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -8,677 +8,649 @@ require('./core/server/overrides'); -var config = require('./core/server/config'), - urlService = require('./core/server/services/url'), - _ = require('lodash'), - chalk = require('chalk'), - fs = require('fs-extra'), - KnexMigrator = require('knex-migrator'), - knexMigrator = new KnexMigrator({ - knexMigratorFilePath: config.get('paths:appRoot') - }), +const config = require('./core/server/config'); +const urlService = require('./core/server/services/url'); +const _ = require('lodash'); +const chalk = require('chalk'); +const fs = require('fs-extra'); +const KnexMigrator = require('knex-migrator'); +const knexMigrator = new KnexMigrator({ + knexMigratorFilePath: config.get('paths:appRoot') +}); - path = require('path'), +const path = require('path'); +const escapeChar = process.platform.match(/^win/) ? '^' : '\\'; +const cwd = process.cwd().replace(/( |\(|\))/g, escapeChar + '$1'); +const buildDirectory = path.resolve(cwd, '.build'); +const distDirectory = path.resolve(cwd, '.dist'); - escapeChar = process.platform.match(/^win/) ? '^' : '\\', - cwd = process.cwd().replace(/( |\(|\))/g, escapeChar + '$1'), - buildDirectory = path.resolve(cwd, '.build'), - distDirectory = path.resolve(cwd, '.dist'), +let hasBuiltClient = false; +const logBuildingClient = function (grunt) { + if (!hasBuiltClient) { + grunt.log.writeln('Building admin client... (can take ~1min)'); + setTimeout(logBuildingClient, 5000, grunt); + } +}; - hasBuiltClient = false, - logBuildingClient = function (grunt) { - if (!hasBuiltClient) { - grunt.log.writeln('Building admin client... (can take ~1min)'); - setTimeout(logBuildingClient, 5000, grunt); - } - }, +// ## Grunt configuration +const configureGrunt = function (grunt) { + // #### Load all grunt tasks + // + // Find all of the task which start with `grunt-` and load them, rather than explicitly declaring them all + require('matchdep').filterDev(['grunt-*', '!grunt-cli']).forEach(grunt.loadNpmTasks); - // ## Grunt configuration + var cfg = { + // #### Common paths used by tasks + paths: { + build: buildDirectory, + releaseBuild: path.join(buildDirectory, 'release'), + dist: distDirectory, + releaseDist: path.join(distDirectory, 'release') + }, + // Standard build type, for when we have nightlies again. + buildType: 'Build', + // Load package.json so that we can create correctly versioned releases. + pkg: grunt.file.readJSON('package.json'), - configureGrunt = function (grunt) { - // #### Load all grunt tasks - // - // Find all of the task which start with `grunt-` and load them, rather than explicitly declaring them all - require('matchdep').filterDev(['grunt-*', '!grunt-cli']).forEach(grunt.loadNpmTasks); + clientFiles: [ + 'server/web/admin/views/default.html', + 'built/assets/ghost.js', + 'built/assets/ghost.css', + 'built/assets/vendor.js', + 'built/assets/vendor.css' + ], - var cfg = { - // #### Common paths used by tasks - paths: { - build: buildDirectory, - releaseBuild: path.join(buildDirectory, 'release'), - dist: distDirectory, - releaseDist: path.join(distDirectory, 'release') - }, - // Standard build type, for when we have nightlies again. - buildType: 'Build', - // Load package.json so that we can create correctly versioned releases. - pkg: grunt.file.readJSON('package.json'), - - clientFiles: [ - 'server/web/admin/views/default.html', - 'built/assets/ghost.js', - 'built/assets/ghost.css', - 'built/assets/vendor.js', - 'built/assets/vendor.css' - ], - - // ### grunt-contrib-watch - // Watch files and livereload in the browser during development. - // See the [grunt dev](#live%20reload) task for how this is used. - watch: grunt.option('no-server-watch') ? {files: []} : { - livereload: { - files: [ - 'content/themes/casper/assets/css/*.css', - 'content/themes/casper/assets/js/*.js' - ], - options: { - livereload: true - } - }, - express: { - files: [ - 'core/ghost-server.js', - 'core/server/**/*.js', - '!core/server/lib/members/static/auth/**/*.js', - 'config.*.json', - '!config.testing.json' - ], - tasks: ['express:dev'], - options: { - spawn: false, - livereload: true - } + // ### grunt-contrib-watch + // Watch files and livereload in the browser during development. + // See the [grunt dev](#live%20reload) task for how this is used. + watch: grunt.option('no-server-watch') ? {files: []} : { + livereload: { + files: [ + 'content/themes/casper/assets/css/*.css', + 'content/themes/casper/assets/js/*.js' + ], + options: { + livereload: true } }, - - // ### grunt-express-server - // Start a Ghost express server for use in development and testing express: { + files: [ + 'core/ghost-server.js', + 'core/server/**/*.js', + '!core/server/lib/members/static/auth/**/*.js', + 'config.*.json', + '!config.testing.json' + ], + tasks: ['express:dev'], options: { - script: 'index.js', - output: 'Ghost is running' - }, - - dev: { - options: {} - }, - test: { - options: { - node_env: 'testing' - } + spawn: false, + livereload: true } + } + }, + + // ### grunt-express-server + // Start a Ghost express server for use in development and testing + express: { + options: { + script: 'index.js', + output: 'Ghost is running' }, - // ### grunt-eslint - // Linting rules, run as part of `grunt validate`. See [grunt validate](#validate) and its subtasks for - // more information. - eslint: { - server: { - options: { - config: '.eslintrc.json' - }, - src: [ - '*.js', - 'core/*.js', - 'core/server/*.js', - 'core/server/**/*.js', - '!core/server/public/**/*.js', - '!core/server/lib/members/static/auth/**/*.js' - ] - }, - test: { - options: { - config: './core/test/.eslintrc.json' - }, - src: [ - 'core/test/*.js', - 'core/test/**/*.js', - '!core/test/coverage/**' - ] - } + dev: { + options: {} }, - - // ### grunt-mocha-cli - mochacli: { + test: { options: { - ui: 'bdd', - reporter: grunt.option('reporter') || 'spec', - timeout: '30000', - save: grunt.option('reporter-output'), - require: ['core/server/overrides'], - exit: true - }, + node_env: 'testing' + } + } + }, - unit: { - src: [ - 'core/test/unit/**/*_spec.js' - ] - }, - - acceptance: { - src: [ - 'core/test/acceptance/**/*_spec.js' - ] - }, - - regression: { - src: [ - 'core/test/regression/**/*_spec.js' - ] - }, - - // #### Run single test (src is set dynamically, see grunt task 'test') - single: {} + // ### grunt-mocha-cli + mochacli: { + options: { + ui: 'bdd', + reporter: grunt.option('reporter') || 'spec', + timeout: '30000', + save: grunt.option('reporter-output'), + require: ['core/server/overrides'], + exit: true }, - // ### grunt-mocha-istanbul - // Configuration for the mocha test coverage generator - // `grunt coverage`. - mocha_istanbul: { - coverage: { - // they can also have coverage generated for them & the order doesn't matter - src: [ - 'core/test/unit' - ], - options: { - mask: '**/*_spec.js', - coverageFolder: 'core/test/coverage/unit', - mochaOptions: ['--timeout=15000', '--require', 'core/server/overrides', '--exit'], - excludes: ['core/client', 'core/server/built'] - } - }, - coverage_all: { - src: [ - 'core/test/acceptance', - 'core/test/regression', - 'core/test/unit' - ], - options: { - coverageFolder: 'core/test/coverage/all', - mask: '**/*_spec.js', - mochaOptions: ['--timeout=15000', '--require', 'core/server/overrides', '--exit'], - excludes: ['core/client', 'core/server/built'] - } + unit: { + src: [ + 'core/test/unit/**/*_spec.js' + ] + }, + acceptance: { + src: [ + 'core/test/acceptance/**/*_spec.js' + ] + }, + + regression: { + src: [ + 'core/test/regression/**/*_spec.js' + ] + }, + + // #### Run single test (src is set dynamically, see grunt task 'test') + single: {} + }, + + // ### grunt-mocha-istanbul + // Configuration for the mocha test coverage generator + // `grunt coverage`. + mocha_istanbul: { + coverage: { + // they can also have coverage generated for them & the order doesn't matter + src: [ + 'core/test/unit' + ], + options: { + mask: '**/*_spec.js', + coverageFolder: 'core/test/coverage/unit', + mochaOptions: ['--timeout=15000', '--require', 'core/server/overrides', '--exit'], + excludes: ['core/client', 'core/server/built'] } }, + coverage_all: { + src: [ + 'core/test/acceptance', + 'core/test/regression', + 'core/test/unit' + ], + options: { + coverageFolder: 'core/test/coverage/all', + mask: '**/*_spec.js', + mochaOptions: ['--timeout=15000', '--require', 'core/server/overrides', '--exit'], + excludes: ['core/client', 'core/server/built'] + } - bgShell: { - client: { - cmd: function () { - logBuildingClient(grunt); - return 'grunt subgrunt:watch'; - }, - bg: grunt.option('client') ? false : true, - stdout: function (chunk) { - // hide certain output to prevent confusion when running alongside server - var filter = grunt.option('client') ? false : [ - /> ghost-admin/, - /^Livereload/, - /^Serving on/ - ].some(function (regexp) { - return regexp.test(chunk); - }); + } + }, - if (!filter) { - grunt.log.write(chunk); - } + bgShell: { + client: { + cmd: function () { + logBuildingClient(grunt); + return 'grunt subgrunt:watch'; + }, + bg: grunt.option('client') ? false : true, + stdout: function (chunk) { + // hide certain output to prevent confusion when running alongside server + var filter = grunt.option('client') ? false : [ + /> ghost-admin/, + /^Livereload/, + /^Serving on/ + ].some(function (regexp) { + return regexp.test(chunk); + }); - if (chunk.indexOf('Build successful') !== -1) { - hasBuiltClient = true; - } - }, - stderr: function (chunk) { + if (!filter) { + grunt.log.write(chunk); + } + + if (chunk.indexOf('Build successful') !== -1) { hasBuiltClient = true; - grunt.log.error(chunk); } - } - }, - - // ### grunt-shell - // Command line tools where it's easier to run a command directly than configure a grunt plugin - shell: { - master: { - command: function () { - var upstream = grunt.option('upstream') || process.env.GHOST_UPSTREAM || 'upstream'; - grunt.log.writeln('Pulling down the latest master from ' + upstream); - return 'git checkout master; git pull ' + upstream + ' master; ' + - 'yarn; git submodule foreach "git checkout master && git pull ' + - upstream + ' master"'; - } - } - }, - - // ### grunt-contrib-clean - // Clean up files as part of other tasks - clean: { - built: { - src: [ - 'core/built/**' - ] }, - release: { - src: ['<%= paths.releaseBuild %>/**'] - }, - test: { - src: ['content/data/ghost-test.db'] - }, - tmp: { - src: ['.tmp/**'] - }, - dependencies: { - src: ['node_modules/**', 'core/client/bower_components/**', 'core/client/node_modules/**'] + stderr: function (chunk) { + hasBuiltClient = true; + grunt.log.error(chunk); } - }, + } + }, - // ### grunt-contrib-compress - // Zip up files for builds / releases - compress: { - release: { - options: { - archive: '<%= paths.releaseDist %>/Ghost-<%= pkg.version %>.zip' - }, - expand: true, - cwd: '<%= paths.releaseBuild %>/', - src: ['**'] + // ### grunt-shell + // Command line tools where it's easier to run a command directly than configure a grunt plugin + shell: { + lint: { + command: 'yarn lint' + }, + master: { + command: function () { + var upstream = grunt.option('upstream') || process.env.GHOST_UPSTREAM || 'upstream'; + grunt.log.writeln('Pulling down the latest master from ' + upstream); + return 'git checkout master; git pull ' + upstream + ' master; ' + + 'yarn; git submodule foreach "git checkout master && git pull ' + + upstream + ' master"'; } - }, + } + }, - // ### grunt-update-submodules - // Grunt task to update git submodules - update_submodules: { - pinned: { - options: { - params: '--init' - } - } + // ### grunt-contrib-clean + // Clean up files as part of other tasks + clean: { + built: { + src: [ + 'core/built/**' + ] }, - - uglify: { - prod: { - options: { - sourceMap: false - }, - files: { - 'core/server/public/ghost-sdk.min.js': 'core/server/public/ghost-sdk.js' - } - } + release: { + src: ['<%= paths.releaseBuild %>/**'] }, - - cssnano: { - prod: { - options: { - sourcemap: false - }, - files: { - 'core/server/public/ghost.min.css': 'core/server/public/ghost.css' - } - } + test: { + src: ['content/data/ghost-test.db'] }, + tmp: { + src: ['.tmp/**'] + }, + dependencies: { + src: ['node_modules/**', 'core/client/bower_components/**', 'core/client/node_modules/**'] + } + }, - // ### grunt-subgrunt - // Run grunt tasks in submodule Gruntfiles - subgrunt: { + // ### grunt-contrib-compress + // Zip up files for builds / releases + compress: { + release: { options: { - npmInstall: false, - npmPath: 'yarn' + archive: '<%= paths.releaseDist %>/Ghost-<%= pkg.version %>.zip' }, + expand: true, + cwd: '<%= paths.releaseBuild %>/', + src: ['**'] + } + }, - init: { - options: { - npmInstall: true - }, - projects: { - 'core/client': 'init', - 'core/server/lib/members/static/auth': 'init' - } + // ### grunt-update-submodules + // Grunt task to update git submodules + update_submodules: { + pinned: { + options: { + params: '--init' + } + } + }, + + uglify: { + prod: { + options: { + sourceMap: false }, + files: { + 'core/server/public/ghost-sdk.min.js': 'core/server/public/ghost-sdk.js' + } + } + }, - dev: { - 'core/client': 'shell:ember:dev' + cssnano: { + prod: { + options: { + sourcemap: false }, + files: { + 'core/server/public/ghost.min.css': 'core/server/public/ghost.css' + } + } + }, - prod: { - 'core/client': 'shell:ember:prod', - 'core/server/lib/members/static/auth': 'shell:preact:prod' + // ### grunt-subgrunt + // Run grunt tasks in submodule Gruntfiles + subgrunt: { + options: { + npmInstall: false, + npmPath: 'yarn' + }, + + init: { + options: { + npmInstall: true }, - - watch: { - projects: { - 'core/client': ['shell:ember:watch', '--live-reload-base-url="' + urlService.utils.getSubdir() + '/ghost/"'], - 'core/server/lib/members/static/auth': ['shell:preact:dev'] - } + projects: { + 'core/client': 'init', + 'core/server/lib/members/static/auth': 'init' } }, - // ### grunt-contrib-symlink - // Create symlink for git hooks - symlink: { - githooks: { - // Enable overwrite to delete symlinks before recreating them - overwrite: false, - // Enable force to overwrite symlinks outside the current working directory - force: false, - // Expand to all files in /hooks - expand: true, - cwd: '.github/hooks', - src: ['*'], - dest: '.git/hooks' + dev: { + 'core/client': 'shell:ember:dev' + }, + + prod: { + 'core/client': 'shell:ember:prod', + 'core/server/lib/members/static/auth': 'shell:preact:prod' + }, + + watch: { + projects: { + 'core/client': ['shell:ember:watch', '--live-reload-base-url="' + urlService.utils.getSubdir() + '/ghost/"'], + 'core/server/lib/members/static/auth': ['shell:preact:dev'] } } - }; + }, - // Load the configuration - grunt.initConfig(cfg); - - // # Custom Tasks - - // Ghost has a number of useful tasks that we use every day in development. Tasks marked as *Utility* are used - // by grunt to perform current actions, but isn't useful to developers. - // - // Skip ahead to the section on: - // - // * [Building assets](#building%20assets): - // `grunt init`, `grunt` & `grunt prod` or live reload with `grunt dev` - // * [Testing](#testing): - // `grunt validate`, the `grunt test-*` sub-tasks or generate a coverage report with `grunt coverage`. - - // ### Help - // Run `grunt help` on the commandline to get a print out of the available tasks and details of - // what each one does along with any available options. This is an alias for `grunt --help` - grunt.registerTask('help', - 'Outputs help information if you type `grunt help` instead of `grunt --help`', - function () { - grunt.log.writeln('Type `grunt --help` to get the details of available grunt tasks.'); - }); - - // ### Documentation - // Run `grunt docs` to generate annotated source code using the documentation described in the code comments. - grunt.registerTask('docs', 'Generate Docs', ['docker']); - - // Run `grunt watch-docs` to setup livereload & watch whilst you're editing the docs - grunt.registerTask('watch-docs', function () { - grunt.config.merge({ - watch: { - docs: { - files: ['core/server/**/*', 'index.js', 'Gruntfile.js'], - tasks: ['docker'], - options: { - livereload: true - } - } - } - }); - - grunt.task.run('watch:docs'); - }); - - // ## Testing - - // Ghost has an extensive set of test suites. The following section documents the various types of tests - // and how to run them. - // - // TLDR; run `grunt validate` - - // #### Set Test Env *(Utility Task)* - // Set the NODE_ENV to 'testing' unless the environment is already set to TRAVIS. - // This ensures that the tests get run under the correct environment, using the correct database, and - // that they work as expected. Trying to run tests with no ENV set will throw an error to do with `client`. - grunt.registerTask('setTestEnv', - 'Use "testing" Ghost config; unless we are running on travis (then show queries for debugging)', - function () { - process.env.NODE_ENV = process.env.TRAVIS ? process.env.NODE_ENV : 'testing'; - cfg.express.test.options.node_env = process.env.NODE_ENV; - }); - - // ### Test - // **Testing utility** - // - // `grunt test:unit/apps_spec.js` will run just the tests inside the apps_spec.js file - // - // It works for any path relative to the core/test folder. It will also run all the tests in a single directory - // You can also run a test with grunt test:core/test/unit/... to get bash autocompletion - // - // `grunt test:regression/api` - runs the api regression tests - grunt.registerTask('test', 'Run a particular spec file from the core/test directory e.g. `grunt test:unit/apps_spec.js`', function (test) { - if (!test) { - grunt.fail.fatal('No test provided. `grunt test` expects a filename. e.g.: `grunt test:unit/apps_spec.js`. Did you mean `npm test` or `grunt validate`?'); + // ### grunt-contrib-symlink + // Create symlink for git hooks + symlink: { + githooks: { + // Enable overwrite to delete symlinks before recreating them + overwrite: false, + // Enable force to overwrite symlinks outside the current working directory + force: false, + // Expand to all files in /hooks + expand: true, + cwd: '.github/hooks', + src: ['*'], + dest: '.git/hooks' } - - if (!test.match(/core\/test/) && !test.match(/core\/server/)) { - test = 'core/test/' + test; - } - - // CASE: execute folder - if (!test.match(/.js/)) { - test += '/**'; - } else if (!fs.existsSync(test)) { - grunt.fail.fatal('This file does not exist!'); - } - - cfg.mochacli.single.src = [test]; - grunt.initConfig(cfg); - grunt.task.run('test-setup', 'mochacli:single'); - }); - - // #### Stub out ghost files *(Utility Task)* - // Creates stub files in the built directory and the views directory - // so that the test environments do not need to build out the client files - grunt.registerTask('stubClientFiles', function () { - _.each(cfg.clientFiles, function (file) { - var filePath = path.resolve(cwd + '/core/' + file); - fs.ensureFileSync(filePath); - }); - }); - - /** - * Ensures the target database get's automatically created. - */ - grunt.registerTask('knex-migrator', function () { - return knexMigrator.init({noScripts: true}); - }); - - // ### Validate - // **Main testing task** - // - // `grunt validate` will either run all tests or run linting - // `grunt validate` is called by `npm test` and is used by Travis. - grunt.registerTask('validate', 'Run tests or lint code', function () { - if (process.env.TEST_SUITE === 'lint') { - return grunt.task.run(['lint']); - } - - grunt.task.run(['test-acceptance', 'test-unit']); - }); - - grunt.registerTask('test-all', 'Run all server tests', - ['test-acceptance', 'test-unit', 'test-regression']); - - // ### Lint - // - // `grunt lint` will run the linter - grunt.registerTask('lint', 'Run the code style checks for server & tests', - ['eslint'] - ); - - // ### test-setup *(utility)( - // `grunt test-setup` will run all the setup tasks required for running tests - grunt.registerTask('test-setup', 'Setup ready to run tests', - ['knex-migrator', 'clean:test', 'setTestEnv', 'stubClientFiles'] - ); - - // ### Unit Tests *(sub task)* - // `grunt test-unit` will run just the unit tests - // - // If you need to run an individual unit test file, you can use the `grunt test:` task: - // - // `grunt test:unit/config_spec.js` - // - // This also works for folders (although it isn't recursive), E.g. - // - // `grunt test:unit/helpers` - // - // Unit tests are run with [mocha](http://mochajs.org/) using - // [should](https://github.com/visionmedia/should.js) to describe the tests in a highly readable style. - // Unit tests do **not** touch the database. - // A coverage report can be generated for these tests using the `grunt test-coverage` task. - grunt.registerTask('test-unit', 'Run unit tests (mocha)', - ['test-setup', 'mochacli:unit'] - ); - - grunt.registerTask('test-regression', 'Run regression tests.', - ['test-setup', 'mochacli:regression'] - ); - - grunt.registerTask('test-acceptance', 'Run acceptance tests', - ['test-setup', 'mochacli:acceptance'] - ); - - // ### Coverage - // `grunt coverage` will generate a report for the code coverage. - grunt.registerTask('coverage', 'Generate unit tests coverage report', - ['test-setup', 'mocha_istanbul:coverage'] - ); - - grunt.registerTask('coverage-all', 'Generate full coverage report', - ['test-setup', 'mocha_istanbul:coverage_all'] - ); - - // #### Master Warning *(Utility Task)* - // Warns git users not ot use the `master` branch in production. - // `master` is an unstable branch and shouldn't be used in production as you run the risk of ending up with a - // database in an unrecoverable state. Instead there is a branch called `stable` which is the equivalent of the - // release zip for git users. - grunt.registerTask('master-warn', - 'Outputs a warning to runners of grunt prod, that master shouldn\'t be used for live blogs', - function () { - grunt.log.writeln(chalk.red( - 'Use the ' + chalk.bold('stable') + ' branch for live blogs. ' - + chalk.bold.underline('Never') + ' master!' - )); - grunt.log.writeln('>', 'Always two there are, no more, no less. A master and a ' + chalk.bold('stable') + '.'); - }); - - // ## Building assets - // - // Ghost's GitHub repository contains the un-built source code for Ghost. If you're looking for the already - // built release zips, you can get these from the [release page](https://github.com/TryGhost/Ghost/releases) on - // GitHub or from https://ghost.org/download. These zip files are created using the [grunt release](#release) - // task. - // - // If you want to work on Ghost core, or you want to use the source files from GitHub, then you have to build - // the Ghost assets in order to make them work. - // - // There are a number of grunt tasks available to help with this. Firstly after fetching an updated version of - // the Ghost codebase, after running `yarn install`, you will need to run [grunt init](#init%20assets). - // - // For production blogs you will need to run [grunt prod](#production%20assets). - // - // For updating assets during development, the tasks [grunt](#default%20asset%20build) and - // [grunt dev](#live%20reload) are available. - - // ### Init assets - // `grunt init` - will run an initial asset build for you - // - // Grunt init runs `yarn install && bower install` inside `core/client` as well as the standard asset build - // tasks which occur when you run just `grunt`. This fetches the latest client-side dependencies. - // - // This task is very important, and should always be run when fetching down an updated code base just after - // running `yarn install`. - // - // `bower` does have some quirks, such as not running as root. If you have problems please try running - // `grunt init --verbose` to see if there are any errors. - grunt.registerTask('init', 'Prepare the project for development', - ['update_submodules:pinned', 'subgrunt:init', 'clean:tmp', 'default']); - - // ### Build assets - // `grunt build` - will build client assets (without updating the submodule) - // - // This task is identical to `grunt init`, except it does not build client dependencies - grunt.registerTask('build', 'Build client app', - ['subgrunt:init', 'clean:tmp', 'default']); - - // ### Default asset build - // `grunt` - default grunt task - // - // Build assets and dev version of the admin app. - grunt.registerTask('default', 'Build JS & templates for development', - ['subgrunt:dev']); - - // ### Production assets - // `grunt prod` - will build the minified assets used in production. - // - // It is otherwise the same as running `grunt`, but is only used when running Ghost in the `production` env. - grunt.registerTask('prod', 'Build JS & templates for production', - ['subgrunt:prod', 'uglify:prod', 'cssnano:prod', 'master-warn']); - - // ### Live reload - // `grunt dev` - build assets on the fly whilst developing - // - // If you want Ghost to live reload for you whilst you're developing, you can do this by running `grunt dev`. - // This works hand-in-hand with the [livereload](http://livereload.com/) chrome extension. - // - // `grunt dev` manages starting an express server and restarting the server whenever core files change (which - // require a server restart for the changes to take effect) and also manage reloading the browser whenever - // frontend code changes. - // - // Note that the current implementation of watch only works with casper, not other themes. - grunt.registerTask('dev', 'Dev Mode; watch files and restart server on changes', function () { - if (grunt.option('client')) { - grunt.task.run(['clean:built', 'bgShell:client']); - } else if (grunt.option('server')) { - grunt.task.run(['express:dev', 'watch']); - } else { - grunt.task.run(['clean:built', 'bgShell:client', 'express:dev', 'watch']); - } - }); - - // ### grunt master - // This command helps you to bring your working directory back to current master. - // It will also update your dependencies to master and shows you if your database is healthy. - // It won't build the client! - // - // `grunt master` [`upstream` is the default upstream to pull from] - // `grunt master --upstream=parent` - grunt.registerTask('master', 'Update your current working folder to latest master.', - ['shell:master', 'subgrunt:init'] - ); - - // ### Release - // Run `grunt release` to create a Ghost release zip file. - // Uses the files specified by `.npmignore` to know what should and should not be included. - // Runs the asset generation tasks for production and duplicates default-prod.html to default.html - // so it can be run in either production or development, and packages all the files up into a zip. - grunt.registerTask('release', - 'Release task - creates a final built zip\n' + - ' - Do our standard build steps \n' + - ' - Copy files to release-folder/#/#{version} directory\n' + - ' - Clean out unnecessary files (travis, .git*, etc)\n' + - ' - Zip files in release-folder to dist-folder/#{version} directory', - function () { - grunt.config.set('copy.release', { - expand: true, - // #### Build File Patterns - // A list of files and patterns to include when creating a release zip. - // This is read from the `.npmignore` file and all patterns are inverted as the `.npmignore` - // file defines what to ignore, whereas we want to define what to include. - src: fs.readFileSync('.npmignore', 'utf8').split('\n').filter(Boolean).map(function (pattern) { - return pattern[0] === '!' ? pattern.substr(1) : '!' + pattern; - }), - dest: '<%= paths.releaseBuild %>/' - }); - - grunt.config.set('copy.admin_html', { - files: [{ - cwd: '.', - src: 'core/server/web/admin/views/default-prod.html', - dest: 'core/server/web/admin/views/default.html' - }] - }); - - grunt.task.run(['update_submodules:pinned', 'subgrunt:init', 'clean:built', 'clean:tmp', 'prod', 'clean:release', 'copy:admin_html', 'copy:release', 'compress:release']); - } - ); + } }; + // Load the configuration + grunt.initConfig(cfg); + + // # Custom Tasks + + // Ghost has a number of useful tasks that we use every day in development. Tasks marked as *Utility* are used + // by grunt to perform current actions, but isn't useful to developers. + // + // Skip ahead to the section on: + // + // * [Building assets](#building%20assets): + // `grunt init`, `grunt` & `grunt prod` or live reload with `grunt dev` + // * [Testing](#testing): + // `grunt validate`, the `grunt test-*` sub-tasks or generate a coverage report with `grunt coverage`. + + // ### Help + // Run `grunt help` on the commandline to get a print out of the available tasks and details of + // what each one does along with any available options. This is an alias for `grunt --help` + grunt.registerTask('help', + 'Outputs help information if you type `grunt help` instead of `grunt --help`', + function () { + grunt.log.writeln('Type `grunt --help` to get the details of available grunt tasks.'); + }); + + // ### Documentation + // Run `grunt docs` to generate annotated source code using the documentation described in the code comments. + grunt.registerTask('docs', 'Generate Docs', ['docker']); + + // Run `grunt watch-docs` to setup livereload & watch whilst you're editing the docs + grunt.registerTask('watch-docs', function () { + grunt.config.merge({ + watch: { + docs: { + files: ['core/server/**/*', 'index.js', 'Gruntfile.js'], + tasks: ['docker'], + options: { + livereload: true + } + } + } + }); + + grunt.task.run('watch:docs'); + }); + + // ## Testing + + // Ghost has an extensive set of test suites. The following section documents the various types of tests + // and how to run them. + // + // TLDR; run `grunt validate` + + // #### Set Test Env *(Utility Task)* + // Set the NODE_ENV to 'testing' unless the environment is already set to TRAVIS. + // This ensures that the tests get run under the correct environment, using the correct database, and + // that they work as expected. Trying to run tests with no ENV set will throw an error to do with `client`. + grunt.registerTask('setTestEnv', + 'Use "testing" Ghost config; unless we are running on travis (then show queries for debugging)', + function () { + process.env.NODE_ENV = process.env.TRAVIS ? process.env.NODE_ENV : 'testing'; + cfg.express.test.options.node_env = process.env.NODE_ENV; + }); + + // ### Test + // **Testing utility** + // + // `grunt test:unit/apps_spec.js` will run just the tests inside the apps_spec.js file + // + // It works for any path relative to the core/test folder. It will also run all the tests in a single directory + // You can also run a test with grunt test:core/test/unit/... to get bash autocompletion + // + // `grunt test:regression/api` - runs the api regression tests + grunt.registerTask('test', 'Run a particular spec file from the core/test directory e.g. `grunt test:unit/apps_spec.js`', function (test) { + if (!test) { + grunt.fail.fatal('No test provided. `grunt test` expects a filename. e.g.: `grunt test:unit/apps_spec.js`. Did you mean `npm test` or `grunt validate`?'); + } + + if (!test.match(/core\/test/) && !test.match(/core\/server/)) { + test = 'core/test/' + test; + } + + // CASE: execute folder + if (!test.match(/.js/)) { + test += '/**'; + } else if (!fs.existsSync(test)) { + grunt.fail.fatal('This file does not exist!'); + } + + cfg.mochacli.single.src = [test]; + grunt.initConfig(cfg); + grunt.task.run('test-setup', 'mochacli:single'); + }); + + // #### Stub out ghost files *(Utility Task)* + // Creates stub files in the built directory and the views directory + // so that the test environments do not need to build out the client files + grunt.registerTask('stubClientFiles', function () { + _.each(cfg.clientFiles, function (file) { + var filePath = path.resolve(cwd + '/core/' + file); + fs.ensureFileSync(filePath); + }); + }); + + /** + * Ensures the target database get's automatically created. + */ + grunt.registerTask('knex-migrator', function () { + return knexMigrator.init({noScripts: true}); + }); + + // ### Validate + // **Main testing task** + // + // `grunt validate` will either run all tests or run linting + // `grunt validate` is called by `npm test` and is used by Travis. + grunt.registerTask('validate', 'Run tests or lint code', function () { + if (process.env.TEST_SUITE === 'lint') { + return grunt.task.run(['lint']); + } + + grunt.task.run(['test-acceptance', 'test-unit']); + }); + + grunt.registerTask('test-all', 'Run all server tests', + ['test-acceptance', 'test-unit', 'test-regression']); + + // ### Lint + // + // `grunt lint` will run the linter + grunt.registerTask('lint', 'Run the code style checks for server & tests', + ['shell:lint'] + ); + + // ### test-setup *(utility)( + // `grunt test-setup` will run all the setup tasks required for running tests + grunt.registerTask('test-setup', 'Setup ready to run tests', + ['knex-migrator', 'clean:test', 'setTestEnv', 'stubClientFiles'] + ); + + // ### Unit Tests *(sub task)* + // `grunt test-unit` will run just the unit tests + // + // If you need to run an individual unit test file, you can use the `grunt test:` task: + // + // `grunt test:unit/config_spec.js` + // + // This also works for folders (although it isn't recursive), E.g. + // + // `grunt test:unit/helpers` + // + // Unit tests are run with [mocha](http://mochajs.org/) using + // [should](https://github.com/visionmedia/should.js) to describe the tests in a highly readable style. + // Unit tests do **not** touch the database. + // A coverage report can be generated for these tests using the `grunt test-coverage` task. + grunt.registerTask('test-unit', 'Run unit tests (mocha)', + ['test-setup', 'mochacli:unit'] + ); + + grunt.registerTask('test-regression', 'Run regression tests.', + ['test-setup', 'mochacli:regression'] + ); + + grunt.registerTask('test-acceptance', 'Run acceptance tests', + ['test-setup', 'mochacli:acceptance'] + ); + + // ### Coverage + // `grunt coverage` will generate a report for the code coverage. + grunt.registerTask('coverage', 'Generate unit tests coverage report', + ['test-setup', 'mocha_istanbul:coverage'] + ); + + grunt.registerTask('coverage-all', 'Generate full coverage report', + ['test-setup', 'mocha_istanbul:coverage_all'] + ); + + // #### Master Warning *(Utility Task)* + // Warns git users not ot use the `master` branch in production. + // `master` is an unstable branch and shouldn't be used in production as you run the risk of ending up with a + // database in an unrecoverable state. Instead there is a branch called `stable` which is the equivalent of the + // release zip for git users. + grunt.registerTask('master-warn', + 'Outputs a warning to runners of grunt prod, that master shouldn\'t be used for live blogs', + function () { + grunt.log.writeln(chalk.red( + 'Use the ' + chalk.bold('stable') + ' branch for live blogs. ' + + chalk.bold.underline('Never') + ' master!' + )); + grunt.log.writeln('>', 'Always two there are, no more, no less. A master and a ' + chalk.bold('stable') + '.'); + }); + + // ## Building assets + // + // Ghost's GitHub repository contains the un-built source code for Ghost. If you're looking for the already + // built release zips, you can get these from the [release page](https://github.com/TryGhost/Ghost/releases) on + // GitHub or from https://ghost.org/download. These zip files are created using the [grunt release](#release) + // task. + // + // If you want to work on Ghost core, or you want to use the source files from GitHub, then you have to build + // the Ghost assets in order to make them work. + // + // There are a number of grunt tasks available to help with this. Firstly after fetching an updated version of + // the Ghost codebase, after running `yarn install`, you will need to run [grunt init](#init%20assets). + // + // For production blogs you will need to run [grunt prod](#production%20assets). + // + // For updating assets during development, the tasks [grunt](#default%20asset%20build) and + // [grunt dev](#live%20reload) are available. + + // ### Init assets + // `grunt init` - will run an initial asset build for you + // + // Grunt init runs `yarn install && bower install` inside `core/client` as well as the standard asset build + // tasks which occur when you run just `grunt`. This fetches the latest client-side dependencies. + // + // This task is very important, and should always be run when fetching down an updated code base just after + // running `yarn install`. + // + // `bower` does have some quirks, such as not running as root. If you have problems please try running + // `grunt init --verbose` to see if there are any errors. + grunt.registerTask('init', 'Prepare the project for development', + ['update_submodules:pinned', 'subgrunt:init', 'clean:tmp', 'default']); + + // ### Build assets + // `grunt build` - will build client assets (without updating the submodule) + // + // This task is identical to `grunt init`, except it does not build client dependencies + grunt.registerTask('build', 'Build client app', + ['subgrunt:init', 'clean:tmp', 'default']); + + // ### Default asset build + // `grunt` - default grunt task + // + // Build assets and dev version of the admin app. + grunt.registerTask('default', 'Build JS & templates for development', + ['subgrunt:dev']); + + // ### Production assets + // `grunt prod` - will build the minified assets used in production. + // + // It is otherwise the same as running `grunt`, but is only used when running Ghost in the `production` env. + grunt.registerTask('prod', 'Build JS & templates for production', + ['subgrunt:prod', 'uglify:prod', 'cssnano:prod', 'master-warn']); + + // ### Live reload + // `grunt dev` - build assets on the fly whilst developing + // + // If you want Ghost to live reload for you whilst you're developing, you can do this by running `grunt dev`. + // This works hand-in-hand with the [livereload](http://livereload.com/) chrome extension. + // + // `grunt dev` manages starting an express server and restarting the server whenever core files change (which + // require a server restart for the changes to take effect) and also manage reloading the browser whenever + // frontend code changes. + // + // Note that the current implementation of watch only works with casper, not other themes. + grunt.registerTask('dev', 'Dev Mode; watch files and restart server on changes', function () { + if (grunt.option('client')) { + grunt.task.run(['clean:built', 'bgShell:client']); + } else if (grunt.option('server')) { + grunt.task.run(['express:dev', 'watch']); + } else { + grunt.task.run(['clean:built', 'bgShell:client', 'express:dev', 'watch']); + } + }); + + // ### grunt master + // This command helps you to bring your working directory back to current master. + // It will also update your dependencies to master and shows you if your database is healthy. + // It won't build the client! + // + // `grunt master` [`upstream` is the default upstream to pull from] + // `grunt master --upstream=parent` + grunt.registerTask('master', 'Update your current working folder to latest master.', + ['shell:master', 'subgrunt:init'] + ); + + // ### Release + // Run `grunt release` to create a Ghost release zip file. + // Uses the files specified by `.npmignore` to know what should and should not be included. + // Runs the asset generation tasks for production and duplicates default-prod.html to default.html + // so it can be run in either production or development, and packages all the files up into a zip. + grunt.registerTask('release', + 'Release task - creates a final built zip\n' + + ' - Do our standard build steps \n' + + ' - Copy files to release-folder/#/#{version} directory\n' + + ' - Clean out unnecessary files (travis, .git*, etc)\n' + + ' - Zip files in release-folder to dist-folder/#{version} directory', + function () { + grunt.config.set('copy.release', { + expand: true, + // #### Build File Patterns + // A list of files and patterns to include when creating a release zip. + // This is read from the `.npmignore` file and all patterns are inverted as the `.npmignore` + // file defines what to ignore, whereas we want to define what to include. + src: fs.readFileSync('.npmignore', 'utf8').split('\n').filter(Boolean).map(function (pattern) { + return pattern[0] === '!' ? pattern.substr(1) : '!' + pattern; + }), + dest: '<%= paths.releaseBuild %>/' + }); + + grunt.config.set('copy.admin_html', { + files: [{ + cwd: '.', + src: 'core/server/web/admin/views/default-prod.html', + dest: 'core/server/web/admin/views/default.html' + }] + }); + + grunt.task.run(['update_submodules:pinned', 'subgrunt:init', 'clean:built', 'clean:tmp', 'prod', 'clean:release', 'copy:admin_html', 'copy:release', 'compress:release']); + } + ); +}; + module.exports = configureGrunt; diff --git a/core/test/.eslintignore b/core/test/.eslintignore new file mode 100644 index 0000000000..c7f13e5863 --- /dev/null +++ b/core/test/.eslintignore @@ -0,0 +1 @@ +core/test/coverage/** diff --git a/package.json b/package.json index 263cf8983d..f16cae8890 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,9 @@ "test": "grunt validate --verbose", "test:regression": "grunt test-regression --verbose", "setup": "yarn install && knex-migrator init && grunt symlink && grunt init || true", - "lint": "grunt lint", + "lint:server": "eslint --ignore-path .eslintignore 'core/server/**/*.js' 'core/*.js' '*.js'", + "lint:test": "eslint -c core/test/.eslintrc.json --ignore-path core/test/.eslintignore 'core/test/**/*.js'", + "lint": "yarn lint:server && yarn lint:test", "fixmodulenotdefined": "yarn cache clean && cd core/client && rm -rf node_modules tmp dist && yarn && cd ../../" }, "engines": { @@ -127,7 +129,6 @@ "grunt-contrib-uglify": "4.0.0", "grunt-contrib-watch": "1.1.0", "grunt-cssnano": "2.1.0", - "grunt-eslint": "21.0.0", "grunt-express-server": "0.5.4", "grunt-mocha-cli": "4.0.0", "grunt-mocha-istanbul": "5.0.2", diff --git a/yarn.lock b/yarn.lock index 53b5871c4b..d8c6fedfc5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1625,7 +1625,7 @@ eslint-visitor-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" -eslint@5.12.1, eslint@^5.0.0: +eslint@5.12.1: version "5.12.1" resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.12.1.tgz#5ca9931fb9029d04e7be92b03ce3b58edfac7e3b" dependencies: @@ -2565,13 +2565,6 @@ grunt-cssnano@2.1.0: dependencies: cssnano "^3.0.0" -grunt-eslint@21.0.0: - version "21.0.0" - resolved "https://registry.yarnpkg.com/grunt-eslint/-/grunt-eslint-21.0.0.tgz#5863f593d328c27ffec2a183319e5ad5380eed9e" - dependencies: - chalk "^2.1.0" - eslint "^5.0.0" - grunt-express-server@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/grunt-express-server/-/grunt-express-server-0.5.4.tgz#8ce79c335c6cbb9ef50ee1dfaa61942028f43aeb"