polymer-build
Version:
A library of Gulp build tasks
304 lines (268 loc) • 9.92 kB
text/typescript
/**
* @license
* Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import * as cssSlam from 'css-slam';
import * as gulpif from 'gulp-if';
import * as logging from 'plylog';
import {JsCompileTarget, ModuleResolutionStrategy} from 'polymer-project-config';
import {Transform} from 'stream';
import * as vinyl from 'vinyl';
import matcher = require('matcher');
import {jsTransform} from './js-transform';
import {htmlTransform} from './html-transform';
import {isHtmlSplitterFile} from './html-splitter';
// TODO(fks) 09-22-2016: Latest npm type declaration resolves to a non-module
// entity. Upgrade to proper JS import once compatible .d.ts file is released,
// or consider writing a custom declaration in the `custom_typings/` folder.
import File = require('vinyl');
const logger = logging.getLogger('cli.build.optimize-streams');
export type FileCB = (error?: Error, file?: File) => void;
export type CSSOptimizeOptions = {
stripWhitespace?: boolean;
};
export interface OptimizeOptions {
html?: {
minify?: boolean|{exclude?: string[]},
};
css?: {
minify?: boolean|{exclude?: string[]},
};
js?: JsOptimizeOptions;
entrypointPath?: string;
rootDir?: string;
}
export type JsCompileOptions = boolean|JsCompileTarget|{
target?: JsCompileTarget;
exclude?: string[];
};
export interface JsOptimizeOptions {
minify?: boolean|{exclude?: string[]};
compile?: JsCompileOptions;
moduleResolution?: ModuleResolutionStrategy;
transformModulesToAmd?: boolean;
}
/**
* GenericOptimizeTransform is a generic optimization stream. It can be extended
* to create a new kind of specific file-type optimizer, or it can be used
* directly to create an ad-hoc optimization stream for different libraries.
* If the transform library throws an exception when run, the file will pass
* through unaffected.
*/
export class GenericOptimizeTransform extends Transform {
optimizer: (content: string, file: File) => string;
optimizerName: string;
constructor(
optimizerName: string,
optimizer: (content: string, file: File) => string) {
super({objectMode: true});
this.optimizer = optimizer;
this.optimizerName = optimizerName;
}
_transform(file: File, _encoding: string, callback: FileCB): void {
// TODO(fks) 03-07-2017: This is a quick fix to make sure that
// "webcomponentsjs" files aren't compiled down to ES5, because they contain
// an important ES6 shim to make custom elements possible. Remove/refactor
// when we have a better plan for excluding some files from optimization.
if (!file.path || file.path.indexOf('webcomponentsjs/') >= 0 ||
file.path.indexOf('webcomponentsjs\\') >= 0) {
callback(undefined, file);
return;
}
if (file.contents) {
try {
let contents = file.contents.toString();
contents = this.optimizer(contents, file);
file.contents = Buffer.from(contents);
} catch (error) {
logger.warn(
`${this.optimizerName}: Unable to optimize ${file.path}`,
{err: error.message || error});
}
}
callback(undefined, file);
}
}
function getCompileTarget(
file: vinyl, options: JsOptimizeOptions): JsCompileTarget|boolean {
let target: JsCompileTarget|boolean|undefined;
const compileOptions = options.compile;
if (notExcluded(options.compile)(file)) {
if (typeof compileOptions === 'object') {
target =
(compileOptions.target === undefined) ? true : compileOptions.target;
} else {
target = compileOptions;
}
if (target === undefined) {
target = false;
}
} else {
target = false;
}
return target;
}
/**
* Transform JavaScript.
*/
export class JsTransform extends GenericOptimizeTransform {
constructor(options: OptimizeOptions) {
const jsOptions: JsOptimizeOptions = options.js || {};
const shouldMinifyFile =
jsOptions.minify ? notExcluded(jsOptions.minify) : () => false;
const transformer = (content: string, file: File) => {
let transformModulesToAmd: boolean|'auto' = false;
if (jsOptions.transformModulesToAmd) {
if (isHtmlSplitterFile(file)) {
// This is a type=module script in an HTML file. Definitely AMD
// transform.
transformModulesToAmd = file.isModule === true;
} else {
// This is an external script file. Only AMD transform it if it looks
// like a module.
transformModulesToAmd = 'auto';
}
}
return jsTransform(content, {
compile: getCompileTarget(file, jsOptions),
externalHelpers: true,
minify: shouldMinifyFile(file),
moduleResolution: jsOptions.moduleResolution,
filePath: file.path,
rootDir: options.rootDir,
transformModulesToAmd,
});
};
super('js-transform', transformer);
}
}
/**
* Transform HTML.
*/
export class HtmlTransform extends GenericOptimizeTransform {
constructor(options: OptimizeOptions) {
const jsOptions: JsOptimizeOptions = options.js || {};
const shouldMinifyFile = options.html && options.html.minify ?
notExcluded(options.html.minify) :
() => false;
const transformer = (content: string, file: File) => {
const transformModulesToAmd =
options.js && options.js.transformModulesToAmd;
const isEntryPoint =
!!options.entrypointPath && file.path === options.entrypointPath;
let injectBabelHelpers: 'none'|'full'|'amd' = 'none';
let injectRegeneratorRuntime = false;
if (isEntryPoint) {
const compileTarget = getCompileTarget(file, jsOptions);
switch (compileTarget) {
case 'es5':
case true:
injectBabelHelpers = 'full';
injectRegeneratorRuntime = true;
break;
case 'es2015':
case 'es2016':
case 'es2017':
injectBabelHelpers = 'full';
injectRegeneratorRuntime = false;
break;
case 'es2018':
case false:
injectBabelHelpers = transformModulesToAmd ? 'amd' : 'none';
injectRegeneratorRuntime = false;
break;
default:
const never: never = compileTarget;
throw new Error(`Unexpected compile target ${never}`);
}
}
return htmlTransform(content, {
js: {
transformModulesToAmd,
externalHelpers: true,
// Note we don't do any other JS transforms here (like compilation),
// because we're assuming that HtmlSplitter has run and any inline
// scripts will be compiled in their own stream.
},
minifyHtml: shouldMinifyFile(file),
injectBabelHelpers,
injectRegeneratorRuntime,
injectAmdLoader: isEntryPoint && transformModulesToAmd,
});
};
super('html-transform', transformer);
}
}
/**
* CSSMinifyTransform minifies CSS that pass through it (via css-slam).
*/
export class CSSMinifyTransform extends GenericOptimizeTransform {
constructor(private options: CSSOptimizeOptions) {
super('css-slam-minify', cssSlam.css);
}
_transform(file: File, encoding: string, callback: FileCB): void {
// css-slam will only be run if the `stripWhitespace` option is true.
if (this.options.stripWhitespace) {
super._transform(file, encoding, callback);
}
}
}
/**
* InlineCSSOptimizeTransform minifies inlined CSS (found in HTML files) that
* passes through it (via css-slam).
*/
export class InlineCSSOptimizeTransform extends GenericOptimizeTransform {
constructor(private options: CSSOptimizeOptions) {
super('css-slam-inline', cssSlam.html);
}
_transform(file: File, encoding: string, callback: FileCB): void {
// css-slam will only be run if the `stripWhitespace` option is true.
if (this.options.stripWhitespace) {
super._transform(file, encoding, callback);
}
}
}
/**
* Returns an array of optimization streams to use in your build, based on the
* OptimizeOptions given.
*/
export function getOptimizeStreams(options?: OptimizeOptions):
NodeJS.ReadWriteStream[] {
options = options || {};
const streams = [];
streams.push(gulpif(matchesExt('.js'), new JsTransform(options)));
streams.push(gulpif(matchesExt('.html'), new HtmlTransform(options)));
if (options.css && options.css.minify) {
streams.push(gulpif(
matchesExtAndNotExcluded('.css', options.css.minify),
new CSSMinifyTransform({stripWhitespace: true})));
// TODO(fks): Remove this InlineCSSOptimizeTransform stream once CSS
// is properly being isolated by splitHtml() & rejoinHtml().
streams.push(gulpif(
matchesExtAndNotExcluded('.html', options.css.minify),
new InlineCSSOptimizeTransform({stripWhitespace: true})));
}
return streams;
}
export function matchesExt(extension: string) {
return (fs: vinyl) => !!fs.path && fs.relative.endsWith(extension);
}
function notExcluded(option?: JsCompileOptions) {
const exclude = typeof option === 'object' && option.exclude || [];
return (fs: vinyl) => !exclude.some(
(pattern: string) => matcher.isMatch(fs.relative, pattern));
}
function matchesExtAndNotExcluded(extension: string, option: JsCompileOptions) {
const a = matchesExt(extension);
const b = notExcluded(option);
return (fs: vinyl) => a(fs) && b(fs);
}