From 0626c8845011d688992a385872a0834e0d9cf658 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Thu, 19 Sep 2024 20:06:19 -0700 Subject: [PATCH 1/3] feat(clerk-js): Add bundle-check script --- .changeset/sweet-owls-bake.md | 2 + packages/clerk-js/bundle-check.mjs | 238 +++++++++++++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 .changeset/sweet-owls-bake.md create mode 100644 packages/clerk-js/bundle-check.mjs diff --git a/.changeset/sweet-owls-bake.md b/.changeset/sweet-owls-bake.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/sweet-owls-bake.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/clerk-js/bundle-check.mjs b/packages/clerk-js/bundle-check.mjs new file mode 100644 index 00000000000..e4c45a52428 --- /dev/null +++ b/packages/clerk-js/bundle-check.mjs @@ -0,0 +1,238 @@ +//@ts-check +import fs from 'node:fs'; +import http from 'node:http'; +import path from 'node:path'; +import { pipeline } from 'node:stream'; +import zlib from 'node:zlib'; + +import { chromium } from 'playwright'; + +/** + * This script generates a CLI report detailing the gzipped size of JavaScript resources loaded by `clerk-js` for a + * given configuration. This is useful to ensure that the total amount of loaded JavaScript does not exceed the + * anticipated amount for a particular invocation. + */ + +/** + * The base HTML file that each case will be executed against. Loads `clerk-js` from the local server in addition to + * creating an instance of `VirtualRouter`. + * @param {string} script The case-specific code to execute, typically to mount a component like `SignIn`. + */ +function template(script) { + return ` + + +
+ + + + + + +`; +} + +function signIn() { + const script = ` +window.Clerk.load({ router: window.VIRTUAL_ROUTER }).then(() => { + window.Clerk.mountSignIn(document.getElementById("app"), {}); +}); +`; + return template(script); +} + +function signUp() { + const script = ` +window.Clerk.load({ router: window.VIRTUAL_ROUTER }).then(() => { + window.Clerk.mountSignUp(document.getElementById("app"), {}); +}); +`; + return template(script); +} + +/** + * Map of URL routes to functions that return the HTML content of the page. This script will generate a report for each + * of the routes defined here. + */ +const routes = { + '/sign-in': signIn(), + '/sign-up': signUp(), +}; + +const server = http + .createServer((req, res) => { + const onError = err => { + if (err) { + res.end(); + console.error(err); + } + }; + + if (req.url && req.url in routes) { + res.writeHead(200, { 'content-type': 'text/html' }); + res.end(routes[req.url]); + } else { + const filePath = `./dist${req.url}`; + const extname = path.extname(filePath); + if (fs.existsSync(filePath) && (extname === '.js' || extname === '.css')) { + const contentType = extname === '.js' ? 'text/javascript' : 'text/css'; + res.writeHead(200, { 'content-encoding': 'gzip', 'content-type': contentType, vary: 'Accept-Encoding' }); + // We specifically use gzip here since that's the bundle size we really care about. + pipeline(fs.createReadStream(filePath), zlib.createGzip(), res, onError); + } else { + res.writeHead(404, { 'content-type': 'text/plain' }); + res.end('404 Not Found\n'); + } + } + }) + .listen(4000); + +const byteFormatter = Intl.NumberFormat('en', { + notation: 'compact', + style: 'unit', + unit: 'byte', + unitDisplay: 'narrow', +}); +/** + * Format bytes into kilobytes + * @param {number} bytes + */ +function formatFileSize(bytes) { + return byteFormatter.format(bytes); +} + +/** + * Generate and print a table detailing the scripts loaded and their response sizes. + * @param {string} url + * @param {{ url: string; sizes: { responseBodySize: number; }; }[]} responses + */ +function report(url, responses) { + // Start with a new line for readability + console.log('\n' + url); + + // We only care about JavaScript files loaded from localhost. This removes stuff like favicon and API calls. + const matchingFiles = responses.filter(r => { + return r.url.startsWith('http://localhost:4000') && r.url.endsWith('.js'); + }); + + const data = Object.fromEntries( + matchingFiles.map(r => { + const [, path] = r.url.split('4000/'); + return [path, { size: formatFileSize(r.sizes.responseBodySize) }]; + }), + ); + + // Calculate a total size of all matching resources. + data['(total)'] = { + size: formatFileSize( + matchingFiles.reduce((a, b) => { + return a + b.sizes.responseBodySize; + }, 0), + ), + }; + + console.table(data); +} + +/** + * Loads the given `url` in `browser`, capturing all HTTP requests that occur. + * @param {import('playwright').Browser} browser + * @param {string} url + */ +async function getResponseSizes(browser, url) { + const page = await browser.newPage(); + + /** @type {Promise<{ url: string, sizes: { responseBodySize: number } }>[]} */ + const promises = []; + + page.on('response', res => { + promises.push( + res + .request() + .sizes() + .then(sizes => { + return { url: res.url(), sizes }; + }), + ); + return res; + }); + + await page.goto(`http://localhost:4000${url}`); + // Instead of waiting for a specific element, we simply wait for the network to be idle. This is because there aren't + // any elements that exist reliably in every test case. + await page.waitForLoadState('networkidle'); + const sizes = await Promise.all(promises); + await page.close(); + + return sizes; +} + +(async () => { + const browser = await chromium.launch(); + + const testUrls = Object.keys(routes); + for (const url of testUrls) { + const sizes = await getResponseSizes(browser, url); + report(url, sizes); + } + + await browser.close(); + server.close(); +})(); From 5401e68ef83bd78d9a9073437f7ccb9be6f5fc14 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 20 Sep 2024 10:47:59 -0700 Subject: [PATCH 2/3] docs(clerk-js): Make JSDoc comment more accurate Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> --- packages/clerk-js/bundle-check.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/bundle-check.mjs b/packages/clerk-js/bundle-check.mjs index e4c45a52428..c02ac1c548a 100644 --- a/packages/clerk-js/bundle-check.mjs +++ b/packages/clerk-js/bundle-check.mjs @@ -151,7 +151,7 @@ const byteFormatter = Intl.NumberFormat('en', { unitDisplay: 'narrow', }); /** - * Format bytes into kilobytes + * Format bytes into a human-readable string with appropriate units * @param {number} bytes */ function formatFileSize(bytes) { From 6a9f46e9cb944f04a2d04579bcb78caadba163e2 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Wed, 2 Oct 2024 09:54:30 -0700 Subject: [PATCH 3/3] fix(clerk-js): Resolve CodeQL warning --- packages/clerk-js/bundle-check.mjs | 45 +++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/packages/clerk-js/bundle-check.mjs b/packages/clerk-js/bundle-check.mjs index c02ac1c548a..b3058837813 100644 --- a/packages/clerk-js/bundle-check.mjs +++ b/packages/clerk-js/bundle-check.mjs @@ -11,6 +11,9 @@ import { chromium } from 'playwright'; * This script generates a CLI report detailing the gzipped size of JavaScript resources loaded by `clerk-js` for a * given configuration. This is useful to ensure that the total amount of loaded JavaScript does not exceed the * anticipated amount for a particular invocation. + * + * Note: The publishable key embedded in this script isn't anything special; any publishable key will do, so feel free + * to replace it with another key should the current one stop working. */ /** @@ -27,7 +30,7 @@ function template(script) {