UNPKG

size-limit

Version:
272 lines (249 loc) 7.96 kB
import bytes from 'bytes-iec' import { lilconfig } from 'lilconfig' import { glob } from 'node:fs/promises' import { createRequire } from 'node:module' import { dirname, isAbsolute, join, relative, resolve } from 'node:path' import { pathToFileURL } from 'node:url' import { SizeLimitError } from './size-limit-error.js' let require = createRequire(import.meta.url) const OPTIONS = { brotli: 'file', compareWith: 'webpack', config: ['webpack', 'esbuild'], disableModuleConcatenation: 'webpack', disablePlugins: true, entry: 'webpack', gzip: 'file', hidePassed: false, highlightLess: false, ignore: ['webpack', 'esbuild'], import: ['webpack', 'esbuild'], limit: true, message: true, modifyEsbuildConfig: 'esbuild', modifyWebpackConfig: 'webpack', module: true, name: true, path: true, running: 'time', time: 'time', uiReports: 'webpack', webpack: 'webpack' } function isStrings(value) { if (!Array.isArray(value)) return false return value.every(i => typeof i === 'string') } function isStringsOrUndefined(value) { let type = typeof value return type === 'undefined' || type === 'string' || isStrings(value) } function isStringArrayOrUndefined(value) { return typeof value === 'undefined' || isStrings(value) } function endsWithMs(value) { return / ?ms/i.test(value) } function endsWithS(value) { return / ?s/i.test(value) } function checkChecks(plugins, checks) { if (!Array.isArray(checks)) { throw new SizeLimitError('noArrayConfig') } if (checks.length === 0) { throw new SizeLimitError('emptyConfig') } for (let check of checks) { if (typeof check !== 'object') { throw new SizeLimitError('noObjectCheck') } if (!isStringsOrUndefined(check.path)) { throw new SizeLimitError('pathNotString') } if (!isStringsOrUndefined(check.entry)) { throw new SizeLimitError('entryNotString') } if (!isStringArrayOrUndefined(check.disablePlugins)) { throw new SizeLimitError('disablePluginsNotArray') } for (let opt in check) { let available = OPTIONS[opt] if (typeof available === 'string') { if (!plugins.has(available)) { throw new SizeLimitError('pluginlessConfig', opt, available) } } else if (Array.isArray(available)) { if (available.every(i => !plugins.has(i))) { throw new SizeLimitError('multiPluginlessConfig', opt, ...available) } } else if (available !== true) { throw new SizeLimitError('unknownOption', opt) } } } } function toAbsolute(file, cwd) { return isAbsolute(file) ? file : join(cwd, file) } async function findFiles(patterns, cwd) { // Node.js `glob` matches a bare directory as a single entry, while // `tinyglobby` expanded it to the files inside. Add a `/**` sibling for // every pattern to keep that behavior (e.g. `dist/ui` → `dist/ui/**`). let expanded = patterns.flatMap(i => [i, `${i}/**`]) let found = await Array.fromAsync( glob(expanded, { cwd, withFileTypes: true }) ) let files = new Set( found .filter(i => i.isFile()) .map(i => relative(cwd, join(i.parentPath, i.name))) ) return [...files] } async function globFiles(patterns, cwd) { let ignore = patterns.filter(i => i.startsWith('!')) let files = await findFiles( patterns.filter(i => !i.startsWith('!')), cwd ) if (ignore.length === 0) return files let ignored = new Set(await findFiles(ignore.map(i => i.slice(1)), cwd)) return files.filter(i => !ignored.has(i)) } function toName(files, cwd) { return files.map(i => (i.startsWith(cwd) ? relative(cwd, i) : i)).join(', ') } async function dynamicImport(filePath) { return (await import(pathToFileURL(filePath).href)).default } export default async function getConfig(plugins, process, args, pkg) { let config = { cwd: process.cwd() } if (args.why) { config.project = pkg.packageJson.name config.why = args.why } if (args.compareWith) { config.compareWith = toAbsolute(args.compareWith, process.cwd()) } if (args.saveBundle) { config.saveBundle = toAbsolute(args.saveBundle, process.cwd()) } if (args.cleanDir) { config.cleanDir = args.cleanDir } if (args.hidePassed) { config.hidePassed = args.hidePassed } if (args.highlightLess) { config.highlightLess = args.highlightLess } if (args.files.length > 0) { config.checks = [{ files: args.files }] } else { let explorer = lilconfig('size-limit', { loaders: { '.cjs': dynamicImport, '.cts': dynamicImport, '.js': dynamicImport, '.mjs': dynamicImport, '.mts': dynamicImport, '.ts': dynamicImport }, searchPlaces: [ 'package.json', '.size-limit.json', '.size-limit', '.size-limit.js', '.size-limit.mjs', '.size-limit.cjs', '.size-limit.ts', '.size-limit.mts', '.size-limit.cts' ] }) let result = args.config?.trim() ? await explorer.load(resolve(args.config.trim())) : await explorer.search(process.cwd()) if (result === null) throw new SizeLimitError('noConfig') checkChecks(plugins, result.config) config.configPath = relative(process.cwd(), result.filepath) config.cwd = dirname(result.filepath) config.checks = await Promise.all( result.config.map(async check => { let processed = { ...check } if (check.path) { let patterns = Array.isArray(check.path) ? check.path : [check.path] processed.files = await globFiles(patterns, config.cwd) } else if (!check.entry) { if (pkg.packageJson.main) { processed.files = [ require.resolve(join(dirname(pkg.path), pkg.packageJson.main)) ] } else { processed.files = [join(dirname(pkg.path), 'index.js')] } } return processed }) ) } let peer = Object.keys(pkg.packageJson.peerDependencies || {}) for (let check of config.checks) { if (peer.length > 0) check.ignore = peer.concat(check.ignore || []) if (check.entry && !Array.isArray(check.entry)) check.entry = [check.entry] if (!check.name) check.name = toName(check.entry || check.files, config.cwd) if (args.limit) check.limit = args.limit if (check.limit) { if (endsWithMs(check.limit)) { check.timeLimit = parseFloat(check.limit) / 1000 } else if (endsWithS(check.limit)) { check.timeLimit = parseFloat(check.limit) } else { check.sizeLimit = bytes.parse(check.limit) } if (check.timeLimit && !plugins.has('time')) { throw new SizeLimitError('timeWithoutPlugin') } } if (config.highlightLess) check.highlightLess = true if (/\sB$|\dB$/.test(check.limit)) check.highlightLess = true if (check.files) { check.files = check.files.map(i => toAbsolute(i, config.cwd)) } if (check.config) check.config = toAbsolute(check.config, config.cwd) if (typeof check.import === 'string') { check.import = { [check.files[0]]: check.import } } if (check.import) { let imports = {} for (let i in check.import) { if (peer.includes(i)) { check.ignore = check.ignore.filter(j => j !== i) imports[require.resolve(i, config.cwd)] = check.import[i] } else { imports[toAbsolute(i, config.cwd)] = check.import[i] } } check.import = imports } if (check.time) { let { latency, networkSpeed } = check.time if (latency) { if (endsWithMs(latency)) { check.time.latency = parseFloat(latency) / 1000 } else { check.time.latency = parseFloat(latency) || 0 } } if (networkSpeed) { check.time.networkSpeed = bytes.parse(networkSpeed) } } } return config }