Skip to content
Draft
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
15 changes: 3 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,30 +37,21 @@ To get started clone the repo and get the web application started.
1. Run `git clone git@github.com:firefox-devtools/profiler.git`
2. Run `cd profiler`
3. Run `yarn install`, this will install all of the dependencies.
4. Run `yarn start`, this will start up the webpack server.
4. Run `yarn start`, this will start up the development server.
5. Point your browser to [http://localhost:4242](http://localhost:4242).
6. If port `4242` is taken, then you can run the web app on a different port: `FX_PROFILER_PORT=1234 yarn start`

Other [webpack](https://webpack.js.org/configuration/) and [webpack server](https://webpack.js.org/configuration/dev-server/) options can be set in a `webpack.local-config.js` file at the repo root. For example, if you want to disable caching and the server to automatically open the home page, put in there the following code:

```js
module.exports = function (config, serverConfig) {
config.cache = false;
serverConfig.open = true;
};
```

This project uses [TypeScript](https://www.typescriptlang.org/).

## Using GitHub Codespaces

Alternatively, you can also develop the Firefox Profiler online in a pre-configured development environment using [GitHub Codespaces](https://github.com/features/codespaces).

GitHub Codespaces will automatically install all dependencies, start the webpack server for you, and forward port 4242 so you can access the web app. Please look at our [GitHub Codespaces documentation](./docs-developer/codespaces.md) for more information.
GitHub Codespaces will automatically install all dependencies, start the development server for you, and forward port 4242 so you can access the web app. Please look at our [GitHub Codespaces documentation](./docs-developer/codespaces.md) for more information.

## Loading in profiles for development

The web app doesn't include any performance profiles by default, so you'll need to load some in. Make sure the local Webpack web server is running, and then try one of the following:
The web app doesn't include any performance profiles by default, so you'll need to load some in. Make sure the local development server is running, and then try one of the following:

#### 1. Record a profile:

Expand Down
6 changes: 3 additions & 3 deletions __mocks__/gecko-profiler-demangle.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// This module replaces the wasm-pack generated module 'gecko-profiler-demangle'
// in our tests.
// The reason for this replacement is the fact that wasm-pack (or rather,
// wasm-bindgen), when targeting the browser + webpack, generates an ES6 module
// wasm-bindgen), when targeting the browser + bundlers, generates an ES6 module
// that node cannot deal with. Most importantly, it uses the syntax
// "import * as wasm from './gecko_profiler_demangle_bg';" in order to load
// the wasm module, which is currently only supported by webpack.
// the wasm module, which is currently only supported by bundlers.
// The long-term path to make this work correctly is to wait for node to
// support ES6 modules (and WASM as ES6 modules) natively [1]. It's possible
// that in the medium term, wasm-bindgen will get support for outputting JS
// files which work in both webpack and in node natively [2].
// files which work in both bundlers and in node natively [2].
// [1] https://medium.com/@giltayar/native-es-modules-in-nodejs-status-and-future-directions-part-i-ee5ea3001f71
// [2] https://github.com/rustwasm/wasm-bindgen/issues/233

Expand Down
215 changes: 215 additions & 0 deletions esbuild.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import esbuild from 'esbuild';
import copy from 'esbuild-plugin-copy';
import { wasmLoader } from 'esbuild-plugin-wasm';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const isProduction = process.env.NODE_ENV === 'production';

function generateHtmlPlugin(options) {
return {
name: 'firefox-profiler-generate-html',
setup(build) {
const { outdir, publicPath } = build.initialOptions;
build.initialOptions.metafile = true;
build.onEnd(async (result) => {
await generateHTML(result.metafile, { ...options, outdir, publicPath });
});
},
};
}

const baseConfig = {
bundle: true,
minify: isProduction,
absWorkingDir: __dirname,
loader: {
'.png': 'file',
'.jpg': 'file',
'.svg': 'file',
'.worker.js': 'file',
},
alias: {
'firefox-profiler': './src',
'firefox-profiler-res': './res',
},
};

const templateHTML = fs.readFileSync(
path.join(__dirname, 'res/index.html'),
'utf8'
);

export const mainBundleConfig = {
...baseConfig,
format: 'esm',
platform: 'browser',
target: 'es2022',
sourcemap: true,
splitting: true,
entryPoints: ['src/index.tsx'],
outdir: 'dist',
metafile: true,
publicPath: '/',
entryNames: '[name]-[hash]',
define: {
'process.env.L10N': process.env.L10N
? JSON.stringify(process.env.L10N)
: 'undefined',
AVAILABLE_STAGING_LOCALES: process.env.L10N
? JSON.stringify(fs.readdirSync('./locales'))
: 'undefined',
// no need to define NODE_ENV:
// esbuild automatically defines NODE_ENV based on the value for "minify"
},
external: ['zlib'],
plugins: [
wasmLoader(),
copy({
resolveFrom: __dirname,
assets: [
{ from: ['res/_headers'], to: ['dist'] },
{ from: ['res/_redirects'], to: ['dist'] },
{ from: ['res/contribute.json'], to: ['dist'] },
{ from: ['res/robots.txt'], to: ['dist'] },
{ from: ['res/service-worker-compat.js'], to: ['dist'] },
{ from: ['res/img/favicon.png'], to: ['dist/res/img'] },
{ from: ['docs-user/**/*'], to: ['dist/docs'] },
{ from: ['locales/**/*'], to: ['dist/locales'] },
],
}),
generateHtmlPlugin({
filename: 'index.html',
entryPoint: 'src/index.tsx',
templateHTML,
}),
],
};

// Common build configuration for node-based tools
export const nodeBaseConfig = {
...baseConfig,
platform: 'node',
target: 'node16',
splitting: false,
format: 'cjs',
bundle: true,
external: ['fs', 'path', 'crypto', 'zlib'],
plugins: [
wasmLoader({
mode: 'embedded',
}),
],
};

async function buildAll() {
// Clean dist directory
if (fs.existsSync('dist')) {
fs.rmSync('dist', { recursive: true });
}

const builds = [];

// Main app build
builds.push(esbuild.build(mainBundleConfig));

// Node tools (if requested)
if (process.argv.includes('--node-tools')) {
// Symbolicator CLI
builds.push(
esbuild.build({
...nodeBaseConfig,
entryPoints: ['src/symbolicator-cli/index.ts'],
outfile: 'dist/symbolicator-cli.js',
})
);
}

// Wait for all builds to complete
const buildResults = await Promise.all(builds);

// Save metafile data to a file, for example to allow visualizing bundle size.
if (buildResults[0].metafile) {
fs.writeFileSync(
'dist/metafile.json',
JSON.stringify(buildResults[0].metafile, null, 2)
);
console.log('📊 Metafile saved to dist/metafile.json');
}

console.log('✅ Build completed');
}

async function generateHTML(metafileJson, options) {
const { entryPoint, templateHTML, filename, outdir, publicPath } = options;

const htmlOutputPath = outdir + '/' + filename;

function convertPath(oldPath) {
const prefixToStrip = outdir + '/';

if (!oldPath || !oldPath.startsWith || !oldPath.startsWith(prefixToStrip)) {
throw new Error(
`Unexpected path ${oldPath} which seems to be outside the outdir (which is set to ${outdir})`
);
}

const relativePath = oldPath.slice(prefixToStrip.length);
if (publicPath) {
// e.g. publicPath === '/'
return publicPath + relativePath;
}
return relativePath;
}

if (!metafileJson || !metafileJson.outputs) {
throw new Error('No outputs detected');
}

const [mainBundlePath, mainBundle] = Object.entries(
metafileJson.outputs
).find(([_bundlePath, bundle]) => bundle.entryPoint === entryPoint);

const extraHeadTags = [];

// Main JS bundle
extraHeadTags.push(
`<script src="${convertPath(mainBundlePath)}" type="module" async></script>`
);

// Main Stylesheet
if (mainBundle.cssBundle) {
extraHeadTags.push(
`<link rel="stylesheet" href="${convertPath(mainBundle.cssBundle)}">`
);
}

// Preload startup chunks
const startupChunks = mainBundle.imports.filter(
(imp) => imp.kind === 'import-statement' // as opposed to 'dynamic-import'
);
for (const startupChunk of startupChunks) {
extraHeadTags.push(
`<link rel="modulepreload" href="${convertPath(startupChunk.path)}">`
);
}

// Insert tags before </head>
const extraHeadStr = extraHeadTags.map((s) => ' ' + s).join('\n');
const html = templateHTML.replace(
'</head>',
'\n' + extraHeadStr + '\n </head>'
);

// Write the final HTML
fs.writeFileSync(htmlOutputPath, html);
}

// Run build if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
buildAll().catch(console.error);
}
62 changes: 62 additions & 0 deletions jest-resolver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/* 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 packageJsonPath = path.join(PROJECT_ROOT, 'package.json');
const browserMappings = {};

if (USE_BROWSER) {
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(PROJECT_ROOT, source);
const absoluteTarget = path.resolve(PROJECT_ROOT, 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}`);
}
}

module.exports = (request, options) => {
const resolved = options.defaultResolver(request, options);

// If in Node mode or no browser mappings, use default resolution
if (!USE_BROWSER || Object.keys(browserMappings).length === 0) {
return resolved;
}

// Check if this resolved file has a browser mapping
const browserPath = browserMappings[resolved];
if (browserPath) {
return browserPath;
}

return resolved;
};
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
2 changes: 1 addition & 1 deletion netlify.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[context.l10n]
command = "yarn build-l10n-prod:quiet"
command = "yarn build-l10n-prod"
Loading
Loading