Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions jest-resolver.js
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 3 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ module.exports = {
testMatch: ['<rootDir>/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'],

Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
55 changes: 55 additions & 0 deletions src/utils/gz.browser.ts
Original file line number Diff line number Diff line change
@@ -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<ArrayBuffer>
): Promise<Uint8Array<ArrayBuffer>> {
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<ArrayBuffer>);
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<ArrayBuffer>
): Promise<Uint8Array<ArrayBuffer>> {
// 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<ArrayBuffer>
): Promise<Uint8Array<ArrayBuffer>> {
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
);
}
82 changes: 21 additions & 61 deletions src/utils/gz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArrayBuffer>
): Promise<Uint8Array<ArrayBuffer>> {
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<ArrayBuffer>);
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<ArrayBuffer>
): Promise<Uint8Array<ArrayBuffer>> {
// 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<ArrayBuffer>
): Promise<Uint8Array<ArrayBuffer>> {
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 {
Expand Down