Skip to content

Commit

Permalink
Merge pull request #433 from dcastil/other/replace-size-limit-with-ow…
Browse files Browse the repository at this point in the history
…n-report

Replace size-limit action with own metrics-report action
  • Loading branch information
dcastil authored Jun 23, 2024
2 parents b756669 + 2b01a89 commit 5ff654d
Show file tree
Hide file tree
Showing 20 changed files with 1,505 additions and 367 deletions.
1 change: 1 addition & 0 deletions .github/actions/metrics-report/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
temp
11 changes: 11 additions & 0 deletions .github/actions/metrics-report/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: 'metrics-report-action'
author: 'Dany Castillo'
description: 'Posts a comment with a report about changes in important metrics related to tailwind-merge'
inputs:
github_token:
description: 'Github token of the repository (automatically created by Github)'
default: '${{ github.token }}'
required: false
runs:
using: 'node20'
main: 'src/main.mjs'
17 changes: 17 additions & 0 deletions .github/actions/metrics-report/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "metrics-report-action",
"version": "0.1.0",
"private": true,
"author": "Dany Castillo",
"scripts": {
"start": "node src/main.mjs"
},
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0",
"@octokit/types": "^13.5.0",
"esbuild": "^0.21.5",
"rollup": "^4.18.0"
}
}
235 changes: 235 additions & 0 deletions .github/actions/metrics-report/src/get-package-size.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
// @ts-check

import fs from 'fs/promises'
import path from 'path'
import { promisify } from 'util'
import zlib from 'zlib'

import core from '@actions/core'
import { exec } from '@actions/exec'
import { transform } from 'esbuild'
import { rollup } from 'rollup'

import { actionRootPath, repoRootPath } from './utils/path.mjs'

/**
* @typedef {object} GetPackageSizeOptions
* @property {boolean=} shouldOmitFailures
*/

/**
* @param {GetPackageSizeOptions=} options
*/
export async function getPackageSize(options = {}) {
await buildPackage()

return getEntryPointSizes(options)
}

async function buildPackage() {
core.info('Installing dependencies')
await exec('yarn install --frozen-lockfile', [], { cwd: repoRootPath })

core.info('Building package')
await exec('yarn build', [], { cwd: repoRootPath })
}

/**
* @typedef {object} EntryPointSize
* @property {BundleSize} bundleSize
* @property {BundleSize[]=} singleExportSizes
*/

/**
* @param {GetPackageSizeOptions} param0
* @returns {Promise<EntryPointSize[]>}
*/
async function getEntryPointSizes({ shouldOmitFailures }) {
core.info('Getting entry point configs')
const entryPointConfigs = await getEntryPointConfigs()

core.info('Getting bundle sizes')

const maybeEntryPointSizes = await Promise.all(
entryPointConfigs.map(async (entryPointConfig, entryPointIndex) => {
const entryPointBundlePath = path.resolve(repoRootPath, entryPointConfig.bundlePath)

const bundle = await getBundle(entryPointConfig, entryPointBundlePath).catch(
(error) => {
if (shouldOmitFailures) {
core.info(
`Failed to get bundle for ${entryPointConfig.entryPoint}: ${error.message}`,
)
return
}

throw error
},
)

if (!bundle) {
return
}

const [bundleSize, singleExportSizes] = await Promise.all([
getBundleSize(entryPointConfig.entryPoint, bundle),
getSingleExportBundleSizes(
entryPointConfig,
entryPointIndex,
entryPointBundlePath,
bundle,
),
])

return {
bundleSize,
singleExportSizes,
}
}),
)

/** @type {any} */
const entryPointSizes = maybeEntryPointSizes.filter((bundleSize) => bundleSize !== undefined)

return entryPointSizes
}

/**
* @typedef {object} EntryPointConfiguration
* @property {string} entryPoint
* @property {string} bundlePath
* @property {'esm' | 'cjs'} format
*/

/**
* @returns {Promise<EntryPointConfiguration[]>}
*/
async function getEntryPointConfigs() {
const pkg = (await import('../../../../package.json', { assert: { type: 'json' } })).default

return Object.entries(pkg.exports).flatMap(([relativeEntryPointPath, bundleObject]) => {
const entryPointPath = path.join('tailwind-merge', relativeEntryPointPath)

/** @type {EntryPointConfiguration[]} */
const entryPointConfigs = []

if (bundleObject.import) {
entryPointConfigs.push({
entryPoint: entryPointPath + ' esm',
bundlePath: bundleObject.import,
format: 'esm',
})
}

if (bundleObject.require) {
entryPointConfigs.push({
entryPoint: entryPointPath + ' cjs',
bundlePath: bundleObject.require,
format: 'cjs',
})
}

return entryPointConfigs
})
}

/**
* @param {EntryPointConfiguration} entryPointConfig
* @param {string} entryPoint
*/
async function getBundle(entryPointConfig, entryPoint) {
const rollupBuild = await rollup({ input: entryPoint })
let rollupOutput

try {
rollupOutput = await rollupBuild.generate({
format: entryPointConfig.format,
})
} catch (error) {
await rollupBuild.close()
throw error
}

await rollupBuild.close()

if (rollupOutput.output.length !== 1) {
throw Error(`Expected a single output chunk for bundle ${entryPoint}`)
}

const outputChunk = rollupOutput.output[0]

return outputChunk
}

/**
* @param {EntryPointConfiguration} entryPointConfig
* @param {number} entryPointIndex
* @param {string} bundlePath
* @param {import('rollup').OutputChunk} bundle
*/
async function getSingleExportBundleSizes(entryPointConfig, entryPointIndex, bundlePath, bundle) {
if (entryPointConfig.format === 'esm' && bundle.exports.length !== 0) {
const singleExportBundlesDirPath = path.resolve(
actionRootPath,
`temp/bundle-${entryPointIndex}`,
)

await fs.mkdir(singleExportBundlesDirPath, { recursive: true })

return Promise.all(
bundle.exports.map(async (exportName) => {
const entryPoint = await createEntryPoint(
singleExportBundlesDirPath,
bundlePath,
exportName,
)

const singleExportBundle = await getBundle(entryPointConfig, entryPoint)

return getBundleSize(exportName, singleExportBundle)
}),
)
}
}

/**
* @param {string} singleExportBundlesDirPath
* @param {string} bundlePath
* @param {string} exportName
*/
async function createEntryPoint(singleExportBundlesDirPath, bundlePath, exportName) {
const filePath = path.resolve(singleExportBundlesDirPath, `${exportName}.mjs`)
const fileContent = `export { ${exportName} } from '${bundlePath}'`

await fs.writeFile(filePath, fileContent)

return filePath
}

/**
* @typedef {object} BundleSize
* @property {string} label
* @property {number} size
* @property {number} sizeMinified
* @property {number} sizeBrotliCompressed
*/

/**
* @param {string} label
* @param {import('rollup').OutputChunk} bundle
* @returns {Promise<BundleSize>}
*/
async function getBundleSize(label, bundle) {
const esBuildTransformResult = await transform(bundle.code, { minify: true })
const minifiedCode = esBuildTransformResult.code
const brotliCompressedCode = (await brotliCompress(minifiedCode)).toString()

return {
label,
size: bundle.code.length,
sizeMinified: minifiedCode.length,
sizeBrotliCompressed: brotliCompressedCode.length,
}
}

const brotliCompress = promisify(zlib.brotliCompress)
Loading

0 comments on commit 5ff654d

Please sign in to comment.