This is something we need to solve before we can go on.
The problems
We have packages (Settings and Frontity packages) defined dynamically, not statically:
-
Dynamically: defined in variables
settingsPackage = "my-pkg"
-
Statically: written in code
require("my-pkg")
For example, the Settings Package is defined in frontity.config.js
like this:
// frontity.config.js
module.exports = {
settingsPackage: '@frontity/file-settings'
}
That means that Frontity has to do something like this to import them:
// sever.js
const config = require("./frontity.config.js")
// ⬇️ this works in Node but not in Webpack
const getSettings = require(config.settingsPackage)
const { settings, packages } = await getSettings()
- Node runs the code and requires on runtime, so that works.
- Webpack analyzes the static code to generate the bundle. When it gets to that dynamic import, it doesn’t know what to import and throws an error.
Webpack has a way to overcome this problem, which is what we used in the past. It accepts a regexp or a string that it later converts to a regexp:
const config = require("./frontity.config.js")
const getSettings = require(`./settings/${config.settingsPackage}/index.js`)
const { settings, packages } = await getSettings()
package.forEach(pkg => {
require(`./packages/${pkg.name}/index.js`)
})
If you do that, Webpack is able to analyze ./settings/${config.settingsPackage}/index.js
and import all the possibilities of that regexp, no matter if they are used or not. For example:
./settings/file-settings/index.js
./settings/wp-org-settings/index.js
There’s a couple of problems with the way we want to do things now:
1. Webpack imports all the packages it finds in the regexp
If users forget to npm uninstall a package they are not using anymore, it will be included anyway. This is really bad and counterintuitive. In my opinion, unacceptable.
=> In the past we solved this because each package created its own chunk in the client and we didn’t care about the bundle size in the server.
2. It doesn’t work with node_modules
There’s no way to create a regexp to distinguish between Settings and Frontity packages from the rest in node_modules
.
=> In the past we solved this because all packages were created in the ./packages/
folder.
Some possible solutions
1. Force a custom name for Settings or Frontity packages
For example:
-
@frontity/file-frontity-settings
(official) wp-org-frontity-settings
-
@frontity/saturn-frontity-theme
(official) mars-frontity-theme
-
@frontity/wp-org-frontity-extension
(official) graphql-wp-org-frontity-extension
And use a regexp like:
const getSettings = require(`./node_modules/${config.settingsPackage}-frontity-settings/index.js`)
package.forEach(pkg => {
require(`./packages/${pkg.name}-frontity-(theme|extension)/index.js`)
})
But it’s not ideal because Webpack will include all packages, no matter if they are actually used or not
2. Analyze what packages are needed and write a file to disk
This must be done in frontity dev
or frontity build
before Webpack is run:
// build.js
const config = require("./frontity.config.js")
const settingsContent = `export default from ${config.settingsPackage}`
writeFile("./dynamic-settings.js", fileContent)
Then, this is what Webpack sees:
// server.js
const getSettings = require("./dynamic-settings.js")
I don’t like having to write a file to disk to solve this, but I guess it’s ok for settings. But for packages, this approach has a new problem.
The problem of solution 2
A user can have multiple packages for different sites or environments.
For example, it’s frontity.settings.js
could be:
module.exports = {
site: "my-blog",
matches: {
amp: "/amp/$",
main: "*"
},
settings: {
url: "https://my-blog.com"
},
packages: {
"@my-org/amp-theme": {
matches: "amp",
settings: {
color: "#AAA"
}
},
"@my-org/main-theme": {
matches: "main",
settings: {
color: "#BBB"
}
},
"@frontity/wp-org-source": {
settings: {
endpoint: "https://my-blog.com/wp-json/",
perPage: 13
}
}
}
}
If we extract the packages used here and write them to disk…
@my-org/amp-theme
@my-org/main-theme
@frontity/wp-org-source
…the client bundle will include both the amp and the main theme. That is bad. It’ll be even worse if the user has different themes for different sites.
…the server bundle will include both but that is ok because it is responsible for rendering both the amp and the main version.
The possible solutions
1. Code split each package
If we go back to code splitting each package, the client bundles can import only the bundles they need on runtime.
There’s a couple of problems with this approach (yes! third generation problems):
- We need to hack
loadable-components
to load each store before the SSR or go back toreact-universal-component
. It seems likereact-universal-component
won’t work with React Concurrent. - Some Frontity packages are really small (a few kbs) and we will be adding an additional HTTP call for each one, which seems unnecessary.
2. Create one entry point for each site/match
If we create a bundle for each match (amp vs main) and for each site, we could have different client bundles for each case and those will only contain the packages needed in each case. It’d be like creating a different app for each case.
It seems like an ideal solution but I don’t know if it has any other drawback.
Ok, this was longer and more complex than expected
@development-team If you want to chime in and have more ideas or crazy solutions you’re very welcome!