UNPKG

16.1 kBJavaScriptView Raw
1import fs from 'fs';
2import { resolve, relative, dirname, basename, extname } from 'path';
3import camelCase from 'camelcase';
4import escapeStringRegexp from 'escape-string-regexp';
5import { blue } from 'kleur';
6import { map, series } from 'asyncro';
7import glob from 'tiny-glob/sync';
8import autoprefixer from 'autoprefixer';
9import cssnano from 'cssnano';
10import { rollup, watch } from 'rollup';
11import commonjs from '@rollup/plugin-commonjs';
12import babel from '@rollup/plugin-babel';
13import customBabel from './lib/babel-custom';
14import nodeResolve from '@rollup/plugin-node-resolve';
15import { terser } from 'rollup-plugin-terser';
16import alias from '@rollup/plugin-alias';
17import postcss from 'rollup-plugin-postcss';
18import typescript from 'rollup-plugin-typescript2';
19import json from '@rollup/plugin-json';
20import logError from './log-error';
21import { isDir, isFile, stdout, isTruthy, removeScope } from './utils';
22import { getSizeInfo } from './lib/compressed-size';
23import { normalizeMinifyOptions } from './lib/terser';
24import {
25 parseAliasArgument,
26 parseMappingArgument,
27 toReplacementExpression,
28} from './lib/option-normalization';
29import { getConfigFromPkgJson, getName } from './lib/package-info';
30import { shouldCssModules, cssModulesConfig } from './lib/css-modules';
31
32// Extensions to use when resolving modules
33const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.es6', '.es', '.mjs'];
34
35const WATCH_OPTS = {
36 exclude: 'node_modules/**',
37};
38
39export default async function microbundle(inputOptions) {
40 let options = { ...inputOptions };
41
42 options.cwd = resolve(process.cwd(), inputOptions.cwd);
43 const cwd = options.cwd;
44
45 const { hasPackageJson, pkg } = await getConfigFromPkgJson(cwd);
46 options.pkg = pkg;
47
48 const { finalName, pkgName } = getName({
49 name: options.name,
50 pkgName: options.pkg.name,
51 amdName: options.pkg.amdName,
52 hasPackageJson,
53 cwd,
54 });
55
56 options.name = finalName;
57 options.pkg.name = pkgName;
58
59 if (options.sourcemap !== false) {
60 options.sourcemap = true;
61 }
62
63 options.input = await getInput({
64 entries: options.entries,
65 cwd,
66 source: options.pkg.source,
67 module: options.pkg.module,
68 });
69
70 options.output = await getOutput({
71 cwd,
72 output: options.output,
73 pkgMain: options.pkg.main,
74 pkgName: options.pkg.name,
75 });
76
77 options.entries = await getEntries({
78 cwd,
79 input: options.input,
80 });
81
82 options.multipleEntries = options.entries.length > 1;
83
84 let formats = (options.format || options.formats).split(',');
85 // always compile cjs first if it's there:
86 formats.sort((a, b) => (a === 'cjs' ? -1 : a > b ? 1 : 0));
87
88 let steps = [];
89 for (let i = 0; i < options.entries.length; i++) {
90 for (let j = 0; j < formats.length; j++) {
91 steps.push(
92 createConfig(
93 options,
94 options.entries[i],
95 formats[j],
96 i === 0 && j === 0,
97 ),
98 );
99 }
100 }
101
102 if (options.watch) {
103 return doWatch(options, cwd, steps);
104 }
105
106 let cache;
107 let out = await series(
108 steps.map(config => async () => {
109 const { inputOptions, outputOptions } = config;
110 if (inputOptions.cache !== false) {
111 inputOptions.cache = cache;
112 }
113 let bundle = await rollup(inputOptions);
114 cache = bundle;
115 await bundle.write(outputOptions);
116 return await config._sizeInfo;
117 }),
118 );
119
120 const targetDir = relative(cwd, dirname(options.output)) || '.';
121 const banner = blue(`Build "${options.name}" to ${targetDir}:`);
122 return {
123 output: `${banner}\n ${out.join('\n ')}`,
124 };
125}
126
127function doWatch(options, cwd, steps) {
128 const { onStart, onBuild, onError } = options;
129
130 return new Promise((resolve, reject) => {
131 const targetDir = relative(cwd, dirname(options.output));
132 stdout(blue(`Watching source, compiling to ${targetDir}:`));
133
134 const watchers = steps.reduce((acc, options) => {
135 acc[options.inputOptions.input] = watch(
136 Object.assign(
137 {
138 output: options.outputOptions,
139 watch: WATCH_OPTS,
140 },
141 options.inputOptions,
142 ),
143 ).on('event', e => {
144 if (e.code === 'START') {
145 if (typeof onStart === 'function') {
146 onStart(e);
147 }
148 }
149 if (e.code === 'ERROR') {
150 logError(e.error);
151 if (typeof onError === 'function') {
152 onError(e);
153 }
154 }
155 if (e.code === 'END') {
156 options._sizeInfo.then(text => {
157 stdout(`Wrote ${text.trim()}`);
158 });
159 if (typeof onBuild === 'function') {
160 onBuild(e);
161 }
162 }
163 });
164
165 return acc;
166 }, {});
167
168 resolve({ watchers });
169 });
170}
171
172async function jsOrTs(cwd, filename) {
173 const extension = (await isFile(resolve(cwd, filename + '.ts')))
174 ? '.ts'
175 : (await isFile(resolve(cwd, filename + '.tsx')))
176 ? '.tsx'
177 : '.js';
178
179 return resolve(cwd, `${filename}${extension}`);
180}
181
182async function getInput({ entries, cwd, source, module }) {
183 const input = [];
184
185 []
186 .concat(
187 entries && entries.length
188 ? entries
189 : (source &&
190 (Array.isArray(source) ? source : [source]).map(file =>
191 resolve(cwd, file),
192 )) ||
193 ((await isDir(resolve(cwd, 'src'))) &&
194 (await jsOrTs(cwd, 'src/index'))) ||
195 (await jsOrTs(cwd, 'index')) ||
196 module,
197 )
198 .map(file => glob(file))
199 .forEach(file => input.push(...file));
200
201 return input;
202}
203
204async function getOutput({ cwd, output, pkgMain, pkgName }) {
205 let main = resolve(cwd, output || pkgMain || 'dist');
206 if (!main.match(/\.[a-z]+$/) || (await isDir(main))) {
207 main = resolve(main, `${removeScope(pkgName)}.js`);
208 }
209 return main;
210}
211
212function getDeclarationDir({ options, pkg }) {
213 const { cwd, output } = options;
214
215 let result = output;
216
217 if (pkg.types || pkg.typings) {
218 result = pkg.types || pkg.typings;
219 result = resolve(cwd, result);
220 }
221
222 result = dirname(result);
223
224 return result;
225}
226
227async function getEntries({ input, cwd }) {
228 let entries = (
229 await map([].concat(input), async file => {
230 file = resolve(cwd, file);
231 if (await isDir(file)) {
232 file = resolve(file, 'index.js');
233 }
234 return file;
235 })
236 ).filter((item, i, arr) => arr.indexOf(item) === i);
237 return entries;
238}
239
240function replaceName(filename, name) {
241 return resolve(
242 dirname(filename),
243 name + basename(filename).replace(/^[^.]+/, ''),
244 );
245}
246
247function getMain({ options, entry, format }) {
248 const { pkg } = options;
249 const pkgMain = options['pkg-main'];
250
251 if (!pkgMain) {
252 return options.output;
253 }
254
255 let mainNoExtension = options.output;
256 if (options.multipleEntries) {
257 let name = entry.match(/([\\/])index(\.(umd|cjs|es|m))?\.(mjs|[tj]sx?)$/)
258 ? mainNoExtension
259 : entry;
260 mainNoExtension = resolve(dirname(mainNoExtension), basename(name));
261 }
262 mainNoExtension = mainNoExtension.replace(
263 /(\.(umd|cjs|es|m))?\.(mjs|[tj]sx?)$/,
264 '',
265 );
266
267 const mainsByFormat = {};
268
269 mainsByFormat.es = replaceName(
270 pkg.module && !pkg.module.match(/src\//)
271 ? pkg.module
272 : pkg['jsnext:main'] || 'x.esm.js',
273 mainNoExtension,
274 );
275 mainsByFormat.modern = replaceName(
276 (pkg.syntax && pkg.syntax.esmodules) || pkg.esmodule || 'x.modern.js',
277 mainNoExtension,
278 );
279 mainsByFormat.cjs = replaceName(pkg['cjs:main'] || 'x.js', mainNoExtension);
280 mainsByFormat.umd = replaceName(
281 pkg['umd:main'] || 'x.umd.js',
282 mainNoExtension,
283 );
284
285 return mainsByFormat[format] || mainsByFormat.cjs;
286}
287
288// shebang cache map because the transform only gets run once
289const shebang = {};
290
291function createConfig(options, entry, format, writeMeta) {
292 let { pkg } = options;
293
294 /** @type {(string|RegExp)[]} */
295 let external = ['dns', 'fs', 'path', 'url'].concat(
296 options.entries.filter(e => e !== entry),
297 );
298
299 /** @type {Record<string, string>} */
300 let outputAliases = {};
301 // since we transform src/index.js, we need to rename imports for it:
302 if (options.multipleEntries) {
303 outputAliases['.'] = './' + basename(options.output);
304 }
305
306 const moduleAliases = options.alias ? parseAliasArgument(options.alias) : [];
307 const aliasIds = moduleAliases.map(alias => alias.find);
308
309 const peerDeps = Object.keys(pkg.peerDependencies || {});
310 if (options.external === 'none') {
311 // bundle everything (external=[])
312 } else if (options.external) {
313 external = external.concat(peerDeps).concat(
314 // CLI --external supports regular expressions:
315 options.external.split(',').map(str => new RegExp(str)),
316 );
317 } else {
318 external = external
319 .concat(peerDeps)
320 .concat(Object.keys(pkg.dependencies || {}));
321 }
322
323 let globals = external.reduce((globals, name) => {
324 // Use raw value for CLI-provided RegExp externals:
325 if (name instanceof RegExp) name = name.source;
326
327 // valid JS identifiers are usually library globals:
328 if (name.match(/^[a-z_$][a-z0-9_\-$]*$/)) {
329 globals[name] = camelCase(name);
330 }
331 return globals;
332 }, {});
333 if (options.globals && options.globals !== 'none') {
334 globals = Object.assign(globals, parseMappingArgument(options.globals));
335 }
336
337 let defines = {};
338 if (options.define) {
339 defines = Object.assign(
340 defines,
341 parseMappingArgument(options.define, toReplacementExpression),
342 );
343 }
344
345 const modern = format === 'modern';
346
347 // let rollupName = safeVariableName(basename(entry).replace(/\.js$/, ''));
348
349 let nameCache = {};
350 const bareNameCache = nameCache;
351 // Support "minify" field and legacy "mangle" field via package.json:
352 const rawMinifyValue = options.pkg.minify || options.pkg.mangle || {};
353 let minifyOptions = typeof rawMinifyValue === 'string' ? {} : rawMinifyValue;
354 const getNameCachePath =
355 typeof rawMinifyValue === 'string'
356 ? () => resolve(options.cwd, rawMinifyValue)
357 : () => resolve(options.cwd, 'mangle.json');
358
359 const useTypescript = extname(entry) === '.ts' || extname(entry) === '.tsx';
360
361 const escapeStringExternals = ext =>
362 ext instanceof RegExp ? ext.source : escapeStringRegexp(ext);
363 const externalPredicate = new RegExp(
364 `^(${external.map(escapeStringExternals).join('|')})($|/)`,
365 );
366 const externalTest =
367 external.length === 0 ? id => false : id => externalPredicate.test(id);
368
369 function loadNameCache() {
370 try {
371 nameCache = JSON.parse(fs.readFileSync(getNameCachePath(), 'utf8'));
372 // mangle.json can contain a "minify" field, same format as the pkg.mangle:
373 if (nameCache.minify) {
374 minifyOptions = Object.assign(
375 {},
376 minifyOptions || {},
377 nameCache.minify,
378 );
379 }
380 } catch (e) {}
381 }
382 loadNameCache();
383
384 normalizeMinifyOptions(minifyOptions);
385
386 if (nameCache === bareNameCache) nameCache = null;
387
388 /** @type {false | import('rollup').RollupCache} */
389 let cache;
390 if (modern) cache = false;
391
392 const absMain = resolve(options.cwd, getMain({ options, entry, format }));
393 const outputDir = dirname(absMain);
394 const outputEntryFileName = basename(absMain);
395
396 let config = {
397 /** @type {import('rollup').InputOptions} */
398 inputOptions: {
399 // disable Rollup's cache for the modern build to prevent re-use of legacy transpiled modules:
400 cache,
401
402 input: entry,
403 external: id => {
404 if (id === 'babel-plugin-transform-async-to-promises/helpers') {
405 return false;
406 }
407 if (options.multipleEntries && id === '.') {
408 return true;
409 }
410 if (aliasIds.indexOf(id) >= 0) {
411 return false;
412 }
413 return externalTest(id);
414 },
415 treeshake: {
416 propertyReadSideEffects: false,
417 },
418 plugins: []
419 .concat(
420 postcss({
421 plugins: [
422 autoprefixer(),
423 options.compress !== false &&
424 cssnano({
425 preset: 'default',
426 }),
427 ].filter(Boolean),
428 autoModules: shouldCssModules(options),
429 modules: cssModulesConfig(options),
430 // only write out CSS for the first bundle (avoids pointless extra files):
431 inject: false,
432 extract: !!writeMeta,
433 }),
434 moduleAliases.length > 0 &&
435 alias({
436 // @TODO: this is no longer supported, but didn't appear to be required?
437 // resolve: EXTENSIONS,
438 entries: moduleAliases,
439 }),
440 nodeResolve({
441 mainFields: ['module', 'jsnext', 'main'],
442 browser: options.target !== 'node',
443 // defaults + .jsx
444 extensions: ['.mjs', '.js', '.jsx', '.json', '.node'],
445 preferBuiltins: options.target === 'node',
446 }),
447 commonjs({
448 // use a regex to make sure to include eventual hoisted packages
449 include: /\/node_modules\//,
450 }),
451 json(),
452 {
453 // We have to remove shebang so it doesn't end up in the middle of the code somewhere
454 transform: code => ({
455 code: code.replace(/^#![^\n]*/, bang => {
456 shebang[options.name] = bang;
457 }),
458 map: null,
459 }),
460 },
461 useTypescript &&
462 typescript({
463 typescript: require('typescript'),
464 cacheRoot: `./node_modules/.cache/.rts2_cache_${format}`,
465 useTsconfigDeclarationDir: true,
466 tsconfigDefaults: {
467 compilerOptions: {
468 sourceMap: options.sourcemap,
469 declaration: true,
470 declarationDir: getDeclarationDir({ options, pkg }),
471 jsx: 'react',
472 jsxFactory:
473 // TypeScript fails to resolve Fragments when jsxFactory
474 // is set, even when it's the same as the default value.
475 options.jsx === 'React.createElement'
476 ? undefined
477 : options.jsx || 'h',
478 },
479 files: options.entries,
480 },
481 tsconfig: options.tsconfig,
482 tsconfigOverride: {
483 compilerOptions: {
484 module: 'ESNext',
485 target: 'esnext',
486 },
487 },
488 }),
489 // if defines is not set, we shouldn't run babel through node_modules
490 isTruthy(defines) &&
491 babel({
492 babelHelpers: 'bundled',
493 babelrc: false,
494 compact: false,
495 configFile: false,
496 include: 'node_modules/**',
497 plugins: [
498 [
499 require.resolve('babel-plugin-transform-replace-expressions'),
500 { replace: defines },
501 ],
502 ],
503 }),
504 customBabel()({
505 babelHelpers: 'bundled',
506 extensions: EXTENSIONS,
507 exclude: 'node_modules/**',
508 passPerPreset: true, // @see https://babeljs.io/docs/en/options#passperpreset
509 custom: {
510 defines,
511 modern,
512 compress: options.compress !== false,
513 targets: options.target === 'node' ? { node: '8' } : undefined,
514 pragma: options.jsx || 'h',
515 pragmaFrag: options.jsxFragment || 'Fragment',
516 typescript: !!useTypescript,
517 },
518 }),
519 options.compress !== false && [
520 terser({
521 sourcemap: true,
522 compress: Object.assign(
523 {
524 keep_infinity: true,
525 pure_getters: true,
526 // Ideally we'd just get Terser to respect existing Arrow functions...
527 // unsafe_arrows: true,
528 passes: 10,
529 },
530 minifyOptions.compress || {},
531 ),
532 output: {
533 // By default, Terser wraps function arguments in extra parens to trigger eager parsing.
534 // Whether this is a good idea is way too specific to guess, so we optimize for size by default:
535 wrap_func_args: false,
536 comments: false,
537 },
538 warnings: true,
539 ecma: modern ? 9 : 5,
540 toplevel: modern || format === 'cjs' || format === 'es',
541 mangle: Object.assign({}, minifyOptions.mangle || {}),
542 nameCache,
543 }),
544 nameCache && {
545 // before hook
546 options: loadNameCache,
547 // after hook
548 writeBundle() {
549 if (writeMeta && nameCache) {
550 fs.writeFile(
551 getNameCachePath(),
552 JSON.stringify(nameCache, null, 2),
553 () => {},
554 );
555 }
556 },
557 },
558 ],
559 {
560 writeBundle(bundle) {
561 config._sizeInfo = Promise.all(
562 Object.values(bundle).map(({ code, fileName }) => {
563 if (code) {
564 return getSizeInfo(code, fileName, options.raw);
565 }
566 }),
567 ).then(results => results.filter(Boolean).join('\n'));
568 },
569 },
570 )
571 .filter(Boolean),
572 },
573
574 /** @type {import('rollup').OutputOptions} */
575 outputOptions: {
576 paths: outputAliases,
577 globals,
578 strict: options.strict === true,
579 freeze: false,
580 esModule: false,
581 sourcemap: options.sourcemap,
582 get banner() {
583 return shebang[options.name];
584 },
585 format: modern ? 'es' : format,
586 name: options.name,
587 dir: outputDir,
588 entryFileNames: outputEntryFileName,
589 },
590 };
591
592 return config;
593}