#!/usr/bin/env node /** * Fail the build when the bundle uses a JS feature our oldest supported browser * cannot parse (#2971). * * Why this exists as a grep rather than a build target: Vite's `build.target` * only governs *syntax lowering*. esbuild does not rewrite regular expressions, * so a lookbehind assertion - unsupported before Safari 16.4 - builds silently * under `safari15`, `safari16.0` and `es2020` alike (measured, all three). That * is exactly how #2971 shipped: `remark-gfm` pulled a lookbehind regex literal * into the entry chunk, iOS 16.0-16.3 refused to compile the module, and every * page rendered as a blank white screen from v1.2.5 until it was found in the * field two months later. * * A regex literal is validated when its module is *compiled*, so one of these * anywhere in the entry chunk takes down the entire app, not just the feature * that pulled it in. There is no graceful degradation to fall back on, which is * why this is a hard build failure and not a warning. * * BASELINE: Safari 16.0 / iOS 16.0. Raising it is a product decision - if you * do, drop the entries that the new floor supports rather than deleting the * check. * * Scope: parse-time failures only. Runtime APIs (`Object.groupBy`, * `Promise.withResolvers`, ...) break one feature rather than the whole bundle * and are better caught by real-browser testing, so they are deliberately not * listed here. */ import { readdirSync, readFileSync } from 'node:fs'; import { join, dirname, relative, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const ASSETS = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'static', 'assets'); /** * Every extension the build can emit executable JavaScript under. `.mjs` * because a dependency's file imported with `?url` is copied verbatim under * its own extension and bypasses `build.target` - the pdf.js worker shipped a * class static block that way (#2976). */ const SCRIPT_EXTENSIONS = ['.js', '.mjs', '.cjs']; /** * Every script under `dir`, recursively, as paths relative to ASSETS. The * subdirectories matter: pdf.js's decoder fallbacks are published verbatim * under assets/pdfjs/ (vite.config.ts) and run in its worker (#2976). */ function collectScripts(dir) { const found = []; for (const entry of readdirSync(dir, { withFileTypes: true })) { const full = join(dir, entry.name); if (entry.isDirectory()) { found.push(...collectScripts(full)); } else if (SCRIPT_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) { found.push(relative(ASSETS, full)); } } return found; } /** * Each pattern must match only real occurrences of the feature. Anything that * needs context to tell a false positive from a real hit (regex flags, for * instance, are indistinguishable from division by a variable in a minified * bundle without parsing) is left out rather than made noisy. */ const FORBIDDEN = [ { pattern: /\(\?<[=!]/g, feature: 'regex lookbehind assertion', since: 'Safari 16.4', hint: 'A dependency shipped `(?<=` or `(? 0) { console.error( `\ncheck-browser-baseline: bundle uses syntax that Safari 16.0 / iOS 16.0 cannot parse.\n` + `A parse error takes down the WHOLE app on those browsers - blank white screen (#2971).\n\n` + `${failures.join('\n\n')}\n`, ); process.exit(1); } console.log(`✓ ${bundles.length} bundle(s) parse-compatible with the Safari 16.0 baseline.`);