diff --git a/client/.gitignore b/client/.gitignore
index f5f41512776..9dfba12e68f 100644
--- a/client/.gitignore
+++ b/client/.gitignore
@@ -8,8 +8,8 @@ yarn-error.log
.DS_Store
/static/js
-./static/_redirects
-static/curriculum-data
+/static/css
+/static/curriculum-data
# Generated config
i18n/locales/**/trending.json
diff --git a/client/src/templates/Challenges/classic/xterm.tsx b/client/src/templates/Challenges/classic/xterm.tsx
index 47f758534df..8bb04f543aa 100644
--- a/client/src/templates/Challenges/classic/xterm.tsx
+++ b/client/src/templates/Challenges/classic/xterm.tsx
@@ -139,7 +139,7 @@ export const XtermTerminal = ({
return (
-
+
);
};
diff --git a/client/tools/copy-browser-scripts.ts b/client/tools/copy-browser-scripts.ts
index aadf261a712..26673bdd0a7 100644
--- a/client/tools/copy-browser-scripts.ts
+++ b/client/tools/copy-browser-scripts.ts
@@ -8,12 +8,20 @@ const browserScriptDist = resolve(
);
const destJsDir = resolve(__dirname, '../static/js');
+const srcJsDir = resolve(browserScriptDist, './js');
+const destCssDir = resolve(__dirname, '../static/css');
+const srcCssDir = resolve(browserScriptDist, './css');
// 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 });
+rmSync(destCssDir, { recursive: true, force: true });
mkdirSync(destJsDir, { recursive: true });
+mkdirSync(destCssDir, { recursive: true });
-cpSync(resolve(browserScriptDist, 'artifacts'), destJsDir, {
+cpSync(srcJsDir, destJsDir, {
+ recursive: true
+});
+cpSync(srcCssDir, destCssDir, {
recursive: true
});
diff --git a/curriculum/src/test/vitest-global-setup.mjs b/curriculum/src/test/vitest-global-setup.mjs
index 16a68bbb27e..4a9f512ab62 100644
--- a/curriculum/src/test/vitest-global-setup.mjs
+++ b/curriculum/src/test/vitest-global-setup.mjs
@@ -28,7 +28,7 @@ function setupStubs() {
rmSync(destArtifactsDir, { recursive: true, force: true });
mkdirSync(destArtifactsDir, { recursive: true });
- cpSync(path.resolve(browserScriptDist, 'artifacts'), destArtifactsDir, {
+ cpSync(path.resolve(browserScriptDist, 'js'), destArtifactsDir, {
recursive: true
});
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e9cf122fcca..6f3f595bad5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1090,6 +1090,12 @@ importers:
'@freecodecamp/curriculum-helpers':
specifier: ^7.2.0
version: 7.2.0(debug@4.3.4)(typescript@5.9.3)
+ pyodide:
+ specifier: ^0.23.3
+ version: 0.23.4
+ sass.js:
+ specifier: 0.11.1
+ version: 0.11.1
xterm:
specifier: ^5.2.1
version: 5.3.0
@@ -1109,30 +1115,21 @@ importers:
'@freecodecamp/eslint-config':
specifier: workspace:*
version: link:../../../packages/eslint-config
- '@types/copy-webpack-plugin':
- specifier: ^8.0.1
- version: 8.0.1(webpack-cli@4.10.0)
'@typescript/vfs':
specifier: 1.6.1
version: 1.6.1(typescript@5.9.3)
babel-loader:
specifier: 8.3.0
version: 8.3.0(@babel/core@7.28.5)(webpack@5.90.3)
- copy-webpack-plugin:
- specifier: 9.1.0
- version: 9.1.0(webpack@5.90.3)
eslint:
specifier: ^9.39.1
version: 9.39.2(jiti@2.6.1)
process:
specifier: 0.11.10
version: 0.11.10
- pyodide:
- specifier: ^0.23.3
- version: 0.23.4
- sass.js:
- specifier: 0.11.1
- version: 0.11.1
+ tsx:
+ specifier: ^4.21.0
+ version: 4.21.0
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -5123,9 +5120,6 @@ packages:
'@types/cookiejar@2.1.2':
resolution: {integrity: sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==}
- '@types/copy-webpack-plugin@8.0.1':
- resolution: {integrity: sha512-TwEeGse0/wq+t3SFW0DEwroMS/cDkwVZT+vj7tMAYTp7llt/yz6NuW2n04X2M5P/kSfBQOORhrHAN2mqZdmybg==}
-
'@types/cors@2.8.18':
resolution: {integrity: sha512-nX3d0sxJW41CqQvfOzVG1NCTXfFDrDWIghCZncpHeWlVFd81zxB/DLhg7avFg6eHLCRX7ckBmoIIcqa++upvJA==}
@@ -7016,12 +7010,6 @@ packages:
resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==}
engines: {node: '>=0.10.0'}
- copy-webpack-plugin@9.1.0:
- resolution: {integrity: sha512-rxnR7PaGigJzhqETHGmAcxKnLZSR5u1Y3/bcIv/1FnqXedcL/E2ewK7ZCNrArJKCiSv8yVXhTqetJh8inDvfsA==}
- engines: {node: '>= 12.13.0'}
- peerDependencies:
- webpack: ^5.1.0
-
core-js-compat@3.36.0:
resolution: {integrity: sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==}
@@ -8189,10 +8177,6 @@ packages:
fast-fifo@1.3.2:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
- fast-glob@3.3.1:
- resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==}
- engines: {node: '>=8.6.0'}
-
fast-glob@3.3.2:
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
engines: {node: '>=8.6.0'}
@@ -20383,17 +20367,6 @@ snapshots:
'@types/cookiejar@2.1.2': {}
- '@types/copy-webpack-plugin@8.0.1(webpack-cli@4.10.0)':
- dependencies:
- '@types/node': 24.10.9
- tapable: 2.2.1
- webpack: 5.90.3(webpack-cli@4.10.0)
- transitivePeerDependencies:
- - '@swc/core'
- - esbuild
- - uglify-js
- - webpack-cli
-
'@types/cors@2.8.18':
dependencies:
'@types/node': 24.10.9
@@ -21133,7 +21106,7 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
- vitest: 4.0.15(@opentelemetry/api@1.9.0)(@types/node@24.10.9)(@vitest/ui@4.0.15)(jiti@2.6.1)(jsdom@26.1.0)(msw@2.12.7(@types/node@24.10.9)(typescript@5.9.3))(terser@5.28.1)(tsx@4.21.0)(yaml@2.8.1)
+ vitest: 4.0.15(@opentelemetry/api@1.9.0)(@types/node@24.10.9)(@vitest/ui@4.0.15)(jiti@2.6.1)(jsdom@26.1.0)(msw@2.12.7(@types/node@24.10.9)(typescript@5.9.3))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.1)
'@vitest/utils@3.2.4':
dependencies:
@@ -22687,16 +22660,6 @@ snapshots:
copy-descriptor@0.1.1: {}
- copy-webpack-plugin@9.1.0(webpack@5.90.3):
- dependencies:
- fast-glob: 3.3.1
- glob-parent: 6.0.2
- globby: 11.1.0
- normalize-path: 3.0.0
- schema-utils: 3.3.0
- serialize-javascript: 6.0.1
- webpack: 5.90.3(webpack-cli@4.10.0)
-
core-js-compat@3.36.0:
dependencies:
browserslist: 4.28.1
@@ -23735,7 +23698,7 @@ snapshots:
confusing-browser-globals: 1.0.11
eslint: 7.32.0
eslint-plugin-flowtype: 5.10.0(eslint@7.32.0)
- eslint-plugin-import: 2.31.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@4.33.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react: 7.37.4(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react-hooks: 4.6.0(eslint@9.39.2(jiti@2.6.1))
@@ -23765,7 +23728,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)):
+ eslint-module-utils@2.12.0(@typescript-eslint/parser@4.33.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
@@ -23819,7 +23782,7 @@ snapshots:
- typescript
- utf-8-validate
- eslint-plugin-import@2.31.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)):
+ eslint-plugin-import@2.31.0(@typescript-eslint/parser@4.33.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.8
@@ -23830,7 +23793,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@4.33.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -24322,14 +24285,6 @@ snapshots:
fast-fifo@1.3.2: {}
- fast-glob@3.3.1:
- dependencies:
- '@nodelib/fs.stat': 2.0.5
- '@nodelib/fs.walk': 1.2.8
- glob-parent: 5.1.2
- merge2: 1.4.1
- micromatch: 4.0.8
-
fast-glob@3.3.2:
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -25068,7 +25023,7 @@ snapshots:
eslint-config-react-app: 6.0.0(@typescript-eslint/eslint-plugin@4.33.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint@7.32.0)(typescript@5.9.3))(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(babel-eslint@10.1.0(eslint@9.39.2(jiti@2.6.1)))(eslint-plugin-flowtype@5.10.0(eslint@7.32.0))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint@7.32.0))(eslint-plugin-jsx-a11y@6.10.2(eslint@7.32.0))(eslint-plugin-react-hooks@4.6.0(eslint@7.32.0))(eslint-plugin-react@7.37.4(eslint@7.32.0))(eslint@7.32.0)(typescript@5.9.3)
eslint-plugin-flowtype: 5.10.0(eslint@7.32.0)
eslint-plugin-graphql: 4.0.0(@types/node@24.10.9)(graphql@15.8.0)(typescript@5.9.3)
- eslint-plugin-import: 2.31.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@4.33.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react: 7.37.4(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react-hooks: 4.6.0(eslint@9.39.2(jiti@2.6.1))
diff --git a/tools/client-plugins/browser-scripts/copy-scripts.ts b/tools/client-plugins/browser-scripts/copy-scripts.ts
new file mode 100644
index 00000000000..1e58e827394
--- /dev/null
+++ b/tools/client-plugins/browser-scripts/copy-scripts.ts
@@ -0,0 +1,33 @@
+import { cpSync, mkdirSync, rmSync } from 'node:fs';
+import { resolve } from 'node:path';
+
+import { version as workerVersion } from '@freecodecamp/browser-scripts/package.json';
+import { version as helperVersion } from '@freecodecamp/curriculum-helpers/package.json';
+
+const __dirname = import.meta.dirname;
+
+const distDir = resolve(__dirname, 'dist');
+
+const destJsDir = resolve(distDir, './js');
+const destCssDir = resolve(distDir, './css');
+
+rmSync(distDir, { recursive: true, force: true });
+mkdirSync(destJsDir, { recursive: true });
+mkdirSync(destCssDir, { recursive: true });
+
+cpSync(
+ resolve(__dirname, './node_modules/sass.js/dist/sass.sync.js'),
+ resolve(destJsDir, 'workers', workerVersion, 'sass.sync.js')
+);
+cpSync(
+ resolve(__dirname, './node_modules/xterm/css/xterm.css'),
+ resolve(destCssDir, 'xterm.css')
+);
+cpSync(
+ resolve(
+ __dirname,
+ './node_modules/@freecodecamp/curriculum-helpers/dist/test-runner'
+ ),
+ resolve(destJsDir, `test-runner/${helperVersion}/`),
+ { recursive: true }
+);
diff --git a/tools/client-plugins/browser-scripts/package.json b/tools/client-plugins/browser-scripts/package.json
index 6a679e0faca..3a2b1c7023e 100644
--- a/tools/client-plugins/browser-scripts/package.json
+++ b/tools/client-plugins/browser-scripts/package.json
@@ -29,7 +29,9 @@
"main": "index.js",
"scripts": {
"lint": "eslint --max-warnings 0",
- "build": "NODE_OPTIONS=\"--max-old-space-size=7168\" webpack -c webpack.config.cjs --env production"
+ "build": "pnpm copy-scripts && pnpm bundle",
+ "bundle": "NODE_OPTIONS=\"--max-old-space-size=7168\" webpack -c webpack.config.cjs --env production",
+ "copy-scripts": "tsx ./copy-scripts.ts"
},
"type": "module",
"keywords": [],
@@ -39,14 +41,11 @@
"@babel/preset-env": "7.23.7",
"@babel/preset-typescript": "7.23.3",
"@freecodecamp/eslint-config": "workspace:*",
- "@types/copy-webpack-plugin": "^8.0.1",
"@typescript/vfs": "1.6.1",
"babel-loader": "8.3.0",
- "copy-webpack-plugin": "9.1.0",
"eslint": "^9.39.1",
"process": "0.11.10",
- "pyodide": "^0.23.3",
- "sass.js": "0.11.1",
+ "tsx": "^4.21.0",
"typescript": "5.9.3",
"util": "0.12.5",
"webpack": "5.90.3",
@@ -54,6 +53,8 @@
},
"dependencies": {
"@freecodecamp/curriculum-helpers": "^7.2.0",
+ "pyodide": "^0.23.3",
+ "sass.js": "0.11.1",
"xterm": "^5.2.1"
}
}
diff --git a/tools/client-plugins/browser-scripts/webpack.config.cjs b/tools/client-plugins/browser-scripts/webpack.config.cjs
index 09217df9606..efe47304e38 100644
--- a/tools/client-plugins/browser-scripts/webpack.config.cjs
+++ b/tools/client-plugins/browser-scripts/webpack.config.cjs
@@ -1,9 +1,6 @@
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 = {}) => {
@@ -20,8 +17,8 @@ module.exports = (env = {}) => {
devtool: __DEV__ ? 'inline-source-map' : 'source-map',
output: {
chunkFilename: '[name]-[contenthash].js',
- path: path.resolve(__dirname, `dist/artifacts/workers/${version}`),
- clean: true
+ path: path.resolve(__dirname, `dist/js/workers/${version}`),
+ clean: false // We handle cleaning in copy-scripts.ts
},
stats: {
// Display bailout reasons
@@ -53,17 +50,6 @@ module.exports = (env = {}) => {
]
},
plugins: [
- new CopyWebpackPlugin({
- patterns: [
- './node_modules/sass.js/dist/sass.sync.js',
- // TODO: copy this into the css folder, not the js folder
- './node_modules/xterm/css/xterm.css',
- {
- from: './node_modules/@freecodecamp/curriculum-helpers/dist/test-runner',
- to: `../../test-runner/${helperVersion}/`
- }
- ]
- }),
new webpack.ProvidePlugin({
process: 'process/browser'
}),