From 033c5ad868e33e9f655dfc6c3934774cefbefb6e Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Fri, 19 Jul 2019 16:20:45 +0200 Subject: [PATCH] gulp: Split gulpfile.js and expanded `changes` task (#1835) This splits the `gulpfile.js` files into multiple files and expands the `changes` tasks to generate a commit-based changelog. --- gulpfile.js/changelog.js | 381 ++++++++++++++++++++++++++++ gulpfile.js => gulpfile.js/index.js | 139 +++------- gulpfile.js/paths.js | 19 ++ gulpfile.js/premerge.js | 18 ++ package-lock.json | 11 - package.json | 1 - 6 files changed, 450 insertions(+), 119 deletions(-) create mode 100644 gulpfile.js/changelog.js rename gulpfile.js => gulpfile.js/index.js (58%) create mode 100644 gulpfile.js/paths.js create mode 100644 gulpfile.js/premerge.js diff --git a/gulpfile.js/changelog.js b/gulpfile.js/changelog.js new file mode 100644 index 0000000000..1fb9fe12c9 --- /dev/null +++ b/gulpfile.js/changelog.js @@ -0,0 +1,381 @@ +"use strict"; + +const { src, dest } = require('gulp'); + +const replace = require('gulp-replace'); +const pump = require('pump'); +const git = require('simple-git/promise')(__dirname); + +const { changelog } = require('./paths'); + + +const ISSUE_RE = /#(\d+)(?![\d\]])/g; +const ISSUE_SUB = '[#$1](https://github.com/PrismJS/prism/issues/$1)'; + +function linkify(cb) { + return pump([ + src(changelog), + replace(ISSUE_RE, ISSUE_SUB), + replace( + /\[[\da-f]+(?:, *[\da-f]+)*\]/g, + m => m.replace(/([\da-f]{7})[\da-f]*/g, '[`$1`](https://github.com/PrismJS/prism/commit/$1)') + ), + dest('.') + ], cb); +} + +/** + * Creates an array which iterates its items in the order given by `compareFn`. + * + * The array may not be sorted at all times. + * + * @param {(a: T, b: T) => number} compareFn + * @returns {T[]} + * @template T + */ +function createSortedArray(compareFn) { + /** @type {T[]} */ + const a = []; + + a['sort'] = function () { + return Array.prototype.sort.call(this, compareFn); + }; + a[Symbol.iterator] = function () { + return this.slice().sort(compareFn)[Symbol.iterator](); + }; + + return a; +} + +/** + * Parses the given log line and adds the list of the changed files to the output. + * + * @param {string} line A one-liner log line consisting of the commit hash and the commit message. + * @returns {Promise} + * + * @typedef {{ message: string, hash: string, changes: CommitChange[] }} CommitInfo + * @typedef {{ file: string, mode: ChangeMode }} CommitChange + * @typedef {"A" | "C" | "D" | "M" | "R" | "T" | "U" | "X" | "B"} ChangeMode + */ +async function getCommitInfo(line) { + const [, hash, message] = /^([a-f\d]+)\s+(.*)$/i.exec(line); + + /* The output looks like this: + * + * M components.js + * M components.json + * + * or nothing for e.g. reverts. + */ + const output = await git.raw(['diff-tree', '--no-commit-id', '--name-status', '-r', hash]); + + const changes = !output ? [] : output.trim().split(/\n/g).map(line => { + const [, mode, file] = /(\w)\s+(.+)/.exec(line); + return { mode: /** @type {ChangeMode} */ (mode), file }; + }); + + return { hash, message, changes }; +} + +/** + * Parses the output of `git log` with the given revision range. + * + * @param {string | Promise} range The revision range in which the log will be parsed. + * @returns {Promise} + */ +async function getLog(range) { + /* The output looks like this: + * + * bfbe4464 Invoke `callback` after `after-highlight` hook (#1588) + * b41fb8f1 Fixes regex for JS examples (#1591) + */ + const output = await git.raw(['log', await Promise.resolve(range), '--oneline']); + + if (output) { + const commits = output.trim().split(/\n/g); + return Promise.all(commits.map(getCommitInfo)); + } else { + return []; + } +} + +const revisionRanges = { + nextRelease: git.raw(['describe', '--abbrev=0', '--tags']).then(res => `${res.trim()}..HEAD`) +}; +const strCompare = (a, b) => a.localeCompare(b, 'en'); + +async function changes() { + const { languages, plugins } = require('../components'); + + const infos = await getLog(revisionRanges.nextRelease); + + const entries = { + 'TODO:': {}, + 'New components': { + ['']: createSortedArray(strCompare) + }, + 'Updated components': {}, + 'Updated plugins': {}, + 'Updated themes': {}, + 'Other': {}, + }; + /** + * + * @param {string} category + * @param {string | { message: string, hash: string }} info + */ + function addEntry(category, info) { + const path = category.split(/\s*>>\s*/g); + if (path[path.length - 1] !== '') { + path.push(''); + } + + let current = entries; + for (const key of path) { + if (key) { + current = current[key] = current[key] || {}; + } else { + (current[key] = current[key] || []).push(info); + } + } + } + + + /** @param {CommitChange} change */ + function notGenerated(change) { + return !change.file.endsWith('.min.js') && ['prism.js', 'components.js', 'package-lock.json'].indexOf(change.file) === -1; + } + /** @param {CommitChange} change */ + function notPartlyGenerated(change) { + return change.file !== 'plugins/autoloader/prism-autoloader.js' && + change.file !== 'plugins/show-language/prism-show-language.js'; + } + /** @param {CommitChange} change */ + function notTests(change) { + return !/^tests\//.test(change.file); + } + /** @param {CommitChange} change */ + function notExamples(change) { + return !/^examples\//.test(change.file); + } + /** @param {CommitChange} change */ + function notFailures(change) { + return !/^known-failures.html$/.test(change.file); + } + /** @param {CommitChange} change */ + function notComponentsJSON(change) { + return change.file !== 'components.json'; + } + + /** + * @param {((e: T, index: number) => boolean)[]} filters + * @returns {(e: T, index: number) => boolean} + * @template T + */ + function and(...filters) { + return (e, index) => { + for (let i = 0, l = filters.length; i < l; i++) { + if (!filters[i](e, index)) { + return false; + } + } + return true; + }; + } + + /** + * Some commit message have the format `component changed: actual message`. + * This function can be used to remove this prefix. + * + * @param {string} prefix + * @param {CommitInfo} info + * @returns {{ message: string, hash: string }} + */ + function removeMessagePrefix(prefix, info) { + const source = String.raw`^${prefix.replace(/([^-\w\s])/g, '\\$1').replace(/[-\s]/g, '[-\\s]')}:\s*`; + const patter = RegExp(source, 'i'); + return { + message: info.message.replace(patter, ''), + hash: info.hash + }; + } + + + /** + * @type {((info: CommitInfo) => boolean)[]} + */ + const commitSorters = [ + + function rebuild(info) { + if (info.changes.length > 0 && info.changes.filter(notGenerated).length === 0) { + console.log('Rebuild found: ' + info.message); + return true; + } + }, + + function addedComponent(info) { + let relevantChanges = info.changes.filter(and(notGenerated, notTests, notExamples, notFailures)); + + // `components.json` has to be modified + if (relevantChanges.some(c => c.file === 'components.json')) { + relevantChanges = relevantChanges.filter(and(notComponentsJSON, notPartlyGenerated)); + + // now, only the newly added JS should be left + if (relevantChanges.length === 1) { + const change = relevantChanges[0]; + if (change.mode === 'A' && change.file.startsWith('components/prism-')) { + const lang = change.file.match(/prism-([\w-]+)\.js$/)[1]; + const titles = [languages[lang].title]; + if (languages[lang].aliasTitles) { + titles.push(...Object.values(languages[lang].aliasTitles)); + } + addEntry('New components', `__${titles.join('__ & __')}__: ${infoToString(info)}`); + return true; + } + } + } + }, + + function changedComponentOrCore(info) { + let relevantChanges = info.changes.filter(and(notGenerated, notTests, notExamples, notFailures)); + + // if `components.json` changed, then autoloader and show-language also change + if (relevantChanges.some(c => c.file === 'components.json')) { + relevantChanges = relevantChanges.filter(and(notComponentsJSON, notPartlyGenerated)); + } + + if (relevantChanges.length === 1) { + const change = relevantChanges[0]; + if (change.mode === 'M' && change.file.startsWith('components/prism-')) { + const lang = change.file.match(/prism-([\w-]+)\.js$/)[1]; + if (lang === 'core') { + addEntry('Other >> Core', removeMessagePrefix('Core', info)); + } else { + const title = languages[lang].title; + addEntry('Updated components >> ' + title, removeMessagePrefix(title, info)); + } + return true; + } + } + }, + + function changedPlugin(info) { + let relevantChanges = info.changes.filter(and(notGenerated, notTests, notExamples, c => !/\.(?:html|css)$/.test(c.file))); + + if (relevantChanges.length > 0 && + relevantChanges.every(c => c.mode === 'M' && /^plugins\/.*\.js$/.test(c.file))) { + + if (relevantChanges.length === 1) { + const change = relevantChanges[0]; + const id = change.file.match(/\/prism-([\w-]+)\.js/)[1]; + const title = plugins[id].title || plugins[id]; + addEntry('Updated plugins >> ' + title, removeMessagePrefix(title, info)); + } else { + addEntry('Updated plugins', info); + } + return true; + } + }, + + function changedTheme(info) { + if (info.changes.length > 0 && info.changes.every(c => { + return /themes\/.*\.css/.test(c.file) && c.mode === 'M'; + })) { + if (info.changes.length === 1) { + const change = info.changes[0]; + let name = (change.file.match(/prism-(\w+)\.css$/) || [, 'Default'])[1]; + name = name[0].toUpperCase() + name.substr(1); + addEntry('Updated themes >> ' + name, removeMessagePrefix(name, info)); + } else { + addEntry('Updated themes', info); + } + return true; + } + }, + + function changedInfrastructure(info) { + if (info.changes.length > 0 && info.changes.every(c => { + if (c.file.startsWith('gulpfile.js')) { + return true; + } + if (/^\.[\w.]+$/.test(c.file)) { + return true; + } + return ['CNAME', 'composer.json', 'package.json', 'package-lock.json'].indexOf(c.file) >= 0; + })) { + addEntry('Other >> Infrastructure', info); + return true; + } + }, + + function changedWebsite(info) { + if (info.changes.length > 0 && info.changes.every(c => { + if (/[\w-]+\.(?:html|svg)$/.test(c.file)) { + return true; + } + if (/^scripts(?:\/[\w-]+)*\/[\w-]+\.js$/.test(c.file)) { + return true; + } + return ['style.css'].indexOf(c.file) >= 0; + })) { + addEntry('Other >> Website', info); + return true; + } + }, + + function otherChanges(info) { + // detect changes of the Github setup + // This assumes that .md files are related to GitHub + if (info.changes.length > 0 && info.changes.every(c => /\.md$/i.test(c.file))) { + addEntry('Other', info); + return true; + } + }, + + ]; + + for (const info of infos) { + if (!commitSorters.some(sorter => sorter(info))) { + addEntry('TODO:', info); + } + } + + + /** + * Stringifies the given commit info. + * + * @param {string | CommitInfo} info + * @returns {string} + */ + function infoToString(info) { + if (typeof info === 'string') { + return info; + } + return `${info.message} [\`${info.hash}\`](https://github.com/PrismJS/prism/commit/${info.hash})`; + } + function printCategory(category, indentation = '') { + for (const subCategory of Object.keys(category).sort(strCompare)) { + if (subCategory) { + md += `${indentation}* __${subCategory}__\n`; + printCategory(category[subCategory], indentation + ' ') + } else { + for (const info of category['']) { + md += `${indentation}* ${infoToString(info)}\n`; + } + } + } + } + + let md = ''; + for (const category of Object.keys(entries)) { + md += `\n### ${category}\n\n`; + printCategory(entries[category]); + } + console.log(md); +} + + +module.exports = { + linkify, + changes +}; diff --git a/gulpfile.js b/gulpfile.js/index.js similarity index 58% rename from gulpfile.js rename to gulpfile.js/index.js index 9c09a766eb..14e8a19dc3 100644 --- a/gulpfile.js +++ b/gulpfile.js/index.js @@ -1,3 +1,5 @@ +"use strict"; + const { src, dest, series, parallel, watch } = require('gulp'); const rename = require('gulp-rename'); @@ -8,26 +10,11 @@ const replace = require('gulp-replace'); const pump = require('pump'); const util = require('util'); const fs = require('fs'); -const simpleGit = require('simple-git'); -const shelljs = require('shelljs'); -const paths = { - componentsFile: 'components.json', - componentsFileJS: 'components.js', - components: ['components/**/*.js', '!components/index.js', '!components/**/*.min.js'], - main: [ - 'components/prism-core.js', - 'components/prism-markup.js', - 'components/prism-css.js', - 'components/prism-clike.js', - 'components/prism-javascript.js', - 'plugins/file-highlight/prism-file-highlight.js' - ], - plugins: ['plugins/**/*.js', '!plugins/**/*.min.js'], - showLanguagePlugin: 'plugins/show-language/prism-show-language.js', - autoloaderPlugin: 'plugins/autoloader/prism-autoloader.js', - changelog: 'CHANGELOG.md' -}; +const paths = require('./paths'); +const { premerge } = require('./premerge'); +const { changes, linkify } = require('./changelog'); + const componentsPromise = new Promise((resolve, reject) => { fs.readFile(paths.componentsFile, { @@ -163,7 +150,7 @@ async function languagePlugins() { const tasks = [ { plugin: paths.showLanguagePlugin, - maps: { languages: jsonLanguagesMap} + maps: { languages: jsonLanguagesMap } }, { plugin: paths.autoloaderPlugin, @@ -172,20 +159,24 @@ async function languagePlugins() { ]; // TODO: Use `Promise.allSettled` (https://github.com/tc39/proposal-promise-allSettled) - const taskResults = await Promise.all(tasks.map(task => new Promise((resolve, reject) => { - const stream = src(task.plugin) - .pipe(replace( - /\/\*(\w+)_placeholder\[\*\/[\s\S]*?\/\*\]\*\//g, - (m, mapName) => `/*${mapName}_placeholder[*/${task.maps[mapName]}/*]*/` - )) - .pipe(dest(task.plugin.substring(0, task.plugin.lastIndexOf('/')))); - - stream.on('error', reject); - stream.on('end', resolve); - }).then( - /** @type {(value: T) => {status: 'fulfilled', value: T}} */ value => ({status: 'fulfilled', value}), - /** @type {(error: T) => {status: 'rejected', reason: T}} */ error => ({status: 'rejected', reason: error}), - ))); + const taskResults = await Promise.all(tasks.map(async task => { + try { + const value = await new Promise((resolve, reject) => { + const stream = src(task.plugin) + .pipe(replace( + /\/\*(\w+)_placeholder\[\*\/[\s\S]*?\/\*\]\*\//g, + (m, mapName) => `/*${mapName}_placeholder[*/${task.maps[mapName]}/*]*/` + )) + .pipe(dest(task.plugin.substring(0, task.plugin.lastIndexOf('/')))); + + stream.on('error', reject); + stream.on('end', resolve); + }); + return { status: 'fulfilled', value }; + } catch (error) { + return { status: 'rejected', reason: error }; + } + })); const rejectedTasks = taskResults.filter(/** @return {r is {status: 'rejected', reason: any}} */ r => r.status === 'rejected'); if (rejectedTasks.length > 0) { @@ -193,80 +184,14 @@ async function languagePlugins() { } } -const ISSUE_RE = /#(\d+)(?![\d\]])/g; -const ISSUE_SUB = '[#$1](https://github.com/PrismJS/prism/issues/$1)'; - -function linkify(cb) { - return pump([ - src(paths.changelog), - replace(ISSUE_RE, ISSUE_SUB), - replace( - /\[[\da-f]+(?:, *[\da-f]+)*\]/g, - m => m.replace(/([\da-f]{7})[\da-f]*/g, '[`$1`](https://github.com/PrismJS/prism/commit/$1)') - ), - dest('.') - ], cb); -} - -const COMMIT_RE = /^([\da-z]{8})\s(.*)/; - -function changes(cb) { - const tag = shelljs.exec('git describe --abbrev=0 --tags', { silent: true }).stdout; - const commits = shelljs - .exec( - `git log ${tag.trim()}..HEAD --oneline`, - { silent: true } - ) - .stdout.split('\n') - .map(line => line.trim()) - .filter(line => line !== '') - .map(line => { - const [,hash, msg] = COMMIT_RE.exec(line); - return `* ${msg.replace(ISSUE_RE, ISSUE_SUB)} [\`${hash}\`](https://github.com/PrismJS/prism/commit/${hash})` - }) - .join('\n'); - - const changes = `## Unreleased - -${commits} - -### New components - -### Updated components - -### Updated plugins - -### Updated themes - -### Other changes - -* __Website__`; - - console.log(changes); - cb(); -} - const components = minifyComponents; const plugins = series(languagePlugins, minifyPlugins); -function gitChanges(cb) { - const git = simpleGit(__dirname); - git.status((err, res) => { - if (err) { - cb(new Error(`Something went wrong!\n${err}`)); - } else if (res.files.length > 0) { - console.log(res); - cb(new Error('There are changes in the file system. Did you forget to run gulp?')); - } else { - cb(); - } - }); -} - - -exports.watch = watchComponentsAndPlugins; -exports.default = parallel(components, plugins, componentsJsonToJs, build); -exports.premerge = gitChanges; -exports.linkify = linkify; -exports.changes = changes; +module.exports = { + watch: watchComponentsAndPlugins, + default: parallel(components, plugins, componentsJsonToJs, build), + premerge, + linkify, + changes +}; diff --git a/gulpfile.js/paths.js b/gulpfile.js/paths.js new file mode 100644 index 0000000000..1d5b7a3f38 --- /dev/null +++ b/gulpfile.js/paths.js @@ -0,0 +1,19 @@ +"use strict"; + +module.exports = { + componentsFile: 'components.json', + componentsFileJS: 'components.js', + components: ['components/**/*.js', '!components/index.js', '!components/**/*.min.js'], + main: [ + 'components/prism-core.js', + 'components/prism-markup.js', + 'components/prism-css.js', + 'components/prism-clike.js', + 'components/prism-javascript.js', + 'plugins/file-highlight/prism-file-highlight.js' + ], + plugins: ['plugins/**/*.js', '!plugins/**/*.min.js'], + showLanguagePlugin: 'plugins/show-language/prism-show-language.js', + autoloaderPlugin: 'plugins/autoloader/prism-autoloader.js', + changelog: 'CHANGELOG.md' +}; diff --git a/gulpfile.js/premerge.js b/gulpfile.js/premerge.js new file mode 100644 index 0000000000..a78c21b691 --- /dev/null +++ b/gulpfile.js/premerge.js @@ -0,0 +1,18 @@ +"use strict"; + +const git = require('simple-git/promise')(__dirname); + + +function gitChanges() { + return git.status().then(res => { + if (res.files.length > 0) { + console.log(res); + throw new Error('There are changes in the file system. Did you forget to run gulp?'); + } + }); +} + + +module.exports = { + premerge: gitChanges +}; diff --git a/package-lock.json b/package-lock.json index bb2538c3fc..2f042d7890 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4252,17 +4252,6 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, - "shelljs": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", - "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==", - "dev": true, - "requires": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - } - }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", diff --git a/package.json b/package.json index 1a3fcb6637..ddb65e3dca 100755 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "jsdom": "^13.0.0", "mocha": "^6.0.0", "pump": "^3.0.0", - "shelljs": "^0.8.3", "simple-git": "^1.107.0", "yargs": "^13.2.2" },