UNPKG

techor

Version:

Author technology like a top leader

385 lines (382 loc) 18.7 kB
import { readJSONFileSync } from '@techor/fs'; import log from '@techor/log'; import { existsSync, rmSync } from 'fs'; import { watch, rollup } from 'rollup'; import extend from '@techor/extend'; import { explorePathsSync } from '@techor/glob'; import { normalize, extname, basename, relative } from 'path'; import upath from 'upath'; import prettyBytes from 'pretty-bytes'; import align from 'wide-align'; import prettyHartime from 'pretty-hrtime'; import { logRollupWarning, logRollupError } from '../utils/log-rollup.mjs'; import { execaCommand } from 'execa'; import clsx from 'clsx'; import getWideExternal from '../utils/get-wide-external.mjs'; import config from '../config.mjs'; import yargsParser from 'yargs-parser'; import loadConfig from '../load-config.mjs'; import replace from '@rollup/plugin-replace'; import esmShim from '../plugins/esm-shim.mjs'; import { nodeResolve } from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import swc from '../plugins/swc.mjs'; import preserveDirectives from 'rollup-plugin-preserve-directives'; const yargsParserOptions = { alias: { formats: 'f', watch: 'w', clean: 'c', 'output.file': 'o' }, configuration: { 'strip-aliased': true, 'strip-dashed': true }, array: [ 'formats' ] }; async function build() { const { _, ...cmdConfig } = yargsParser(process.argv.slice(2), yargsParserOptions); const [commandName, ...commandInputs] = _; try { const useConfig = loadConfig(); const config$1 = extend(config, useConfig, { build: cmdConfig }); if (commandName === 'dev') { process.env.NODE_ENV = 'development'; config$1.build.watch = true; } else { process.env.NODE_ENV = 'production'; } if (process.env.DEBUG) console.log('[techor] cmdConfig', cmdConfig); const pkg = readJSONFileSync('package.json') || {}; const { dependencies, peerDependencies, optionalDependencies, types } = pkg; const buildMap = new Map(); if (config$1.build.declare === undefined && types) config$1.build.declare = true; if (Array.isArray(config$1.build.input.external)) { if (dependencies) config$1.build.input.external.push(...Object.keys(dependencies)); if (peerDependencies) config$1.build.input.external.push(...Object.keys(peerDependencies)); if (optionalDependencies) config$1.build.input.external.push(...Object.keys(optionalDependencies)); } // remove output dir first if (config$1.build.clean && existsSync(config$1.build.output.dir)) { rmSync(config$1.build.output.dir, { force: true, recursive: true }); log.i`Cleaned up **${config$1.build.output.dir}**`; } function addBuild(entries, rollupOutputOptions) { if (Array.isArray(entries)) { entries = entries.map((eachEntry)=>normalize(eachEntry)); } else { entries = normalize(entries); } let forceBundle; const extendedBuild = extend(config$1.build, { output: rollupOutputOptions }); // single entry if (extendedBuild.output.file) { if (!extendedBuild.output.format) { extendedBuild.output.format = config$1.build.formatOfExt[extname(extendedBuild.output.file)]; } const fileBasenameSplits = basename(extendedBuild.output.file).split('.'); if (fileBasenameSplits.includes('min')) { forceBundle = true; extendedBuild.minify = true; } if (fileBasenameSplits.includes('global') || fileBasenameSplits.includes('iife')) extendedBuild.output.format = 'iife'; extendedBuild.env = fileBasenameSplits.includes('development') ? 'development' : 'production'; for (const [eachInput, eachBuildOptions] of buildMap){ for (const eachOutputOptions of eachBuildOptions.outputOptionsList){ if (normalize(eachOutputOptions.output.file) === normalize(extendedBuild.output.file)) { if (process.env.DEBUG) console.log('[techor] extendedBuild', extendedBuild); return; } } } } else { extendedBuild.output.entryFileNames = (chunkInfo)=>{ return `${chunkInfo.name}${config$1.build.extOfFormat[extendedBuild.output.format]}`; }; } if (extendedBuild.output.preserveModules && !extendedBuild.output.preserveModulesRoot) { extendedBuild.output.preserveModulesRoot = extendedBuild.srcDir; } let buildOptions = buildMap.get(entries); if (buildOptions) { // 合併同一個 input 來源並對應多個 RollupOutputOptions 避免重新 parse 相同的 input buildOptions.outputOptionsList.push(extendedBuild); } else { buildOptions = { outputOptionsList: [ extendedBuild ] }; buildOptions.input = extend({ onwarn: (warning, warn)=>{ switch(warning.code){ case 'MODULE_LEVEL_DIRECTIVE': // https://github.com/Ephem/rollup-plugin-preserve-directives?tab=readme-ov-file#rollup-warning if (config$1.build.preserveDirectives && extendedBuild.output.preserveModules) { return; } break; } logRollupWarning(warning); } }, config$1.build.input); buildOptions.input.input = entries; buildOptions.input.external = config$1.build.input.external && !forceBundle && getWideExternal(config$1.build.input.external); const extendedSWCOptions = extend(config$1.build.swc, { tsconfigFile: config$1.build.tsconfig }); if (extendedBuild.minify) { extendedSWCOptions.minify = true; } else { delete extendedSWCOptions.minify; delete extendedSWCOptions.jsc.minify; } buildOptions.input.plugins.unshift(...[ swc(extendedSWCOptions), config$1.build.commonjs && commonjs(config$1.build.commonjs), config$1.build.nodeResolve && nodeResolve(config$1.build.nodeResolve), config$1.build.esmShim && esmShim(), config$1.build.preserveDirectives && !extendedBuild.output.file && preserveDirectives(config$1.build.preserveDirectives), replace({ preventAssignment: true, 'process.env.NODE_ENV': JSON.stringify(extendedBuild.env) }) ].filter((existence)=>existence)); if (process.env.DEBUG) console.log('[techor] buildOptions', buildOptions); buildMap.set(entries, buildOptions); } } if (commandInputs.length) { const foundEntries = explorePathsSync(commandInputs); config$1.build.formats.map((eachFormat)=>{ addBuild(foundEntries, { format: eachFormat }); }); } else { log.i`Detected entries (package.json)`; const exploreMapptedEntry = (filePath)=>{ const subFilePath = upath.relative(config$1.build.output.dir, filePath); const srcFilePath = upath.join(config$1.build.srcDir, subFilePath); const pattern = upath.changeExt(srcFilePath, `.{${config$1.build.sourceExtensions.join(',')}}`); const foundEntries = explorePathsSync(pattern); if (!foundEntries.length) { throw new Error(`Cannot find the entry file **${pattern}**`); } return foundEntries[0]; }; if (pkg.exports) { (function handleExports(eachExports, eachFormat) { if (typeof eachExports === 'string') { addBuild(exploreMapptedEntry(eachExports), { format: eachFormat, file: eachExports }); } else { for(const eachExportKey in eachExports){ const eachUnknowExports = eachExports[eachExportKey]; if (eachExportKey.startsWith('.')) { handleExports(eachUnknowExports, eachFormat); } else { switch(eachExportKey){ case 'node': handleExports(eachUnknowExports, eachFormat); break; case 'browser': handleExports(eachUnknowExports, eachFormat); break; case 'default': handleExports(eachUnknowExports, eachFormat); break; case 'require': handleExports(eachUnknowExports, 'cjs'); break; case 'import': handleExports(eachUnknowExports, 'esm'); break; case 'development': handleExports(eachUnknowExports, eachFormat); break; case 'production': handleExports(eachUnknowExports, eachFormat); break; } } } } })(pkg.exports); } if (pkg.main) { addBuild(exploreMapptedEntry(pkg.main), { file: pkg.main }); } if (pkg.module) { addBuild(exploreMapptedEntry(pkg.module), { format: 'esm', file: pkg.module }); } if (pkg.browser) { if (typeof pkg.browser !== 'string') { throw new Error('Not implemented for browser field object.'); } addBuild(exploreMapptedEntry(pkg.browser), { format: 'iife', file: pkg.browser }); } if (pkg.bin) { if (typeof pkg.bin === 'string') { addBuild(exploreMapptedEntry(pkg.bin), { file: pkg.bin }); } else { for(const eachCommandName in pkg.bin){ const eachCommandFile = pkg.bin[eachCommandName]; addBuild(exploreMapptedEntry(eachCommandFile), { file: eachCommandFile }); } } } } if (buildMap.size === 0) { log.x`No build tasks created`; log.i`Please check your **package.json** or specify **entries**`; return; } if (config$1.build.watch) log.i`Start watching for file changes...`; const outputResults = []; const printOutputResults = (eachOutputResults, eachBuildStartTime)=>{ const buildTime = process.hrtime(eachBuildStartTime); const colSizes = {}; const chunks = []; const resolveFilename = (eachOutputResult)=>{ if (eachOutputResult.output.preserveModules) { return eachOutputResult.artifact.fileName; } else { return relative(eachOutputResult.output.dir, eachOutputResult.output.file); } }; for (const eachOutputResult of eachOutputResults){ switch(eachOutputResult.artifact.type){ case 'asset': throw new Error('Not implemented for assets.'); case 'chunk': chunks.push(eachOutputResult.artifact); colSizes[1] = Math.max(colSizes[1] || 0, resolveFilename(eachOutputResult).length); colSizes[2] = Math.max(colSizes[2] || 0, eachOutputResult.output.format.length); colSizes[3] = Math.max(colSizes[3] || 0, prettyBytes(eachOutputResult.artifact.code.length, { space: false }).length); break; } } console.log(''); eachOutputResults.sort((a, b)=>resolveFilename(a).length - resolveFilename(b).length || resolveFilename(a).localeCompare(resolveFilename(b))).forEach((eachOutputResult)=>{ let fileName, format, codeLength; switch(eachOutputResult.artifact.type){ case 'asset': throw new Error('Not implemented for assets.'); case 'chunk': fileName = align.left('**' + resolveFilename(eachOutputResult) + '**', colSizes[1] + 6); format = align.right(eachOutputResult.output.format, colSizes[2] + 2); codeLength = align.right(prettyBytes(eachOutputResult.artifact.code.length, { space: false }), colSizes[3] + 2); break; } log.o(`${fileName} ${format} ${codeLength} ${eachOutputResult.minify && '(minified)' || ''}`); }); console.log(''); log.ok(clsx(`Built **${chunks.length}** chunks`, config$1.build.declare && `and types`, `in ${prettyHartime(buildTime).replace(' ', '')}`)); }; const buildStartTime = process.hrtime(); const output = async (rollupBuild, eachOutputOptionsList, outputResults)=>{ await Promise.all(eachOutputOptionsList.map(async (eachOutputOptions)=>{ const eachRollupOutputOptions = extend({}, eachOutputOptions.output); if (eachRollupOutputOptions.file) { // fix: Invalid value for option "output.dir" - you must set either "output.file" for a single-file build or "output.dir" when generating multiple chunks. delete eachRollupOutputOptions.dir; // fix: Invalid value for option "output.file" - you must set "output.dir" instead of "output.file" when using the "output.preserveModules" option. delete eachRollupOutputOptions.preserveModules; } const result = await rollupBuild.write(eachRollupOutputOptions); result.output.forEach((chunkOrAsset)=>{ outputResults.push({ ...eachOutputOptions, artifact: chunkOrAsset }); }); })); }; await Promise.all([ ...Array.from(buildMap.entries()).map(async ([input, eachBuildOptions])=>{ if (config$1.build.watch) { const watcher = watch({ ...eachBuildOptions.input, watch: { skipWrite: true } }); let buildStartTime; watcher.on('event', async (event)=>{ if (event.code === 'BUNDLE_START') { buildStartTime = process.hrtime(); } if (event.code === 'BUNDLE_END') { const eachOutputResults = []; await output(event.result, eachBuildOptions.outputOptionsList, eachOutputResults); printOutputResults(eachOutputResults, buildStartTime); } if (event.code === 'ERROR') { console.error(event.error); } }); watcher.on('change', (id, { event })=>{ console.log(''); log`[${event.toUpperCase()}] ${relative(process.cwd(), id)}`; }); } else { const rollupBuild = await rollup(eachBuildOptions.input); await output(rollupBuild, eachBuildOptions.outputOptionsList, outputResults); if (rollupBuild) { // closes the rollupBuild await rollupBuild.close(); } } }), config$1.build.declare && new Promise((resolve)=>{ execaCommand(clsx('npx tsc --emitDeclarationOnly --preserveWatchOutput --declaration', config$1.build.output.dir && ' --project ' + config$1.build.tsconfig, config$1.build.watch && '--watch'), { stdio: 'inherit', stripFinalNewline: false, cwd: process.cwd() }).catch(()=>{ process.exit(); }).finally(resolve); }) ]); if (!config$1.build.watch) { printOutputResults(outputResults, buildStartTime); } } catch (error) { if (error.name === 'RollupError') { logRollupError(error); } else { log(error); } process.exit(1); } } export { build as default, yargsParserOptions };