import {mkdir, readFile, writeFile} from 'fs'; import {platform} from 'os'; import {basename, dirname, join, relative, resolve} from 'path'; import * as mhl from 'minify-html-literals'; import {minify as terserMinify} from 'terser'; import umap from 'umap'; import umeta from 'umeta'; import compressed from './compressed.js'; import compress from './compress.js'; import minifyOptions from './html-minifier.js'; const {parse, stringify} = JSON; const {minifyHTMLLiterals} = (mhl.default || mhl); const {require: $require} = umeta(import.meta); const isWindows = /^win/i.test(platform()); const terserArgs = {output: {comments: /^!/}}; compressed.add('.js'); compressed.add('.mjs'); compressed.add('.map'); const minify = (source, {noMinify, sourceMap}) => new Promise((res, rej) => { readFile(source, (err, data) => { if (err) rej(err); else { const original = data.toString(); /* istanbul ignore if */ if (noMinify) res({original, code: original, map: ''}); else { try { const mini = sourceMap ? // TODO: find a way to integrate literals minification {code: original} : minifyHTMLLiterals(original, {minifyOptions}); /* istanbul ignore next */ const js = mini ? mini.code : original; const module = /\.mjs$/.test(source) || /\b(?:import|export)\b/.test(js); terserMinify( js, sourceMap ? { ...terserArgs, module, sourceMap: { filename: source, url: `${source}.map` } } : { ...terserArgs, module } ) .then(({code, map}) => { res({original, code, map: sourceMap ? map : ''}); }) .catch(rej); } catch (error) { /* istanbul ignore next */ rej(error); } } } }); }); const uModules = path => path.replace(/\bnode_modules\b/g, 'u_modules'); /* istanbul ignore next */ const noBackSlashes = s => (isWindows ? s.replace(/\\(?!\s)/g, '/') : s); const saveCode = (source, dest, code, options) => new Promise((res, rej) => { dest = uModules(dest); mkdir(dirname(dest), {recursive: true}, err => { /* istanbul ignore if */ if (err) rej(err); else { writeFile(dest, code, err => { /* istanbul ignore if */ if (err) rej(err); else if (options.createFiles) compress(source, dest, 'text', options) .then(() => res(dest), rej); else res(dest); }); } }); }); /** * Create a file after minifying it via `uglify-es`. * @param {string} source The source JS file to minify. * @param {string} dest The minified destination file. * @param {Options} [options] Options to deal with extra computation. * @return {Promise<string>} A promise that resolves with the destination file. */ const JS = ( source, dest, options = {}, /* istanbul ignore next */ known = umap(new Map), initialSource = dirname(source), initialDest = dirname(dest) ) => known.get(dest) || known.set(dest, minify(source, options).then( ({original, code, map}) => { const modules = []; const newCode = []; if (options.noImport) newCode.push(code); else { const baseSource = dirname(source); const baseDest = dirname(dest); const re = /(["'`])(?:(?=(\\?))\2.)*?\1/g; let i = 0, match; while (match = re.exec(code)) { const {0: whole, 1: quote, index} = match; const chunk = code.slice(i, index); const next = index + whole.length; let content = whole; newCode.push(chunk); /* istanbul ignore else */ if ( /(?:\bfrom\b|\bimport\b\(?)\s*$/.test(chunk) && (!/\(\s*$/.test(chunk) || /^\s*\)/.test(code.slice(next))) ) { const module = whole.slice(1, -1); if (/^[a-z@][a-z0-9/._-]+$/i.test(module)) { try { const {length} = module; let path = $require.resolve(module, {paths: [baseSource]}); /* istanbul ignore next */ if (!path.includes(module) && /(\/|\\[^ ])/.test(module)) { const sep = RegExp.$1[0]; const source = module.split(sep); const target = path.split(sep); const js = source.length; for (let j = 0, i = target.indexOf(source[0]); i < target.length; i++) { if (j < js && target[i] !== source[j++]) target[i] = source[j - 1]; } path = target.join(sep); path = [ path.slice(0, path.lastIndexOf('node_modules')), 'node_modules', source[0] ].join(sep); } else { let oldPath = path; do path = dirname(oldPath); while ( path !== oldPath && path.slice(-length) !== module && (oldPath = path) ); } const i = path.lastIndexOf('node_modules'); /* istanbul ignore if */ if (i < 0) throw new Error('node_modules folder not found'); const {exports: e, module: m, main, type} = $require( join(path, 'package.json') ); /* istanbul ignore next */ const index = (e && (e.import || e['.'].import)) || m || (type === 'module' && main); /* istanbul ignore if */ if (!index) throw new Error('no entry file found'); const newSource = resolve(path, index); const newDest = resolve(initialDest, path.slice(i), index); modules.push(JS( newSource, newDest, options, known, initialSource, initialDest )); path = uModules( noBackSlashes(relative(dirname(source), newSource)) ); /* istanbul ignore next */ content = `${quote}${path[0] === '.' ? path : `./${path}`}${quote}`; } catch ({message}) { console.warn(`unable to import "${module}"`, message); } } /* istanbul ignore else */ else if (!/^([a-z]+:)?\/\//i.test(module)) { modules.push(JS( resolve(baseSource, module), resolve(baseDest, module), options, known, initialSource, initialDest )); } } newCode.push(content); i = next; } newCode.push(code.slice(i)); } let smCode = newCode.join(''); if (options.sourceMap) { const destSource = dest.replace(/(\.m?js)$/i, `$1.source$1`); const json = parse(map); const file = basename(dest); json.file = file; json.sources = [basename(destSource)]; smCode = smCode.replace(source, file); modules.push( saveCode(source, `${dest}.map`, stringify(json), options), saveCode(source, destSource, original, options) ); } return Promise.all( modules.concat(saveCode(source, dest, smCode, options)) ).then( () => dest, /* istanbul ignore next */ err => Promise.reject(err) ); } )); export default JS;