Skip to content

⚑ Single-spa application example which imports registered applications from NPM packages πŸ“¦ and manages authentication features as login πŸ”

License

Notifications You must be signed in to change notification settings

jualoppaz/single-spa-login-example-with-npm-packages

Repository files navigation

npm version

single-spa-login-example-with-npm-packages

Single-spa application example which imports registered applications from NPM packages and manages authentication features as login.

✍🏻 Motivation

This application is a little demo of how you can use single-spa splitting code with Option 2: NPM packages

▢️ Demo

You can see a working demo of this application in the next link: https://single-spa-with-npm-packages.herokuapp.com.

The login validation is harcoded in code and the credentials are:

User Password
admin 12345

πŸ’» Run in localhost

If you prefer run this application in localhost you must follow next steps:

npm install
npm run serve

Finally you only have to open http://localhost:8080 in a browser to see the app running

single-spa applications

This application is a root-application that inits a single-spa application that integrates several registered applications:

single-spa-auth-app

This application is displayed by default as there is no logged in user. A login form is printed and the credentials can be set for perform the login and access to the private views.

single-spa-layout-app

This application contains header, navbar and footer sections, navigation between registered applications and /login redirection if logout link is clicked or token is removed from sessionStorage.

single-spa-home-app

This application is developed with Angular JS and is mounted when home icon in navbar is clicked. In that case the url will be / and all Angular JS routes will be managed by angular-ui-router. By the moment this application has only one default routed view.

single-spa-angular-app

This application is developed with Angular v8 and is mounted when Angular navbar item is clicked. In that case the url will be /angular and all Angular routes will be managed by angular router.

single-spa-vue-app

This application is developed with Vue and is mounted when Vue navbar item is clicked. In that case the url will be /vue and all Vue routes will be managed by vue-router.

single-spa-react-app

This application is developed with React and is mounted when React navbar item is clicked. In that case the url will be /react and all React routes will be managed by react-router-dom.

How it works ❓

There are several files for the right working of this application and they are:

  • package.json
  • root-application.js
  • server.js
  • webpack.config.js

package.json

{
  "name": "single-spa-login-example-with-npm-packages",
  "version": "0.2.5",
  "description": "Single-spa application example which imports registered applications from NPM packages and manages authentication features as login.",
  "main": "root-application.js",
  "scripts": {
    "serve": "webpack-dev-server",
    "start": "node server.js",
    "build": "webpack --config webpack.config.js -p",
    "heroku-postbuild": "npm run build",
    "lint": "eslint . --ext .js --fix",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/jualoppaz/single-spa-login-example-with-npm-packages.git"
  },
  "author": "Juan Manuel LΓ³pez Pazos",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/jualoppaz/single-spa-login-example-with-npm-packages/issues"
  },
  "homepage": "https://github.com/jualoppaz/single-spa-login-example-with-npm-packages#readme",
  "devDependencies": {
    "@babel/core": "7.8.3",
    "babel-eslint": "10.0.3",
    "babel-loader": "8.0.6",
    "clean-webpack-plugin": "3.0.0",
    "copy-webpack-plugin": "5.1.1",
    "css-loader": "3.4.2",
    "eslint": "6.8.0",
    "eslint-config-airbnb-base": "14.0.0",
    "eslint-loader": "3.0.3",
    "eslint-plugin-import": "2.20.1",
    "html-webpack-plugin": "3.2.0",
    "webpack": "4.41.5",
    "webpack-cli": "3.3.10",
    "webpack-dev-server": "3.11.0"
  },
  "dependencies": {
    "@fortawesome/fontawesome-svg-core": "1.2.27",
    "@fortawesome/free-brands-svg-icons": "5.12.1",
    "@fortawesome/free-solid-svg-icons": "5.12.0",
    "@fortawesome/vue-fontawesome": "0.1.9",
    "bootstrap": "4.4.1",
    "bootstrap-vue": "2.2.2",
    "single-spa": "4.4.2",
    "single-spa-angular-app": "0.1.5",
    "single-spa-auth-app": "0.2.0",
    "single-spa-home-app": "0.1.4",
    "single-spa-layout-app": "0.2.5",
    "single-spa-react-app": "0.1.4",
    "single-spa-vue": "1.7.0",
    "single-spa-vue-app": "0.1.9",
    "vue": "2.6.11",
    "vue-router": "3.1.4",
    "vue-toastr": "2.1.2",
    "zone.js": "0.10.2"
  }
}

There are several scripts in this project:

  • serve: for compile and serve the application in a development environment
  • start: for run the compiled dist folder in a production environment, in this case Heroku
  • build: for compile the application and build it as a libray in umd format
  • heroku-postbuild: for compile the application in Heroku inside its custom lifecycle
  • lint: for run eslint in all project

In dependencies config there are several libraries like bootstrap or fontawesome configured as webpack externals in dependencies like single-spa-auth-app or single-spa-vue-app.

root-application.js

/* eslint-disable no-unused-vars */
/* eslint-disable func-names */
import * as singleSpa from 'single-spa';
import 'zone.js';

import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap-vue/dist/bootstrap-vue.css';

function showWhenAnyOf(routes) {
  return function (location) {
    return routes.some((route) => location.pathname === route);
  };
}

function showWhenPrefix(routes) {
  return function (location) {
    return routes.some((route) => location.pathname.startsWith(route));
  };
}

function showExcept(routes) {
  return function (location) {
    return routes.every((route) => location.pathname !== route);
  };
}

singleSpa.registerApplication(
  'login',
  () => import('single-spa-auth-app'),
  showWhenAnyOf(['/login']),
);

singleSpa.registerApplication(
  'layout',
  () => import('single-spa-layout-app'),
  showExcept(['/login']),
);

singleSpa.registerApplication(
  'home',
  () => import('single-spa-home-app'),
  showWhenAnyOf(['/']),
);

singleSpa.registerApplication(
  'angular',
  () => import('single-spa-angular-app'),
  showWhenPrefix(['/angular']),
);

singleSpa.registerApplication(
  'vue',
  () => import('single-spa-vue-app'),
  showWhenPrefix(['/vue']),
);

singleSpa.registerApplication(
  'react',
  () => import('single-spa-react-app'),
  showWhenPrefix(['/react']),
);

singleSpa.start();

The eslint comments are indicated due to webpack external dependencies. Without the eslint comments the build process will fail.
In order to avoid duplicated css imports, bootstrap import is performed in root-application instead of every registered apps. All registered apps are declared here. Login and Home apps must be mounted when url matchs to given path, Layout app always but login path and others must be mounted when url starts with its own prefixes. Finally single-spa is started for run its lifecycle.

server.js

const express = require('express');
const path = require('path');

const port = process.env.PORT || 8080;
const app = express();

// the __dirname is the current directory from where the script is running
app.use(express.static(path.resolve(__dirname, 'dist')));

// send the user to index html page inspite of the url
app.get('*', (req, res) => {
  res.sendFile(path.resolve(__dirname, 'index.html'));
});

app.listen(port, () => {
  // eslint-disable-next-line no-console
  console.log('App listening on port:', port);
});

This express server is designed for start this application in Heroku and serve the generated dist folder and index.html.

webpack.config.js

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    'root-application': 'root-application.js',
  },
  output: {
    publicPath: '/',
    filename: '[name].js',
  },
  module: {
    rules: [
      {
        parser: {
          system: false,
        },
      },
      {
        test: /\.js?$/,
        exclude: [path.resolve(__dirname, 'node_modules')],
        loader: ['babel-loader', 'eslint-loader'],
      },
      {
        test: /\.tsx?$/,
        loader: 'ts-loader',
      },
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  node: {
    fs: 'empty',
  },
  resolve: {
    modules: [__dirname, 'node_modules'],
  },
  plugins: [
    new CleanWebpackPlugin({
      cleanAfterEveryBuildPatterns: ['dist'],
    }),
    new CopyWebpackPlugin([
      {
        from: path.resolve(
          __dirname,
          'node_modules/single-spa-layout-app/dist/img',
        ),
        to: path.resolve(__dirname, 'dist/img'),
      },
    ]),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'index.html'),
      inject: false,
    }),
  ],
  devtool: 'source-map',
  externals: [],
  devServer: {
    historyApiFallback: true,
    writeToDisk: true,
  },
};

It's very important to config root-application.js as entrypoint. Then are defined all rule parsers for compile all file types. And finally there are several plugins for clean dist folder and process index.html with webpack.