Update build process

This commit is contained in:
Sam Potts
2019-02-19 01:05:59 +11:00
parent 522135adaf
commit 54110f8358
24 changed files with 1918 additions and 945 deletions

View File

@ -4,38 +4,65 @@
/* global require, __dirname */
/* eslint no-console: "off" */
const del = require('del');
const path = require('path');
const gulp = require('gulp');
const gutil = require('gulp-util');
const concat = require('gulp-concat');
const filter = require('gulp-filter');
const sass = require('gulp-sass');
const cleancss = require('gulp-clean-css');
const header = require('gulp-header');
const prefix = require('gulp-autoprefixer');
const gitbranch = require('git-branch');
const svgstore = require('gulp-svgstore');
const svgmin = require('gulp-svgmin');
const rename = require('gulp-rename');
const s3 = require('gulp-s3');
const replace = require('gulp-replace');
const open = require('gulp-open');
const size = require('gulp-size');
// JavaScript
const terser = require('gulp-terser');
const rollup = require('gulp-better-rollup');
const babel = require('rollup-plugin-babel');
const sourcemaps = require('gulp-sourcemaps');
const uglify = require('gulp-uglify-es').default;
const commonjs = require('rollup-plugin-commonjs');
const resolve = require('rollup-plugin-node-resolve');
const FastlyPurge = require('fastly-purge');
// CSS
const sass = require('gulp-sass');
const clean = require('gulp-clean-css');
const prefix = require('gulp-autoprefixer');
// Images
const svgstore = require('gulp-svgstore');
const imagemin = require('gulp-imagemin');
// Utils
const del = require('del');
const filter = require('gulp-filter');
const header = require('gulp-header');
const gitbranch = require('git-branch');
const rename = require('gulp-rename');
const replace = require('gulp-replace');
const log = require('fancy-log');
const open = require('gulp-open');
const plumber = require('gulp-plumber');
const size = require('gulp-size');
const sourcemaps = require('gulp-sourcemaps');
const through = require('through2');
const bundles = require('./bundles.json');
// Deployment
const aws = require('aws-sdk');
const publish = require('gulp-awspublish');
const FastlyPurge = require('fastly-purge');
const pkg = require('./package.json');
const build = require('./build.json');
const deploy = require('./deploy.json');
const { browserslist: browsers, version } = pkg;
const minSuffix = '.min';
// Get AWS config
Object.values(deploy).forEach(target => {
Object.assign(target, {
publisher: publish.create({
region: target.region,
params: {
Bucket: target.bucket,
},
credentials: new aws.SharedIniFileCredentials({ profile: 'plyr' }),
}),
});
});
// Paths
const paths = {
plyr: {
@ -73,7 +100,7 @@ const paths = {
// Task arrays
const tasks = {
sass: [],
css: [],
js: [],
sprite: [],
clean: ['clean'],
@ -82,9 +109,6 @@ const tasks = {
// Size plugin
const sizeOptions = { showFiles: true, gzip: true };
// Browserlist
const browsers = ['> 1%'];
// Babel config
const babelrc = (polyfill = false) => ({
presets: [
@ -115,95 +139,91 @@ gulp.task('clean', done => {
done();
});
const build = {
js(files, bundle, options) {
Object.keys(files).forEach(key => {
const { format } = options;
const name = `js:${key}:${format}`;
tasks.js.push(name);
const { output } = paths[bundle];
const polyfill = name.includes('polyfilled');
const extension = format === 'es' ? '.mjs' : '.js';
// JAvaScript
return gulp.task(name, () =>
gulp
.src(bundles[bundle].js[key])
.pipe(sourcemaps.init())
.pipe(concat(key))
.pipe(
rollup(
{
plugins: [resolve(), commonjs(), babel(babelrc(polyfill))],
},
options,
),
)
.pipe(header('typeof navigator === "object" && ')) // "Support" SSR (#935)
.pipe(rename({ extname: extension }))
.pipe(gulp.dest(output))
.pipe(filter(`**/*${extension}`))
.pipe(uglify())
.pipe(size(sizeOptions))
.pipe(rename({ suffix: minSuffix }))
.pipe(sourcemaps.write(''))
.pipe(gulp.dest(output)),
);
});
},
sass(files, bundle) {
Object.keys(files).forEach(key => {
const name = `sass:${key}`;
tasks.sass.push(name);
const namespace = 'Plyr';
return gulp.task(name, () =>
gulp
.src(bundles[bundle].sass[key])
.pipe(sass())
.on('error', gutil.log)
.pipe(concat(key))
.pipe(prefix(browsers, { cascade: false }))
.pipe(cleancss())
.pipe(size(sizeOptions))
.pipe(gulp.dest(paths[bundle].output)),
);
});
},
sprite(bundle) {
const name = `svg:sprite:${bundle}`;
tasks.sprite.push(name);
Object.entries(build.js).forEach(([filename, entry]) => {
entry.formats.forEach(format => {
const name = `js:${filename}:${format}`;
tasks.js.push(name);
const polyfill = filename.includes('polyfilled');
const extension = format === 'es' ? 'mjs' : 'js';
// Process Icons
return gulp.task(name, () =>
gulp
.src(paths[bundle].src.sprite)
gulp.task(name, () => {
return gulp
.src(entry.src)
.pipe(plumber())
.pipe(sourcemaps.init())
.pipe(
svgmin({
plugins: [
{
removeDesc: true,
},
],
rollup(
{
plugins: [resolve(), commonjs(), babel(babelrc(polyfill))],
},
{
name: namespace,
// exports: 'named',
format,
},
),
)
.pipe(header('typeof navigator === "object" && ')) // "Support" SSR (#935)
.pipe(
rename({
extname: `.${extension}`,
}),
)
.pipe(svgstore())
.pipe(rename({ basename: bundle }))
.pipe(size(sizeOptions))
.pipe(gulp.dest(paths[bundle].output))
.pipe(gulp.dest(paths.demo.output)),
);
},
};
.pipe(gulp.dest(entry.dist))
.pipe(filter(`**/*${extension}`))
.pipe(terser())
.pipe(rename({ suffix: minSuffix }))
.pipe(size(sizeOptions))
.pipe(sourcemaps.write(''))
.pipe(gulp.dest(entry.dist));
});
});
});
// Plyr core files
const namespace = 'Plyr';
build.js(bundles.plyr.js, 'plyr', { name: namespace, format: 'umd' });
build.js(bundles.plyr.js, 'plyr', { name: namespace, format: 'es' });
build.sass(bundles.plyr.sass, 'plyr');
build.sprite('plyr');
// CSS
Object.entries(build.css).forEach(([filename, entry]) => {
const name = `css:${filename}`;
tasks.css.push(name);
// Demo files
build.sass(bundles.demo.sass, 'demo');
build.js(bundles.demo.js, 'demo', { format: 'iife' });
gulp.task(name, () => {
return gulp
.src(entry.src)
.pipe(plumber())
.pipe(sass())
.pipe(
prefix(browsers, {
cascade: false,
}),
)
.pipe(clean())
.pipe(size(sizeOptions))
.pipe(gulp.dest(entry.dist));
});
});
// SVG Sprites
Object.entries(build.sprite).forEach(([filename, entry]) => {
const name = `sprite:${filename}`;
tasks.sprite.push(name);
log(path.basename(filename));
gulp.task(name, () => {
return gulp
.src(entry.src)
.pipe(plumber())
.pipe(imagemin())
.pipe(svgstore())
.pipe(rename({ basename: path.parse(filename).name }))
.pipe(size(sizeOptions))
.pipe(gulp.dest(entry.dist));
});
});
// Build all JS
gulp.task('js', () => gulp.parallel(...tasks.js));
@ -212,16 +232,16 @@ gulp.task('js', () => gulp.parallel(...tasks.js));
gulp.task('watch', () => {
// Plyr core
gulp.watch(paths.plyr.src.js, gulp.parallel(...tasks.js));
gulp.watch(paths.plyr.src.sass, gulp.parallel(...tasks.sass));
gulp.watch(paths.plyr.src.sass, gulp.parallel(...tasks.css));
gulp.watch(paths.plyr.src.sprite, gulp.parallel(...tasks.sprite));
// Demo
gulp.watch(paths.demo.src.js, gulp.parallel(...tasks.js));
gulp.watch(paths.demo.src.sass, gulp.parallel(...tasks.sass));
gulp.watch(paths.demo.src.sass, gulp.parallel(...tasks.css));
});
// Build distribution
gulp.task('build', gulp.series(tasks.clean, gulp.parallel(...tasks.js, ...tasks.sass, ...tasks.sprite)));
gulp.task('build', gulp.series(tasks.clean, gulp.parallel(...tasks.js, ...tasks.css, ...tasks.sprite)));
// Default gulp task
gulp.task('default', gulp.series('build', 'watch'));
@ -236,234 +256,233 @@ try {
// Do nothing
}
// If deployment is setup
// TODO: Use gulp-awspublish and use AWS CLI credentials
if (Object.keys(credentials).includes('aws') && Object.keys(credentials).includes('fastly')) {
const { version } = pkg;
const { aws, fastly } = credentials;
// Get branch info
const branch = {
current: gitbranch.sync(),
master: 'master',
beta: 'beta',
};
// Get branch info
const branch = {
current: gitbranch.sync(),
master: 'master',
beta: 'beta',
};
const maxAge = 31536000; // 1 year
const options = {
cdn: {
headers: {
'Cache-Control': `max-age=${maxAge}`,
Vary: 'Accept-Encoding',
},
const maxAge = 31536000; // 1 year
const options = {
cdn: {
headers: {
'Cache-Control': `max-age=${maxAge}`,
Vary: 'Accept-Encoding',
},
demo: {
uploadPath: branch.current === branch.beta ? 'beta' : null,
},
demo: {
uploadPath: branch.current === branch.beta ? 'beta' : null,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
Vary: 'Accept-Encoding',
},
},
symlinks(ver, filename) {
return {
headers: {
// http://stackoverflow.com/questions/2272835/amazon-s3-object-redirect
'x-amz-website-redirect-location': `/${ver}/${filename}`,
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
Vary: 'Accept-Encoding',
},
},
symlinks(ver, filename) {
return {
headers: {
// http://stackoverflow.com/questions/2272835/amazon-s3-object-redirect
'x-amz-website-redirect-location': `/${ver}/${filename}`,
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
},
};
},
};
};
},
};
const regex =
'(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*).(?:0|[1-9][0-9]*)(?:-[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?(?:\\+[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?';
const semver = new RegExp(`v${regex}`, 'gi');
const localPath = new RegExp('(../)?dist', 'gi');
const versionPath = `https://${aws.cdn.domain}/${version}`;
const cdnpath = new RegExp(`${aws.cdn.domain}/${regex}/`, 'gi');
const regex =
'(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*).(?:0|[1-9][0-9]*)(?:-[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?(?:\\+[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?';
const semver = new RegExp(`v${regex}`, 'gi');
const localPath = new RegExp('(../)?dist', 'gi');
const versionPath = `https://${deploy.cdn.domain}/${version}`;
const cdnpath = new RegExp(`${deploy.cdn.domain}/${regex}/`, 'gi');
const renameFile = rename(p => {
p.basename = p.basename.replace(minSuffix, ''); // eslint-disable-line
p.dirname = p.dirname.replace('.', version); // eslint-disable-line
});
const renameFile = rename(p => {
p.basename = p.basename.replace(minSuffix, ''); // eslint-disable-line
p.dirname = p.dirname.replace('.', version); // eslint-disable-line
});
// Check we're on the correct branch to deploy
const canDeploy = () => {
const allowed = [branch.master, branch.beta];
// Check we're on the correct branch to deploy
const canDeploy = () => {
const allowed = [branch.master, branch.beta];
if (!allowed.includes(branch.current)) {
console.error(`Must be on ${allowed.join(', ')} to publish! (current: ${branch.current})`);
if (!allowed.includes(branch.current)) {
console.error(`Must be on ${allowed.join(', ')} to publish! (current: ${branch.current})`);
return false;
}
return false;
}
return true;
};
return true;
};
gulp.task('version', done => {
if (!canDeploy()) {
done();
return null;
}
gulp.task('version', done => {
if (!canDeploy()) {
done();
return null;
}
console.log(`Updating versions to '${version}'...`);
const { domain } = deploy.cdn;
// Replace versioned URLs in source
const files = ['plyr.js', 'plyr.polyfilled.js', 'config/defaults.js'];
console.log(`Updating versions to '${version}'...`);
return gulp
.src(files.map(file => path.join(__dirname, `src/js/${file}`)), { base: '.' })
.pipe(replace(semver, `v${version}`))
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
.pipe(gulp.dest('./'));
});
// Replace versioned URLs in source
const files = ['plyr.js', 'plyr.polyfilled.js', 'config/defaults.js'];
// Publish version to CDN bucket
gulp.task('cdn', done => {
if (!canDeploy()) {
done();
return null;
}
return gulp
.src(files.map(file => path.join(__dirname, `src/js/${file}`)), { base: '.' })
.pipe(replace(semver, `v${version}`))
.pipe(replace(cdnpath, `${domain}/${version}/`))
.pipe(gulp.dest('./'));
});
console.log(`Uploading '${version}' to ${aws.cdn.domain}...`);
// Publish version to CDN bucket
gulp.task('cdn', done => {
if (!canDeploy()) {
done();
return null;
}
// Upload to CDN
return (
gulp
.src(paths.upload)
.pipe(renameFile)
// Remove min suffix from source map URL
.pipe(
replace(
/sourceMappingURL=([\w-?.]+)/,
(match, filename) => `sourceMappingURL=${filename.replace(minSuffix, '')}`,
),
)
.pipe(
size({
showFiles: true,
gzip: true,
}),
)
.pipe(replace(localPath, versionPath))
.pipe(s3(aws.cdn, options.cdn))
);
});
const { domain, publisher } = deploy.cdn;
// Purge the fastly cache incase any 403/404 are cached
gulp.task('purge', () => {
const list = [];
if (!publisher) {
throw new Error('No publisher instance. Check AWS configuration.');
}
return gulp
console.log(`Uploading '${version}' to ${domain}...`);
// Upload to CDN
return (
gulp
.src(paths.upload)
.pipe(renameFile)
// Remove min suffix from source map URL
.pipe(
through.obj((file, enc, cb) => {
const filename = file.path.split('/').pop();
list.push(`${versionPath}/${filename.replace(minSuffix, '')}`);
cb(null);
}),
replace(
/sourceMappingURL=([\w-?.]+)/,
(match, filename) => `sourceMappingURL=${filename.replace(minSuffix, '')}`,
),
)
.on('end', () => {
const purge = new FastlyPurge(fastly.token);
.pipe(size(sizeOptions))
.pipe(replace(localPath, versionPath))
.pipe(publisher.publish(options.cdn.headers))
.pipe(publish.reporter())
);
});
list.forEach(url => {
console.log(`Purging ${url}...`);
// Purge the fastly cache incase any 403/404 are cached
gulp.task('purge', () => {
if (!Object.keys(credentials).includes('fastly')) {
throw new Error('Fastly credentials required to purge cache.');
}
purge.url(url, (error, result) => {
if (error) {
console.log(error);
} else if (result) {
console.log(result);
}
});
const { fastly } = credentials;
const list = [];
return gulp
.src(paths.upload)
.pipe(
through.obj((file, enc, cb) => {
const filename = file.path.split('/').pop();
list.push(`${versionPath}/${filename.replace(minSuffix, '')}`);
cb(null);
}),
)
.on('end', () => {
const purge = new FastlyPurge(fastly.token);
list.forEach(url => {
console.log(`Purging ${url}...`);
purge.url(url, (error, result) => {
if (error) {
console.log(error);
} else if (result) {
console.log(result);
}
});
});
});
});
});
// Publish to demo bucket
gulp.task('demo', done => {
if (!canDeploy()) {
done();
return null;
}
// Publish to demo bucket
gulp.task('demo', done => {
if (!canDeploy()) {
done();
return null;
}
console.log(`Uploading '${version}' demo to ${aws.demo.domain}...`);
const { publisher } = deploy.demo;
const { domain } = deploy.cdn;
// Replace versioned files in readme.md
gulp.src([`${__dirname}/readme.md`])
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
.pipe(gulp.dest(__dirname));
if (!publisher) {
throw new Error('No publisher instance. Check AWS configuration.');
}
// Replace local file paths with remote paths in demo HTML
// e.g. "../dist/plyr.js" to "https://cdn.plyr.io/x.x.x/plyr.js"
const index = `${paths.demo.root}index.html`;
const error = `${paths.demo.root}error.html`;
const pages = [index];
console.log(`Uploading '${version}' demo to ${deploy.demo.domain}...`);
if (branch.current === branch.master) {
pages.push(error);
}
// Replace versioned files in readme.md
gulp.src([`${__dirname}/readme.md`])
.pipe(replace(cdnpath, `${domain}/${version}/`))
.pipe(gulp.dest(__dirname));
gulp.src(pages)
.pipe(replace(localPath, versionPath))
.pipe(s3(aws.demo, options.demo));
// Replace local file paths with remote paths in demo HTML
// e.g. "../dist/plyr.js" to "https://cdn.plyr.io/x.x.x/plyr.js"
const index = `${paths.demo.root}index.html`;
const error = `${paths.demo.root}error.html`;
const pages = [index, error];
// Only update CDN for master (prod)
if (branch.current !== branch.master) {
done();
return null;
}
if (branch.current === branch.master) {
pages.push(error);
}
// Upload error.html to cdn (as well as demo site)
return gulp
.src([error])
.pipe(replace(localPath, versionPath))
.pipe(s3(aws.cdn, options.demo));
});
return gulp
.src(pages)
.pipe(replace(localPath, versionPath))
.pipe(publisher.publish(options.demo.headers))
.pipe(publish.reporter());
});
// Update symlinks for latest
/* gulp.task("symlinks", function () {
console.log("Updating symlinks...");
gulp.task('error', done => {
// Only update CDN for master (prod)
if (!canDeploy() || branch.current !== branch.master) {
done();
return null;
}
return gulp.src(paths.upload)
.pipe(through.obj(function (chunk, enc, callback) {
if (chunk.stat.isFile()) {
// Get the filename
var filename = chunk.path.split("/").reverse()[0];
const { publisher } = deploy.cdn;
// Create the 0 byte redirect files to upload
createFile(filename, "")
.pipe(rename(function (path) {
path.dirname = path.dirname.replace(".", "latest");
}))
// Upload to S3 with correct headers
.pipe(s3(aws.cdn, options.symlinks(version, filename)));
}
if (!publisher) {
throw new Error('No publisher instance. Check AWS configuration.');
}
callback(null, chunk);
}));
}); */
// Replace local file paths with remote paths in demo HTML
// e.g. "../dist/plyr.js" to "https://cdn.plyr.io/x.x.x/plyr.js"
// Upload error.html to cdn
return gulp
.src(`${paths.demo.root}error.html`)
.pipe(replace(localPath, versionPath))
.pipe(publisher.publish(options.demo.headers))
.pipe(publish.reporter());
});
// Open the demo site to check it's ok
gulp.task('open', () => {
return gulp.src(__filename).pipe(
open({
uri: `https://${aws.demo.domain}/${branch.current === branch.beta ? 'beta' : ''}`,
}),
);
});
// Do everything
gulp.task(
'deploy',
gulp.series(
'version',
tasks.clean,
gulp.parallel(...tasks.js, ...tasks.sass, ...tasks.sprite),
'cdn',
'demo',
'purge',
'open',
),
// Open the demo site to check it's ok
gulp.task('open', () => {
return gulp.src(__filename).pipe(
open({
uri: `https://${aws.demo.domain}/${branch.current === branch.beta ? 'beta' : ''}`,
}),
);
}
});
// Do everything
gulp.task(
'deploy',
gulp.series(
'version',
tasks.clean,
gulp.parallel(...tasks.js, ...tasks.css, ...tasks.sprite),
'cdn',
'demo',
'purge',
'open',
),
);