1 | import fs from 'fs';
|
2 | import { resolve, relative, dirname, basename, extname } from 'path';
|
3 | import camelCase from 'camelcase';
|
4 | import escapeStringRegexp from 'escape-string-regexp';
|
5 | import { blue } from 'kleur';
|
6 | import { map, series } from 'asyncro';
|
7 | import glob from 'tiny-glob/sync';
|
8 | import autoprefixer from 'autoprefixer';
|
9 | import cssnano from 'cssnano';
|
10 | import { rollup, watch } from 'rollup';
|
11 | import commonjs from '@rollup/plugin-commonjs';
|
12 | import babel from '@rollup/plugin-babel';
|
13 | import customBabel from './lib/babel-custom';
|
14 | import nodeResolve from '@rollup/plugin-node-resolve';
|
15 | import { terser } from 'rollup-plugin-terser';
|
16 | import alias from '@rollup/plugin-alias';
|
17 | import postcss from 'rollup-plugin-postcss';
|
18 | import typescript from 'rollup-plugin-typescript2';
|
19 | import json from '@rollup/plugin-json';
|
20 | import logError from './log-error';
|
21 | import { isDir, isFile, stdout, isTruthy, removeScope } from './utils';
|
22 | import { getSizeInfo } from './lib/compressed-size';
|
23 | import { normalizeMinifyOptions } from './lib/terser';
|
24 | import {
|
25 | parseAliasArgument,
|
26 | parseMappingArgument,
|
27 | toReplacementExpression,
|
28 | } from './lib/option-normalization';
|
29 | import { getConfigFromPkgJson, getName } from './lib/package-info';
|
30 | import { shouldCssModules, cssModulesConfig } from './lib/css-modules';
|
31 |
|
32 |
|
33 | const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.es6', '.es', '.mjs'];
|
34 |
|
35 | const WATCH_OPTS = {
|
36 | exclude: 'node_modules/**',
|
37 | };
|
38 |
|
39 | export 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 |
|
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 |
|
127 | function 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 |
|
172 | async 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 |
|
182 | async 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 |
|
204 | async 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 |
|
212 | function 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 |
|
227 | async 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 |
|
240 | function replaceName(filename, name) {
|
241 | return resolve(
|
242 | dirname(filename),
|
243 | name + basename(filename).replace(/^[^.]+/, ''),
|
244 | );
|
245 | }
|
246 |
|
247 | function 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 |
|
289 | const shebang = {};
|
290 |
|
291 | function createConfig(options, entry, format, writeMeta) {
|
292 | let { pkg } = options;
|
293 |
|
294 |
|
295 | let external = ['dns', 'fs', 'path', 'url'].concat(
|
296 | options.entries.filter(e => e !== entry),
|
297 | );
|
298 |
|
299 |
|
300 | let outputAliases = {};
|
301 |
|
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 |
|
312 | } else if (options.external) {
|
313 | external = external.concat(peerDeps).concat(
|
314 |
|
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 |
|
325 | if (name instanceof RegExp) name = name.source;
|
326 |
|
327 |
|
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 |
|
348 |
|
349 | let nameCache = {};
|
350 | const bareNameCache = nameCache;
|
351 |
|
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 |
|
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 |
|
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 |
|
398 | inputOptions: {
|
399 |
|
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 |
|
431 | inject: false,
|
432 | extract: !!writeMeta,
|
433 | }),
|
434 | moduleAliases.length > 0 &&
|
435 | alias({
|
436 |
|
437 |
|
438 | entries: moduleAliases,
|
439 | }),
|
440 | nodeResolve({
|
441 | mainFields: ['module', 'jsnext', 'main'],
|
442 | browser: options.target !== 'node',
|
443 |
|
444 | extensions: ['.mjs', '.js', '.jsx', '.json', '.node'],
|
445 | preferBuiltins: options.target === 'node',
|
446 | }),
|
447 | commonjs({
|
448 |
|
449 | include: /\/node_modules\//,
|
450 | }),
|
451 | json(),
|
452 | {
|
453 |
|
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 |
|
474 |
|
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 |
|
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,
|
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 |
|
527 |
|
528 | passes: 10,
|
529 | },
|
530 | minifyOptions.compress || {},
|
531 | ),
|
532 | output: {
|
533 |
|
534 |
|
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 |
|
546 | options: loadNameCache,
|
547 |
|
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 |
|
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 | }
|