single-spa / standalone-single-spa-webpack-plugin

A webpack plugin for running microfrontends in standalone mode.
MIT License
42 stars 8 forks source link

HtmlWebpackPlugin inject html/variables/etc #22

Open mstruensee opened 2 years ago

mstruensee commented 2 years ago

is there a way currently to inject something like ... html or templateParameters into the HtmlWebpackPlugin ... my use case is ... the root-config is setting API_ENDPOINTS on the window for all other micro services to use, works great, they have access to it and everything ... problem is standalone development ... they can't get the endpoint as its not on the window

or even a feature to set window variables?

https://github.com/single-spa/create-single-spa/blob/main/packages/webpack-config-single-spa/lib/webpack-config-single-spa.js#L126

might have to change the design to pass it as a customProp as that is supported in the standalone-single-spa-webpack-plugin

    customProps: {
        authToken: "sadf7889fds8u70df9s8fsd"
      },

EDIT: attempted this, there is no way for me to send customProps from webpack.config inside the root-config ... can do that from a non root-config, as it uses the standalone-single-spa-webpack-plugin, but in root-config, that is disabled.

https://github.com/single-spa/standalone-single-spa-webpack-plugin#customizing-the-html-file ^^ can i use teplateParamters and put it in the index file and pass to standalone-single-spa-webpack-plugin?

mstruensee commented 2 years ago

so far this is a solution that works ... but hackkkkkky ...

in webpack.config in root-config, set window.endpoints inside html plugin ... index.ejs

  <script> window.endpoints = <%= endpoints %> </script> 

webpack.config.js

new HtmlWebpackPlugin({
          inject: false,
          template: "src/index.ejs",
          templateParameters: {
            endpoints: JSON.stringify(endpoints),
          },
        }),

in webpack.config in app1, pass endpoints as customProps

webpack.config.js

    const defaultConfig = singleSpaDefaults({
      orgName,
      projectName,
      webpackConfigEnv,
      argv,
      standaloneOptions: {
        customProps: {
          endpoints
        }
      }
    });

grab endpoints inside of the mount lifecycle and set window.endpoints (if not set, as all front end services will be running this logic)

org-app1.tsx

export const mount = (props) => {
  if(!window.endpoints) {
    console.log("setting endpoints!", props.endpoints)
    window.endpoints = props.endpoints
  } else {
    console.log("endpoints already set")
  }
  return lifecycles.mount(props);
}

this works for standalone and for deployed to environment ... not ideal but works, as all apps can get host endpoints from window.endpoints

let me know if there is or can or will be an easier way :D

mstruensee commented 2 years ago

update: this has 1 flaw so far, when trying to use redux toolkit query, when trying to setup createApi, fetchBaseQuery seems to be compiled and generated at import time, so grabbing the baseUrl from window.endpoints throws an error as window.endpoints is undefined as it is not set until mount when doing development in standalone mode. I think the best solution would be to have it put on via HtmlWebpackPlugin in the webpack config, just like the root-config, this way it is there during start/load/import.

If HtmlWebpackPlugin options can be passed down just like standaloneOptions then when adding a src/index.ejs, then templateParameters should be able to be referenced and then swapped/replaced prior to standalone app loading. This would be a good approach as it is also how the root config does it.

mstruensee commented 2 years ago

update: modifying webpack-config-single-spa and adding options to the HtmlWebpackPlugin, worked successfully.

const defaultConfig = singleSpaDefaults({
      orgName,
      projectName,
      webpackConfigEnv,
      argv,
      htmlWebpackPluginOptions: {
        templateParameters: {
          endpoints: JSON.stringify(endpoints),
        },
      }
    });

https://github.com/single-spa/create-single-spa/blob/main/packages/webpack-config-single-spa/lib/webpack-config-single-spa.js#L126 !isProduction && !opts.disableHtmlGeneration && new HtmlWebpackPlugin(opts.htmlWebpackPluginOptions? opts.htmlWebpackPluginOptions : null),

can there be a feature request for htmlWebpackPluginOptions?

mstruensee commented 2 years ago

any update on this?

mstruensee commented 2 years ago

bump @filoxo

mstruensee commented 2 years ago

a temporary solution that seems to be working

const {merge} = require("webpack-merge")
const singleSpaDefaults = require("webpack-config-single-spa-react-ts")
const packageJson = require("./package.json")
const {fetchServiceEndpoints} = require("./util")
const HtmlWebpackPlugin = require("html-webpack-plugin")

const { groups: {orgName, projectName} } = /@(?<orgName>.*)\/(?<projectName>.*)/.exec(packageJson.name)

module.exports = fetchServiceEndpoints().then((endpoints) => {
  class ExtendHtmlWebpackPlugin extends HtmlWebpackPlugin {
    constructor() {
      super({
        templateParameters: {
          title: projectName,
          serviceEndpoints: JSON.stringify(endpoints),
        },
      })
    }
  }

  return (webpackConfigEnv, argv) => {
    const defaultConfig = singleSpaDefaults({
      orgName,
      projectName,
      webpackConfigEnv,
      argv,
      HtmlWebpackPlugin: ExtendHtmlWebpackPlugin,
    })

    return merge(defaultConfig, {
      output: {
        clean: true,
      },
      devServer: {
        server: "https",
      },
    })
  }
})

this now unlocks the capability to have a custom html file used for the generation of the standalone component, other stuff can be added, like pattern library classes, themes, etc. Currently all documentation only allows all this stuff to work via root-config, and not in standalone mode for individual components.

filoxo commented 2 years ago

Sorry about the lack of response, I hadn't seen this ping.

I wonder if webpack-merge's mergeWithCustomize can be used to clean this up a little bit:

const { mergeWithCustomize, unique } = require('webpack-merge');
const singleSpaDefaults = require('webpack-config-single-spa');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const merge = mergeWithCustomize({
  customizeArray: unique(
    'plugins',
    ['HtmlWebpackPlugin'],
    (plugin) => plugin.constructor && plugin.constructor.name
  ),
});

which I've used in an example repo that I maintain here: https://github.com/filoxo/single-spa-example-rxjs-shared-state/blob/main/packages/root-config/webpack.config.js#L5-L11 except that that example doesn't make use of standalone mode. Maybe that could work for this case still though?

I think your solution should be taken and made into an org-specific single-spa-webpack-config package to share across your applications. Its basically just taking our config and wrapping with your org's needs/opinions/rules.

mstruensee commented 2 years ago

Thanks for the reply! I will give that approach a try ... Yeah it is hard to be 100% standalone, in my case it is just the theme, so you can do the development without it, it just looks different :D

stephenwil commented 2 years ago

I'm wanting to use standalone mode in a similar fashion, and I can't get it working with either mergeWithCustomize or instantiating an extended HtmlWebpackPlugin class.

I'd vote for going with the options param for HtmlWebpackPlugin, similar to standaloneOptions.

And/or the ability to expand to be not limited to 1 html file that gets generated with the various import-map etc

filoxo commented 2 years ago

I disagree with expanding the configuration API... mostly because where does it end? The base single-spa-webpack-config includes other plugins too so if we allow passing through all options for one plugin, we'd probably have to allow all options for all plugins. This is why I prefer webpack-merge: you can extend the inner parts of the config without it having to create a top-level API for overrides.

Alternatively, using webpack-chain could provides a different way of extending plugin options that you might prefer.