UNPKG

9.92 kBPlain TextView Raw
1/**
2 * @license
3 * Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
4 * This code may only be used under the BSD style license found at
5 * http://polymer.github.io/LICENSE.txt
6 * The complete set of authors may be found at
7 * http://polymer.github.io/AUTHORS.txt
8 * The complete set of contributors may be found at
9 * http://polymer.github.io/CONTRIBUTORS.txt
10 * Code distributed by Google as part of the polymer project is also
11 * subject to an additional IP rights grant found at
12 * http://polymer.github.io/PATENTS.txt
13 */
14
15import * as cssSlam from 'css-slam';
16import * as gulpif from 'gulp-if';
17import * as logging from 'plylog';
18import {JsCompileTarget, ModuleResolutionStrategy} from 'polymer-project-config';
19import {Transform} from 'stream';
20import * as vinyl from 'vinyl';
21
22import matcher = require('matcher');
23
24import {jsTransform} from './js-transform';
25import {htmlTransform} from './html-transform';
26import {isHtmlSplitterFile} from './html-splitter';
27
28// TODO(fks) 09-22-2016: Latest npm type declaration resolves to a non-module
29// entity. Upgrade to proper JS import once compatible .d.ts file is released,
30// or consider writing a custom declaration in the `custom_typings/` folder.
31import File = require('vinyl');
32
33const logger = logging.getLogger('cli.build.optimize-streams');
34
35export type FileCB = (error?: Error, file?: File) => void;
36export type CSSOptimizeOptions = {
37 stripWhitespace?: boolean;
38};
39export interface OptimizeOptions {
40 html?: {
41 minify?: boolean|{exclude?: string[]},
42 };
43 css?: {
44 minify?: boolean|{exclude?: string[]},
45 };
46 js?: JsOptimizeOptions;
47 entrypointPath?: string;
48 rootDir?: string;
49}
50
51export type JsCompileOptions = boolean|JsCompileTarget|{
52 target?: JsCompileTarget;
53 exclude?: string[];
54};
55
56export interface JsOptimizeOptions {
57 minify?: boolean|{exclude?: string[]};
58 compile?: JsCompileOptions;
59 moduleResolution?: ModuleResolutionStrategy;
60 transformModulesToAmd?: boolean;
61}
62
63/**
64 * GenericOptimizeTransform is a generic optimization stream. It can be extended
65 * to create a new kind of specific file-type optimizer, or it can be used
66 * directly to create an ad-hoc optimization stream for different libraries.
67 * If the transform library throws an exception when run, the file will pass
68 * through unaffected.
69 */
70export class GenericOptimizeTransform extends Transform {
71 optimizer: (content: string, file: File) => string;
72 optimizerName: string;
73
74 constructor(
75 optimizerName: string,
76 optimizer: (content: string, file: File) => string) {
77 super({objectMode: true});
78 this.optimizer = optimizer;
79 this.optimizerName = optimizerName;
80 }
81
82 _transform(file: File, _encoding: string, callback: FileCB): void {
83 // TODO(fks) 03-07-2017: This is a quick fix to make sure that
84 // "webcomponentsjs" files aren't compiled down to ES5, because they contain
85 // an important ES6 shim to make custom elements possible. Remove/refactor
86 // when we have a better plan for excluding some files from optimization.
87 if (!file.path || file.path.indexOf('webcomponentsjs/') >= 0 ||
88 file.path.indexOf('webcomponentsjs\\') >= 0) {
89 callback(undefined, file);
90 return;
91 }
92
93 if (file.contents) {
94 try {
95 let contents = file.contents.toString();
96 contents = this.optimizer(contents, file);
97 file.contents = Buffer.from(contents);
98 } catch (error) {
99 logger.warn(
100 `${this.optimizerName}: Unable to optimize ${file.path}`,
101 {err: error.message || error});
102 }
103 }
104 callback(undefined, file);
105 }
106}
107
108function getCompileTarget(
109 file: vinyl, options: JsOptimizeOptions): JsCompileTarget|boolean {
110 let target: JsCompileTarget|boolean|undefined;
111 const compileOptions = options.compile;
112 if (notExcluded(options.compile)(file)) {
113 if (typeof compileOptions === 'object') {
114 target =
115 (compileOptions.target === undefined) ? true : compileOptions.target;
116 } else {
117 target = compileOptions;
118 }
119 if (target === undefined) {
120 target = false;
121 }
122 } else {
123 target = false;
124 }
125 return target;
126}
127
128/**
129 * Transform JavaScript.
130 */
131export class JsTransform extends GenericOptimizeTransform {
132 constructor(options: OptimizeOptions) {
133 const jsOptions: JsOptimizeOptions = options.js || {};
134
135 const shouldMinifyFile =
136 jsOptions.minify ? notExcluded(jsOptions.minify) : () => false;
137
138 const transformer = (content: string, file: File) => {
139 let transformModulesToAmd: boolean|'auto' = false;
140
141 if (jsOptions.transformModulesToAmd) {
142 if (isHtmlSplitterFile(file)) {
143 // This is a type=module script in an HTML file. Definitely AMD
144 // transform.
145 transformModulesToAmd = file.isModule === true;
146 } else {
147 // This is an external script file. Only AMD transform it if it looks
148 // like a module.
149 transformModulesToAmd = 'auto';
150 }
151 }
152
153 return jsTransform(content, {
154 compile: getCompileTarget(file, jsOptions),
155 externalHelpers: true,
156 minify: shouldMinifyFile(file),
157 moduleResolution: jsOptions.moduleResolution,
158 filePath: file.path,
159 rootDir: options.rootDir,
160 transformModulesToAmd,
161 });
162 };
163
164 super('js-transform', transformer);
165 }
166}
167
168/**
169 * Transform HTML.
170 */
171export class HtmlTransform extends GenericOptimizeTransform {
172 constructor(options: OptimizeOptions) {
173 const jsOptions: JsOptimizeOptions = options.js || {};
174
175 const shouldMinifyFile = options.html && options.html.minify ?
176 notExcluded(options.html.minify) :
177 () => false;
178
179 const transformer = (content: string, file: File) => {
180 const transformModulesToAmd =
181 options.js && options.js.transformModulesToAmd;
182 const isEntryPoint =
183 !!options.entrypointPath && file.path === options.entrypointPath;
184
185 let injectBabelHelpers: 'none'|'full'|'amd' = 'none';
186 let injectRegeneratorRuntime = false;
187 if (isEntryPoint) {
188 const compileTarget = getCompileTarget(file, jsOptions);
189 switch (compileTarget) {
190 case 'es5':
191 case true:
192 injectBabelHelpers = 'full';
193 injectRegeneratorRuntime = true;
194 break;
195 case 'es2015':
196 case 'es2016':
197 case 'es2017':
198 injectBabelHelpers = 'full';
199 injectRegeneratorRuntime = false;
200 break;
201 case 'es2018':
202 case false:
203 injectBabelHelpers = transformModulesToAmd ? 'amd' : 'none';
204 injectRegeneratorRuntime = false;
205 break;
206 default:
207 const never: never = compileTarget;
208 throw new Error(`Unexpected compile target ${never}`);
209 }
210 }
211
212 return htmlTransform(content, {
213 js: {
214 transformModulesToAmd,
215 externalHelpers: true,
216 // Note we don't do any other JS transforms here (like compilation),
217 // because we're assuming that HtmlSplitter has run and any inline
218 // scripts will be compiled in their own stream.
219 },
220 minifyHtml: shouldMinifyFile(file),
221 injectBabelHelpers,
222 injectRegeneratorRuntime,
223 injectAmdLoader: isEntryPoint && transformModulesToAmd,
224 });
225 };
226 super('html-transform', transformer);
227 }
228}
229
230/**
231 * CSSMinifyTransform minifies CSS that pass through it (via css-slam).
232 */
233export class CSSMinifyTransform extends GenericOptimizeTransform {
234 constructor(private options: CSSOptimizeOptions) {
235 super('css-slam-minify', cssSlam.css);
236 }
237
238 _transform(file: File, encoding: string, callback: FileCB): void {
239 // css-slam will only be run if the `stripWhitespace` option is true.
240 if (this.options.stripWhitespace) {
241 super._transform(file, encoding, callback);
242 }
243 }
244}
245
246/**
247 * InlineCSSOptimizeTransform minifies inlined CSS (found in HTML files) that
248 * passes through it (via css-slam).
249 */
250export class InlineCSSOptimizeTransform extends GenericOptimizeTransform {
251 constructor(private options: CSSOptimizeOptions) {
252 super('css-slam-inline', cssSlam.html);
253 }
254
255 _transform(file: File, encoding: string, callback: FileCB): void {
256 // css-slam will only be run if the `stripWhitespace` option is true.
257 if (this.options.stripWhitespace) {
258 super._transform(file, encoding, callback);
259 }
260 }
261}
262
263/**
264 * Returns an array of optimization streams to use in your build, based on the
265 * OptimizeOptions given.
266 */
267export function getOptimizeStreams(options?: OptimizeOptions):
268 NodeJS.ReadWriteStream[] {
269 options = options || {};
270 const streams = [];
271
272 streams.push(gulpif(matchesExt('.js'), new JsTransform(options)));
273 streams.push(gulpif(matchesExt('.html'), new HtmlTransform(options)));
274
275 if (options.css && options.css.minify) {
276 streams.push(gulpif(
277 matchesExtAndNotExcluded('.css', options.css.minify),
278 new CSSMinifyTransform({stripWhitespace: true})));
279 // TODO(fks): Remove this InlineCSSOptimizeTransform stream once CSS
280 // is properly being isolated by splitHtml() & rejoinHtml().
281 streams.push(gulpif(
282 matchesExtAndNotExcluded('.html', options.css.minify),
283 new InlineCSSOptimizeTransform({stripWhitespace: true})));
284 }
285
286 return streams;
287}
288
289export function matchesExt(extension: string) {
290 return (fs: vinyl) => !!fs.path && fs.relative.endsWith(extension);
291}
292
293function notExcluded(option?: JsCompileOptions) {
294 const exclude = typeof option === 'object' && option.exclude || [];
295 return (fs: vinyl) => !exclude.some(
296 (pattern: string) => matcher.isMatch(fs.relative, pattern));
297}
298
299function matchesExtAndNotExcluded(extension: string, option: JsCompileOptions) {
300 const a = matchesExt(extension);
301 const b = notExcluded(option);
302 return (fs: vinyl) => a(fs) && b(fs);
303}