diff --git a/jest-resolver.js b/jest-resolver.js new file mode 100644 index 0000000000..beb8262852 --- /dev/null +++ b/jest-resolver.js @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Custom Jest resolver that respects the "browser" field in package.json +// This allows tests to use browser implementations instead of Node.js implementations +// +// Set JEST_ENVIRONMENT=node to use Node.js implementations (default: browser) + +const fs = require('fs'); +const path = require('path'); + +// Determine environment mode: "browser" or "node" +const USE_BROWSER = process.env.JEST_ENVIRONMENT !== 'node'; + +// Read package.json once at module load time +const PROJECT_ROOT = __dirname; +const BROWSER_MAPPINGS = parseBrowserMappingsFromPackageJson(PROJECT_ROOT); + +module.exports = (request, options) => { + const resolved = options.defaultResolver(request, options); + + if (USE_BROWSER) { + return BROWSER_MAPPINGS[resolved] ?? resolved; + } + + return resolved; +}; + +function parseBrowserMappingsFromPackageJson(projectRoot) { + const browserMappings = {}; + const packageJsonPath = path.join(projectRoot, 'package.json'); + try { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + const browserField = packageJson.browser; + + if (browserField && typeof browserField === 'object') { + // Pre-validate all browser mappings and convert to absolute paths + for (const [source, target] of Object.entries(browserField)) { + const absoluteSource = path.resolve(projectRoot, source); + const absoluteTarget = path.resolve(projectRoot, target); + + if (!fs.existsSync(absoluteTarget)) { + console.warn( + `Warning: Browser mapping target does not exist: ${target}` + ); + continue; + } + + browserMappings[absoluteSource] = absoluteTarget; + } + } + } catch (error) { + console.error(`Error reading package.json for browser field: ${error}`); + } + return browserMappings; +} diff --git a/jest.config.js b/jest.config.js index 54682dfe31..dbb1aa6adc 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,6 +6,9 @@ module.exports = { testMatch: ['/src/**/*.test.{js,jsx,ts,tsx}'], moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'], + // Use custom resolver that respects the "browser" field in package.json + resolver: './jest-resolver.js', + testEnvironment: './src/test/custom-environment', setupFilesAfterEnv: ['jest-extended/all', './src/test/setup.ts'], diff --git a/package.json b/package.json index 57bde3be2b..0a952fd0f4 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,9 @@ "version": ">= 22 < 23" } }, + "browser": { + "./src/utils/gz.ts": "./src/utils/gz.browser.ts" + }, "scripts": { "build:clean": "rimraf dist && mkdirp dist", "build:quiet": "yarn build:clean && cross-env NODE_ENV=development webpack", @@ -44,6 +47,7 @@ "start-docs": "ws -d docs-user/ -p 3000", "start-photon": "node res/photon/server", "test": "node bin/output-fixing-commands.js cross-env LC_ALL=C TZ=UTC NODE_ENV=test jest", + "test-node": "node bin/output-fixing-commands.js cross-env LC_ALL=C TZ=UTC NODE_ENV=test JEST_ENVIRONMENT=node jest", "test-all": "run-p --max-parallel 4 ts license-check lint test test-alex test-lockfile", "test-build-coverage": "yarn test --coverage --coverageReporters=html", "test-serve-coverage": "ws -d coverage/ -p 4343", diff --git a/src/utils/gz.browser.ts b/src/utils/gz.browser.ts new file mode 100644 index 0000000000..6cc485bdbc --- /dev/null +++ b/src/utils/gz.browser.ts @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import gzWorkerPath from './gz.worker.js'; + +function runGzWorker( + kind: 'compress' | 'decompress', + arrayData: Uint8Array +): Promise> { + return new Promise((resolve, reject) => { + // On-demand spawn the worker. If this is too slow we can look into keeping + // a pool of workers around. + const worker = new Worker(gzWorkerPath); + + worker.onmessage = (e) => { + resolve(e.data as Uint8Array); + worker.terminate(); + }; + + worker.onerror = (e) => { + reject(e.error); + worker.terminate(); + }; + + worker.postMessage({ kind, arrayData }, [arrayData.buffer]); + }); +} + +// This will transfer `data` if it is an array buffer. +export function compress( + data: string | Uint8Array +): Promise> { + // Encode the data if it's a string + const arrayData = + typeof data === 'string' ? new TextEncoder().encode(data) : data; + + return runGzWorker('compress', arrayData); +} + +export function decompress( + data: Uint8Array +): Promise> { + return runGzWorker('decompress', data); +} + +export function isGzip(data: Uint8Array): boolean { + // Detect the gzip magic bytes 1f 8b 08. + return ( + data.byteLength >= 3 && + data[0] === 0x1f && + data[1] === 0x8b && + data[2] === 0x08 + ); +} diff --git a/src/utils/gz.ts b/src/utils/gz.ts index 60c4a7202e..eabc4db4a1 100644 --- a/src/utils/gz.ts +++ b/src/utils/gz.ts @@ -2,78 +2,38 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import gzWorkerPath from './gz.worker.js'; +// Node.js implementation using zlib +// For browser builds, this file is replaced with gz.browser.ts via package.json "browser" field -function runGzWorker( - kind: 'compress' | 'decompress', - arrayData: Uint8Array -): Promise> { - return new Promise((resolve, reject) => { - // On-demand spawn the worker. If this is too slow we can look into keeping - // a pool of workers around. - const worker = new Worker(gzWorkerPath); - - worker.onmessage = (e) => { - resolve(e.data as Uint8Array); - worker.terminate(); - }; - - worker.onerror = (e) => { - reject(e.error); - worker.terminate(); - }; - - worker.postMessage({ kind, arrayData }, [arrayData.buffer]); - }); -} +import * as zlib from 'zlib'; // This will transfer `data` if it is an array buffer. -export async function compress( +export function compress( data: string | Uint8Array ): Promise> { - // Encode the data if it's a string - const arrayData = - typeof data === 'string' ? new TextEncoder().encode(data) : data; - - if (!(typeof window === 'object' && 'Worker' in window)) { - // Try to fall back to Node's zlib library. - const zlib = await import('zlib'); - return new Promise((resolve, reject) => { - zlib.gzip(data, (errorOrNull, result) => { - if (errorOrNull) { - reject(errorOrNull); - } else { - resolve(result); - } - }); + return new Promise((resolve, reject) => { + zlib.gzip(data, (errorOrNull, result) => { + if (errorOrNull) { + reject(errorOrNull); + } else { + resolve(result); + } }); - } - - return runGzWorker('compress', arrayData); + }); } -export async function decompress( +export function decompress( data: Uint8Array ): Promise> { - if (!(typeof window === 'object' && 'Worker' in window)) { - // Handle the case where we're not running in the browser, e.g. when - // this code is used as part of a library in a Node project. - // We don't get here when running Firefox profiler tests, because our - // tests create a mock window with a mock Worker class. - // Try to fall back to Node's zlib library. - const zlib = await import('zlib'); - return new Promise((resolve, reject) => { - zlib.gunzip(data, (errorOrNull, result) => { - if (errorOrNull) { - reject(errorOrNull); - } else { - resolve(result); - } - }); + return new Promise((resolve, reject) => { + zlib.gunzip(data, (errorOrNull, result) => { + if (errorOrNull) { + reject(errorOrNull); + } else { + resolve(result); + } }); - } - - return runGzWorker('decompress', data); + }); } export function isGzip(data: Uint8Array): boolean {