size-limit
Version:
CLI tool for Size Limit
272 lines (249 loc) • 7.96 kB
JavaScript
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
}