feat(tools): modularize browser-scripts (#65399)

This commit is contained in:
Oliver Eyton-Williams 2026-01-26 13:21:20 +01:00 committed by GitHub
parent 3152bff893
commit e5cae6909c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 74 additions and 38 deletions

1
client/.gitignore vendored
View File

@ -12,7 +12,6 @@ yarn-error.log
static/curriculum-data
# Generated config
config/browser-scripts/*.json
i18n/locales/**/trending.json
i18n/locales/**/search-bar.json

View File

@ -21,7 +21,7 @@
"scripts": {
"prebuild": "pnpm run common-setup && pnpm run build:scripts --env production",
"build": "NODE_OPTIONS=\"--max-old-space-size=7168 --no-deprecation\" gatsby build --prefix-paths",
"build:scripts": "pnpm run -F=browser-scripts compile",
"build:scripts": "pnpm run -F=browser-scripts compile && tsx ./tools/copy-browser-scripts.ts",
"build:external-curriculum": "tsx ./tools/external-curriculum/build",
"clean": "gatsby clean",
"common-setup": "pnpm -w turbo compile && pnpm run create:env && pnpm run create:trending && pnpm run create:search-placeholder",
@ -144,6 +144,7 @@
"@freecodecamp/eslint-config": "workspace:*",
"@freecodecamp/shared": "workspace:*",
"@freecodecamp/curriculum": "workspace:*",
"@freecodecamp/browser-scripts": "workspace:*",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "12.1.5",
"@testing-library/react-hooks": "^8.0.1",

View File

@ -9,21 +9,20 @@ import {
stubTrue
} from 'lodash-es';
import sassData from '../../../../config/browser-scripts/sass-compile.json';
import {
transformContents,
transformHeadTailAndContents,
compileHeadTail,
createSource
} from '@freecodecamp/shared/utils/polyvinyl';
import { version } from '@freecodecamp/browser-scripts/package.json';
import { WorkerExecutor } from '../utils/worker-executor';
import {
compileTypeScriptCode,
checkTSServiceIsReady
} from '../utils/typescript-worker-handler';
const { filename: sassCompile } = sassData;
const protectTimeout = 100;
const testProtectTimeout = 1500;
const loopsPerTimeoutCheck = 100;
@ -209,7 +208,9 @@ function getBabelOptions(
return presets;
}
const sassWorkerExecutor = new WorkerExecutor(sassCompile);
const sassWorkerExecutor = new WorkerExecutor(
`workers/${version}/sass-compile`
);
async function transformSASS(documentElement) {
// we only teach scss syntax, not sass. Also the compiler does not seem to be
// able to deal with sass.

View File

@ -1,7 +1,7 @@
// TODO: This might be cleaner as a class.
import pythonWorkerData from '../../../../config/browser-scripts/python-worker.json';
import { version } from '@freecodecamp/browser-scripts/package.json';
const pythonWorkerSrc = `/js/${pythonWorkerData.filename}.js`;
// TODO: This might be cleaner as a class.
const pythonWorkerSrc = `/js/workers/${version}/python-worker.js`;
let worker: Worker | null = null;
let listener: ((event: MessageEvent) => void) | null = null;

View File

@ -1,7 +1,8 @@
import typeScriptWorkerData from '../../../../config/browser-scripts/typescript-worker.json';
import { version } from '@freecodecamp/browser-scripts/package.json';
import { awaitResponse } from './awaitable-messenger';
const typeScriptWorkerSrc = `/js/${typeScriptWorkerData.filename}.js`;
const typeScriptWorkerSrc = `/js/workers/${version}/typescript-worker.js`;
let worker: Worker | null = null;

View File

@ -0,0 +1,19 @@
import { cpSync, mkdirSync, rmSync } from 'node:fs';
import { resolve } from 'node:path';
const browserScriptDist = resolve(
__dirname,
'../../tools/client-plugins/browser-scripts/dist'
);
const destJsDir = resolve(__dirname, '../static/js');
// Everything is done synchronously to keep the script simple. There's no
// performance benefit to doing this asynchronously since it's already so fast.
rmSync(destJsDir, { recursive: true, force: true });
mkdirSync(destJsDir, { recursive: true });
cpSync(resolve(browserScriptDist, 'artifacts'), destJsDir, {
recursive: true
});

View File

@ -1 +1,2 @@
generated
src/test/stubs/js

View File

@ -50,6 +50,7 @@
"@babel/register": "7.23.7",
"@freecodecamp/eslint-config": "workspace:*",
"@freecodecamp/shared": "workspace:*",
"@freecodecamp/browser-scripts": "workspace:*",
"@total-typescript/ts-reset": "^0.6.1",
"@types/debug": "^4.1.12",
"@types/js-yaml": "4.0.5",

View File

@ -3,10 +3,8 @@ import path from 'node:path';
import sirv from 'sirv';
import polka from 'polka';
import puppeteer from 'puppeteer';
import { helperVersion } from '../../../client/src/templates/Challenges/utils/frame';
const clientPath = path.resolve(__dirname, '../../../client');
import { cpSync, mkdirSync, rmSync } from 'node:fs';
import { version } from '@freecodecamp/browser-scripts/test-runner';
async function createBrowser() {
return puppeteer.launch({
@ -21,6 +19,20 @@ async function createBrowser() {
let browser, server;
function setupStubs() {
const browserScriptDist = path.resolve(
__dirname,
'../../../tools/client-plugins/browser-scripts/dist'
);
const destArtifactsDir = path.resolve(__dirname, 'stubs/js');
rmSync(destArtifactsDir, { recursive: true, force: true });
mkdirSync(destArtifactsDir, { recursive: true });
cpSync(path.resolve(browserScriptDist, 'artifacts'), destArtifactsDir, {
recursive: true
});
}
async function startServer() {
const host = '127.0.0.1';
const port = 8080;
@ -29,16 +41,16 @@ async function startServer() {
// Mount static files used by the tests
app.use(
'/dist',
sirv(path.join(clientPath, `static/js/test-runner/${helperVersion}`))
'/dist', // the runner is mounted at dist so we don't need to specify the asset path when initializing
sirv(path.resolve(__dirname, `stubs/js/test-runner/${version}`))
);
app.use('/js', sirv(path.join(clientPath, 'static/js')));
app.use('/', sirv(path.resolve(__dirname, 'stubs')));
app.listen(port, host);
return app.server;
}
export async function setup() {
setupStubs();
server = await startServer();
browser = await createBrowser();
// Sharing the Websocket endpoint so that setup files can connect. This allows

View File

@ -550,6 +550,9 @@ importers:
'@babel/plugin-syntax-dynamic-import':
specifier: 7.8.3
version: 7.8.3(@babel/core@7.28.5)
'@freecodecamp/browser-scripts':
specifier: workspace:*
version: link:../tools/client-plugins/browser-scripts
'@freecodecamp/curriculum':
specifier: workspace:*
version: link:../curriculum
@ -710,6 +713,9 @@ importers:
'@babel/register':
specifier: 7.23.7
version: 7.23.7(@babel/core@7.23.7)
'@freecodecamp/browser-scripts':
specifier: workspace:*
version: link:../tools/client-plugins/browser-scripts
'@freecodecamp/eslint-config':
specifier: workspace:*
version: link:../packages/eslint-config

View File

@ -0,0 +1 @@
dist

View File

@ -8,6 +8,13 @@
"node": ">=24",
"pnpm": ">=10"
},
"files": [
"dist"
],
"exports": {
"./test-runner": "./test-runner.ts",
"./package.json": "./package.json"
},
"repository": {
"type": "git",
"url": "git+https://github.com/freeCodeCamp/freeCodeCamp.git"

View File

@ -1,3 +1,4 @@
import { version } from '@freecodecamp/browser-scripts/package.json';
// work around for SASS error in Edge
// https://github.com/medialize/sass.js/issues/96#issuecomment-424386171
interface WorkerWithSass extends Worker {
@ -20,7 +21,7 @@ if (!ctx.crypto) {
};
}
ctx.importScripts('/js/sass.sync.js');
ctx.importScripts(`/js/workers/${version}/sass.sync.js`);
ctx.onmessage = e => {
const data: unknown = e.data;

View File

@ -1,18 +1,14 @@
const { writeFileSync } = require('fs');
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const webpack = require('webpack');
const {
version: helperVersion
} = require('@freecodecamp/curriculum-helpers/package.json');
const { version } = require('./package.json');
module.exports = (env = {}) => {
const __DEV__ = env.production !== true;
const staticPath = path.join(__dirname, '../../../client/static/js');
const configPath = path.join(
__dirname,
'../../../client/config/browser-scripts/'
);
return {
cache: __DEV__ ? { type: 'filesystem' } : false,
mode: __DEV__ ? 'development' : 'production',
@ -23,19 +19,9 @@ module.exports = (env = {}) => {
},
devtool: __DEV__ ? 'inline-source-map' : 'source-map',
output: {
publicPath: '/js/',
filename: chunkData => {
// construct and output the filename here, so the client can use the
// json to find the file.
const filename = `${chunkData.chunk.name}-${chunkData.chunk.contentHash.javascript}`;
writeFileSync(
path.join(configPath, `${chunkData.chunk.name}.json`),
`{"filename": "${filename}"}`
);
return filename + '.js';
},
chunkFilename: '[name]-[contenthash].js',
path: staticPath
path: path.resolve(__dirname, `dist/artifacts/workers/${version}`),
clean: true
},
stats: {
// Display bailout reasons
@ -74,7 +60,7 @@ module.exports = (env = {}) => {
'./node_modules/xterm/css/xterm.css',
{
from: './node_modules/@freecodecamp/curriculum-helpers/dist/test-runner',
to: `test-runner/${helperVersion}/`
to: `../../test-runner/${helperVersion}/`
}
]
}),