const config = require('./core/shared/config'); const fs = require('fs-extra'); const path = require('path'); // Utility for outputting messages indicating that the admin is building, as it can take a while. let hasBuiltClient = false; const logBuildingClient = function (grunt) { if (!hasBuiltClient) { grunt.log.writeln('Building admin client... (can take ~1min)'); setTimeout(logBuildingClient, 5000, grunt); } }; module.exports = function (grunt) { // grunt dev - use yarn dev instead! // - Start a server & build assets on the fly whilst developing 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 build -- use yarn build instead! // - Builds the client without a watch task grunt.registerTask('build', 'Build client app in development mode', ['subgrunt:init', 'clean:tmp', 'ember']); // Helpers for common deprecated tasks grunt.registerTask('main', function () { grunt.log.error('@deprecated: Run `yarn main` instead'); }); grunt.registerTask('validate', function () { grunt.log.error('@deprecated: Run `yarn test` instead'); }); // --- Sub Commands // Used to make other commands work // Updates submodules, then installs and builds the client for you grunt.registerTask('init', 'Prepare the project for development', ['update_submodules:pinned', 'build']); // Runs ember dev grunt.registerTask('ember', 'Build JS & templates for development', ['subgrunt:dev']); // Production asset build grunt.registerTask('prod', 'Build JS & templates for production', ['subgrunt:prod', 'postcss:prod']); // --- Configuration const cfg = { // grunt-contrib-watch // Watch files and livereload in the browser during development. // See the grunt dev 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, interval: 500 } }, express: { files: [ 'core/server/**/*.js', 'core/shared/**/*.js', 'core/frontend/**/*.js', 'core/*.js', 'index.js', 'config.*.json', '!config.testing.json' ], tasks: ['express:dev'], options: { spawn: false, livereload: true, interval: 500 } } }, // grunt-express-server // Start a Ghost express server for use in development and testing express: { dev: { options: { script: 'index.js', output: 'Ghost is running' } } }, // grunt-bg-shell // Tools for building the client 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 const filter = grunt.option('client') ? false : [ /> ghost-admin/, /^Livereload/, /^Serving on/ ].some(function (regexp) { return regexp.test(chunk); }); if (!filter) { grunt.log.write(chunk); } if (chunk.indexOf('Slowest Nodes') !== -1) { hasBuiltClient = true; } }, stderr: function (chunk) { const skipFilter = grunt.option('client') ? false : [ /- building/ ].some(function (regexp) { return regexp.test(chunk); }); const errorFilter = grunt.option('client') ? false : [ /^>>/ ].some(function (regexp) { return regexp.test(chunk); }); if (!skipFilter) { hasBuiltClient = errorFilter ? 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: { main: { command: function () { const upstream = grunt.option('upstream') || process.env.GHOST_UPSTREAM || 'upstream'; grunt.log.writeln('Pulling down the latest main from ' + upstream); return ` git submodule sync && \ git submodule update if ! git diff --exit-code --quiet --ignore-submodules=untracked; then echo "Working directory is not clean, do you have uncommitted changes? Please commit, stash or discard changes to continue." exit 1 fi git checkout main if git config remote.${upstream}.url > /dev/null; then git pull ${upstream} main else git pull origin main fi yarn && \ git submodule foreach " git checkout main if git config remote.${upstream}.url > /dev/null; then git pull ${upstream} main else git pull origin main fi " `; } } }, // grunt-contrib-clean // Clean up files as part of other tasks clean: { built: { src: [ 'core/built/**' ] }, tmp: { src: ['.tmp/**'] } }, // grunt-update-submodules // Grunt task to update git submodules update_submodules: { pinned: { options: { params: '--init' } } }, // @lodder/grunt-postcss // Generate processed, minified css files postcss: { prod: { options: { processors: [ require('cssnano')() // minify the result ] }, files: { 'core/server/frontend/ghost.min.css': 'core/frontend/public/ghost.css' } } }, // grunt-subgrunt // Run grunt tasks in submodule Gruntfiles subgrunt: { options: { npmInstall: false, npmPath: 'yarn' }, init: { options: { npmInstall: true }, projects: { 'core/client': 'init' } }, dev: { 'core/client': 'shell:ember:dev' }, prod: { 'core/client': 'shell:ember:prod' }, watch: { projects: { 'core/client': ['shell:ember:watch', '--live-reload-base-url="' + config.getSubdir() + '/ghost/"'] } } }, // 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' } } }; // --- Grunt Initialisation // Load all grunt tasks grunt.loadNpmTasks('@lodder/grunt-postcss'); grunt.loadNpmTasks('grunt-bg-shell'); grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-contrib-compress'); grunt.loadNpmTasks('grunt-contrib-copy'); grunt.loadNpmTasks('grunt-contrib-symlink'); grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-express-server'); grunt.loadNpmTasks('grunt-shell'); grunt.loadNpmTasks('grunt-subgrunt'); grunt.loadNpmTasks('grunt-update-submodules'); // This little bit of weirdness gives the express server chance to shutdown properly const waitBeforeExit = () => { setTimeout(() => { process.exit(0); }, 1000); }; process.on('SIGINT', waitBeforeExit); process.on('SIGTERM', waitBeforeExit); // Load the configuration grunt.initConfig(cfg); // --- Release Tooling // grunt release // - 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 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 (.git*, etc)\n' + ' - Zip files in release-folder to dist-folder/#{version} directory', function () { 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'); // Common paths used by release grunt.config.set('paths', { build: buildDirectory, releaseBuild: path.join(buildDirectory, 'release'), dist: distDirectory, releaseDist: path.join(distDirectory, 'release') }); // Load package.json so that we can create correctly versioned releases. grunt.config.set('pkg', grunt.file.readJSON('package.json')); // grunt-contrib-copy grunt.config.set('copy.release', { expand: true, // 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 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-contrib-compress grunt.config.set('compress.release', { options: { archive: '<%= paths.releaseDist %>/Ghost-<%= pkg.version %>.zip' }, expand: true, cwd: '<%= paths.releaseBuild %>/', src: ['**'] }); // grunt-contrib-clean grunt.config.set('clean.release', { src: ['<%= paths.releaseBuild %>/**'] }); if (!grunt.option('skip-update')) { grunt.task .run('update_submodules:pinned') .run('subgrunt:init'); } grunt.task .run('clean:built') .run('clean:tmp') .run('prod') .run('clean:release') .run('copy:admin_html') .run('copy:release') .run('compress:release'); } ); };