Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds a rudimentary shell that works for dev environments #22

Merged
merged 13 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ cat_docs_command = cat ./docs/_API-header.md ./docs/_API-body.md > ./docs/API.md

build:
rm -rf ./dist
./node_modules/.bin/fedx-scripts babel src --out-dir dist --source-maps --ignore **/*.test.jsx,**/*.test.js,**/setupTest.js --copy-files
@# --copy-files will bring in everything else that wasn't processed by babel. Remove what we don't want.
@find dist -name '*.test.js*' -delete
rm ./dist/setupTest.js
cp ./package.json ./dist/package.json
cp ./LICENSE ./dist/LICENSE
cp ./README.md ./dist/README.md
tsc
cp index.d.ts dist/index.d.ts
mkdir -p ./scss/header/studio-header

cp shell/index.scss dist/shell/index.scss
cp shell/header/index.scss dist/shell/header/index.scss
cp shell/header/Menu/menu.scss dist/shell/header/Menu/menu.scss
cp shell/header/studio-header/StudioHeader.scss dist/shell/header/studio-header/StudioHeader.scss

docs-build:
${doc_command}
Expand Down
51 changes: 50 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ module.exports = {

Follow the steps below to migrate an MFE to use frontend-base.

## Migrating to frontend-base
## Migrating to frontend-base (no shell)

### 1. Edit package.json `scripts`

Expand Down Expand Up @@ -256,6 +256,55 @@ Remember to make the following substitution for these functions:
+ import { configureLogging } from '@openedx/frontend-base';
```

## Migrating to the frontend-base shell (:rotating_light: Work In Progress)

This is an interim migration that helps prepare an MFE to be released as a set of modules suitable for module federation or being included as a plugin into the shell application.

This migration may require some refactoring of the top-level files of the application; it is less straight-forward than simply replacing frontend-build and frontend-platform with frontend-base.

Prior to attempting this, you _must_ complete the steps above to migrate your MFE to use frontend-base instead of frontend-build and frontend-platform.

In spirit, in this migration we remove the initialization and header/footer code from the MFE and instead rely on the shell to manage our application. We turn the MFE into a set of exported modules, and to maintain backwards compatibility, we create a small 'project' in the repository that helps us build the MFE as an independent application.

### 1. Remove initialization

In your index.(jsx|tsx) file, you need to remove the subscribe and initialization code. If you have customizations here, they will need to migrate to your env.config.tsx file instead and take advantage of the shell's provided customization mechanisms.

### 2. Migrate header/footer dependencies

If your application uses a custom header or footer, you can use the shell's header and footer plugin slots to provide your custom header/footer components. This is done through the env.config.tsx file.

### 3. Export the modules of your app as a component.

This may require a little interpretation. In spirit, the modules of your app are the 'pages' of an Open edX Frontend site that it provides. This likely corresponds to the top-level react-router routes in your app. At the time of this writing, we don't have module federation yet, so to use the shell, you export all of your application code in a single component. In frontend-app-profile, for instance, this is the ProfilePage component. Some MFEs have put their router and pages directly into the index.jsx file inside the initialization callback - this code will need to be moved to a single component that can be exported.

### 4. Create a project.scss file

Create a new project.scss file at the top of your application. It's responsible for:

1. Importing the shell's stylesheet, which includes Paragon's core stylesheet.
2. Importing your brand stylesheet.
3. Importing the stylesheets from your application.

You must then import this new stylesheet into your env.config.tsx file:

```diff
+ import './project.scss';

const config = {
// config document
}

export default config;
```

### 5. Add new build scripts to package.json

After the previous steps, the legacy `build` and `start` scripts in package.json will no longer work properly. They need to be replaced with versions from the `openedx` CLI that:

- Build the MFE for production using the shell application.
- Build the MFE for dev using the shell application.
- Build the MFE for release as a library.

## Merging repositories

Expand Down
4 changes: 4 additions & 0 deletions bin/openedx.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ switch (commandName) {
ensureConfigOption(presets.webpackDevServer);
require('webpack-dev-server/bin/webpack-dev-server');
break;
case 'shell-dev-server':
ensureConfigOption(presets.shellDevServer);
require('webpack-dev-server/bin/webpack-dev-server');
break;
case 'formatjs': {
const commonArgs = [
'--format', 'node_modules/@openedx/frontend-base/cli/formatter.js',
Expand Down
7 changes: 7 additions & 0 deletions cli/presets.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ const webpackDevServer = new ConfigPreset({
searchFilepaths,
});

const shellDevServer = new ConfigPreset({
defaultFilename: 'webpack.shell.dev.config.js',
searchFilenames: ['webpack.shell.dev.config.js'],
searchFilepaths,
});

const webpackDevServerStage = new ConfigPreset({
defaultFilename: 'webpack.dev-stage.config.js',
searchFilenames: ['webpack.dev-stage.config.js'],
Expand All @@ -46,6 +52,7 @@ module.exports = {
webpackDevServer,
'webpack-dev': webpackDevServer,
'webpack-dev-server': webpackDevServer,
shellDevServer,
webpackDevServerStage,
'webpack-dev-server-stage': webpackDevServerStage,
'webpack-dev-stage': webpackDevServerStage,
Expand Down
2 changes: 1 addition & 1 deletion config/getLocalAliases.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Some working examples, as of the time of this writing:

{ moduleName: '@openedx/paragon/scss', dir: '../paragon', dist: 'scss' }
{ moduleName: '@openedx/paragon', dir: '../paragon', dist: 'dist' }
{ moduleName: '@edx/frontend-platform', dir: '../frontend-platform', dist: 'dist' }
{ moduleName: '@openedx/frontend-base', dir: '../frontend-base', dist: 'dist' }

*/
function getLocalAliases() {
Expand Down
225 changes: 225 additions & 0 deletions config/webpack.shell.dev.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
// This is the dev Webpack config. All settings here should prefer a fast build
// time at the expense of creating larger, unoptimized bundles.
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');

const Dotenv = require('dotenv-webpack');
const dotenv = require('dotenv');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
const PostCssAutoprefixerPlugin = require('autoprefixer');
const PostCssRTLCSS = require('postcss-rtlcss');
const PostCssCustomMediaCSS = require('postcss-custom-media');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const { transform } = require('@formatjs/ts-transformer');

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

// 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');

const aliases = getLocalAliases();
const PUBLIC_PATH = process.env.PUBLIC_PATH || '/';

module.exports = {
entry: {
app: path.resolve(__dirname, '../shell/index'),
},
output: {
path: path.resolve(process.cwd(), './dist'),
publicPath: PUBLIC_PATH,
},
resolve: {
alias: {
...aliases,
'env.config': path.resolve(process.cwd(), './env.config'),
},
fallback: {
// This causes the system to return an empty object if it can't find an env.config.js file in
// the application being built.
'env.config': false,
},
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')
);
},
],
mode: 'development',
devtool: 'eval-source-map',
module: {
// Specify file-by-file rules to Webpack. Some file-types need a particular kind of loader.
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
include: [
[
/src/,
path.resolve(process.cwd(), './env.config.tsx'),
// /env.config/ // \.(js|jsx|ts|tsx)/,
]
],
use: {
loader: require.resolve('ts-loader'),
options: {
transpileOnly: true,
compilerOptions: {
noEmit: false,
},
getCustomTransformers() {
return {
before: [
transform({
overrideIdFn: '[sha512:contenthash:base64:6]',
}),
],
};
},
},
},
},
// We are not extracting CSS from the javascript bundles in development because extracting
// prevents hot-reloading from working, it increases build time, and we don't care about
// flash-of-unstyled-content issues in development.
{
test: /(.scss|.css)$/,
use: [
require.resolve('style-loader'), // creates style nodes from JS strings
{
loader: require.resolve('css-loader'), // translates CSS into CommonJS
options: {
sourceMap: true,
modules: {
compileType: 'icss',
},
},
},
{
loader: require.resolve('postcss-loader'),
options: {
postcssOptions: {
plugins: [
PostCssAutoprefixerPlugin(),
PostCssRTLCSS(),
PostCssCustomMediaCSS(),
],
},
},
},
require.resolve('resolve-url-loader'),
{
loader: require.resolve('sass-loader'), // compiles Sass to CSS
options: {
sourceMap: true,
sassOptions: {
includePaths: [
path.join(process.cwd(), 'node_modules'),
path.join(process.cwd(), 'src'),
],
// silences compiler warnings regarding deprecation warnings
quietDeps: true,
},
},
},
],
},
// Webpack, by default, uses the url-loader for images and fonts that are required/included by
// files it processes, which just base64 encodes them and inlines them in the javascript
// bundles. This makes the javascript bundles ginormous and defeats caching so we will use the
// file-loader instead to copy the files directly to the output directory.
{
test: /\.(woff2?|ttf|svg|eot)(\?v=\d+\.\d+\.\d+)?$/,
loader: require.resolve('file-loader'),
},
{
test: /favicon.ico$/,
loader: require.resolve('file-loader'),
options: {
name: '[name].[ext]', // <-- retain original file name
},
},
{
test: /\.(jpe?g|png|gif)(\?v=\d+\.\d+\.\d+)?$/,
loader: require.resolve('file-loader'),
},
],
},
optimization: {
minimizer: [
'...',
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.sharpMinify,
options: {
encodeOptions: {
...['png', 'jpeg', 'jpg'].reduce((accumulator, value) => (
{ ...accumulator, [value]: { progressive: true, quality: 65 } }
), {}),
gif: {
effort: 5,
},
},
},
},
}),
],
},
// Specify additional processing or side-effects done on the Webpack output bundles as a whole.
plugins: [
// Generates an HTML file in the output directory.
new HtmlWebpackPlugin({
inject: true, // Appends script tags linking to the webpack bundles at the end of the body
template: path.resolve(process.cwd(), 'public/index.html'),
FAVICON_URL: process.env.FAVICON_URL || null,
OPTIMIZELY_PROJECT_ID: process.env.OPTIMIZELY_PROJECT_ID || null,
NODE_ENV: process.env.NODE_ENV || null,
}),
new Dotenv({
path: path.resolve(process.cwd(), '.env.development'),
systemvars: true,
}),
new ReactRefreshWebpackPlugin(),
],
// This configures webpack-dev-server which serves bundles from memory and provides live
// reloading.
devServer: {
host: '0.0.0.0',
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
// updated. The WebpackDevServer client is included as an entry point
// in the webpack development configuration. Note that only changes
// to CSS are currently hot reloaded. JS changes will refresh the browser.
hot: true,
webSocketServer: 'ws',
devMiddleware: {
publicPath: PUBLIC_PATH,
},
},
};
39 changes: 39 additions & 0 deletions docs/decisions/0008-stylesheet-import-in-env-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Shell stylesheet must be imported in env.config file.

## Summary

A project must import the stylesheet for the application shell in its env.config file.

## Context

There is a particular quirk of the stylesheet loaders for webpack (style-loader and/or css-loader) where the import of stylesheets into JavaScript files must take place in a JS file in the project, not in library dependency like frontend-base. Further, the stylesheet imported into JS must _itself_ be a part of the project.

If, for instance, we try to import a stylesheet from frontend-base (shell, header, footer, etc.) inside a React component inside the shell, webpack silently ignores the import and refuses to load the stylesheet. If we try to import a stylesheet from frontend-base directly into the env.config file in the project, that will also fail with webpack silently ignoring the stylesheet. If, however, frontend-base exports the stylesheet and it's loaded into a SCSS file in the project and _that_ is imported into env.config, everything works correctly.

This slight indirection through a SCSS file in the project is necessary, and arguably desirable. It ensure as common, unified entry point for SCSS from dependencies of the project. SCSS from the project or micro-frontend itself can be imported into its own components, or can be imported into this top-level SCSS file as desired. Further, this ensures that every aspect of the style of a project or MFE can easily be customized since the stylesheet is supplied through the env.config file.

## Decision

As a best practice, a project should have a top-level SCSS file as a peer to the env.config file. This SCSS file should import the stylesheet from the frontend-base shell application. It should, in turn, be imported into the env.config file.

## Implementation

The `project.scss` file should import the stylesheet from the shell:

```diff
+ @import '@openedx/frontend-base/shell/index.scss';

// other styles
```

The env.config file should then import the top-level SCSS file:

```diff
+ import './project.scss';

const config = {
// config document
}

export default config;
```
Loading
Loading