Skip to content

Commit 78d15a0

Browse files
committed
WIP: Migrate to esbuild.
1 parent 8048c57 commit 78d15a0

24 files changed

Lines changed: 1418 additions & 2328 deletions

CONTRIBUTING.md

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,30 +37,19 @@ To get started clone the repo and get the web application started.
3737
1. Run `git clone git@github.com:firefox-devtools/profiler.git`
3838
2. Run `cd profiler`
3939
3. Run `yarn install`, this will install all of the dependencies.
40-
4. Run `yarn start`, this will start up the webpack server.
40+
4. Run `yarn start`, this will start up the development server.
4141
5. Point your browser to [http://localhost:4242](http://localhost:4242).
4242
6. If port `4242` is taken, then you can run the web app on a different port: `FX_PROFILER_PORT=1234 yarn start`
4343

44-
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:
45-
46-
```js
47-
module.exports = function (config, serverConfig) {
48-
config.cache = false;
49-
serverConfig.open = true;
50-
};
51-
```
52-
5344
This project uses [TypeScript](https://www.typescriptlang.org/).
5445

5546
## Using GitHub Codespaces
5647

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

59-
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.
60-
61-
## Loading in profiles for development
50+
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.
6251

63-
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:
52+
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:
6453

6554
#### 1. Record a profile:
6655

__mocks__/gecko-profiler-demangle.ts

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

appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ test_script:
2828
- yarn versions
2929
# run tests
3030
- yarn test-all
31-
- yarn build-prod:quiet
31+
- yarn build-prod
3232

3333
cache:
3434
- node_modules

esbuild.mjs

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import esbuild from 'esbuild';
2+
import copy from 'esbuild-plugin-copy';
3+
import { wasmLoader } from 'esbuild-plugin-wasm';
4+
import path from 'path';
5+
import fs from 'fs';
6+
import { fileURLToPath } from 'url';
7+
8+
const __filename = fileURLToPath(import.meta.url);
9+
const __dirname = path.dirname(__filename);
10+
11+
const isProduction = process.env.NODE_ENV === 'production';
12+
13+
function generateHtmlPlugin(options) {
14+
return {
15+
name: 'firefox-profiler-generate-html',
16+
setup(build) {
17+
const { outdir, publicPath } = build.initialOptions;
18+
build.initialOptions.metafile = true;
19+
build.onEnd(async (result) => {
20+
await generateHTML(result.metafile, { ...options, outdir, publicPath });
21+
});
22+
},
23+
};
24+
}
25+
26+
const baseConfig = {
27+
bundle: true,
28+
absWorkingDir: __dirname,
29+
loader: {
30+
'.png': 'file',
31+
'.jpg': 'file',
32+
'.svg': 'file',
33+
'.worker.js': 'file',
34+
},
35+
alias: {
36+
'firefox-profiler': './src',
37+
'firefox-profiler-res': './res',
38+
},
39+
plugins: [wasmLoader()],
40+
};
41+
42+
const templateHTML = fs.readFileSync(
43+
path.join(__dirname, 'res/index.html'),
44+
'utf8'
45+
);
46+
47+
export const mainBundleConfig = {
48+
...baseConfig,
49+
format: 'esm',
50+
platform: 'browser',
51+
target: 'es2022',
52+
sourcemap: true,
53+
minify: isProduction,
54+
splitting: true,
55+
entryPoints: ['src/index.tsx'],
56+
outdir: 'dist',
57+
metafile: true,
58+
publicPath: '/',
59+
entryNames: '[name]-[hash]',
60+
define: {
61+
'process.env.L10N': process.env.L10N
62+
? JSON.stringify(process.env.L10N)
63+
: 'undefined',
64+
AVAILABLE_STAGING_LOCALES: process.env.L10N
65+
? JSON.stringify(fs.readdirSync('./locales'))
66+
: 'undefined',
67+
// no need to define NODE_ENV:
68+
// esbuild automatically defines NODE_ENV based on the value for "minify"
69+
},
70+
external: ['zlib'],
71+
plugins: [
72+
wasmLoader(),
73+
copy({
74+
resolveFrom: __dirname,
75+
assets: [
76+
{ from: ['res/_headers'], to: ['dist'] },
77+
{ from: ['res/_redirects'], to: ['dist'] },
78+
{ from: ['res/contribute.json'], to: ['dist'] },
79+
{ from: ['res/robots.txt'], to: ['dist'] },
80+
{ from: ['res/service-worker-compat.js'], to: ['dist'] },
81+
{ from: ['res/img/favicon.png'], to: ['dist/res/img'] },
82+
{ from: ['docs-user/**/*'], to: ['dist/docs'] },
83+
{ from: ['locales/**/*'], to: ['dist/locales'] },
84+
],
85+
}),
86+
generateHtmlPlugin({
87+
filename: 'index.html',
88+
entryPoint: 'src/index.tsx',
89+
templateHTML,
90+
}),
91+
],
92+
};
93+
94+
// Common build configuration for node-based tools
95+
export const nodeBaseConfig = {
96+
...baseConfig,
97+
platform: 'node',
98+
target: 'node16',
99+
format: 'cjs',
100+
external: ['fs', 'path', 'crypto', 'zlib'],
101+
};
102+
103+
async function buildAll() {
104+
// Clean dist directory
105+
if (fs.existsSync('dist')) {
106+
fs.rmSync('dist', { recursive: true });
107+
}
108+
109+
const builds = [];
110+
111+
// Main app build
112+
builds.push(esbuild.build(mainBundleConfig));
113+
114+
// Node tools (if requested)
115+
if (process.argv.includes('--node-tools')) {
116+
// Symbolicator CLI
117+
builds.push(
118+
esbuild.build({
119+
...nodeBaseConfig,
120+
entryPoints: ['src/symbolicator-cli/index.ts'],
121+
outfile: 'dist/symbolicator-cli.js',
122+
})
123+
);
124+
}
125+
126+
// Wait for all builds to complete
127+
const buildResults = await Promise.all(builds);
128+
129+
// Save metafile for analysis
130+
if (buildResults[0].metafile) {
131+
fs.writeFileSync(
132+
'dist/metafile.json',
133+
JSON.stringify(buildResults[0].metafile, null, 2)
134+
);
135+
console.log('📊 Metafile saved to dist/metafile.json');
136+
}
137+
138+
console.log('✅ Build completed');
139+
}
140+
141+
async function generateHTML(metafileJson, options) {
142+
const { entryPoint, templateHTML, filename, outdir, publicPath } = options;
143+
144+
const htmlOutputPath = outdir + '/' + filename;
145+
146+
function convertPath(oldPath) {
147+
const prefixToStrip = outdir + '/';
148+
149+
if (!oldPath || !oldPath.startsWith || !oldPath.startsWith(prefixToStrip)) {
150+
throw new Error(
151+
`Unexpected path ${oldPath} which seems to be outside the outdir (which is set to ${outdir})`
152+
);
153+
}
154+
155+
const relativePath = oldPath.slice(prefixToStrip.length);
156+
if (publicPath) {
157+
// e.g. publicPath === '/'
158+
return publicPath + relativePath;
159+
}
160+
return relativePath;
161+
}
162+
163+
if (!metafileJson || !metafileJson.outputs) {
164+
throw new Error('No outputs detected');
165+
}
166+
167+
const [mainBundlePath, mainBundle] = Object.entries(
168+
metafileJson.outputs
169+
).find(([_bundlePath, bundle]) => bundle.entryPoint === entryPoint);
170+
171+
const extraHeadTags = [];
172+
173+
// Main JS bundle
174+
extraHeadTags.push(
175+
`<script src="${convertPath(mainBundlePath)}" type="module" async></script>`
176+
);
177+
178+
// Main Stylesheet
179+
if (mainBundle.cssBundle) {
180+
extraHeadTags.push(
181+
`<link rel="stylesheet" href="${convertPath(mainBundle.cssBundle)}">`
182+
);
183+
}
184+
185+
// Preload startup chunks
186+
const startupChunks = mainBundle.imports.filter(
187+
(imp) => imp.kind === 'import-statement' // as opposed to 'dynamic-import'
188+
);
189+
for (const startupChunk of startupChunks) {
190+
extraHeadTags.push(
191+
`<link rel="modulepreload" href="${convertPath(startupChunk.path)}">`
192+
);
193+
}
194+
195+
// Insert tags before </head>
196+
const extraHeadStr = extraHeadTags.map((s) => ' ' + s).join('\n');
197+
const html = templateHTML.replace(
198+
'</head>',
199+
'\n' + extraHeadStr + '\n </head>'
200+
);
201+
202+
// Write the final HTML
203+
fs.writeFileSync(htmlOutputPath, html);
204+
}
205+
206+
// Run build if called directly
207+
if (import.meta.url === `file://${process.argv[1]}`) {
208+
buildAll().catch(console.error);
209+
}

netlify.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
[context.l10n]
2-
command = "yarn build-l10n-prod:quiet"
2+
command = "yarn build-l10n-prod"

package.json

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,14 @@
1010
}
1111
},
1212
"scripts": {
13-
"build:clean": "rimraf dist && mkdirp dist",
14-
"build:quiet": "yarn build:clean && cross-env NODE_ENV=development webpack",
15-
"build": "yarn build:quiet --progress",
16-
"build-prod:quiet": "yarn build:clean && yarn build-photon && cross-env NODE_ENV=production webpack",
17-
"build-prod": "yarn build-prod:quiet --progress",
18-
"build-l10n": "yarn build:clean && cross-env NODE_ENV=development L10N=1 webpack --progress",
19-
"build-l10n-prod:quiet": "yarn build:clean && yarn build-photon && cross-env NODE_ENV=production L10N=1 webpack",
20-
"build-l10n-prod": "yarn build-l10n-prod:quiet --progress",
21-
"build-photon": "webpack --config res/photon/webpack.config.js",
22-
"build-symbolicator-cli": "yarn build-symbolicator-cli:quiet --progress",
23-
"build-symbolicator-cli:quiet": "yarn build:clean && cross-env NODE_ENV=production webpack --config src/symbolicator-cli/webpack.config.js",
13+
"build": "cross-env NODE_ENV=development node esbuild.mjs",
14+
"build-prod": "cross-env NODE_ENV=production node esbuild.mjs && yarn build-sw && yarn build-photon",
15+
"build-l10n": "cross-env NODE_ENV=development L10N=1 node esbuild.mjs",
16+
"build-l10n-prod": "cross-env NODE_ENV=production L10N=1 node esbuild.mjs && yarn build-sw && yarn build-photon",
17+
"build-photon": "cross-env NODE_ENV=production node res/photon/esbuild.mjs",
18+
"build-sw": "workbox generateSW workbox-config.js",
19+
"build-symbolicator-cli": "cross-env NODE_ENV=production node src/symbolicator-cli/esbuild.mjs",
20+
"build-prod:quiet": "yarn build-prod",
2421
"lint": "node bin/output-fixing-commands.js run-p lint-js lint-css prettier-run",
2522
"lint-fix": "run-p lint-fix-js lint-fix-css prettier-fix",
2623
"lint-js": "node bin/output-fixing-commands.js eslint . --report-unused-disable-directives --cache --cache-strategy content",
@@ -37,11 +34,10 @@
3734
"serve-static": "ws -d dist/ -s index.html -p 4243",
3835
"start": "yarn build:clean && cross-env NODE_ENV=development node server.js",
3936
"start-prod": "yarn build-prod && yarn serve-static",
40-
"start-l10n": "yarn build:clean && cross-env NODE_ENV=development L10N=1 node server.js",
37+
"start-l10n": "cross-env NODE_ENV=development L10N=1 node server.mjs",
4138
"start-l10n-prod": "yarn build-l10n-prod && yarn serve-static",
4239
"start-examples": "ws -d examples/ -s index.html -p 4244",
4340
"start-docs": "ws -d docs-user/ -p 3000",
44-
"start-photon": "node res/photon/server",
4541
"test": "node bin/output-fixing-commands.js cross-env LC_ALL=C TZ=UTC NODE_ENV=test jest",
4642
"test-all": "run-p --max-parallel 4 ts license-check lint test test-alex test-lockfile",
4743
"test-build-coverage": "yarn test --coverage --coverageReporters=html",
@@ -133,18 +129,15 @@
133129
"@typescript-eslint/eslint-plugin": "^8.46.2",
134130
"@typescript-eslint/parser": "^8.46.2",
135131
"alex": "^11.0.1",
136-
"autoprefixer": "^10.4.21",
137132
"babel-jest": "^30.2.0",
138-
"babel-loader": "^10.0.0",
139133
"babel-plugin-module-resolver": "^5.0.2",
140134
"browserslist": "^4.27.0",
141135
"caniuse-lite": "^1.0.30001751",
142-
"circular-dependency-plugin": "^5.2.1",
143-
"copy-webpack-plugin": "^13.0.1",
144136
"cross-env": "^10.1.0",
145-
"css-loader": "^7.1.2",
146-
"cssnano": "^7.1.1",
147137
"devtools-license-check": "^0.9.0",
138+
"esbuild": "^0.25.9",
139+
"esbuild-plugin-copy": "^2.1.1",
140+
"esbuild-plugin-wasm": "^1.1.0",
148141
"eslint": "^9.37.0",
149142
"eslint-config-prettier": "^10.1.8",
150143
"eslint-import-resolver-alias": "^1.1.2",
@@ -157,36 +150,27 @@
157150
"espree": "^10.4.0",
158151
"fake-indexeddb": "^6.2.4",
159152
"fetch-mock": "^12.5.5",
160-
"file-loader": "^6.2.0",
161153
"glob": "^11.0.3",
162154
"globals": "^16.4.0",
163-
"html-webpack-plugin": "^5.6.4",
164155
"husky": "^4.3.8",
165156
"jest": "^30.2.0",
166157
"jest-environment-jsdom": "^30.2.0",
167158
"jest-extended": "^6.0.0",
168-
"json-loader": "^0.5.7",
169159
"local-web-server": "^5.4.0",
170160
"lockfile-lint": "^4.14.1",
171161
"mkdirp": "^3.0.1",
172162
"npm-run-all2": "^8.0.4",
173163
"open": "^10.2.0",
174164
"patch-package": "^8.0.1",
175165
"postcss": "^8.5.6",
176-
"postcss-loader": "^8.2.0",
177166
"postinstall-postinstall": "^2.1.0",
178167
"prettier": "^3.6.2",
179-
"raw-loader": "^4.0.2",
180168
"rimraf": "^5.0.10",
181-
"style-loader": "^4.0.0",
182169
"stylelint": "^16.25.0",
183170
"stylelint-config-idiomatic-order": "^10.0.0",
184171
"stylelint-config-standard": "^39.0.1",
185172
"typescript": "^5.9.3",
186-
"webpack": "^5.102.1",
187-
"webpack-cli": "^6.0.1",
188-
"webpack-dev-server": "^5.2.2",
189-
"workbox-webpack-plugin": "^7.3.0",
173+
"workbox-cli": "^7.3.0",
190174
"yargs": "^18.0.0"
191175
},
192176
"resolutions": {

postcss.config.js

Lines changed: 0 additions & 6 deletions
This file was deleted.

res/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<meta name="viewport" content="initial-scale=1" />
99
<title>Firefox Profiler</title>
1010
<link rel="preload" href="/locales/en-US/app.ftl" as="fetch" />
11+
<link rel="icon" type="image/png" href="/res/img/favicon.png" />
1112
</head>
1213
<body style="background-color: #363959; /* ink-70 */">
1314
<svg id="svg-filters"></svg>

0 commit comments

Comments
 (0)