Skip to content

Commit 088793c

Browse files
adamstankiewiczmuselesscreator
authored andcommitted
feat: add fedx-scripts serve (#404)
* feat: add serve command to fedx-scripts to run production bundle * docs: add ADR about serve command * feat: support .env.development and .env.private * docs: update ADR to include .env* support * chore: update package-lock.json take 3 * chore: pin express devDep
1 parent cd408cd commit 088793c

File tree

10 files changed

+609
-3265
lines changed

10 files changed

+609
-3265
lines changed

README.rst

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Usage
2020
-----
2121

2222
CLI commands are structured: ``fedx-scripts <targetScript> <options>``. Options
23-
are passed on to the target script, so refer to each target script's cli
23+
are passed on to the target script, so refer to each target script's CLI
2424
documentation to learn what options are available. Example package.json::
2525

2626
{
@@ -31,7 +31,8 @@ documentation to learn what options are available. Example package.json::
3131
"precommit": "npm run lint",
3232
"snapshot": "fedx-scripts jest --updateSnapshot",
3333
"start": "fedx-scripts webpack-dev-server --progress",
34-
"test": "fedx-scripts jest --coverage --passWithNoTests"
34+
"test": "fedx-scripts jest --coverage --passWithNoTests",
35+
"serve": "fedx-scripts serve"
3536
},
3637
"dependencies": {
3738
...
@@ -177,6 +178,16 @@ Local module configuration for TypeScript
177178
}
178179
```
179180

181+
Serving a production Webpack build locally
182+
------------------------------------------
183+
184+
In some scenarios, you may want to run a production Webpack build locally. To serve a production build locally:
185+
186+
#. Create an ``env.config.js`` file containing the configuration for local development, with the exception of ``NODE_ENV='production'``.
187+
#. Run ``npm run build`` to build the production assets. The output assets will rely on the local development configuration specified in the prior step.
188+
#. Add an NPM script ``serve`` to your application's ``package.json`` (i.e., ``"serve": "fedx-scripts serve"``).
189+
#. Run ``npm run serve`` to serve your production build assets. It will attempt to run the build on the same port specified in the ``env.config.js`` file.
190+
180191
Development
181192
-----------
182193

bin/fedx-scripts.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
#!/usr/bin/env node
2+
3+
const chalk = require('chalk');
4+
25
const presets = require('../lib/presets');
36

47
/**
@@ -65,6 +68,9 @@ switch (commandName) {
6568
ensureConfigOption(presets.webpackDevServer);
6669
require('webpack-dev-server/bin/webpack-dev-server');
6770
break;
71+
case 'serve':
72+
require('../lib/scripts/serve');
73+
break;
6874
default:
69-
console.warn(`fedx-scripts: The command ${commandName} is unsupported`);
75+
console.log(chalk.red(`[ERROR] fedx-scripts: The command ${chalk.bold.red(commandName)} is unsupported.`));
7076
}

config/webpack.prod.config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// optimized bundles at the expense of a longer build time.
33

44
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
5-
65
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
76
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
87
const { merge } = require('webpack-merge');

docs/0003-fedx-scripts-serve.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Serving production Webpack builds locally with `fedx-scripts`
2+
3+
## Summary
4+
5+
The build-and-deploy process for micro-frontends (MFEs) throughout Open edX include running the MFE through a production Webpack build process, relying on configuration specified in a `webpack.prod.config.js` file. The resulting file assets are what ultimately get released to production and served to users. However, it is currently non-obvious how to preview the production file assets generated by `npm run build` locally should the need arise (e.g., to have more confidence in the resulting Webpack output and/or behavior before relying on the build-and-deploy process to release to a staging/production environment).
6+
7+
## Context
8+
9+
Most micro-frontends (MFEs) throughout the Open edX platform rely on a `npm run build` script that runs a production Webpack build based on the configuration specified in a `webpack.prod.config.js` file. The `webpack-prod.config.js` may be provided either by consumers in the root of their MFE's repository (i.e., typically using `createConfig` to extend/override parts of the default production Webpack configuration), or simply rely on the default `webpack.prod.config.js` configuration file provided by `@edx/frontend-build`.
10+
11+
The output from `npm run build` is generated in a Git-ignored `dist` directory, and contains the actual files that should be deployed to production.
12+
13+
Included in the `dist` directory's files is the MFE's `index.html` file that needs to be served for all routes the user may try loading. By simply loading `index.html` in the browser, it will inevitably run into some issues (e.g., not supporting React routing, etc.).
14+
15+
To mitigate this, this ADR describes a new mechanism to provide a standard, documented way to serve the generated assets from the production Webpack build when running `npm run build`.
16+
17+
# Decision
18+
19+
We will create a new `serve` command for `fedx-scripts` that creates an Express.js server to run the generated `dist` file assets (e.g., `index.html`) on the `PORT` specified in the MFE's `env.config.js` and/or `.env.development|private` file(s) on `localhost`.
20+
21+
If no `env.config.js` and/or `.env.development|private` file(s) exist and/or no `PORT` setting is specified those files, the `serve` command will fallback to a default port 8080, which is similar to the default ports our typical example MFE applications use.
22+
23+
# Implementation
24+
25+
The new `serve` command will live as under a new `scripts` directory under `lib`.
26+
27+
Once in place, a MFE application may add a `serve` script to its NPM scripts in the `package.json` file:
28+
29+
```json
30+
{
31+
"scripts": {
32+
"serve": "fedx-scripts serve"
33+
}
34+
}
35+
```
36+
37+
Then, running `npm run serve` in the root of that MFE application will run the new `serve` command in `@edx/frontend-build`, serving the assets in the MFE's `dist` directory on the `PORT` specified in the `env.config.js` file or `.env.development|private` file(s).

example/.env

Whitespace-only changes.

example/.env.development

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
PORT=3000
12
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
23
TEST_VARIABLE='foo'

example/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"build": "../bin/fedx-scripts.js webpack",
99
"lint": "../bin/fedx-scripts.js eslint . --ext .jsx,.js",
1010
"babel": "../bin/fedx-scripts.js babel src --out-dir dist/babel --source-maps --ignore **/*.test.jsx,**/*.test.js --copy-files",
11-
"start": "../bin/fedx-scripts.js webpack-dev-server"
11+
"start": "../bin/fedx-scripts.js webpack-dev-server",
12+
"serve": "../bin/fedx-scripts.js serve"
1213
},
1314
"keywords": [],
1415
"author": "",

lib/scripts/serve.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
const express = require('express');
2+
const path = require('path');
3+
const fs = require('fs');
4+
const chalk = require('chalk');
5+
const dotenv = require('dotenv');
6+
7+
const resolvePrivateEnvConfig = require('../resolvePrivateEnvConfig');
8+
9+
// Add process env vars. Currently used only for setting the
10+
// server port and the publicPath
11+
dotenv.config({
12+
path: path.resolve(process.cwd(), '.env.development'),
13+
});
14+
15+
// Allow private/local overrides of env vars from .env.development for config settings
16+
// that you'd like to persist locally during development, without the risk of checking
17+
// in temporary modifications to .env.development.
18+
resolvePrivateEnvConfig('.env.private');
19+
20+
function isDirectoryEmpty(directoryPath) {
21+
try {
22+
const files = fs.readdirSync(directoryPath);
23+
return files.length === 0;
24+
} catch (error) {
25+
if (error.code === 'ENOENT') {
26+
// Directory does not exist, so treat it as empty.
27+
return true;
28+
}
29+
throw error; // Throw the error for other cases
30+
}
31+
}
32+
33+
const buildPath = path.join(process.cwd(), 'dist');
34+
const buildPathIndex = path.join(buildPath, 'index.html');
35+
36+
const fallbackPort = 8080;
37+
38+
if (isDirectoryEmpty(buildPath)) {
39+
const formattedBuildCmd = chalk.bold.redBright('``npm run build``');
40+
console.log(chalk.bold.red(`ERROR: No build found. Please run ${formattedBuildCmd} first.`));
41+
} else {
42+
let configuredPort;
43+
let envConfig;
44+
45+
try {
46+
envConfig = require(path.join(process.cwd(), 'env.config.js'));
47+
configuredPort = envConfig?.PORT || process.env.PORT;
48+
} catch (error) {
49+
// pass, consuming applications may not have an `env.config.js` file. This is OK.
50+
}
51+
52+
// No `PORT` found in `env.config.js` and/or `.env.development|private`, so output a warning.
53+
if (!configuredPort) {
54+
const formattedEnvDev = chalk.bold.yellowBright('.env.development');
55+
const formattedEnvConfig = chalk.bold.yellowBright('env.config.js');
56+
const formattedPort = chalk.bold.yellowBright(fallbackPort);
57+
console.log(chalk.yellow(`No port found in ${formattedEnvDev} and/or ${formattedEnvConfig} file(s). Falling back to port ${formattedPort}.\n`));
58+
}
59+
60+
const app = express();
61+
62+
// Fallback to standard example port if no PORT config is set.
63+
const PORT = configuredPort || fallbackPort;
64+
65+
app.use(express.static(buildPath));
66+
67+
app.use('*', (req, res) => {
68+
res.sendFile(buildPathIndex);
69+
});
70+
71+
app.listen(PORT, () => {
72+
const formattedServedFile = chalk.bold.cyanBright(buildPathIndex);
73+
const formattedPort = chalk.bold.cyanBright(PORT);
74+
console.log(chalk.greenBright(`Serving ${formattedServedFile} on port ${formattedPort}...`));
75+
});
76+
}

0 commit comments

Comments
 (0)