Skip to content

Commit

Permalink
Merge pull request #431 from openedx/bw/alpha_to_master
Browse files Browse the repository at this point in the history
Bw/alpha to master
  • Loading branch information
muselesscreator authored Aug 7, 2023
2 parents 8e64e9e + 6245a84 commit 536bff7
Show file tree
Hide file tree
Showing 14 changed files with 3,326 additions and 4,773 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/sync-master-alpha.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ jobs:
name: Syncing branches
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: 18
- name: Create Pull Request
Expand All @@ -29,7 +29,7 @@ jobs:
pull-request-number: ${{ steps.cpr.outputs.PULL_REQUEST_NUMBER }}
github-token: ${{ secrets.requirements_bot_github_token }}
- name: Enable Pull Request Automerge
uses: peter-evans/enable-pull-request-automerge@v2
uses: peter-evans/enable-pull-request-automerge@v3
with:
pull-request-number: ${{ steps.cpr.outputs.PULL_REQUEST_NUMBER }}
token: ${{ secrets.requirements_bot_github_token }}
35 changes: 33 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Usage
-----

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

{
Expand All @@ -31,7 +31,8 @@ documentation to learn what options are available. Example package.json::
"precommit": "npm run lint",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"test": "fedx-scripts jest --coverage --passWithNoTests"
"test": "fedx-scripts jest --coverage --passWithNoTests",
"serve": "fedx-scripts serve"
},
"dependencies": {
...
Expand Down Expand Up @@ -177,6 +178,36 @@ Local module configuration for TypeScript
}
```

Serving a production Webpack build locally
------------------------------------------

In some scenarios, you may want to run a production Webpack build locally. To serve a production build locally:

#. Create an ``env.config.js`` file containing the configuration for local development, with the exception of ``NODE_ENV='production'``.
#. Run ``npm run build`` to build the production assets. The output assets will rely on the local development configuration specified in the prior step.
#. Add an NPM script ``serve`` to your application's ``package.json`` (i.e., ``"serve": "fedx-scripts serve"``).
#. 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.

Local module configuration for TypeScript
-----------------------------------------

#. Create file in repository `tsconfig.json`, with a clause `"extends": "@edx/frontend-build"`
#. Set "rootDir" to the root of the source code folders
#. Set "include" to wildcard patterns specifying the subdirectories/files under rootDir where source code can be found
#. Include any wildcards under rootDir that should be excluded using "exclude"

```Sample json
{
"extends": "@edx/frontend-build",
"compilerOptions": {
"rootDir": ".",
"outDir": "dist"
},
"include": ["src/**/*"],
"exclude": ["dist", "node_modules"]
}
```

Development
-----------

Expand Down
8 changes: 7 additions & 1 deletion bin/fedx-scripts.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#!/usr/bin/env node

const chalk = require('chalk');

const presets = require('../lib/presets');

/**
Expand Down Expand Up @@ -65,6 +68,9 @@ switch (commandName) {
ensureConfigOption(presets.webpackDevServer);
require('webpack-dev-server/bin/webpack-dev-server');
break;
case 'serve':
require('../lib/scripts/serve');
break;
default:
console.warn(`fedx-scripts: The command ${commandName} is unsupported`);
console.log(chalk.red(`[ERROR] fedx-scripts: The command ${chalk.bold.red(commandName)} is unsupported.`));
}
18 changes: 18 additions & 0 deletions config/webpack.common.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,22 @@ module.exports = {
},
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
ignoreWarnings: [
// Ignore warnings raised by source-map-loader.
// some third party packages may ship miss-configured sourcemaps, that interrupts the build
// See: https://github.com/facebook/create-react-app/discussions/11278#discussioncomment-1780169
/**
*
* @param {import('webpack').WebpackError} warning
* @returns {boolean}
*/
function ignoreSourcemapsloaderWarnings(warning) {
return (
warning.module
&& warning.module.resource.includes('node_modules')
&& warning.details
&& warning.details.includes('source-map-loader')
);
},
],
};
3 changes: 3 additions & 0 deletions config/webpack.dev-stage.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ module.exports = merge(commonConfig, {
path.join(process.cwd(), 'node_modules'),
path.join(process.cwd(), 'src'),
],
// silences compiler warnings regarding deprecation warnings
quietDeps: true,
},
},
},
Expand Down Expand Up @@ -173,6 +175,7 @@ module.exports = merge(commonConfig, {
https: true,
historyApiFallback: {
index: path.join(PUBLIC_PATH, 'index.html'),
disableDotRule: true,
},
// Enable hot reloading server. It will provide WDS_SOCKET_PATH endpoint
// for the WebpackDevServer client so it can learn when the files were
Expand Down
3 changes: 3 additions & 0 deletions config/webpack.dev.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ module.exports = merge(commonConfig, {
path.join(process.cwd(), 'node_modules'),
path.join(process.cwd(), 'src'),
],
// silences compiler warnings regarding deprecation warnings
quietDeps: true,
},
},
},
Expand Down Expand Up @@ -175,6 +177,7 @@ module.exports = merge(commonConfig, {
port: process.env.PORT || 8080,
historyApiFallback: {
index: path.join(PUBLIC_PATH, 'index.html'),
disableDotRule: true,
},
// Enable hot reloading server. It will provide WDS_SOCKET_PATH endpoint
// for the WebpackDevServer client so it can learn when the files were
Expand Down
3 changes: 2 additions & 1 deletion config/webpack.prod.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// optimized bundles at the expense of a longer build time.

const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const { merge } = require('webpack-merge');
Expand Down Expand Up @@ -132,6 +131,8 @@ module.exports = merge(commonConfig, {
path.join(process.cwd(), 'node_modules'),
path.join(process.cwd(), 'src'),
],
// silences compiler warnings regarding deprecation warnings
quietDeps: true,
},
},
},
Expand Down
37 changes: 37 additions & 0 deletions docs/0003-fedx-scripts-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Serving production Webpack builds locally with `fedx-scripts`

## Summary

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).

## Context

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`.

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.

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.).

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`.

# Decision

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`.

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.

# Implementation

The new `serve` command will live as under a new `scripts` directory under `lib`.

Once in place, a MFE application may add a `serve` script to its NPM scripts in the `package.json` file:

```json
{
"scripts": {
"serve": "fedx-scripts serve"
}
}
```

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).
Empty file added example/.env
Empty file.
1 change: 1 addition & 0 deletions example/.env.development
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
PORT=3000
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
TEST_VARIABLE='foo'
3 changes: 2 additions & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"build": "../bin/fedx-scripts.js webpack",
"lint": "../bin/fedx-scripts.js eslint . --ext .jsx,.js",
"babel": "../bin/fedx-scripts.js babel src --out-dir dist/babel --source-maps --ignore **/*.test.jsx,**/*.test.js --copy-files",
"start": "../bin/fedx-scripts.js webpack-dev-server"
"start": "../bin/fedx-scripts.js webpack-dev-server",
"serve": "../bin/fedx-scripts.js serve"
},
"keywords": [],
"author": "",
Expand Down
78 changes: 78 additions & 0 deletions lib/scripts/serve.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const express = require('express');
const path = require('path');
const fs = require('fs');
const chalk = require('chalk');
const dotenv = require('dotenv');

const resolvePrivateEnvConfig = require('../resolvePrivateEnvConfig');

// Add process env vars. Currently used only for setting the
// server port and the publicPath
dotenv.config({
path: path.resolve(process.cwd(), '.env.development'),
});

// Allow private/local overrides of env vars from .env.development for config settings
// that you'd like to persist locally during development, without the risk of checking
// in temporary modifications to .env.development.
resolvePrivateEnvConfig('.env.private');

function isDirectoryEmpty(directoryPath) {
try {
const files = fs.readdirSync(directoryPath);
return files.length === 0;
} catch (error) {
if (error.code === 'ENOENT') {
// Directory does not exist, so treat it as empty.
return true;
}
throw error; // Throw the error for other cases
}
}

const buildPath = path.join(process.cwd(), 'dist');
const buildPathIndex = path.join(buildPath, 'index.html');

const fallbackPort = 8080;

if (isDirectoryEmpty(buildPath)) {
const formattedBuildCmd = chalk.bold.redBright('``npm run build``');
console.log(chalk.bold.red(`ERROR: No build found. Please run ${formattedBuildCmd} first.`));
} else {
let configuredPort;

try {
configuredPort = require(path.join(process.cwd(), 'env.config.js'))?.PORT;
} catch (error) {
// Pass. Consuming applications may not have an `env.config.js` file. This is OK.
}

if (!configuredPort) {
configuredPort = process.env.PORT;
}

// No `PORT` found in `env.config.js` and/or `.env.development|private`, so output a warning.
if (!configuredPort) {
const formattedEnvDev = chalk.bold.yellowBright('.env.development');
const formattedEnvConfig = chalk.bold.yellowBright('env.config.js');
const formattedPort = chalk.bold.yellowBright(fallbackPort);
console.log(chalk.yellow(`No port found in ${formattedEnvDev} and/or ${formattedEnvConfig} file(s). Falling back to port ${formattedPort}.\n`));
}

const app = express();

// Fallback to standard example port if no PORT config is set.
const PORT = configuredPort || fallbackPort;

app.use(express.static(buildPath));

app.use('*', (req, res) => {
res.sendFile(buildPathIndex);
});

app.listen(PORT, () => {
const formattedServedFile = chalk.bold.cyanBright(buildPathIndex);
const formattedPort = chalk.bold.cyanBright(PORT);
console.log(chalk.greenBright(`Serving ${formattedServedFile} on port ${formattedPort}...`));
});
}
Loading

0 comments on commit 536bff7

Please sign in to comment.