gulpfile.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. const fs = require('fs').promises;
  2. const gulp = require('gulp');
  3. const del = require('del');
  4. const log = require('fancy-log');
  5. const gulpFilter = require('gulp-filter');
  6. const uglify = require('gulp-uglify');
  7. const plumber = require('gulp-plumber');
  8. const yaml = require('js-yaml');
  9. const Sharp = require('sharp');
  10. const { isProd } = require('@gera2ld/plaid/util');
  11. const spawn = require('cross-spawn');
  12. const i18n = require('./scripts/i18n');
  13. const pkg = require('./package.json');
  14. const DIST = 'dist';
  15. const paths = {
  16. manifest: 'src/manifest.yml',
  17. copy: [
  18. 'src/public/lib/**',
  19. ],
  20. locales: [
  21. 'src/_locales/**',
  22. ],
  23. templates: [
  24. 'src/**/*.@(js|html|json|yml|vue)',
  25. ],
  26. };
  27. function clean() {
  28. return del(DIST);
  29. }
  30. function watch() {
  31. gulp.watch(paths.manifest, manifest);
  32. gulp.watch(paths.copy, copyFiles);
  33. gulp.watch(paths.locales.concat(paths.templates), copyI18n);
  34. }
  35. async function jsDev() {
  36. require('@gera2ld/plaid-webpack/bin/develop')();
  37. }
  38. async function jsProd() {
  39. return require('@gera2ld/plaid-webpack/bin/build')({
  40. api: true,
  41. keep: true,
  42. });
  43. }
  44. /**
  45. * Derive extension version from pkg.version and pkg.beta fields.
  46. *
  47. * > manifest.version = `${pkg.version}.${pkg.beta}`
  48. */
  49. function getVersion() {
  50. let version = pkg.version.replace(/-[^.]*/, '');
  51. if (pkg.beta) version += `.${pkg.beta}`;
  52. return version;
  53. }
  54. async function readManifest() {
  55. const input = await fs.readFile(paths.manifest, 'utf8');
  56. const data = yaml.safeLoad(input);
  57. return data;
  58. }
  59. /**
  60. * Versioning
  61. *
  62. * The version of extension is composed of `version` and `beta` fields in `package.json`, i.e.
  63. *
  64. * Note: prerelease is ignored and not recommended since both Chrome and Firefox do not support semver
  65. *
  66. */
  67. async function manifest() {
  68. const data = await readManifest();
  69. data.version = getVersion();
  70. if (process.env.TARGET === 'selfHosted') {
  71. data.browser_specific_settings.gecko.update_url = 'https://raw.githubusercontent.com/violentmonkey/violentmonkey/master/updates.json';
  72. }
  73. await fs.writeFile(`${DIST}/manifest.json`, JSON.stringify(data), 'utf8');
  74. }
  75. async function createIcons() {
  76. const ALPHA = .5;
  77. const dist = `${DIST}/public/images`;
  78. await fs.mkdir(dist, { recursive: true });
  79. const icon = Sharp(`src/resources/icon${pkg.beta ? '-beta' : ''}.png`);
  80. const gray = icon.clone().grayscale();
  81. const transparent = icon.clone().composite([{
  82. input: Buffer.from([255, 255, 255, 256 * ALPHA]),
  83. raw: { width: 1, height: 1, channels: 4 },
  84. tile: true,
  85. blend: 'dest-in',
  86. }]);
  87. const types = [
  88. ['', icon],
  89. ['b', gray],
  90. ['w', transparent],
  91. ];
  92. const handle = (size, type = '', image = icon) => {
  93. let res = image.clone().resize({ width: size });
  94. if (size < 48) res = res.sharpen(size < 32 ? .5 : .25);
  95. return res.toFile(`${dist}/icon${size}${type}.png`);
  96. };
  97. const handle16 = async ([type, image]) => {
  98. const res = image.clone()
  99. .resize({ width: 18 })
  100. .sharpen(.5, 0)
  101. .extract({ left: 1, top: 2, width: 16, height: 16 });
  102. // darken the outer edge
  103. return res.composite([{ input: await res.toBuffer(), blend: 'over' }])
  104. .toFile(`${dist}/icon16${type}.png`);
  105. };
  106. return Promise.all([
  107. handle(48),
  108. handle(128),
  109. ...types.map(handle16),
  110. ...[19, 32, 38].flatMap(size => types.map(t => handle(size, ...t))),
  111. ]);
  112. }
  113. /**
  114. * Bump `beta` in `package.json` to release a new beta version.
  115. */
  116. async function bump() {
  117. if (process.argv.includes('--reset')) {
  118. delete pkg.beta;
  119. } else {
  120. pkg.beta = (+pkg.beta || 0) + 1;
  121. }
  122. await fs.writeFile('package.json', JSON.stringify(pkg, null, 2), 'utf8');
  123. if (process.argv.includes('--commit')) {
  124. const version = `v${getVersion()}`;
  125. spawn.sync('git', ['commit', '-am', version]);
  126. spawn.sync('git', ['tag', '-m', version, version]);
  127. }
  128. }
  129. /**
  130. * Create an update manifest file to announce a new self-hosted release.
  131. */
  132. async function updateVersions() {
  133. const manifest = await readManifest();
  134. const version = getVersion();
  135. const data = {
  136. addons: {
  137. [manifest.browser_specific_settings.gecko.id]: {
  138. updates: [
  139. {
  140. version,
  141. update_link: `https://github.com/violentmonkey/violentmonkey/releases/download/v${version}/violentmonkey-${version}-an.fx.xpi`,
  142. },
  143. ],
  144. }
  145. },
  146. };
  147. await fs.writeFile('updates.json', JSON.stringify(data, null, 2), 'utf8');
  148. }
  149. function copyFiles() {
  150. const jsFilter = gulpFilter(['**/*.js'], { restore: true });
  151. let stream = gulp.src(paths.copy, { base: 'src' });
  152. if (isProd) stream = stream
  153. .pipe(jsFilter)
  154. .pipe(uglify())
  155. .pipe(jsFilter.restore);
  156. return stream
  157. .pipe(gulp.dest(DIST));
  158. }
  159. function checkI18n() {
  160. return i18n.read({
  161. base: 'src/_locales',
  162. extension: '.json',
  163. });
  164. }
  165. function copyI18n() {
  166. return i18n.read({
  167. base: 'src/_locales',
  168. touchedOnly: true,
  169. useDefaultLang: true,
  170. markUntouched: false,
  171. extension: '.json',
  172. })
  173. .pipe(gulp.dest(`${DIST}/_locales`));
  174. }
  175. /**
  176. * Load locale files (src/_locales/<lang>/message.[json|yml]), and
  177. * update them with keys in template files, then store in `message.yml`.
  178. */
  179. function updateI18n() {
  180. return gulp.src(paths.templates)
  181. .pipe(plumber(logError))
  182. .pipe(i18n.extract({
  183. base: 'src/_locales',
  184. touchedOnly: false,
  185. useDefaultLang: false,
  186. markUntouched: true,
  187. extension: '.yml',
  188. }))
  189. .pipe(gulp.dest('src/_locales'));
  190. }
  191. function logError(err) {
  192. log(err.toString());
  193. return this.emit('end');
  194. }
  195. const pack = gulp.parallel(manifest, createIcons, copyFiles, copyI18n);
  196. exports.clean = clean;
  197. exports.dev = gulp.series(gulp.parallel(pack, jsDev), watch);
  198. exports.build = gulp.series(clean, gulp.parallel(pack, jsProd));
  199. exports.i18n = updateI18n;
  200. exports.check = checkI18n;
  201. exports.copyI18n = copyI18n;
  202. exports.bump = bump;
  203. exports.updateVersions = updateVersions;