UNPKG

18.6 kBPlain TextView Raw
1#!/usr/bin/env node
2
3/* eslint indent:0 */
4const chalk = require('chalk');
5const parseExpression = require('assetgraph/lib/parseExpression');
6const yargs = require('yargs')
7 .usage('$0 --root <inputRootDirectory> -o <dir> [options] <htmlFile(s)>')
8 .wrap(72)
9 .options('h', {
10 alias: 'help',
11 describe: 'Show this help',
12 type: 'boolean',
13 default: false,
14 })
15 .options('plugin', {
16 alias: 'p',
17 describe:
18 'Install a plugin. Must be a require-able, eg. assetgraph-i18n. Can be repeated.',
19 type: 'string',
20 default: false,
21 })
22 .options('root', {
23 describe:
24 'Path to your web root (will be deduced from your input files if not specified)',
25 type: 'string',
26 demand: false,
27 })
28 .options('output', {
29 alias: ['o', 'outroot'],
30 describe: 'Path to the output folder. Will be generated if non-existing',
31 type: 'string',
32 demand: true,
33 })
34 .options('canonicalroot', {
35 describe:
36 'URI root where the site will be deployed. Must be either an absolute, a protocol-relative, or a root-relative url',
37 type: 'string',
38 demand: false,
39 })
40 .options('cdnroot', {
41 describe:
42 'URI root where the static assets will be deployed. Must be either an absolute or a protocol-relative url',
43 type: 'string',
44 demand: false,
45 })
46 .options('sourcemaps', {
47 describe: 'Whether to include source maps',
48 type: 'boolean',
49 default: false,
50 })
51 .options('contentsecuritypolicy', {
52 describe: 'Whether to update existing Content-Security-Policy meta tags',
53 type: 'boolean',
54 default: false,
55 })
56 .options('contentsecuritypolicylevel', {
57 describe:
58 'Which Content-Security-Policy level to target. Supported values: 1 and 2. Defaults to a compromise where all target browsers are supported (see --browsers).',
59 type: 'number',
60 })
61 .options('subresourceintegrity', {
62 describe:
63 'Whether to add integrity=... attributes to stylesheets and scripts that are part of the build',
64 type: 'boolean',
65 default: false,
66 })
67 .options('sourcescontent', {
68 describe:
69 'Whether to include the source contents in the source maps (requires --sourcemaps), only works for CSS (not yet JavaScript)',
70 type: 'boolean',
71 default: false,
72 })
73 .options('webpackconfig', {
74 describe:
75 'Path to where your webpack config resides. Will be loaded using require.main.require',
76 type: 'string',
77 })
78 .options('optimizeimages', {
79 describe:
80 'Perform automatic lossless optimization of all images using pngcrush, pngquant, optipng, and jpegtran',
81 type: 'boolean',
82 default: false,
83 })
84 .options('precacheserviceworker', {
85 describe:
86 'Generate service workers that make the static assets available offline (uses https://github.com/GoogleChrome/sw-precache)',
87 type: 'boolean',
88 default: false,
89 })
90 .options('precacheserviceworkerconfig', {
91 describe:
92 'Path to where your sw-precache config resides. Will be loaded using require.main.require',
93 type: 'string',
94 })
95 .options('browsers', {
96 alias: 'b',
97 describe:
98 'Specify which browsers to support. Configures autoprefixer and controls which hacks and fallbacks to apply. Defaults to all known browsers. Syntax: https://github.com/ai/browserslist',
99 type: 'string',
100 demand: false,
101 })
102 .options('debug', {
103 describe:
104 'Keep statement level console.*() calls and debugger statements in JavaScript assets',
105 type: 'boolean',
106 default: false,
107 })
108 .options('version', {
109 describe:
110 'Adds or updates <html data-version="..."> to the specified value. Use {0} to refer to the current value, eg. --version {0}/production or --version `git describe --long --tags --always --dirty 2>/dev/null || echo unknown`',
111 type: 'string',
112 })
113 .options('gzip', {
114 describe:
115 'Include a gzipped copy of text-based assets > 860 bytes for which it yields a saving',
116 default: false,
117 })
118 .options('deferscripts', {
119 describe: 'Sets the "defer" attribute on all script tags',
120 type: 'boolean',
121 default: false,
122 })
123 .options('asyncscripts', {
124 describe: 'Sets the "async" attribute on all script tags',
125 type: 'boolean',
126 default: false,
127 })
128 .options('reservednames', {
129 describe:
130 'Exclude certain variable names from mangling (equivalent to uglifyjs --reserved-names ...)',
131 })
132 .options('stoponwarning', {
133 describe:
134 'Whether to stop with a non-zero exit code when a warning is encountered',
135 type: 'boolean',
136 default: false,
137 })
138 .options('recursive', {
139 describe:
140 'Follow local HTML anchors when populating the graph (use --no-recursive to turn off this behavior)',
141 type: 'boolean',
142 default: true,
143 })
144 .options('exclude', {
145 describe:
146 'Url pattern to exclude from the build. Supports * wildcards. You can create multiple of these: --exclude *.php --exclude http://example.com/*.gif',
147 type: 'string',
148 demand: false,
149 })
150 .options('minify', {
151 describe: 'Minifies HTML, CSS and JavaScript.',
152 type: 'boolean',
153 default: true,
154 })
155 .options('svgo', {
156 describe:
157 'Minify SVG with svgo (requires the svgo module to be installed in your app)',
158 type: 'boolean',
159 default: true,
160 })
161 .options('nocompress', {
162 describe: 'Disables JavaScript compression via UglifyJS',
163 alias: 'no-compress',
164 type: 'boolean',
165 default: false,
166 })
167 .options('pretty', {
168 describe: 'Whether to pretty-print JavaScript, CSS and HTML assets',
169 type: 'boolean',
170 default: false,
171 })
172 .options('space', {
173 describe: 'The space character to use when serializing JavaScript',
174 type: 'string',
175 })
176 .options('newline', {
177 describe: 'The newline character to use when serializing JavaScript',
178 type: 'string',
179 })
180 .options('nofilerev', {
181 describe:
182 'Revision files with a hash of their content in their file name for optimal far future caching',
183 type: 'boolean',
184 default: false,
185 })
186 .options('nocdnflash', {
187 describe:
188 'Avoid putting flash files on the cdnroot. Use this if you have problems setting up CORS',
189 type: 'boolean',
190 default: false,
191 })
192 .options('define', {
193 alias: 'd',
194 describe:
195 '--define SYMBOL[=value] will be passed to UglifyJS as is (see the docs at https://github.com/mishoo/UglifyJS#usage). Can be repeated. Remember to protect quotes from the shell, eg. --define foo=\\"bar\\".',
196 type: 'string',
197 })
198 .options('condition', {
199 describe:
200 '--condition name[=value] will be passed to System.js builder and used in #{...} interpolations (as described on http://jspm.io/0.17-beta-guide/conditional-interpolation.html). Can be repeated.',
201 type: 'string',
202 })
203 .options('splitcondition', {
204 describe:
205 '--splitcondition name will create a separate .html file for each condition value. Can be repeated.',
206 type: 'string',
207 })
208 .options('inline', {
209 describe:
210 'Set size threshold for inlining. Supported values: false (never inline), true (always inline, except when found inside @media queries), number (inline if target is smaller than this number of bytes). Also supported: --inlinehtmlscript true, --inlinecssimage 8192 etc.',
211 default: false,
212 })
213 .options('cdnhtml', {
214 describe:
215 "Put non-initial HTML files on the cdnroot as well. Some CDN packages (such as Akamai's cheapest one) don't allow this",
216 type: 'boolean',
217 default: false,
218 })
219 .options('sharedbundles', {
220 describe:
221 'Try to create shared bundles including commin files across multiple pages',
222 type: 'boolean',
223 default: false,
224 })
225 .options('manifest', {
226 describe:
227 'Generates an appcache manifest file with all static assets included',
228 type: 'boolean',
229 default: false,
230 })
231 .options('repl', {
232 describe: 'Start the REPL after a particular transform (or "error")',
233 type: 'string',
234 })
235 .options('sweep', {
236 describe:
237 'Experimental: Free up memory by cleaning up relations and assets after they have been removed from the graph',
238 type: 'boolean',
239 });
240
241// These names originally come from UglifyJS' code generator, but are now mapped to their escodegen equivalents
242const javaScriptSerializationOptionNames = ['indent_level', 'ascii_only'];
243
244for (const javaScriptSerializationOptionName of javaScriptSerializationOptionNames) {
245 const type =
246 javaScriptSerializationOptionName === 'indent_level' ? 'number' : 'boolean';
247 // Also accept the option without underscores, or with the underscores replaced with dashes:
248 yargs.options(javaScriptSerializationOptionName.replace(/_/g, ''), {
249 type,
250 description:
251 'UglifyJS serialization option, see http://lisperator.net/uglifyjs/codegen',
252 });
253}
254
255const commandLineOptions = yargs.argv;
256
257const javaScriptSerializationOptions = {
258 compact: commandLineOptions.compact,
259 space: commandLineOptions.space,
260 newline: commandLineOptions.newline,
261};
262
263for (const deprecatedUglifyJsOption of [
264 'preserve_line',
265 'preamble',
266 'quote_char',
267 'indent_start',
268 'quote_keys',
269 'space_colon',
270 'unescape_regexps',
271 'width',
272 'max_line_len',
273 'beautify',
274 'bracketize',
275]) {
276 for (const alias of [
277 deprecatedUglifyJsOption,
278 deprecatedUglifyJsOption.replace(/_/g, ''),
279 deprecatedUglifyJsOption.replace(/_/g, '-'),
280 ]) {
281 if (typeof commandLineOptions[alias] !== 'undefined') {
282 throw new Error(
283 '--' +
284 alias +
285 ' is no longer supported after we stopped using UglifyJS as the JavaScript AST provider'
286 );
287 }
288 }
289}
290
291for (const javaScriptSerializationOptionName of javaScriptSerializationOptionNames) {
292 const value =
293 commandLineOptions[javaScriptSerializationOptionName.replace(/_/g, '')];
294 if (typeof value !== 'undefined') {
295 javaScriptSerializationOptions[javaScriptSerializationOptionName] = value;
296 }
297}
298
299let browsers = commandLineOptions.browsers;
300if (Array.isArray(browsers)) {
301 browsers = browsers.join(',');
302}
303
304if (commandLineOptions.h) {
305 yargs.showHelp();
306 process.exit(0);
307}
308
309// Temporary deprecation message
310if (commandLineOptions.stripdebug) {
311 console.warn(
312 chalk.yellow(
313 'INFO: the --stripdebug switch is deprecated. This behavior is now default. Use --debug to keep debugging in build output'
314 )
315 );
316}
317
318// Temporary deprecation message
319if (commandLineOptions.cdnflash) {
320 console.warn(
321 chalk.yellow(
322 'INFO: the --cdnflash switch is deprecated. This is now default functionality. Use --nocdnflash to get the old default behavior.'
323 )
324 );
325}
326
327// Temporary deprecation message
328if (commandLineOptions.cdnoutroot) {
329 console.warn(
330 chalk.yellow(
331 'INFO: the --cdnoutroot switch is deprecated. Default location for your cdn assets is now <outroot>/static/cdn'
332 )
333 );
334}
335
336// Temporary deprecation message
337if (commandLineOptions.canonicalurl) {
338 console.warn(
339 chalk.red(
340 'INFO: the --canonicalurl switch is deprecated. Please use --canonicalroot for the same effect plus more features'
341 )
342 );
343 process.exit(1);
344}
345
346const _ = require('lodash');
347const AssetGraph = require('../lib/AssetGraph');
348const urlTools = require('urltools');
349const output = urlTools.fsDirToFileUrl(commandLineOptions.output);
350const cdnRoot =
351 commandLineOptions.cdnroot &&
352 urlTools.ensureTrailingSlash(commandLineOptions.cdnroot);
353const fullCdnRoot = (/^\/\//.test(cdnRoot) ? 'http:' : '') + cdnRoot;
354let rootUrl =
355 commandLineOptions.root &&
356 urlTools.urlOrFsPathToUrl(commandLineOptions.root, true);
357const excludePatterns =
358 commandLineOptions.exclude &&
359 []
360 .concat(commandLineOptions.exclude)
361 .map((excludePattern) =>
362 excludePattern.replace(/[^\x21-\x7f]/g, encodeURIComponent)
363 );
364
365const reservedNames =
366 commandLineOptions.reservednames &&
367 _.flatten(
368 _.flatten([commandLineOptions.reservednames]).map((reservedName) =>
369 reservedName.split(',')
370 )
371 );
372const plugins = commandLineOptions.plugin
373 ? _.flatten(_.flatten([commandLineOptions.plugin]))
374 : [];
375const inlineByRelationType = {};
376let inputUrls;
377
378if (commandLineOptions.inline) {
379 inlineByRelationType['*'] = true;
380}
381
382// Doesn't touch non-string values or values that don't look like something boolean:
383function convertStringToBoolean(str) {
384 if (typeof str === 'string') {
385 if (/^(?:on|true|yes|)$/.test(str)) {
386 return true;
387 } else if (/^(?:off|false|no)$/.test(str)) {
388 return false;
389 }
390 }
391 return str;
392}
393
394// Look for --inline<relationType> command line arguments:
395for (const propertyName of Object.keys(AssetGraph)) {
396 const inlineThreshold = convertStringToBoolean(
397 commandLineOptions['inline' + propertyName.toLowerCase()]
398 );
399 if (typeof inlineThreshold !== 'undefined') {
400 inlineByRelationType[propertyName] = inlineThreshold;
401 }
402}
403
404// Use a default inline threshold of 8192 bytes for HtmlStyle and HtmlScript, unless --inlinehtmlscript/--inlinehtmlstyle (or --inline) was given
405if (typeof inlineByRelationType['*'] === 'undefined') {
406 for (const relationType of ['HtmlScript', 'HtmlStyle']) {
407 if (typeof inlineByRelationType[relationType] === 'undefined') {
408 inlineByRelationType[relationType] = 8192;
409 }
410 }
411}
412
413if (commandLineOptions.inlinesize) {
414 console.warn(
415 chalk.yellow(
416 'INFO: the --inlinesize switch is deprecated. Please use --inlinecssimage <number> instead'
417 )
418 );
419 inlineByRelationType.CssImage = commandLineOptions.inlinesize;
420} else if (!('CssImage' in inlineByRelationType)) {
421 inlineByRelationType.CssImage = 8192;
422}
423
424const defines = {};
425for (const define of commandLineOptions.define
426 ? _.flatten(_.flatten([commandLineOptions.define]))
427 : []) {
428 const matchDefine = define.match(/^(\w+)(?:=(.*))?$/);
429 if (matchDefine) {
430 let valueAst;
431 if (matchDefine[2]) {
432 try {
433 valueAst = parseExpression(matchDefine[2]);
434 } catch (e) {
435 console.error(
436 'Invalid --define ' +
437 matchDefine[1] +
438 ': Could not parse ' +
439 matchDefine[2] +
440 ' as a JavaScript expression. Missing shell escapes?'
441 );
442 console.error(
443 e.message + ' (line ' + e.line + ', column ' + e.col + ')'
444 );
445 process.exit(1);
446 }
447 } else {
448 valueAst = { type: 'Literal', value: true };
449 }
450 defines[matchDefine[1]] = valueAst;
451 }
452}
453
454const splitConditions =
455 commandLineOptions.splitcondition &&
456 _.flatten(
457 _.flatten([commandLineOptions.splitcondition]).map(function (name) {
458 return name.split(',');
459 })
460 );
461const conditions = {};
462for (const condition of commandLineOptions.condition
463 ? _.flatten(_.flatten([commandLineOptions.condition]))
464 : []) {
465 const matchCondition = condition.match(/^([^=]+)=(.*)?$/);
466 if (matchCondition) {
467 let value = matchCondition[2];
468 if (value.indexOf(',') !== -1) {
469 // Array of values to trace
470 value = value.split(',');
471 }
472 conditions[matchCondition[1]] = value;
473 } else {
474 console.error('Invalid --condition ' + condition);
475 process.exit(1);
476 }
477}
478
479if (commandLineOptions._.length > 0) {
480 inputUrls = commandLineOptions._.map(function (urlOrFsPath) {
481 return urlTools.urlOrFsPathToUrl(String(urlOrFsPath), false);
482 });
483 if (!rootUrl) {
484 rootUrl = urlTools.findCommonUrlPrefix(
485 inputUrls.filter(function (inputUrl) {
486 return /^file:/.test(inputUrl);
487 })
488 );
489 if (rootUrl) {
490 console.warn('Guessing --root from input files: ' + rootUrl);
491 }
492 }
493} else if (rootUrl && /^file:/.test(rootUrl)) {
494 inputUrls = [rootUrl + '*.html'];
495 console.warn('No input files specified, defaulting to ' + inputUrls[0]);
496} else {
497 throw new Error(
498 "No input files and no --root specified (or it isn't file:), cannot proceed"
499 );
500}
501
502const buildProductionOptions = {
503 version: commandLineOptions.version,
504 browsers,
505 sourceMaps: commandLineOptions.sourcemaps,
506 sourcesContent: commandLineOptions.sourcescontent,
507 contentSecurityPolicy: commandLineOptions.contentsecuritypolicy,
508 contentSecurityPolicyLevel: commandLineOptions.contentsecuritypolicylevel,
509 subResourceIntegrity: commandLineOptions.subresourceintegrity,
510 webpackConfigPath: commandLineOptions.webpackconfig,
511 optimizeImages: commandLineOptions.optimizeimages,
512 inlineByRelationType,
513 gzip: commandLineOptions.gzip,
514 noFileRev: commandLineOptions.nofilerev,
515 defines,
516 conditions,
517 splitConditions,
518 reservedNames,
519 localeCookieName: commandLineOptions.localecookiename,
520 manifest: commandLineOptions.manifest,
521 asyncScripts: commandLineOptions.asyncscripts,
522 deferScripts: commandLineOptions.deferscripts,
523 cdnRoot,
524 recursive: commandLineOptions.recursive,
525 excludePatterns,
526 cdnFlash: !commandLineOptions.nocdnflash,
527 cdnHtml: commandLineOptions.cdnhtml,
528 svgo: commandLineOptions.svgo,
529 minify: commandLineOptions.minify,
530 noCompress: commandLineOptions.nocompress,
531 pretty: commandLineOptions.pretty,
532 sharedBundles: commandLineOptions.sharedbundles,
533 stripDebug: !commandLineOptions.debug,
534 addInitialHtmlExtension: commandLineOptions.addinitialhtmlextension,
535 javaScriptSerializationOptions,
536 precacheServiceWorker: commandLineOptions.precacheserviceworker,
537 precacheServiceWorkerConfigPath:
538 commandLineOptions.precacheserviceworkerconfig,
539};
540
541(async () => {
542 const assetGraph = new AssetGraph({
543 root: rootUrl,
544 canonicalRoot: commandLineOptions.canonicalroot,
545 });
546 await assetGraph.logEvents({
547 repl: commandLineOptions.repl,
548 stopOnWarning: commandLineOptions.stoponwarning,
549 suppressJavaScriptCommonJsRequireWarnings: true,
550 });
551
552 for (const plugin of plugins) {
553 require(plugin)(assetGraph, buildProductionOptions, commandLineOptions);
554 }
555 if (commandLineOptions.sweep) {
556 assetGraph
557 .on('removeAsset', (asset) => {
558 setImmediate(() => {
559 if (!asset.assetGraph) {
560 asset.unload();
561 }
562 });
563 })
564 .on('removeRelation', (relation) => {
565 setImmediate(() => {
566 if (!relation.assetGraph) {
567 for (const key of Object.keys(relation)) {
568 relation[key] = null;
569 }
570 }
571 });
572 });
573 }
574
575 await assetGraph.loadAssets(inputUrls);
576 await assetGraph.buildProduction(buildProductionOptions);
577 await assetGraph.writeAssetsToDisc(
578 { protocol: 'file:', isLoaded: true, isRedirect: false },
579 output
580 );
581 if (cdnRoot) {
582 await assetGraph.writeAssetsToDisc(
583 {
584 url: (url) => url && url.startsWith(fullCdnRoot),
585 isLoaded: true,
586 },
587 `${output}static/cdn/`,
588 fullCdnRoot
589 );
590 }
591
592 await assetGraph.writeStatsToStderr();
593})();