-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
0dd44d5
commit 033c5ad
Showing
6 changed files
with
450 additions
and
119 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<CommitInfo>} | ||
* | ||
* @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<string>} range The revision range in which the log will be parsed. | ||
* @returns {Promise<CommitInfo[]>} | ||
*/ | ||
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 | ||
}; |
Oops, something went wrong.