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

[V5] Vite bundling #54

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all 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
172 changes: 172 additions & 0 deletions rfcs/0054-vite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
- Start Date: 2023-09-08
- RFC PR: https://github.com/strapi/rfcs/pull/54

# Summary

The following RFC compliments POC work already done prior to writing, in the RFC we look at improving the internal / external experience of building packages/plugins and the Strapi admin app via CLI commands. Not only this, but we address user issues around not being able to run the admin panel within their existing web applications nor being able to run a Strapi app with a plug-n-play package manager. All of the above contributes to a move to support Vite in v4 alongside the already existing webpack build option we currently use.

# Motivation

Webpack is powerful tool, it’s seen a lot of transformation in the javascript ecosystem and as such it’s architecture is subject to the design decisions of those times. However, in the modern day where _some_ javascript tooling has opted for native binaries e.g. using Rust or GO. Webpack has fallen behind in both speed and simplicity. In addition, webpack configurations are complicated and _extremely extensible_ but don’t provide “good defaults” which results in more developer time to maintain and improve such configurations.

Introducing Vite (pronounced `veet`). With all applications you have two sides – development and production. The development server is not only valuable to engineers at Strapi but also external developers working on their own Strapi apps/plugins. Vite uses `esbuild` to pre-bundle dependencies which improves typical speed by 10-100x. For more information on the development server see [this article](https://vitejs.dev/guide/why.html#slow-server-start). Regarding production, Vite provides a strong default rollup configuration for both applications and libraries, this however, does not mean we don’t have control over the configuration as it is still 100% configurable should we require it.

# Detailed Proposal

This proposal is based of the exploration of the POC found [here](https://github.com/strapi/strapi/pull/17085), and uses code-samples from said POC to illustrate how we _could_ move forward with the ideas proposed. This scope is intentionally kept relatively small focussing primarily on building a foundation for future improvements we can do with Vite such as SSR.

## Moving building scripts out of admin

Right now the `@strapi/admin` package exports a series of commands that can be ran to build/watch and clean the admin panel build, this puts all the pressure of the pipeline on the package which isolates a portion of code which could be used more generically. Therefore, we should focus on having no default export for the package & instead using export maps to distinguish between `admin` and `server` allowing us to export FE focussed code from the admin package to be shared where applicable (this _could_ lead to the deprecation of the `admin-test-utils`). The build pipeline would move to the CLI package as explained below.

## Generic building scripts

We want to create more plugin orientated CLI commands one in particular would be around “building” and “watching” your plugin files during development, the work has already begun to implement such experimental apis [here](https://github.com/strapi/strapi/pull/17747). It therefore becomes apparent the need to have more “generic” building functions (or tasks) that can be combined and configured to our needs, a simple example of this in relation to vite could look like this:

```tsx
import { build } from "vite";

const viteBuildTask = async (ctx: BuildContext, task: ViteBuildTask) => {
await build({
root: ctx.cwd,
cacheDir: "node_modules/.strapi/vite",
configFile: false,
...task,
build: {
outDir: path.resolve(ctx.distDir, "dist"),
...task.build,
},
});
};
```

Each task would receive a build context, whilst the task is dependant on the type being called:

```tsx
import { ParsedCommandLine } from "typescript";
import { InlineConfig } from "vite";

interface BuildContext {
cwd: string;
distDir: string;
external: string[]; // the external dependencies of the package
pkg: PackageJson; // useful for understanding the expected exports etc.
ts: {
config: Pick<ParsedCommandLine, "options">;
configPath: string;
};
}

interface ViteBuildTask
extends Omit<InlineConfig, "cacheDir" | "root" | "configFile" | "build"> {
build: Omit<InlineConfig["build"], "outDir">;
}
```

The benefits are we’re applying some standardised formula to the this build task e.g. defining a custom cache directory for vite to avoid any conflicts and a predetermined root / outDir. The rest would most likely be filed in by the local config (outlined in the aforementioned RFC).

This concept would extend to other tasks such as watching but also to other build tools like webpack, rollup, parcel – whichever we choose to support. It also means we can in the future create experimental support of new builders without conflicting with a stable process. I’d imagine this same system would work for TS compiling of the server and potentially wrapping all the node files into one which avoids developers from importing directly to a path instead of consuming our exposed public API which has previously lead to bugs.

## Building the admin app

As described above with the removal of the build scripts from the `admin` package we’re left without a way to access the `index.html` and rendering of the client app. In conjunction a major source of issues for our users comes from a mismatch between the `node_modules` and the strapi-created `.cache` folder, not only this but the way the `.cache` folder is setup causes errors with plug-n-play package managers such as `pnpm` and `yarn@3`. Moving forward we will remove the `.cache` folder completely moving to a more “library” approach. This initially should solve issues with package managers and the hope is to improve the upgrade process for our users.

When running the `build` command via the Strapi CLI we will create a client folder – `.strapi/client` using the subfolder leaves room to grow the folder should we need it without breaking changes / expectations and isolates the client. The next step is to generate the `index.html` file, in the POC this is done by writing to the FS with string manipulation however, when we have moved to bundling the packages / plugins we will be able to export a `DefaultDocument` react component and use that to render the `index.html`:

```tsx
import type { DocumentProps } from "@strapi/admin/admin";

const getDocumentComponent = () => {
const { DefaultDocument } = require("@strapi/admin/admin");

return DefaultDocument;
};

interface GetDocumentHTMLArgs {
props: DocumentProps;
}

const getDocumentHTML = ({ props }: GetDocumentHTMLArgs) => {
const Document = getDocumentComponent();

const result = renderToStaticMarkup(
createElement(Document, { ...DEFAULT_PROPS, ...props })
);

return `<!DOCTYPE html>${result}`;
};
```

The interesting bit of the above with the document is:

1. we can now give users the ability to render their own document component if they so choose to
2. Using react means we can improve the document experience e.g. supplying a “no javascript” component and even our own error overlay similar to `next.js`.

Finally, we need our entry JS file, in Vite you reference this directly in the Document component – `<script type="module" src="${props.entryPath}" />` meanwhile `webpack` injects this into the HTML – either way, the file should be generated. Which introduces a new export from the `admin` package – `renderAdmin`.

This export removes the need to be copying files from `node_modules` and keeps the pieces isolated as they should be, and ensures Vite can look at the simplest level possible meaning it _should_ be relatively straight forward to ensure monorepo support smoothly. The final implicit benefit of this export is that in theory this would unlock the ability for anyone to render the Strapi admin app wherever – `nextjs` for example. The result of the above leaves your application folder structure looking like this:

```
strapi-app/
├─ .strapi/
│ ├─ client/
│ │ ├─ index.html
│ │ ├─ app.js
├─ config
├─ database
├─ public
├─ src
```

To summarise, I hope this will:

- Enable users to use plug-n-play package managers
- Remove the bugs users face when upgrading their Strapi version by removing the `.cache` folder
- Enable users to render the Strapi admin panel in any JS framework
- Allow us to further extend the build tools we want to explore in the future

## CLI extension

The CLI has been mentioned a few times in the above sections, to give clarity on the proposals I have summarised the expected changes below.

### `strapi build`

At minimum we’ll be adding the `builder` option to the CLI which initially will take the options `vite` or `webpack` with the default being `webpack` to avoid unintended issues in version 4. We will also be looking to improve the DX experience of the CLI by displaying build times of steps and also giving more detail to the build pipeline, this will help us debug in the future if we need to because we can identify which step failed e.g. auto-generating files.

### `strapi package`

This command (name tbc) would hold more generic scripts for building packages in the Strapi ecosystem like the internal packages we have and the design-system. We can also verify our configs here either the build config our a plugin configuration. This DX improvement will empower developers with better feedback meaning they can work quicker and more effectively.

### `strapi watch`

A different (currently unexplored) way of looking at the watch command of Strapi would enable us to run a `vite` dev server for FE changes in the `src` folder of a user’s application as well as hot reload their node Strapi instance when they add their own services / controllers etc. either in the application of their plugin that they’re developing within the app.

# Tradeoffs

Ultimately the biggest trade off is in the current design we're still locking up the bundling for the admin builder inside the strapi codebase as opposed to exposing an adapter entry point where _any_ developer could provide their bundler of choice. This is something we could explore in the future should their be the appetite assuming we can create a generic enough build context to be passed to each bundler, I hope building vite alongside with webpack would help us initially understand what's required of that build context.

Finally, because we want to be more bundler agnostic we're not realistically leveraging vite's plugin API in the same way something like sveltekit does. Realistically being more flexible with our bundler means we can support more options as the ecosystem develops and avoid cases where it's taken us quite a long time to offer a new bundler option.

# Unresolved questions

## Can this be done in v4?

Potentially yes, as outlined with the generic building scripts the ambition would be to decouple building from the admin panel package which in combination with the CLI extensions means users by default will still use the `webpack` way and can opt into `vite` we could even show a warning in the CLI about this being `experimental`.

## Should we _not_ show `index.html` to the user?

This is something we could explore, we could also use an HTML template (assuming webpack supports it) that might look like this:

```html
<!DOCTYPE html>
<html>
<head>
%%strapi_head%%
</head>
<body>
%%strapi_app%%
</body>
</html>
```

The potential benefits of this route is we could be more typesafe APIs for users to inject code into the document file as opposed to them rendering their own entire document and our build script trying to load said react component.