Repository: remix-run/remix
Pull request #6046 (2023-04-12 01:21:36)

feat(dev): enable all CSS bundling features
Since these features are non-breaking, we've decided they can go out in v1, which also gives them a bit of time in the spotlight before the focus shifts to v2.

All `unstable` flags have been removed. If any of these flags remain in the code, a warning is logged to the terminal.

Being mindful of the fact that the new Vanilla Extract compiler uses vite-node, I've also refactored the Vanilla Extract plugin so that we only instantiate the Vanilla Extract compiler when we first discover a `.css.ts`/`.css.js` file.

changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: f1e6a71561f9bd32dd21bf7bd930339700e2416c

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 18 packages</summary>

  | Name                          | Type  |
| ----------------------------- | ----- |
| @remix-run/dev                | Minor |
| @remix-run/react              | Minor |
| @remix-run/server-runtime     | Minor |
| @remix-run/testing            | Minor |
| create-remix                  | Minor |
| @remix-run/css-bundle         | Minor |
| @remix-run/cloudflare         | Minor |
| @remix-run/deno               | Minor |
| @remix-run/node               | Minor |
| @remix-run/cloudflare-pages   | Minor |
| @remix-run/cloudflare-workers | Minor |
| @remix-run/architect          | Minor |
| @remix-run/express            | Minor |
| @remix-run/netlify            | Minor |
| @remix-run/vercel             | Minor |
| @remix-run/serve              | Minor |
| remix                         | Minor |
| @remix-run/eslint-config      | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/remix-run/remix/new/markdalgleish/css-bundling?filename=.changeset/tough-elephants-cross.md&value=---%0A%22remix%22%3A%20patch%0A%22%40remix-run%2Fdev%22%3A%20patch%0A%22%40remix-run%2Freact%22%3A%20patch%0A%22%40remix-run%2Fserve%22%3A%20patch%0A%22%40remix-run%2Fserver-runtime%22%3A%20patch%0A%22%40remix-run%2Ftesting%22%3A%20patch%0A---%0A%0Afeat(dev)%3A%20enable%20all%20CSS%20bundling%20features%0A)



Patch:
diff --git a/.changeset/css-bundling.md b/.changeset/css-bundling.md
new file mode 100644
index 000000000..b918ae92e
--- /dev/null
+++ b/.changeset/css-bundling.md
@@ -0,0 +1,118 @@
+---
+"@remix-run/dev": minor
+"@remix-run/react": minor
+"@remix-run/server-runtime": minor
+"@remix-run/testing": minor
+---
+
+Enable support for [CSS Modules](https://github.com/css-modules/css-modules), [Vanilla Extract](http://vanilla-extract.style) and CSS side-effect imports
+
+These CSS bundling features were previously only available via `future.unstable_cssModules`, `future.unstable_vanillaExtract` and `future.unstable_cssSideEffectImports` options in `remix.config.js`, but they have now been stabilized.
+
+**CSS Bundle Setup**
+
+In order to use these features, you first need to set up CSS bundling in your project. First install the `@remix-run/css-bundle` package.
+  
+```sh
+npm i @remix-run/css-bundle
+```
+
+Then return the exported `cssBundleHref` as a stylesheet link descriptor from the `links` function at the root of your app.
+
+```tsx
+import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno
+import { cssBundleHref } from "@remix-run/css-bundle";
+
+export const links: LinksFunction = () => {
+  return [
+    ...(cssBundleHref
+      ? [{ rel: "stylesheet", href: cssBundleHref }]
+      : []),
+    // ...
+  ];
+};
+```
+
+**CSS Modules**
+
+To use [CSS Modules](https://github.com/css-modules/css-modules), you can opt in via the `.module.css` file name convention. For example:
+
+```css
+.root {
+  border: solid 1px;
+  background: white;
+  color: #454545;
+}
+```
+
+```tsx
+import styles from "./styles.module.css";
+
+export const Button = React.forwardRef(
+  ({ children, ...props }, ref) => {
+    return (
+      <button
+        {...props}
+        ref={ref}
+        className={styles.root}
+      />
+    );
+  }
+);
+Button.displayName = "Button";
+```
+
+**Vanilla Extract**
+
+To use [Vanilla Extract](http://vanilla-extract.style), first install its `css` package as a dev dependency.
+
+```sh
+npm install -D @vanilla-extract/css
+```
+
+You can then opt in via the `.css.ts`/`.css.js` file name convention. For example:
+
+```ts
+import { style } from "@vanilla-extract/css";
+
+export const root = style({
+  border: "solid 1px",
+  background: "white",
+  color: "#454545",
+});
+```
+
+```tsx
+import * as styles from "./styles.css"; // Note that `.ts` is omitted here
+
+export const Button = React.forwardRef(
+  ({ children, ...props }, ref) => {
+    return (
+      <button
+        {...props}
+        ref={ref}
+        className={styles.root}
+      />
+    );
+  }
+);
+Button.displayName = "Button";
+```
+
+**CSS Side-Effect Imports**
+
+Any CSS files that are imported as side-effects (e.g. `import "./styles.css"`) will be automatically included in the CSS bundle.
+
+Since JavaScript runtimes don't support importing CSS in this way, you'll also need to add any packages using CSS side-effect imports to the [`serverDependenciesToBundle`](https://remix.run/docs/en/main/file-conventions/remix-config#serverdependenciestobundle) option in your `remix.config.js` file. This ensures that any CSS imports are compiled out of your code before running it on the server. For example, to use [React Spectrum](https://react-spectrum.adobe.com/react-spectrum/index.html):
+
+```js filename=remix.config.js
+// remix.config.js
+module.exports = {
+  serverDependenciesToBundle: [
+    /^@adobe\/react-spectrum/,
+    /^@react-spectrum/,
+    /^@spectrum-icons/,
+  ],
+  // ...
+};
+```
diff --git a/docs/guides/styling.md b/docs/guides/styling.md
index dce8ffc4a..56821ed3e 100644
--- a/docs/guides/styling.md
+++ b/docs/guides/styling.md
@@ -671,8 +671,6 @@ NOTE: You may run into hydration warnings when using Styled Components. Hopefull
 
 ## CSS Bundling
 
-<docs-warning>CSS-bundling features are unstable and currently only available behind feature flags. We're confident in the use cases they solve, but the API and implementation may change in the future.</docs-warning>
-
 <docs-warning>When using CSS-bundling features, you should avoid using `export *` due to an [issue with esbuild's CSS tree shaking][esbuild-css-tree-shaking-issue].</docs-warning>
 
 Many common approaches to CSS within the React community are only possible when bundling CSS, meaning that the CSS files you write during development are collected into a separate bundle as part of the build process.
@@ -707,23 +705,9 @@ With this link tag inserted into the page, you're now ready to start using the v
 
 ### CSS Modules
 
-<docs-warning>This feature is unstable and currently only available behind a feature flag. We're confident in the use cases it solves but the API and implementation may change in the future.</docs-warning>
-
-First, ensure you've set up [CSS bundling][css-bundling] in your application.
-
-Then, to enable [CSS Modules], set the `future.unstable_cssModules` feature flag in `remix.config.js`.
-
-```js filename=remix.config.js
-/** @type {import('@remix-run/dev').AppConfig} */
-module.exports = {
-  future: {
-    unstable_cssModules: true,
-  },
-  // ...
-};
-```
+To use the built-in CSS Modules support, first ensure you've set up [CSS bundling][css-bundling] in your application.
 
-With this feature flag enabled, you can now opt into CSS Modules via the `.module.css` file name convention. For example:
+You can then opt into [CSS Modules] via the `.module.css` file name convention. For example:
 
 ```css filename=app/components/button/styles.module.css
 .root {
@@ -752,31 +736,17 @@ Button.displayName = "Button";
 
 ### Vanilla Extract
 
-<docs-warning>This feature is unstable and currently only available behind a feature flag. We're confident in the use cases it solves, but the API and implementation may change in the future.</docs-warning>
-
 [Vanilla Extract][vanilla-extract] is a zero-runtime CSS-in-TypeScript (or JavaScript) library that lets you use TypeScript as your CSS preprocessor. Styles are written in separate `*.css.ts` (or `*.css.js`) files and all code within them is executed during the build process rather than in your user's browser. If you want to keep your CSS bundle size to a minimum, Vanilla Extract also provides an official library called [Sprinkles][sprinkles] that lets you define a custom set of utility classes and a type-safe function for accessing them at runtime.
 
-First, ensure you've set up [CSS bundling][css-bundling] in your application.
+To use the built-in Vanilla Extract support, first ensure you've set up [CSS bundling][css-bundling] in your application.
 
-Next, install Vanilla Extract's core styling package as a dev dependency.
+Then, install Vanilla Extract's core styling package as a dev dependency.
 
 ```sh
 npm install -D @vanilla-extract/css
 ```
 
-Then, to enable Vanilla Extract, set the `future.unstable_vanillaExtract` feature flag in `remix.config.js`.
-
-```js filename=remix.config.js
-/** @type {import('@remix-run/dev').AppConfig} */
-module.exports = {
-  future: {
-    unstable_vanillaExtract: true,
-  },
-  // ...
-};
-```
-
-With this feature flag enabled, you can now opt into Vanilla Extract via the `.css.ts`/`.css.js` file name convention. For example:
+You can then opt into Vanilla Extract via the `.css.ts`/`.css.js` file name convention. For example:
 
 ```ts filename=app/components/button/styles.css.ts
 import { style } from "@vanilla-extract/css";
@@ -807,23 +777,9 @@ Button.displayName = "Button";
 
 ### CSS Side-Effect Imports
 
-<docs-warning>This feature is unstable and currently only available behind a feature flag. We're confident in the use cases it solves, but the API and implementation may change in the future.</docs-warning>
-
 Some NPM packages use side-effect imports of plain CSS files (e.g. `import "./styles.css"`) to declare the CSS dependencies of JavaScript files. If you want to consume one of these packages, first ensure you've set up [CSS bundling][css-bundling] in your application.
 
-Then, set the `future.unstable_cssSideEffectImports` feature flag in `remix.config.js`.
-
-```js filename=remix.config.js
-/** @type {import('@remix-run/dev').AppConfig} */
-module.exports = {
-  future: {
-    unstable_cssSideEffectImports: true,
-  },
-  // ...
-};
-```
-
-Finally, since JavaScript runtimes don't support importing CSS in this way, you'll also need to add any relevant packages to the [`serverDependenciesToBundle`][server-dependencies-to-bundle] option in your `remix.config.js` file. This ensures that any CSS imports are compiled out of your code before running it on the server. For example, to use React Spectrum:
+Since JavaScript runtimes don't support importing CSS in this way, you'll need to add any relevant packages to the [`serverDependenciesToBundle`][server-dependencies-to-bundle] option in your `remix.config.js` file. This ensures that any CSS imports are compiled out of your code before running it on the server. For example, to use React Spectrum:
 
 ```js filename=remix.config.js
 /** @type {import('@remix-run/dev').AppConfig} */
@@ -833,9 +789,6 @@ module.exports = {
     /^@react-spectrum/,
     /^@spectrum-icons/,
   ],
-  future: {
-    unstable_cssSideEffectImports: true,
-  },
   // ...
 };
 ```
diff --git a/docs/pages/api-development-strategy.md b/docs/pages/api-development-strategy.md
index deb1e7cb6..828ebf8a7 100644
--- a/docs/pages/api-development-strategy.md
+++ b/docs/pages/api-development-strategy.md
@@ -54,10 +54,7 @@ Here's the current future flags in Remix v1 today:
 
 | Flag                            | Description                                                           |
 | ------------------------------- | --------------------------------------------------------------------- |
-| `unstable_cssModules`           | Enable CSS Modules Support                                            |
-| `unstable_cssSideEffectImports` | Enable CSS Side Effect imports                                        |
 | `unstable_dev`                  | Enable the new development server (including HMR/HDR support)         |
-| `unstable_vanillaExtract`       | Enable Vanilla Extract Support                                        |
 | `v2_errorBoundary`              | Combine `ErrorBoundary`/`CatchBoundary` into a single `ErrorBoundary` |
 | `v2_meta`                       | Enable the new API for your `meta` functions                          |
 | `v2_normalizeFormMethod`        | Normalize `useNavigation().formMethod` to be an uppercase HTTP Method |
diff --git a/packages/remix-architect/package.json b/packages/remix-architect/package.json
index bac011fca..6d9b141db 100644
--- a/packages/remix-architect/package.json
+++ b/packages/remix-architect/package.json
@@ -15,10 +15,10 @@
   "typings": "dist/index.d.ts",
   "dependencies": {
     "@architect/functions": "^5.2.0",
-    "@remix-run/node": "1.15.0",
-    "@types/aws-lambda": "^8.10.82"
+    "@remix-run/node": "1.15.0"
   },
   "devDependencies": {
+    "@types/aws-lambda": "^8.10.82",
     "@types/lambda-tester": "^3.6.1",
     "lambda-tester": "^4.0.1"
   },
diff --git a/packages/remix-dev/compiler/assets/css.ts b/packages/remix-dev/compiler/assets/css.ts
index 40c937124..5184136b1 100644
--- a/packages/remix-dev/compiler/assets/css.ts
+++ b/packages/remix-dev/compiler/assets/css.ts
@@ -24,17 +24,6 @@ import {
 } from "./plugins/cssBundleEntry";
 import type { WriteChannel } from "../../channel";
 
-function isNotNull<Value>(value: Value): value is Exclude<Value, null> {
-  return value !== null;
-}
-
-const isCssBundlingEnabled = (config: RemixConfig): boolean =>
-  Boolean(
-    config.future.unstable_cssModules ||
-      config.future.unstable_cssSideEffectImports ||
-      config.future.unstable_vanillaExtract
-  );
-
 const getExternals = (remixConfig: RemixConfig): string[] => {
   // For the browser build, exclude node built-ins that don't have a
   // browser-safe alternative installed in node_modules. Nothing should
@@ -61,16 +50,10 @@ const createEsbuildConfig = (
   let { mode } = options;
 
   let plugins: esbuild.Plugin[] = [
-    isCssBundlingEnabled(config) ? cssBundleEntryModulePlugin(config) : null,
-    config.future.unstable_cssModules
-      ? cssModulesPlugin({ config, mode, outputCss: true })
-      : null,
-    config.future.unstable_vanillaExtract
-      ? vanillaExtractPlugin({ config, mode, outputCss: true })
-      : null,
-    config.future.unstable_cssSideEffectImports
-      ? cssSideEffectImportsPlugin({ config, options })
-      : null,
+    cssBundleEntryModulePlugin(config),
+    cssModulesPlugin({ config, mode, outputCss: true }),
+    vanillaExtractPlugin({ config, mode, outputCss: true }),
+    cssSideEffectImportsPlugin({ config, options }),
     cssFilePlugin({ config, options }),
     absoluteCssUrlsPlugin(),
     externalPlugin(/^https?:\/\//, { sideEffects: false }),
@@ -78,7 +61,7 @@ const createEsbuildConfig = (
     emptyModulesPlugin(config, /\.server(\.[jt]sx?)?$/),
     NodeModulesPolyfillPlugin(),
     externalPlugin(/^node:.*/, { sideEffects: false }),
-  ].filter(isNotNull);
+  ];
 
   return {
     entryPoints: {
@@ -87,17 +70,7 @@ const createEsbuildConfig = (
     outdir: config.assetsBuildDirectory,
     platform: "browser",
     format: "esm",
-    external: [
-      // This allows Vanilla Extract to bundle asset imports, e.g. `import href
-      // from './image.svg'` resolves to a string like "/build/_assets/XXXX.svg"
-      // which will then appear in the compiled CSS, e.g. `background:
-      // url("/build/_assets/XXXX.svg")`. If we don't mark this path as
-      // external, esbuild will try to bundle it again but won't find it.
-      config.future.unstable_vanillaExtract
-        ? `${config.publicPath}_assets/*`
-        : null,
-      ...getExternals(config),
-    ].filter(isNotNull),
+    external: getExternals(config),
     loader: loaders,
     bundle: true,
     logLevel: "silent",
@@ -139,10 +112,6 @@ export let create = async (
     write: false,
   });
   let compile = async () => {
-    if (!isCssBundlingEnabled(remixConfig)) {
-      return;
-    }
-
     try {
       let { outputFiles } = await ctx.rebuild();
 
diff --git a/packages/remix-dev/compiler/assets/js.ts b/packages/remix-dev/compiler/assets/js.ts
index cf251babc..c0982c9d7 100644
--- a/packages/remix-dev/compiler/assets/js.ts
+++ b/packages/remix-dev/compiler/assets/js.ts
@@ -69,13 +69,6 @@ const getExternals = (remixConfig: RemixConfig): string[] => {
   return nodeBuiltins.filter((mod) => !dependencies.includes(mod));
 };
 
-const isCssBundlingEnabled = (config: RemixConfig): boolean =>
-  Boolean(
-    config.future.unstable_cssModules ||
-      config.future.unstable_cssSideEffectImports ||
-      config.future.unstable_vanillaExtract
-  );
-
 const createEsbuildConfig = (
   config: RemixConfig,
   options: CompileOptions,
@@ -109,15 +102,9 @@ const createEsbuildConfig = (
 
   let plugins: esbuild.Plugin[] = [
     deprecatedRemixPackagePlugin(options.onWarning),
-    config.future.unstable_cssModules
-      ? cssModulesPlugin({ config, mode, outputCss: false })
-      : null,
-    config.future.unstable_vanillaExtract
-      ? vanillaExtractPlugin({ config, mode, outputCss: false })
-      : null,
-    config.future.unstable_cssSideEffectImports
-      ? cssSideEffectImportsPlugin({ config, options })
-      : null,
+    cssModulesPlugin({ config, mode, outputCss: false }),
+    vanillaExtractPlugin({ config, mode, outputCss: false }),
+    cssSideEffectImportsPlugin({ config, options }),
     cssFilePlugin({ config, options }),
     absoluteCssUrlsPlugin(),
     externalPlugin(/^https?:\/\//, { sideEffects: false }),
@@ -169,7 +156,7 @@ const createEsbuildConfig = (
         });
       },
     } as esbuild.Plugin,
-  ].filter(isNotNull);
+  ];
 
   if (mode === "development" && config.future.unstable_dev) {
     // TODO prebundle deps instead of chunking just these ones
@@ -189,10 +176,7 @@ const createEsbuildConfig = (
     };
 
     plugins.push(hmrPlugin({ remixConfig: config }));
-
-    if (isCssBundlingEnabled(config)) {
-      plugins.push(cssBundleUpdatePlugin(channels));
-    }
+    plugins.push(cssBundleUpdatePlugin(channels));
   }
 
   return {
@@ -200,17 +184,7 @@ const createEsbuildConfig = (
     outdir: config.assetsBuildDirectory,
     platform: "browser",
     format: "esm",
-    external: [
-      // This allows Vanilla Extract to bundle asset imports, e.g. `import href
-      // from './image.svg'` resolves to a string like "/build/_assets/XXXX.svg"
-      // which will then appear in the compiled CSS, e.g. `background:
-      // url("/build/_assets/XXXX.svg")`. If we don't mark this path as
-      // external, esbuild will try to bundle it again but won't find it.
-      config.future.unstable_vanillaExtract
-        ? `${config.publicPath}_assets/*`
-        : null,
-      ...getExternals(config),
-    ].filter(isNotNull),
+    external: getExternals(config),
     loader: loaders,
     bundle: true,
     logLevel: "silent",
@@ -289,7 +263,3 @@ export const create = async (
     dispose: ctx.dispose,
   };
 };
-
-function isNotNull<Value>(value: Value): value is Exclude<Value, null> {
-  return value !== null;
-}
diff --git a/packages/remix-dev/compiler/plugins/vanillaExtract/cached.ts b/packages/remix-dev/compiler/plugins/vanillaExtract.ts
similarity index 50%
rename from packages/remix-dev/compiler/plugins/vanillaExtract/cached.ts
rename to packages/remix-dev/compiler/plugins/vanillaExtract.ts
index 2ae9498d5..3dbc9c24d 100644
--- a/packages/remix-dev/compiler/plugins/vanillaExtract/cached.ts
+++ b/packages/remix-dev/compiler/plugins/vanillaExtract.ts
@@ -3,13 +3,12 @@ import type { Compiler } from "@vanilla-extract/integration";
 import { cssFileFilter, createCompiler } from "@vanilla-extract/integration";
 import type { Plugin } from "esbuild";
 
-import type { RemixConfig } from "../../../config";
-import type { CompileOptions } from "../../options";
-import { loaders } from "../../utils/loaders";
-import { getPostcssProcessor } from "../../utils/postcss";
-import { vanillaExtractSideEffectsPlugin } from "./sideEffects";
+import type { RemixConfig } from "../../config";
+import type { CompileOptions } from "../options";
+import { loaders } from "../utils/loaders";
+import { getPostcssProcessor } from "../utils/postcss";
 
-const pluginName = "vanilla-extract-plugin-cached";
+const pluginName = "vanilla-extract-plugin";
 const namespace = `${pluginName}-ns`;
 const virtualCssFileFilter = /\.vanilla.css$/;
 
@@ -19,9 +18,42 @@ const staticAssetRegexp = new RegExp(
     .join("|")})$`
 );
 
-let compiler: Compiler;
+let compiler: Compiler | undefined;
+function getCompiler(root: string, mode: CompileOptions["mode"]) {
+  compiler =
+    compiler ||
+    createCompiler({
+      root,
+      identifiers: mode === "production" ? "short" : "debug",
+      vitePlugins: [
+        {
+          name: "remix-assets",
+          enforce: "pre",
+          async resolveId(source) {
+            // Handle root-relative imports within Vanilla Extract files
+            if (source.startsWith("~")) {
+              return await this.resolve(source.replace("~", ""));
+            }
+            // Handle static asset JS imports
+            if (source.startsWith("/") && staticAssetRegexp.test(source)) {
+              return {
+                external: true,
+                id: "~" + source,
+              };
+            }
+          },
+          transform(code) {
+            // Translate Vite's fs import format for root-relative imports
+            return code.replace(/\/@fs\/~\//g, "~/");
+          },
+        },
+      ],
+    });
+
+  return compiler;
+}
 
-export function vanillaExtractPluginCached({
+export function vanillaExtractPlugin({
   config,
   mode,
   outputCss,
@@ -35,36 +67,6 @@ export function vanillaExtractPluginCached({
     async setup(build) {
       let root = config.appDirectory;
 
-      compiler =
-        compiler ||
-        createCompiler({
-          root,
-          identifiers: mode === "production" ? "short" : "debug",
-          vitePlugins: [
-            {
-              name: "remix-assets",
-              enforce: "pre",
-              async resolveId(source) {
-                // Handle root-relative imports within Vanilla Extract files
-                if (source.startsWith("~")) {
-                  return await this.resolve(source.replace("~", ""));
-                }
-                // Handle static asset JS imports
-                if (source.startsWith("/") && staticAssetRegexp.test(source)) {
-                  return {
-                    external: true,
-                    id: "~" + source,
-                  };
-                }
-              },
-              transform(code) {
-                // Translate Vite's fs import format for root-relative imports
-                return code.replace(/\/@fs\/~\//g, "~/");
-              },
-            },
-          ],
-        });
-
       let postcssProcessor = await getPostcssProcessor({
         config,
         context: {
@@ -82,12 +84,41 @@ export function vanillaExtractPluginCached({
         };
       });
 
-      vanillaExtractSideEffectsPlugin.setup(build);
+      // Mark all .css.ts/js files as having side effects. This is to ensure
+      // that all usages of `globalStyle` are included in the CSS bundle, even
+      // if a .css.ts/js file has no exports or is otherwise tree-shaken.
+      let preventInfiniteLoop = {};
+      build.onResolve(
+        { filter: /\.css(\.(j|t)sx?)?(\?.*)?$/, namespace: "file" },
+        async (args) => {
+          if (args.pluginData === preventInfiniteLoop) {
+            return null;
+          }
+
+          let resolvedPath = (
+            await build.resolve(args.path, {
+              resolveDir: args.resolveDir,
+              kind: args.kind,
+              pluginData: preventInfiniteLoop,
+            })
+          ).path;
+
+          if (!cssFileFilter.test(resolvedPath)) {
+            return null;
+          }
+
+          return {
+            path: resolvedPath,
+            sideEffects: true,
+          };
+        }
+      );
 
       build.onLoad(
         { filter: virtualCssFileFilter, namespace },
         async ({ path }) => {
           let [relativeFilePath] = path.split(".vanilla.css");
+          let compiler = getCompiler(root, mode);
           let { css, filePath } = compiler.getCssForFile(relativeFilePath);
           let resolveDir = dirname(resolve(root, filePath));
 
@@ -109,6 +140,7 @@ export function vanillaExtractPluginCached({
       );
 
       build.onLoad({ filter: cssFileFilter }, async ({ path: filePath }) => {
+        let compiler = getCompiler(root, mode);
         let { source, watchFiles } = await compiler.processVanillaFile(
           filePath,
           { outputCss }
diff --git a/packages/remix-dev/compiler/plugins/vanillaExtract/index.ts b/packages/remix-dev/compiler/plugins/vanillaExtract/index.ts
deleted file mode 100644
index 9607fbf0b..000000000
--- a/packages/remix-dev/compiler/plugins/vanillaExtract/index.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import type { Plugin } from "esbuild";
-
-import type { RemixConfig, VanillaExtractOptions } from "../../../config";
-import type { CompileOptions } from "../../options";
-import { vanillaExtractPluginCached } from "./cached";
-import { vanillaExtractPluginUncached } from "./uncached";
-
-export function vanillaExtractPlugin(options: {
-  config: RemixConfig;
-  mode: CompileOptions["mode"];
-  outputCss: boolean;
-}): Plugin {
-  let defaultPluginOptions: Required<VanillaExtractOptions> = {
-    cache: false,
-  };
-
-  let futureFlag = options.config.future.unstable_vanillaExtract;
-  let pluginOptions = typeof futureFlag === "object" ? futureFlag : {};
-
-  let { cache } = {
-    ...defaultPluginOptions,
-    ...pluginOptions,
-  };
-
-  let resolvedPlugin = cache
-    ? vanillaExtractPluginCached
-    : vanillaExtractPluginUncached;
-
-  return resolvedPlugin(options);
-}
diff --git a/packages/remix-dev/compiler/plugins/vanillaExtract/sideEffects.ts b/packages/remix-dev/compiler/plugins/vanillaExtract/sideEffects.ts
deleted file mode 100644
index c560bedc7..000000000
--- a/packages/remix-dev/compiler/plugins/vanillaExtract/sideEffects.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import type { Plugin } from "esbuild";
-import { cssFileFilter } from "@vanilla-extract/integration";
-
-/**
- * This plugin marks all .css.ts/js files as having side effects. This is
- * to ensure that all usages of `globalStyle` are included in the CSS bundle,
- * even if a .css.ts/js file has no exports or is otherwise tree-shaken.
- */
-export const vanillaExtractSideEffectsPlugin: Plugin = {
-  name: "vanilla-extract-side-effects-plugin",
-  setup(build) {
-    let preventInfiniteLoop = {};
-
-    build.onResolve(
-      { filter: /\.css(\.(j|t)sx?)?(\?.*)?$/, namespace: "file" },
-      async (args) => {
-        if (args.pluginData === preventInfiniteLoop) {
-          return null;
-        }
-
-        let resolvedPath = (
-          await build.resolve(args.path, {
-            resolveDir: args.resolveDir,
-            kind: args.kind,
-            pluginData: preventInfiniteLoop,
-          })
-        ).path;
-
-        if (!cssFileFilter.test(resolvedPath)) {
-          return null;
-        }
-
-        return {
-          path: resolvedPath,
-          sideEffects: true,
-        };
-      }
-    );
-  },
-};
diff --git a/packages/remix-dev/compiler/plugins/vanillaExtract/uncached.ts b/packages/remix-dev/compiler/plugins/vanillaExtract/uncached.ts
deleted file mode 100644
index 06a50a14c..000000000
--- a/packages/remix-dev/compiler/plugins/vanillaExtract/uncached.ts
+++ /dev/null
@@ -1,182 +0,0 @@
-import { dirname, join, extname } from "path";
-import type { IdentifierOption } from "@vanilla-extract/integration";
-import {
-  cssFileFilter,
-  virtualCssFileFilter,
-  processVanillaFile,
-  getSourceFromVirtualCssFile,
-  transform,
-} from "@vanilla-extract/integration";
-import * as fse from "fs-extra";
-import * as esbuild from "esbuild";
-
-import type { RemixConfig } from "../../../config";
-import type { CompileOptions } from "../../options";
-import { loaders } from "../../utils/loaders";
-import { getPostcssProcessor } from "../../utils/postcss";
-import { vanillaExtractSideEffectsPlugin } from "./sideEffects";
-
-const pluginName = "vanilla-extract-plugin-uncached";
-const namespace = `${pluginName}-ns`;
-
-export function vanillaExtractPluginUncached({
-  config,
-  mode,
-  outputCss,
-}: {
-  config: RemixConfig;
-  mode: CompileOptions["mode"];
-  outputCss: boolean;
-}): esbuild.Plugin {
-  return {
-    name: pluginName,
-    async setup(build) {
-      let postcssProcessor = await getPostcssProcessor({
-        config,
-        context: {
-          vanillaExtract: true,
-        },
-      });
-      let { rootDirectory } = config;
-
-      // Resolve virtual CSS files first to avoid resolving the same
-      // file multiple times since this filter is more specific and
-      // doesn't require a file system lookup.
-      build.onResolve({ filter: virtualCssFileFilter }, (args) => {
-        return {
-          path: args.path,
-          namespace,
-        };
-      });
-
-      vanillaExtractSideEffectsPlugin.setup(build);
-
-      build.onLoad(
-        { filter: virtualCssFileFilter, namespace },
-        async ({ path }) => {
-          let { source, fileName } = await getSourceFromVirtualCssFile(path);
-          let resolveDir = dirname(join(rootDirectory, fileName));
-
-          if (postcssProcessor) {
-            source = (
-              await postcssProcessor.process(source, {
-                from: path,
-                to: path,
-              })
-            ).css;
-          }
-
-          return {
-            contents: source,
-            loader: "css",
-            resolveDir,
-          };
-        }
-      );
-
-      build.onLoad({ filter: cssFileFilter }, async ({ path: filePath }) => {
-        let identOption: IdentifierOption =
-          mode === "production" ? "short" : "debug";
-
-        let { outputFiles } = await esbuild.build({
-          entryPoints: [filePath],
-          outdir: config.assetsBuildDirectory,
-          assetNames: build.initialOptions.assetNames,
-          bundle: true,
-          external: ["@vanilla-extract"],
-          platform: "node",
-          write: false,
-          plugins: [
-            vanillaExtractSideEffectsPlugin,
-            vanillaExtractTransformPlugin({ rootDirectory, identOption }),
-          ],
-          loader: loaders,
-          absWorkingDir: rootDirectory,
-          publicPath: config.publicPath,
-        });
-
-        let source = outputFiles.find((file) =>
-          file.path.endsWith(".js")
-        )?.text;
-
-        if (!source) {
-          return null;
-        }
-
-        let [contents] = await Promise.all([
-          processVanillaFile({
-            source,
-            filePath,
-            outputCss,
-            identOption,
-          }),
-          outputCss && writeAssets(outputFiles),
-        ]);
-
-        return {
-          contents,
-          resolveDir: dirname(filePath),
-          loader: "js",
-        };
-      });
-    },
-  };
-}
-
-async function writeAssets(
-  outputFiles: Array<esbuild.OutputFile>
-): Promise<void> {
-  await Promise.all(
-    outputFiles
-      .filter((file) => !file.path.endsWith(".js"))
-      .map(async (file) => {
-        await fse.ensureDir(dirname(file.path));
-        await fse.writeFile(file.path, file.contents);
-      })
-  );
-}
-
-const loaderForExtension: Record<string, esbuild.Loader> = {
-  ".js": "js",
-  ".jsx": "jsx",
-  ".ts": "ts",
-  ".tsx": "tsx",
-};
-
-/**
- * This plugin is used within the child compilation. It applies the Vanilla
- * Extract file transform to all .css.ts/js files. This is used to add "file
- * scope" annotations, which is done via function calls at the beginning and end
- * of each file so that we can tell which CSS file the styles belong to when
- * evaluating the JS. It's also done to automatically apply debug IDs.
- */
-function vanillaExtractTransformPlugin({
-  rootDirectory,
-  identOption,
-}: {
-  identOption: IdentifierOption;
-  rootDirectory: string;
-}): esbuild.Plugin {
-  return {
-    name: "vanilla-extract-transform-plugin",
-    setup(build) {
-      build.onLoad({ filter: cssFileFilter }, async ({ path }) => {
-        let source = await fse.readFile(path, "utf-8");
-
-        let contents = await transform({
-          source,
-          filePath: path,
-          rootPath: rootDirectory,
-          packageName: "remix-app", // This option is designed to support scoping hashes for libraries, we can hard code an arbitrary value for simplicity
-          identOption,
-        });
-
-        return {
-          contents,
-          loader: loaderForExtension[extname(path)],
-          resolveDir: dirname(path),
-        };
-      });
-    },
-  };
-}
diff --git a/packages/remix-dev/compiler/server/index.ts b/packages/remix-dev/compiler/server/index.ts
index 95c57419c..eed672b6d 100644
--- a/packages/remix-dev/compiler/server/index.ts
+++ b/packages/remix-dev/compiler/server/index.ts
@@ -47,19 +47,12 @@ const createEsbuildConfig = (
   }
 
   let { mode } = options;
-  let outputCss = false;
 
   let plugins: esbuild.Plugin[] = [
     deprecatedRemixPackagePlugin(options.onWarning),
-    config.future.unstable_cssModules
-      ? cssModulesPlugin({ config, mode, outputCss })
-      : null,
-    config.future.unstable_vanillaExtract
-      ? vanillaExtractPlugin({ config, mode, outputCss })
-      : null,
-    config.future.unstable_cssSideEffectImports
-      ? cssSideEffectImportsPlugin({ config, options })
-      : null,
+    cssModulesPlugin({ config, mode, outputCss: false }),
+    vanillaExtractPlugin({ config, mode, outputCss: false }),
+    cssSideEffectImportsPlugin({ config, options }),
     cssFilePlugin({ config, options }),
     absoluteCssUrlsPlugin(),
     externalPlugin(/^https?:\/\//, { sideEffects: false }),
@@ -70,7 +63,7 @@ const createEsbuildConfig = (
     serverAssetsManifestPlugin(channels),
     serverBareModulesPlugin(config, options.onWarning),
     externalPlugin(/^node:.*/, { sideEffects: false }),
-  ].filter(isNotNull);
+  ];
 
   if (config.serverPlatform !== "node") {
     plugins.unshift(NodeModulesPolyfillPlugin());
@@ -181,7 +174,3 @@ export const create = async (
     dispose: () => undefined,
   };
 };
-
-function isNotNull<Value>(value: Value): value is Exclude<Value, null> {
-  return value !== null;
-}
diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts
index a6b8e3404..8b4a0a0fe 100644
--- a/packages/remix-dev/config.ts
+++ b/packages/remix-dev/config.ts
@@ -44,19 +44,12 @@ type Dev = {
   rebuildPollIntervalMs?: number;
 };
 
-export type VanillaExtractOptions = {
-  cache?: boolean;
-};
-
 interface FutureConfig {
-  unstable_cssModules: boolean;
-  unstable_cssSideEffectImports: boolean;
   unstable_dev: boolean | Dev;
   /** @deprecated Use the `postcss` config option instead */
   unstable_postcss: boolean;
   /** @deprecated Use the `tailwind` config option instead */
   unstable_tailwind: boolean;
-  unstable_vanillaExtract: boolean | VanillaExtractOptions;
   v2_errorBoundary: boolean;
   v2_meta: boolean;
   v2_normalizeFormMethod: boolean;
@@ -481,16 +474,36 @@ export async function readConfig(
     serverModuleFormat === "esm" ? ["module", "main"] : ["main", "module"];
   serverMinify ??= false;
 
-  if (appConfig.future?.unstable_postcss !== undefined) {
-    warnOnce(
-      'The "future.unstable_postcss" config option is deprecated as this feature is now considered stable. Use the "postcss" config option instead.'
-    );
-  }
+  if (appConfig.future) {
+    if ("unstable_cssModules" in appConfig.future) {
+      warnOnce(
+        'The "future.unstable_cssModules" config option has been removed as this feature is now enabled automatically.'
+      );
+    }
 
-  if (appConfig.future?.unstable_tailwind !== undefined) {
-    warnOnce(
-      'The "future.unstable_tailwind" config option is deprecated as this feature is now considered stable. Use the "tailwind" config option instead.'
-    );
+    if ("unstable_cssSideEffectImports" in appConfig.future) {
+      warnOnce(
+        'The "future.unstable_cssSideEffectImports" config option has been removed as this feature is now enabled automatically.'
+      );
+    }
+
+    if ("unstable_vanillaExtract" in appConfig.future) {
+      warnOnce(
+        'The "future.unstable_vanillaExtract" config option has been removed as this feature is now enabled automatically.'
+      );
+    }
+
+    if (appConfig.future.unstable_postcss !== undefined) {
+      warnOnce(
+        'The "future.unstable_postcss" config option has been deprecated as this feature is now considered stable. Use the "postcss" config option instead.'
+      );
+    }
+
+    if (appConfig.future.unstable_tailwind !== undefined) {
+      warnOnce(
+        'The "future.unstable_tailwind" config option has been deprecated as this feature is now considered stable. Use the "tailwind" config option instead.'
+      );
+    }
   }
 
   let mdx = appConfig.mdx;
@@ -716,13 +729,9 @@ export async function readConfig(
   }
 
   let future: FutureConfig = {
-    unstable_cssModules: appConfig.future?.unstable_cssModules === true,
-    unstable_cssSideEffectImports:
-      appConfig.future?.unstable_cssSideEffectImports === true,
     unstable_dev: appConfig.future?.unstable_dev ?? false,
     unstable_postcss: appConfig.future?.unstable_postcss === true,
     unstable_tailwind: appConfig.future?.unstable_tailwind === true,
-    unstable_vanillaExtract: appConfig.future?.unstable_vanillaExtract ?? false,
     v2_errorBoundary: appConfig.future?.v2_errorBoundary === true,
     v2_meta: appConfig.future?.v2_meta === true,
     v2_normalizeFormMethod: appConfig.future?.v2_normalizeFormMethod === true,
@@ -904,7 +913,7 @@ export const formMethodWarning =
   "https://remix.run/docs/en/v1.15.0/pages/v2#formMethod";
 
 export const metaWarning =
-  "⚠️ REMIX FUTURE CHANGE: The route `meta` export signature is changing in v2" +
+  "⚠️ REMIX FUTURE CHANGE: The route `meta` export signature is changing in v2. " +
   "You can prepare for this change at your convenience with the `v2_meta` future flag. " +
   "For instructions on making this change see " +
   "https://remix.run/docs/en/v1.15.0/pages/v2#meta";
diff --git a/packages/remix-react/entry.ts b/packages/remix-react/entry.ts
index 62dbf4225..4df10fc98 100644
--- a/packages/remix-react/entry.ts
+++ b/packages/remix-react/entry.ts
@@ -26,19 +26,12 @@ type Dev = {
   rebuildPollIntervalMs?: number;
 };
 
-type VanillaExtractOptions = {
-  cache?: boolean;
-};
-
 export interface FutureConfig {
-  unstable_cssModules: boolean;
-  unstable_cssSideEffectImports: boolean;
   unstable_dev: boolean | Dev;
   /** @deprecated Use the `postcss` config option instead */
   unstable_postcss: boolean;
   /** @deprecated Use the `tailwind` config option instead */
   unstable_tailwind: boolean;
-  unstable_vanillaExtract: boolean | VanillaExtractOptions;
   v2_errorBoundary: boolean;
   v2_meta: boolean;
   v2_normalizeFormMethod: boolean;
diff --git a/packages/remix-server-runtime/entry.ts b/packages/remix-server-runtime/entry.ts
index d97218425..1191e01cc 100644
--- a/packages/remix-server-runtime/entry.ts
+++ b/packages/remix-server-runtime/entry.ts
@@ -18,19 +18,12 @@ type Dev = {
   rebuildPollIntervalMs?: number;
 };
 
-type VanillaExtractOptions = {
-  cache?: boolean;
-};
-
 export interface FutureConfig {
-  unstable_cssModules: boolean;
-  unstable_cssSideEffectImports: boolean;
   unstable_dev: boolean | Dev;
   /** @deprecated Use the `postcss` config option instead */
   unstable_postcss: boolean;
   /** @deprecated Use the `tailwind` config option instead */
   unstable_tailwind: boolean;
-  unstable_vanillaExtract: boolean | VanillaExtractOptions;
   v2_errorBoundary: boolean;
   v2_meta: boolean;
   v2_normalizeFormMethod: boolean;
diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json
index 825b6249c..4d265c4c5 100644
--- a/packages/remix-server-runtime/package.json
+++ b/packages/remix-server-runtime/package.json
@@ -17,8 +17,6 @@
   "module": "dist/esm/index.js",
   "dependencies": {
     "@remix-run/router": "1.5.0",
-    "@types/cookie": "^0.4.0",
-    "@types/react": "^18.0.15",
     "@web3-storage/multipart-parser": "^1.0.0",
     "cookie": "^0.4.1",
     "set-cookie-parser": "^2.4.8",
@@ -26,6 +24,8 @@
   },
   "devDependencies": {
     "@remix-run/web-file": "^3.0.2",
+    "@types/cookie": "^0.4.0",
+    "@types/react": "^18.0.15",
     "@types/set-cookie-parser": "^2.4.1"
   },
   "engines": {


Test patch:
diff --git a/integration/css-modules-test.ts b/integration/css-modules-test.ts
index bb8b664b6..e8a1b7c94 100644
--- a/integration/css-modules-test.ts
+++ b/integration/css-modules-test.ts
@@ -21,11 +21,6 @@ test.describe("CSS Modules", () => {
     fixture = await createFixture({
       future: {
         v2_routeConvention: true,
-        // Enable all CSS future flags to
-        // ensure features don't clash
-        unstable_cssModules: true,
-        unstable_cssSideEffectImports: true,
-        unstable_vanillaExtract: true,
       },
       files: {
         "app/root.jsx": js`
diff --git a/integration/css-side-effect-imports-test.ts b/integration/css-side-effect-imports-test.ts
index 33b97ba28..165cdb1f6 100644
--- a/integration/css-side-effect-imports-test.ts
+++ b/integration/css-side-effect-imports-test.ts
@@ -23,11 +23,6 @@ test.describe("CSS side-effect imports", () => {
           module.exports = {
             serverDependenciesToBundle: [/@test-package/],
             future: {
-              // Enable all CSS future flags to
-              // ensure features don't clash
-              unstable_cssModules: true,
-              unstable_cssSideEffectImports: true,
-              unstable_vanillaExtract: true,
               v2_routeConvention: true,
             },
           };
diff --git a/integration/deterministic-build-output-test.ts b/integration/deterministic-build-output-test.ts
index 451f63b89..8dfb52c73 100644
--- a/integration/deterministic-build-output-test.ts
+++ b/integration/deterministic-build-output-test.ts
@@ -27,10 +27,6 @@ test("builds deterministically under different paths", async () => {
   //  * vanillaExtractPlugin (via app/routes/foo.tsx' .css.ts file import)
   let init: FixtureInit = {
     future: {
-      unstable_cssModules: true,
-      unstable_cssSideEffectImports: true,
-      unstable_postcss: true,
-      unstable_vanillaExtract: true,
       v2_routeConvention: true,
     },
     files: {
@@ -116,66 +112,3 @@ test("builds deterministically under different paths", async () => {
     );
   });
 });
-
-test("builds Vanilla Extract files deterministically under different paths with Vanilla Extract cache enabled", async () => {
-  let init: FixtureInit = {
-    future: {
-      unstable_vanillaExtract: { cache: true },
-      v2_routeConvention: true,
-    },
-    files: {
-      "app/routes/foo.tsx": js`
-        import { vanilla } from "~/styles/vanilla.css";
-        export default () => <div className={vanilla}>YAY</div>;
-      `,
-      "app/images/foo.svg": `
-        <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
-          <circle cx="50" cy="50" r="50" fill="coral" />
-        </svg>
-      `,
-      "app/styles/vanilla.css.ts": js`
-        import { style } from "@vanilla-extract/css";
-        import { chocolate } from "./chocolate.css";
-        import imageUrl from "~/images/foo.svg";
-
-        export const vanilla = style([
-          chocolate,
-          {
-            backgroundImage: [
-              "url(" + imageUrl + ")",
-              "url(~/images/foo.svg)",
-            ],
-          }
-        ]);
-      `,
-      "app/styles/chocolate.css.ts": js`
-        import { style } from "@vanilla-extract/css";
-
-        export const chocolate = style({
-          color: "chocolate",
-        });
-      `,
-    },
-  };
-  let dir1 = await createFixtureProject(init);
-  let dir2 = await createFixtureProject(init);
-
-  expect(dir1).not.toEqual(dir2);
-
-  let files1 = await globby(["build/index.js", "public/build/**/*.{js,css}"], {
-    cwd: dir1,
-  });
-  files1 = files1.sort();
-  let files2 = await globby(["build/index.js", "public/build/**/*.{js,css}"], {
-    cwd: dir2,
-  });
-  files2 = files2.sort();
-
-  expect(files1.length).toBeGreaterThan(0);
-  expect(files1).toEqual(files2);
-  files1.forEach((file, i) => {
-    expect(fs.readFileSync(path.join(dir1, file))).toEqual(
-      fs.readFileSync(path.join(dir2, files2[i]))
-    );
-  });
-});
diff --git a/integration/hmr-test.ts b/integration/hmr-test.ts
index bd2626969..61858c787 100644
--- a/integration/hmr-test.ts
+++ b/integration/hmr-test.ts
@@ -8,18 +8,6 @@ import getPort, { makeRange } from "get-port";
 import { createFixtureProject, css, js, json } from "./helpers/create-fixture";
 
 let fixture = (options: { port: number; appServerPort: number }) => ({
-  future: {
-    unstable_dev: {
-      port: options.port,
-      appServerPort: options.appServerPort,
-    },
-    unstable_cssModules: true,
-    unstable_tailwind: true,
-    v2_routeConvention: true,
-    v2_errorBoundary: true,
-    v2_normalizeFormMethod: true,
-    v2_meta: true,
-  },
   files: {
     "remix.config.js": js`
       module.exports = {
@@ -29,7 +17,6 @@ let fixture = (options: { port: number; appServerPort: number }) => ({
             port: ${options.port},
             appServerPort: ${options.appServerPort},
           },
-          unstable_cssModules: true,
           v2_routeConvention: true,
           v2_errorBoundary: true,
           v2_normalizeFormMethod: true,
diff --git a/integration/postcss-test.ts b/integration/postcss-test.ts
index 87869004c..b476f90aa 100644
--- a/integration/postcss-test.ts
+++ b/integration/postcss-test.ts
@@ -40,9 +40,6 @@ test.describe("PostCSS enabled", () => {
             tailwind: true,
             future: {
               v2_routeConvention: true,
-              unstable_cssModules: true,
-              unstable_cssSideEffectImports: true,
-              unstable_vanillaExtract: true,
             },
           };
         `,
diff --git a/integration/tailwind-test.ts b/integration/tailwind-test.ts
index 9daa6a0ba..12e53209b 100644
--- a/integration/tailwind-test.ts
+++ b/integration/tailwind-test.ts
@@ -48,11 +48,6 @@ function runTests(ext: typeof extensions[number]) {
           module.exports = {
             tailwind: true,
             future: {
-              // Enable all CSS future flags to
-              // ensure features don't clash
-              unstable_cssModules: true,
-              unstable_cssSideEffectImports: true,
-              unstable_vanillaExtract: true,
               v2_routeConvention: true,
             },
           };
diff --git a/integration/vanilla-extract-test.ts b/integration/vanilla-extract-test.ts
index 400adef10..119f974f5 100644
--- a/integration/vanilla-extract-test.ts
+++ b/integration/vanilla-extract-test.ts
@@ -6,657 +6,641 @@ import { createAppFixture, createFixture, js } from "./helpers/create-fixture";
 
 const TEST_PADDING_VALUE = "20px";
 
-const configurations = [
-  { name: "Cached", config: { cache: true } },
-  { name: "Uncached", config: true },
-] as const;
-
 test.describe("Vanilla Extract", () => {
-  configurations.forEach((configuration) => {
-    test.describe(configuration.name, () => {
-      let fixture: Fixture;
-      let appFixture: AppFixture;
-
-      test.beforeAll(async () => {
-        fixture = await createFixture({
-          future: {
-            v2_routeConvention: true,
-            // Enable all CSS future flags to
-            // ensure features don't clash
-            unstable_cssModules: true,
-            unstable_cssSideEffectImports: true,
-            unstable_vanillaExtract: configuration.config,
-          },
-          files: {
-            "app/root.jsx": js`
-              import { Links, Outlet } from "@remix-run/react";
-              import { cssBundleHref } from "@remix-run/css-bundle";
-              export function links() {
-                return [{ rel: "stylesheet", href: cssBundleHref }];
-              }
-              export default function Root() {
-                return (
-                  <html>
-                    <head>
-                      <Links />
-                    </head>
-                    <body>
-                      <Outlet />
-                    </body>
-                  </html>
-                )
-              }
-            `,
-            ...typeScriptFixture(),
-            ...javaScriptFixture(),
-            ...classCompositionFixture(),
-            ...rootRelativeClassCompositionFixture(),
-            ...sideEffectImportsFixture(),
-            ...sideEffectImportsWithinChildCompilationFixture(),
-            ...stableIdentifiersFixture(),
-            ...imageUrlsViaCssUrlFixture(),
-            ...imageUrlsViaRootRelativeCssUrlFixture(),
-            ...imageUrlsViaAbsoluteCssUrlFixture(),
-            ...imageUrlsViaJsImportFixture(),
-            ...imageUrlsViaRootRelativeJsImportFixture(),
-            ...imageUrlsViaClassCompositionFixture(),
-            ...imageUrlsViaJsImportClassCompositionFixture(),
-          },
-        });
-        appFixture = await createAppFixture(fixture);
-      });
-
-      test.afterAll(() => appFixture.close());
+  let fixture: Fixture;
+  let appFixture: AppFixture;
 
-      let typeScriptFixture = () => ({
-        "app/fixtures/typescript/styles.css.ts": js`
-          import { style } from "@vanilla-extract/css";
-    
-          export const root = style({
-            background: 'peachpuff',
-            padding: ${JSON.stringify(TEST_PADDING_VALUE)}
-          });
-        `,
-        "app/routes/typescript-test.jsx": js`
-          import * as styles from "../fixtures/typescript/styles.css";
-    
-          export default function() {
-            return (
-              <div data-testid="typescript" className={styles.root}>
-                TypeScript test
-              </div>
-            )
+  test.beforeAll(async () => {
+    fixture = await createFixture({
+      future: {
+        v2_routeConvention: true,
+      },
+      files: {
+        "app/root.jsx": js`
+          import { Links, Outlet } from "@remix-run/react";
+          import { cssBundleHref } from "@remix-run/css-bundle";
+          export function links() {
+            return [{ rel: "stylesheet", href: cssBundleHref }];
           }
-        `,
-      });
-      test("TypeScript", async ({ page }) => {
-        let app = new PlaywrightFixture(appFixture, page);
-        await app.goto("/typescript-test");
-        let locator = await page.locator("[data-testid='typescript']");
-        let padding = await locator.evaluate(
-          (element) => window.getComputedStyle(element).padding
-        );
-        expect(padding).toBe(TEST_PADDING_VALUE);
-      });
-
-      let javaScriptFixture = () => ({
-        "app/fixtures/javascript/styles.css.js": js`
-          import { style } from "@vanilla-extract/css";
-    
-          export const root = style({
-            background: 'peachpuff',
-            padding: ${JSON.stringify(TEST_PADDING_VALUE)}
-          });
-        `,
-        "app/routes/javascript-test.jsx": js`
-          import * as styles from "../fixtures/javascript/styles.css";
-    
-          export default function() {
+          export default function Root() {
             return (
-              <div data-testid="javascript" className={styles.root}>
-                javaScript test
-              </div>
+              <html>
+                <head>
+                  <Links />
+                </head>
+                <body>
+                  <Outlet />
+                </body>
+              </html>
             )
           }
         `,
-      });
-      test("JavaScript", async ({ page }) => {
-        let app = new PlaywrightFixture(appFixture, page);
-        await app.goto("/javascript-test");
-        let locator = await page.locator("[data-testid='javascript']");
-        let padding = await locator.evaluate(
-          (element) => window.getComputedStyle(element).padding
-        );
-        expect(padding).toBe(TEST_PADDING_VALUE);
-      });
+        ...typeScriptFixture(),
+        ...javaScriptFixture(),
+        ...classCompositionFixture(),
+        ...rootRelativeClassCompositionFixture(),
+        ...sideEffectImportsFixture(),
+        ...sideEffectImportsWithinChildCompilationFixture(),
+        ...stableIdentifiersFixture(),
+        ...imageUrlsViaCssUrlFixture(),
+        ...imageUrlsViaRootRelativeCssUrlFixture(),
+        ...imageUrlsViaAbsoluteCssUrlFixture(),
+        ...imageUrlsViaJsImportFixture(),
+        ...imageUrlsViaRootRelativeJsImportFixture(),
+        ...imageUrlsViaClassCompositionFixture(),
+        ...imageUrlsViaJsImportClassCompositionFixture(),
+      },
+    });
+    appFixture = await createAppFixture(fixture);
+  });
 
-      let classCompositionFixture = () => ({
-        "app/fixtures/class-composition/styles.css.ts": js`
-          import { style } from "@vanilla-extract/css";
-          import { padding } from "./padding.css";
-    
-          export const root = style([
-            padding,
-            { background: 'peachpuff' },
-          ]);
-        `,
-        "app/fixtures/class-composition/padding.css.ts": js`
-          import { style } from "@vanilla-extract/css";
-    
-          export const padding = style({
-            padding: ${JSON.stringify(TEST_PADDING_VALUE)}
-          });
-        `,
-        "app/routes/class-composition-test.jsx": js`
-          import * as styles from "../fixtures/class-composition/styles.css";
-    
-          export default function() {
-            return (
-              <div data-testid="class-composition" className={styles.root}>
-                Class composition test
-              </div>
-            )
-          }
-        `,
-      });
-      test("class composition", async ({ page }) => {
-        let app = new PlaywrightFixture(appFixture, page);
-        await app.goto("/class-composition-test");
-        let locator = await page.locator("[data-testid='class-composition']");
-        let padding = await locator.evaluate(
-          (element) => window.getComputedStyle(element).padding
-        );
-        expect(padding).toBe(TEST_PADDING_VALUE);
-      });
+  test.afterAll(() => appFixture.close());
 
-      let rootRelativeClassCompositionFixture = () => ({
-        "app/fixtures/root-relative-class-composition/styles.css.ts": js`
-          import { style } from "@vanilla-extract/css";
-          import { padding } from "~/fixtures/root-relative-class-composition/padding.css";
-    
-          export const root = style([
-            padding,
-            { background: 'peachpuff' },
-          ]);
-        `,
-        "app/fixtures/root-relative-class-composition/padding.css.ts": js`
-          import { style } from "@vanilla-extract/css";
-    
-          export const padding = style({
-            padding: ${JSON.stringify(TEST_PADDING_VALUE)}
-          });
-        `,
-        "app/routes/root-relative-class-composition-test.jsx": js`
-          import * as styles from "../fixtures/root-relative-class-composition/styles.css";
-    
-          export default function() {
-            return (
-              <div data-testid="root-relative-class-composition" className={styles.root}>
-                Root-relative class composition test
-              </div>
-            )
-          }
-        `,
-      });
-      test("root-relative class composition", async ({ page }) => {
-        let app = new PlaywrightFixture(appFixture, page);
-        await app.goto("/root-relative-class-composition-test");
-        let locator = await page.locator(
-          "[data-testid='root-relative-class-composition']"
-        );
-        let padding = await locator.evaluate(
-          (element) => window.getComputedStyle(element).padding
-        );
-        expect(padding).toBe(TEST_PADDING_VALUE);
-      });
+  let typeScriptFixture = () => ({
+    "app/fixtures/typescript/styles.css.ts": js`
+      import { style } from "@vanilla-extract/css";
 
-      let sideEffectImportsFixture = () => ({
-        "app/fixtures/side-effect-imports/styles.css.ts": js`
-          import { globalStyle } from "@vanilla-extract/css";
-    
-          globalStyle(".side-effect-imports", {
-            padding: ${JSON.stringify(TEST_PADDING_VALUE)}
-          });
-        `,
-        "app/routes/side-effect-imports-test.jsx": js`
-          import "../fixtures/side-effect-imports/styles.css";
-    
-          export default function() {
-            return (
-              <div data-testid="side-effect-imports" className="side-effect-imports">
-                Side-effect imports test
-              </div>
-            )
-          }
-        `,
-      });
-      test("side-effect imports", async ({ page }) => {
-        let app = new PlaywrightFixture(appFixture, page);
-        await app.goto("/side-effect-imports-test");
-        let locator = await page.locator("[data-testid='side-effect-imports']");
-        let padding = await locator.evaluate(
-          (element) => window.getComputedStyle(element).padding
-        );
-        expect(padding).toBe(TEST_PADDING_VALUE);
+      export const root = style({
+        background: 'peachpuff',
+        padding: ${JSON.stringify(TEST_PADDING_VALUE)}
       });
+    `,
+    "app/routes/typescript-test.jsx": js`
+      import * as styles from "../fixtures/typescript/styles.css";
 
-      let sideEffectImportsWithinChildCompilationFixture = () => ({
-        "app/fixtures/side-effect-imports-within-child-compilation/styles.css.ts": js`
-          import "./nested-side-effect.css";
-        `,
-        "app/fixtures/side-effect-imports-within-child-compilation/nested-side-effect.css.ts": js`
-          import { globalStyle } from "@vanilla-extract/css";
-    
-          globalStyle(".side-effect-imports-within-child-compilation", {
-            padding: ${JSON.stringify(TEST_PADDING_VALUE)}
-          });
-        `,
-        "app/routes/side-effect-imports-within-child-compilation-test.jsx": js`
-          import "../fixtures/side-effect-imports-within-child-compilation/styles.css";
-    
-          export default function() {
-            return (
-              <div data-testid="side-effect-imports-within-child-compilation" className="side-effect-imports-within-child-compilation">
-                Side-effect imports within child compilation test
-              </div>
-            )
-          }
-        `,
-      });
-      test("side-effect imports within child compilation", async ({ page }) => {
-        let app = new PlaywrightFixture(appFixture, page);
-        await app.goto("/side-effect-imports-within-child-compilation-test");
-        let locator = await page.locator(
-          "[data-testid='side-effect-imports-within-child-compilation']"
-        );
-        let padding = await locator.evaluate(
-          (element) => window.getComputedStyle(element).padding
-        );
-        expect(padding).toBe(TEST_PADDING_VALUE);
-      });
+      export default function() {
+        return (
+          <div data-testid="typescript" className={styles.root}>
+            TypeScript test
+          </div>
+        )
+      }
+    `,
+  });
+  test("TypeScript", async ({ page }) => {
+    let app = new PlaywrightFixture(appFixture, page);
+    await app.goto("/typescript-test");
+    let locator = await page.locator("[data-testid='typescript']");
+    let padding = await locator.evaluate(
+      (element) => window.getComputedStyle(element).padding
+    );
+    expect(padding).toBe(TEST_PADDING_VALUE);
+  });
 
-      let stableIdentifiersFixture = () => ({
-        "app/fixtures/stable-identifiers/styles_a.css.ts": js`
-          import { style } from "@vanilla-extract/css";
-          import { shared } from "./shared.css";
-    
-          export const root = shared;
-        `,
-        "app/fixtures/stable-identifiers/styles_b.css.ts": js`
-          import { style } from "@vanilla-extract/css";
-          import { shared } from "./shared.css";
-    
-          export const root = shared;
-        `,
-        "app/fixtures/stable-identifiers/shared.css.ts": js`
-          import { style } from "@vanilla-extract/css";
-    
-          export const shared = style({
-            padding: ${JSON.stringify(TEST_PADDING_VALUE)},
-            background: 'peachpuff',
-          });
-        `,
-        "app/routes/stable-identifiers-test.jsx": js`
-          import * as styles_a from "../fixtures/stable-identifiers/styles_a.css";
-          import * as styles_b from "../fixtures/stable-identifiers/styles_b.css";
-    
-          const styles = new Set([styles_a.root, styles_b.root]);
-    
-          export default function() {
-            return (
-              <div data-testid="stable-identifiers" className={Array.from(styles).join(' ')}>
-                Stable identifiers test
-              </div>
-            )
-          }
-        `,
-      });
-      test("stable identifiers", async ({ page }) => {
-        // This test ensures that file scoping is working as expected and
-        // identifiers are stable across different .css.ts contexts. We test this by
-        // using the same shared style in two different .css.ts files and then
-        // asserting that it's the same class name.
-        let app = new PlaywrightFixture(appFixture, page);
-        await app.goto("/stable-identifiers-test");
-        let locator = await page.locator("[data-testid='stable-identifiers']");
-        let { padding, classList } = await locator.evaluate((element) => ({
-          padding: window.getComputedStyle(element).padding,
-          classList: Array.from(element.classList),
-        }));
-        expect(padding).toBe(TEST_PADDING_VALUE);
-        expect(classList.length).toBe(1);
-      });
+  let javaScriptFixture = () => ({
+    "app/fixtures/javascript/styles.css.js": js`
+      import { style } from "@vanilla-extract/css";
 
-      let imageUrlsViaCssUrlFixture = () => ({
-        "app/fixtures/imageUrlsViaCssUrl/styles.css.ts": js`
-          import { style } from "@vanilla-extract/css";
-    
-          export const root = style({
-            backgroundColor: 'peachpuff',
-            backgroundImage: 'url("./image.svg")',
-            padding: ${JSON.stringify(TEST_PADDING_VALUE)}
-          });
-        `,
-        "app/fixtures/imageUrlsViaCssUrl/image.svg": `
-          <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
-            <circle cx="50" cy="50" r="50" fill="coral" />
-          </svg>
-        `,
-        "app/routes/image-urls-via-css-url-test.jsx": js`
-          import * as styles from "../fixtures/imageUrlsViaCssUrl/styles.css";
-    
-          export default function() {
-            return (
-              <div data-testid="image-urls-via-css-url" className={styles.root}>
-                Image URLs via CSS URL test
-              </div>
-            )
-          }
-        `,
-      });
-      test("image URLs via CSS URL", async ({ page }) => {
-        let app = new PlaywrightFixture(appFixture, page);
-        let imgStatus: number | null = null;
-        app.page.on("response", (res) => {
-          if (res.url().endsWith(".svg")) imgStatus = res.status();
-        });
-        await app.goto("/image-urls-via-css-url-test");
-        let locator = await page.locator(
-          "[data-testid='image-urls-via-css-url']"
-        );
-        let backgroundImage = await locator.evaluate(
-          (element) => window.getComputedStyle(element).backgroundImage
-        );
-        expect(backgroundImage).toContain(".svg");
-        expect(imgStatus).toBe(200);
+      export const root = style({
+        background: 'peachpuff',
+        padding: ${JSON.stringify(TEST_PADDING_VALUE)}
       });
+    `,
+    "app/routes/javascript-test.jsx": js`
+      import * as styles from "../fixtures/javascript/styles.css";
 
-      let imageUrlsViaRootRelativeCssUrlFixture = () => ({
-        "app/fixtures/imageUrlsViaRootRelativeCssUrl/styles.css.ts": js`
-          import { style } from "@vanilla-extract/css";
-    
-          export const root = style({
-            backgroundColor: 'peachpuff',
-            backgroundImage: 'url("~/fixtures/imageUrlsViaRootRelativeCssUrl/image.svg")',
-            padding: ${JSON.stringify(TEST_PADDING_VALUE)}
-          });
-        `,
-        "app/fixtures/imageUrlsViaRootRelativeCssUrl/image.svg": `
-          <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
-            <circle cx="50" cy="50" r="50" fill="coral" />
-          </svg>
-        `,
-        "app/routes/image-urls-via-root-relative-css-url-test.jsx": js`
-          import * as styles from "../fixtures/imageUrlsViaRootRelativeCssUrl/styles.css";
-    
-          export default function() {
-            return (
-              <div data-testid="image-urls-via-root-relative-css-url" className={styles.root}>
-                Image URLs via CSS URL test
-              </div>
-            )
-          }
-        `,
+      export default function() {
+        return (
+          <div data-testid="javascript" className={styles.root}>
+            javaScript test
+          </div>
+        )
+      }
+    `,
+  });
+  test("JavaScript", async ({ page }) => {
+    let app = new PlaywrightFixture(appFixture, page);
+    await app.goto("/javascript-test");
+    let locator = await page.locator("[data-testid='javascript']");
+    let padding = await locator.evaluate(
+      (element) => window.getComputedStyle(element).padding
+    );
+    expect(padding).toBe(TEST_PADDING_VALUE);
+  });
+
+  let classCompositionFixture = () => ({
+    "app/fixtures/class-composition/styles.css.ts": js`
+      import { style } from "@vanilla-extract/css";
+      import { padding } from "./padding.css";
+
+      export const root = style([
+        padding,
+        { background: 'peachpuff' },
+      ]);
+    `,
+    "app/fixtures/class-composition/padding.css.ts": js`
+      import { style } from "@vanilla-extract/css";
+
+      export const padding = style({
+        padding: ${JSON.stringify(TEST_PADDING_VALUE)}
       });
-      test("image URLs via root-relative CSS URL", async ({ page }) => {
-        let app = new PlaywrightFixture(appFixture, page);
-        let imgStatus: number | null = null;
-        app.page.on("response", (res) => {
-          if (res.url().endsWith(".svg")) imgStatus = res.status();
-        });
-        await app.goto("/image-urls-via-root-relative-css-url-test");
-        let locator = await page.locator(
-          "[data-testid='image-urls-via-root-relative-css-url']"
-        );
-        let backgroundImage = await locator.evaluate(
-          (element) => window.getComputedStyle(element).backgroundImage
-        );
-        expect(backgroundImage).toContain(".svg");
-        expect(imgStatus).toBe(200);
+    `,
+    "app/routes/class-composition-test.jsx": js`
+      import * as styles from "../fixtures/class-composition/styles.css";
+
+      export default function() {
+        return (
+          <div data-testid="class-composition" className={styles.root}>
+            Class composition test
+          </div>
+        )
+      }
+    `,
+  });
+  test("class composition", async ({ page }) => {
+    let app = new PlaywrightFixture(appFixture, page);
+    await app.goto("/class-composition-test");
+    let locator = await page.locator("[data-testid='class-composition']");
+    let padding = await locator.evaluate(
+      (element) => window.getComputedStyle(element).padding
+    );
+    expect(padding).toBe(TEST_PADDING_VALUE);
+  });
+
+  let rootRelativeClassCompositionFixture = () => ({
+    "app/fixtures/root-relative-class-composition/styles.css.ts": js`
+      import { style } from "@vanilla-extract/css";
+      import { padding } from "~/fixtures/root-relative-class-composition/padding.css";
+
+      export const root = style([
+        padding,
+        { background: 'peachpuff' },
+      ]);
+    `,
+    "app/fixtures/root-relative-class-composition/padding.css.ts": js`
+      import { style } from "@vanilla-extract/css";
+
+      export const padding = style({
+        padding: ${JSON.stringify(TEST_PADDING_VALUE)}
       });
+    `,
+    "app/routes/root-relative-class-composition-test.jsx": js`
+      import * as styles from "../fixtures/root-relative-class-composition/styles.css";
 
-      let imageUrlsViaAbsoluteCssUrlFixture = () => ({
-        "app/fixtures/imageUrlsViaAbsoluteCssUrl/styles.css.ts": js`
-          import { style } from "@vanilla-extract/css";
-    
-          export const root = style({
-            backgroundColor: 'peachpuff',
-            backgroundImage: 'url("/imageUrlsViaAbsoluteCssUrl/image.svg")',
-            padding: ${JSON.stringify(TEST_PADDING_VALUE)}
-          });
-        `,
-        "public/imageUrlsViaAbsoluteCssUrl/image.svg": `
-          <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
-            <circle cx="50" cy="50" r="50" fill="coral" />
-          </svg>
-        `,
-        "app/routes/image-urls-via-absolute-css-url-test.jsx": js`
-          import * as styles from "../fixtures/imageUrlsViaAbsoluteCssUrl/styles.css";
-    
-          export default function() {
-            return (
-              <div data-testid="image-urls-via-absolute-css-url" className={styles.root}>
-                Image URLs via absolute CSS URL test
-              </div>
-            )
-          }
-        `,
+      export default function() {
+        return (
+          <div data-testid="root-relative-class-composition" className={styles.root}>
+            Root-relative class composition test
+          </div>
+        )
+      }
+    `,
+  });
+  test("root-relative class composition", async ({ page }) => {
+    let app = new PlaywrightFixture(appFixture, page);
+    await app.goto("/root-relative-class-composition-test");
+    let locator = await page.locator(
+      "[data-testid='root-relative-class-composition']"
+    );
+    let padding = await locator.evaluate(
+      (element) => window.getComputedStyle(element).padding
+    );
+    expect(padding).toBe(TEST_PADDING_VALUE);
+  });
+
+  let sideEffectImportsFixture = () => ({
+    "app/fixtures/side-effect-imports/styles.css.ts": js`
+      import { globalStyle } from "@vanilla-extract/css";
+
+      globalStyle(".side-effect-imports", {
+        padding: ${JSON.stringify(TEST_PADDING_VALUE)}
       });
-      test("image URLs via absolute CSS URL", async ({ page }) => {
-        let app = new PlaywrightFixture(appFixture, page);
-        let imgStatus: number | null = null;
-        app.page.on("response", (res) => {
-          if (res.url().endsWith(".svg")) imgStatus = res.status();
-        });
-        await app.goto("/image-urls-via-absolute-css-url-test");
-        let locator = await page.locator(
-          "[data-testid='image-urls-via-absolute-css-url']"
-        );
-        let backgroundImage = await locator.evaluate(
-          (element) => window.getComputedStyle(element).backgroundImage
-        );
-        expect(backgroundImage).toContain(".svg");
-        expect(imgStatus).toBe(200);
+    `,
+    "app/routes/side-effect-imports-test.jsx": js`
+      import "../fixtures/side-effect-imports/styles.css";
+
+      export default function() {
+        return (
+          <div data-testid="side-effect-imports" className="side-effect-imports">
+            Side-effect imports test
+          </div>
+        )
+      }
+    `,
+  });
+  test("side-effect imports", async ({ page }) => {
+    let app = new PlaywrightFixture(appFixture, page);
+    await app.goto("/side-effect-imports-test");
+    let locator = await page.locator("[data-testid='side-effect-imports']");
+    let padding = await locator.evaluate(
+      (element) => window.getComputedStyle(element).padding
+    );
+    expect(padding).toBe(TEST_PADDING_VALUE);
+  });
+
+  let sideEffectImportsWithinChildCompilationFixture = () => ({
+    "app/fixtures/side-effect-imports-within-child-compilation/styles.css.ts": js`
+      import "./nested-side-effect.css";
+    `,
+    "app/fixtures/side-effect-imports-within-child-compilation/nested-side-effect.css.ts": js`
+      import { globalStyle } from "@vanilla-extract/css";
+
+      globalStyle(".side-effect-imports-within-child-compilation", {
+        padding: ${JSON.stringify(TEST_PADDING_VALUE)}
       });
+    `,
+    "app/routes/side-effect-imports-within-child-compilation-test.jsx": js`
+      import "../fixtures/side-effect-imports-within-child-compilation/styles.css";
 
-      let imageUrlsViaJsImportFixture = () => ({
-        "app/fixtures/imageUrlsViaJsImport/styles.css.ts": js`
-          import { style } from "@vanilla-extract/css";
-          import href from "./image.svg";
-    
-          export const root = style({
-            backgroundColor: 'peachpuff',
-            backgroundImage: 'url(' + href + ')',
-            padding: ${JSON.stringify(TEST_PADDING_VALUE)}
-          });
-        `,
-        "app/fixtures/imageUrlsViaJsImport/image.svg": `
-          <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
-            <circle cx="50" cy="50" r="50" fill="coral" />
-          </svg>
-        `,
-        "app/routes/image-urls-via-js-import-test.jsx": js`
-          import * as styles from "../fixtures/imageUrlsViaJsImport/styles.css";
-    
-          export default function() {
-            return (
-              <div data-testid="image-urls-via-js-import" className={styles.root}>
-                Image URLs via JS import test
-              </div>
-            )
-          }
-        `,
+      export default function() {
+        return (
+          <div data-testid="side-effect-imports-within-child-compilation" className="side-effect-imports-within-child-compilation">
+            Side-effect imports within child compilation test
+          </div>
+        )
+      }
+    `,
+  });
+  test("side-effect imports within child compilation", async ({ page }) => {
+    let app = new PlaywrightFixture(appFixture, page);
+    await app.goto("/side-effect-imports-within-child-compilation-test");
+    let locator = await page.locator(
+      "[data-testid='side-effect-imports-within-child-compilation']"
+    );
+    let padding = await locator.evaluate(
+      (element) => window.getComputedStyle(element).padding
+    );
+    expect(padding).toBe(TEST_PADDING_VALUE);
+  });
+
+  let stableIdentifiersFixture = () => ({
+    "app/fixtures/stable-identifiers/styles_a.css.ts": js`
+      import { style } from "@vanilla-extract/css";
+      import { shared } from "./shared.css";
+
+      export const root = shared;
+    `,
+    "app/fixtures/stable-identifiers/styles_b.css.ts": js`
+      import { style } from "@vanilla-extract/css";
+      import { shared } from "./shared.css";
+
+      export const root = shared;
+    `,
+    "app/fixtures/stable-identifiers/shared.css.ts": js`
+      import { style } from "@vanilla-extract/css";
+
+      export const shared = style({
+        padding: ${JSON.stringify(TEST_PADDING_VALUE)},
+        background: 'peachpuff',
       });
-      test("image URLs via JS import", async ({ page }) => {
-        let app = new PlaywrightFixture(appFixture, page);
-        let imgStatus: number | null = null;
-        app.page.on("response", (res) => {
-          if (res.url().endsWith(".svg")) imgStatus = res.status();
-        });
-        await app.goto("/image-urls-via-js-import-test");
-        let locator = await page.locator(
-          "[data-testid='image-urls-via-js-import']"
-        );
-        let backgroundImage = await locator.evaluate(
-          (element) => window.getComputedStyle(element).backgroundImage
-        );
-        expect(backgroundImage).toContain(".svg");
-        expect(imgStatus).toBe(200);
+    `,
+    "app/routes/stable-identifiers-test.jsx": js`
+      import * as styles_a from "../fixtures/stable-identifiers/styles_a.css";
+      import * as styles_b from "../fixtures/stable-identifiers/styles_b.css";
+
+      const styles = new Set([styles_a.root, styles_b.root]);
+
+      export default function() {
+        return (
+          <div data-testid="stable-identifiers" className={Array.from(styles).join(' ')}>
+            Stable identifiers test
+          </div>
+        )
+      }
+    `,
+  });
+  test("stable identifiers", async ({ page }) => {
+    // This test ensures that file scoping is working as expected and
+    // identifiers are stable across different .css.ts contexts. We test this by
+    // using the same shared style in two different .css.ts files and then
+    // asserting that it's the same class name.
+    let app = new PlaywrightFixture(appFixture, page);
+    await app.goto("/stable-identifiers-test");
+    let locator = await page.locator("[data-testid='stable-identifiers']");
+    let { padding, classList } = await locator.evaluate((element) => ({
+      padding: window.getComputedStyle(element).padding,
+      classList: Array.from(element.classList),
+    }));
+    expect(padding).toBe(TEST_PADDING_VALUE);
+    expect(classList.length).toBe(1);
+  });
+
+  let imageUrlsViaCssUrlFixture = () => ({
+    "app/fixtures/imageUrlsViaCssUrl/styles.css.ts": js`
+      import { style } from "@vanilla-extract/css";
+
+      export const root = style({
+        backgroundColor: 'peachpuff',
+        backgroundImage: 'url("./image.svg")',
+        padding: ${JSON.stringify(TEST_PADDING_VALUE)}
       });
+    `,
+    "app/fixtures/imageUrlsViaCssUrl/image.svg": `
+      <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+        <circle cx="50" cy="50" r="50" fill="coral" />
+      </svg>
+    `,
+    "app/routes/image-urls-via-css-url-test.jsx": js`
+      import * as styles from "../fixtures/imageUrlsViaCssUrl/styles.css";
 
-      let imageUrlsViaRootRelativeJsImportFixture = () => ({
-        "app/fixtures/imageUrlsViaRootRelativeJsImport/styles.css.ts": js`
-          import { style } from "@vanilla-extract/css";
-          import href from "~/fixtures/imageUrlsViaRootRelativeJsImport/image.svg";
-    
-          export const root = style({
-            backgroundColor: 'peachpuff',
-            backgroundImage: 'url(' + href + ')',
-            padding: ${JSON.stringify(TEST_PADDING_VALUE)}
-          });
-        `,
-        "app/fixtures/imageUrlsViaRootRelativeJsImport/image.svg": `
-          <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
-            <circle cx="50" cy="50" r="50" fill="coral" />
-          </svg>
-        `,
-        "app/routes/image-urls-via-root-relative-js-import-test.jsx": js`
-          import * as styles from "../fixtures/imageUrlsViaRootRelativeJsImport/styles.css";
-    
-          export default function() {
-            return (
-              <div data-testid="image-urls-via-root-relative-js-import" className={styles.root}>
-                Image URLs via root-relative JS import test
-              </div>
-            )
-          }
-        `,
+      export default function() {
+        return (
+          <div data-testid="image-urls-via-css-url" className={styles.root}>
+            Image URLs via CSS URL test
+          </div>
+        )
+      }
+    `,
+  });
+  test("image URLs via CSS URL", async ({ page }) => {
+    let app = new PlaywrightFixture(appFixture, page);
+    let imgStatus: number | null = null;
+    app.page.on("response", (res) => {
+      if (res.url().endsWith(".svg")) imgStatus = res.status();
+    });
+    await app.goto("/image-urls-via-css-url-test");
+    let locator = await page.locator("[data-testid='image-urls-via-css-url']");
+    let backgroundImage = await locator.evaluate(
+      (element) => window.getComputedStyle(element).backgroundImage
+    );
+    expect(backgroundImage).toContain(".svg");
+    expect(imgStatus).toBe(200);
+  });
+
+  let imageUrlsViaRootRelativeCssUrlFixture = () => ({
+    "app/fixtures/imageUrlsViaRootRelativeCssUrl/styles.css.ts": js`
+      import { style } from "@vanilla-extract/css";
+
+      export const root = style({
+        backgroundColor: 'peachpuff',
+        backgroundImage: 'url("~/fixtures/imageUrlsViaRootRelativeCssUrl/image.svg")',
+        padding: ${JSON.stringify(TEST_PADDING_VALUE)}
       });
-      test("image URLs via root-relative JS import", async ({ page }) => {
-        let app = new PlaywrightFixture(appFixture, page);
-        let imgStatus: number | null = null;
-        app.page.on("response", (res) => {
-          if (res.url().endsWith(".svg")) imgStatus = res.status();
-        });
-        await app.goto("/image-urls-via-root-relative-js-import-test");
-        let locator = await page.locator(
-          "[data-testid='image-urls-via-root-relative-js-import']"
-        );
-        let backgroundImage = await locator.evaluate(
-          (element) => window.getComputedStyle(element).backgroundImage
-        );
-        expect(backgroundImage).toContain(".svg");
-        expect(imgStatus).toBe(200);
+    `,
+    "app/fixtures/imageUrlsViaRootRelativeCssUrl/image.svg": `
+      <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+        <circle cx="50" cy="50" r="50" fill="coral" />
+      </svg>
+    `,
+    "app/routes/image-urls-via-root-relative-css-url-test.jsx": js`
+      import * as styles from "../fixtures/imageUrlsViaRootRelativeCssUrl/styles.css";
+
+      export default function() {
+        return (
+          <div data-testid="image-urls-via-root-relative-css-url" className={styles.root}>
+            Image URLs via CSS URL test
+          </div>
+        )
+      }
+    `,
+  });
+  test("image URLs via root-relative CSS URL", async ({ page }) => {
+    let app = new PlaywrightFixture(appFixture, page);
+    let imgStatus: number | null = null;
+    app.page.on("response", (res) => {
+      if (res.url().endsWith(".svg")) imgStatus = res.status();
+    });
+    await app.goto("/image-urls-via-root-relative-css-url-test");
+    let locator = await page.locator(
+      "[data-testid='image-urls-via-root-relative-css-url']"
+    );
+    let backgroundImage = await locator.evaluate(
+      (element) => window.getComputedStyle(element).backgroundImage
+    );
+    expect(backgroundImage).toContain(".svg");
+    expect(imgStatus).toBe(200);
+  });
+
+  let imageUrlsViaAbsoluteCssUrlFixture = () => ({
+    "app/fixtures/imageUrlsViaAbsoluteCssUrl/styles.css.ts": js`
+      import { style } from "@vanilla-extract/css";
+
+      export const root = style({
+        backgroundColor: 'peachpuff',
+        backgroundImage: 'url("/imageUrlsViaAbsoluteCssUrl/image.svg")',
+        padding: ${JSON.stringify(TEST_PADDING_VALUE)}
       });
+    `,
+    "public/imageUrlsViaAbsoluteCssUrl/image.svg": `
+      <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+        <circle cx="50" cy="50" r="50" fill="coral" />
+      </svg>
+    `,
+    "app/routes/image-urls-via-absolute-css-url-test.jsx": js`
+      import * as styles from "../fixtures/imageUrlsViaAbsoluteCssUrl/styles.css";
 
-      let imageUrlsViaClassCompositionFixture = () => ({
-        "app/fixtures/imageUrlsViaClassComposition/styles.css.ts": js`
-          import { style } from "@vanilla-extract/css";
-          import { backgroundImage } from "./nested/backgroundImage.css";
-    
-          export const root = style([
-            backgroundImage,
-            {
-              backgroundColor: 'peachpuff',
-              padding: ${JSON.stringify(TEST_PADDING_VALUE)}
-            }
-          ]);
-        `,
-        "app/fixtures/imageUrlsViaClassComposition/nested/backgroundImage.css.ts": js`
-          import { style } from "@vanilla-extract/css";
-    
-          export const backgroundImage = style({
-            backgroundImage: 'url(../image.svg)',
-          });
-        `,
-        "app/fixtures/imageUrlsViaClassComposition/image.svg": `
-          <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
-            <circle cx="50" cy="50" r="50" fill="coral" />
-          </svg>
-        `,
-        "app/routes/image-urls-via-class-composition-test.jsx": js`
-          import * as styles from "../fixtures/imageUrlsViaClassComposition/styles.css";
-    
-          export default function() {
-            return (
-              <div data-testid="image-urls-via-class-composition" className={styles.root}>
-                Image URLs via class composition test
-              </div>
-            )
-          }
-        `,
+      export default function() {
+        return (
+          <div data-testid="image-urls-via-absolute-css-url" className={styles.root}>
+            Image URLs via absolute CSS URL test
+          </div>
+        )
+      }
+    `,
+  });
+  test("image URLs via absolute CSS URL", async ({ page }) => {
+    let app = new PlaywrightFixture(appFixture, page);
+    let imgStatus: number | null = null;
+    app.page.on("response", (res) => {
+      if (res.url().endsWith(".svg")) imgStatus = res.status();
+    });
+    await app.goto("/image-urls-via-absolute-css-url-test");
+    let locator = await page.locator(
+      "[data-testid='image-urls-via-absolute-css-url']"
+    );
+    let backgroundImage = await locator.evaluate(
+      (element) => window.getComputedStyle(element).backgroundImage
+    );
+    expect(backgroundImage).toContain(".svg");
+    expect(imgStatus).toBe(200);
+  });
+
+  let imageUrlsViaJsImportFixture = () => ({
+    "app/fixtures/imageUrlsViaJsImport/styles.css.ts": js`
+      import { style } from "@vanilla-extract/css";
+      import href from "./image.svg";
+
+      export const root = style({
+        backgroundColor: 'peachpuff',
+        backgroundImage: 'url(' + href + ')',
+        padding: ${JSON.stringify(TEST_PADDING_VALUE)}
       });
-      test("image URLs via class composition", async ({ page }) => {
-        let app = new PlaywrightFixture(appFixture, page);
-        let imgStatus: number | null = null;
-        app.page.on("response", (res) => {
-          if (res.url().endsWith(".svg")) imgStatus = res.status();
-        });
-        await app.goto("/image-urls-via-class-composition-test");
-        let locator = await page.locator(
-          "[data-testid='image-urls-via-class-composition']"
-        );
-        let backgroundImage = await locator.evaluate(
-          (element) => window.getComputedStyle(element).backgroundImage
-        );
-        expect(backgroundImage).toContain(".svg");
-        expect(imgStatus).toBe(200);
+    `,
+    "app/fixtures/imageUrlsViaJsImport/image.svg": `
+      <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+        <circle cx="50" cy="50" r="50" fill="coral" />
+      </svg>
+    `,
+    "app/routes/image-urls-via-js-import-test.jsx": js`
+      import * as styles from "../fixtures/imageUrlsViaJsImport/styles.css";
+
+      export default function() {
+        return (
+          <div data-testid="image-urls-via-js-import" className={styles.root}>
+            Image URLs via JS import test
+          </div>
+        )
+      }
+    `,
+  });
+  test("image URLs via JS import", async ({ page }) => {
+    let app = new PlaywrightFixture(appFixture, page);
+    let imgStatus: number | null = null;
+    app.page.on("response", (res) => {
+      if (res.url().endsWith(".svg")) imgStatus = res.status();
+    });
+    await app.goto("/image-urls-via-js-import-test");
+    let locator = await page.locator(
+      "[data-testid='image-urls-via-js-import']"
+    );
+    let backgroundImage = await locator.evaluate(
+      (element) => window.getComputedStyle(element).backgroundImage
+    );
+    expect(backgroundImage).toContain(".svg");
+    expect(imgStatus).toBe(200);
+  });
+
+  let imageUrlsViaRootRelativeJsImportFixture = () => ({
+    "app/fixtures/imageUrlsViaRootRelativeJsImport/styles.css.ts": js`
+      import { style } from "@vanilla-extract/css";
+      import href from "~/fixtures/imageUrlsViaRootRelativeJsImport/image.svg";
+
+      export const root = style({
+        backgroundColor: 'peachpuff',
+        backgroundImage: 'url(' + href + ')',
+        padding: ${JSON.stringify(TEST_PADDING_VALUE)}
       });
+    `,
+    "app/fixtures/imageUrlsViaRootRelativeJsImport/image.svg": `
+      <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+        <circle cx="50" cy="50" r="50" fill="coral" />
+      </svg>
+    `,
+    "app/routes/image-urls-via-root-relative-js-import-test.jsx": js`
+      import * as styles from "../fixtures/imageUrlsViaRootRelativeJsImport/styles.css";
 
-      let imageUrlsViaJsImportClassCompositionFixture = () => ({
-        "app/fixtures/imageUrlsViaJsImportClassComposition/styles.css.ts": js`
-          import { style } from "@vanilla-extract/css";
-          import { backgroundImage } from "./nested/backgroundImage.css";
-    
-          export const root = style([
-            backgroundImage,
-            {
-              backgroundColor: 'peachpuff',
-              padding: ${JSON.stringify(TEST_PADDING_VALUE)}
-            }
-          ]);
-        `,
-        "app/fixtures/imageUrlsViaJsImportClassComposition/nested/backgroundImage.css.ts": js`
-          import { style } from "@vanilla-extract/css";
-          import href from "../image.svg";
-    
-          export const backgroundImage = style({
-            backgroundImage: 'url(' + href + ')',
-          });
-        `,
-        "app/fixtures/imageUrlsViaJsImportClassComposition/image.svg": `
-          <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
-            <circle cx="50" cy="50" r="50" fill="coral" />
-          </svg>
-        `,
-        "app/routes/image-urls-via-js-import-class-composition-test.jsx": js`
-          import * as styles from "../fixtures/imageUrlsViaJsImportClassComposition/styles.css";
-    
-          export default function() {
-            return (
-              <div data-testid="image-urls-via-js-import-class-composition" className={styles.root}>
-                Image URLs via class composition test
-              </div>
-            )
-          }
-        `,
+      export default function() {
+        return (
+          <div data-testid="image-urls-via-root-relative-js-import" className={styles.root}>
+            Image URLs via root-relative JS import test
+          </div>
+        )
+      }
+    `,
+  });
+  test("image URLs via root-relative JS import", async ({ page }) => {
+    let app = new PlaywrightFixture(appFixture, page);
+    let imgStatus: number | null = null;
+    app.page.on("response", (res) => {
+      if (res.url().endsWith(".svg")) imgStatus = res.status();
+    });
+    await app.goto("/image-urls-via-root-relative-js-import-test");
+    let locator = await page.locator(
+      "[data-testid='image-urls-via-root-relative-js-import']"
+    );
+    let backgroundImage = await locator.evaluate(
+      (element) => window.getComputedStyle(element).backgroundImage
+    );
+    expect(backgroundImage).toContain(".svg");
+    expect(imgStatus).toBe(200);
+  });
+
+  let imageUrlsViaClassCompositionFixture = () => ({
+    "app/fixtures/imageUrlsViaClassComposition/styles.css.ts": js`
+      import { style } from "@vanilla-extract/css";
+      import { backgroundImage } from "./nested/backgroundImage.css";
+
+      export const root = style([
+        backgroundImage,
+        {
+          backgroundColor: 'peachpuff',
+          padding: ${JSON.stringify(TEST_PADDING_VALUE)}
+        }
+      ]);
+    `,
+    "app/fixtures/imageUrlsViaClassComposition/nested/backgroundImage.css.ts": js`
+      import { style } from "@vanilla-extract/css";
+
+      export const backgroundImage = style({
+        backgroundImage: 'url(../image.svg)',
       });
-      test("image URLs via JS import class composition", async ({ page }) => {
-        let app = new PlaywrightFixture(appFixture, page);
-        let imgStatus: number | null = null;
-        app.page.on("response", (res) => {
-          if (res.url().endsWith(".svg")) imgStatus = res.status();
-        });
-        await app.goto("/image-urls-via-js-import-class-composition-test");
-        let locator = await page.locator(
-          "[data-testid='image-urls-via-js-import-class-composition']"
-        );
-        let backgroundImage = await locator.evaluate(
-          (element) => window.getComputedStyle(element).backgroundImage
-        );
-        expect(backgroundImage).toContain(".svg");
-        expect(imgStatus).toBe(200);
+    `,
+    "app/fixtures/imageUrlsViaClassComposition/image.svg": `
+      <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+        <circle cx="50" cy="50" r="50" fill="coral" />
+      </svg>
+    `,
+    "app/routes/image-urls-via-class-composition-test.jsx": js`
+      import * as styles from "../fixtures/imageUrlsViaClassComposition/styles.css";
+
+      export default function() {
+        return (
+          <div data-testid="image-urls-via-class-composition" className={styles.root}>
+            Image URLs via class composition test
+          </div>
+        )
+      }
+    `,
+  });
+  test("image URLs via class composition", async ({ page }) => {
+    let app = new PlaywrightFixture(appFixture, page);
+    let imgStatus: number | null = null;
+    app.page.on("response", (res) => {
+      if (res.url().endsWith(".svg")) imgStatus = res.status();
+    });
+    await app.goto("/image-urls-via-class-composition-test");
+    let locator = await page.locator(
+      "[data-testid='image-urls-via-class-composition']"
+    );
+    let backgroundImage = await locator.evaluate(
+      (element) => window.getComputedStyle(element).backgroundImage
+    );
+    expect(backgroundImage).toContain(".svg");
+    expect(imgStatus).toBe(200);
+  });
+
+  let imageUrlsViaJsImportClassCompositionFixture = () => ({
+    "app/fixtures/imageUrlsViaJsImportClassComposition/styles.css.ts": js`
+      import { style } from "@vanilla-extract/css";
+      import { backgroundImage } from "./nested/backgroundImage.css";
+
+      export const root = style([
+        backgroundImage,
+        {
+          backgroundColor: 'peachpuff',
+          padding: ${JSON.stringify(TEST_PADDING_VALUE)}
+        }
+      ]);
+    `,
+    "app/fixtures/imageUrlsViaJsImportClassComposition/nested/backgroundImage.css.ts": js`
+      import { style } from "@vanilla-extract/css";
+      import href from "../image.svg";
+
+      export const backgroundImage = style({
+        backgroundImage: 'url(' + href + ')',
       });
+    `,
+    "app/fixtures/imageUrlsViaJsImportClassComposition/image.svg": `
+      <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+        <circle cx="50" cy="50" r="50" fill="coral" />
+      </svg>
+    `,
+    "app/routes/image-urls-via-js-import-class-composition-test.jsx": js`
+      import * as styles from "../fixtures/imageUrlsViaJsImportClassComposition/styles.css";
+
+      export default function() {
+        return (
+          <div data-testid="image-urls-via-js-import-class-composition" className={styles.root}>
+            Image URLs via class composition test
+          </div>
+        )
+      }
+    `,
+  });
+  test("image URLs via JS import class composition", async ({ page }) => {
+    let app = new PlaywrightFixture(appFixture, page);
+    let imgStatus: number | null = null;
+    app.page.on("response", (res) => {
+      if (res.url().endsWith(".svg")) imgStatus = res.status();
     });
+    await app.goto("/image-urls-via-js-import-class-composition-test");
+    let locator = await page.locator(
+      "[data-testid='image-urls-via-js-import-class-composition']"
+    );
+    let backgroundImage = await locator.evaluate(
+      (element) => window.getComputedStyle(element).backgroundImage
+    );
+    expect(backgroundImage).toContain(".svg");
+    expect(imgStatus).toBe(200);
   });
 });
diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts
index 7a504fdb2..e1f795537 100644
--- a/packages/remix-dev/__tests__/readConfig-test.ts
+++ b/packages/remix-dev/__tests__/readConfig-test.ts
@@ -31,11 +31,8 @@ describe("readConfig", () => {
         entryServerFilePath: expect.any(String),
         tsconfigPath: expect.any(String),
         future: {
-          unstable_cssModules: expect.any(Boolean),
-          unstable_cssSideEffectImports: expect.any(Boolean),
           unstable_postcss: expect.any(Boolean),
           unstable_tailwind: expect.any(Boolean),
-          unstable_vanillaExtract: expect.any(Boolean),
           v2_errorBoundary: expect.any(Boolean),
           v2_meta: expect.any(Boolean),
           v2_normalizeFormMethod: expect.any(Boolean),
@@ -54,12 +51,9 @@ describe("readConfig", () => {
         "entryServerFile": "entry.server.tsx",
         "entryServerFilePath": Any<String>,
         "future": Object {
-          "unstable_cssModules": Any<Boolean>,
-          "unstable_cssSideEffectImports": Any<Boolean>,
           "unstable_dev": false,
           "unstable_postcss": Any<Boolean>,
           "unstable_tailwind": Any<Boolean>,
-          "unstable_vanillaExtract": Any<Boolean>,
           "v2_errorBoundary": Any<Boolean>,
           "v2_meta": Any<Boolean>,
           "v2_normalizeFormMethod": Any<Boolean>,
diff --git a/packages/remix-testing/create-remix-stub.tsx b/packages/remix-testing/create-remix-stub.tsx
index 3d9749e68..93f7e684f 100644
--- a/packages/remix-testing/create-remix-stub.tsx
+++ b/packages/remix-testing/create-remix-stub.tsx
@@ -63,12 +63,9 @@ export function createRemixStub(routes: (RouteObject | DataRouteObject)[]) {
     if (remixContextRef.current == null) {
       remixContextRef.current = {
         future: {
-          unstable_cssModules: false,
-          unstable_cssSideEffectImports: false,
           unstable_dev: false,
           unstable_postcss: false,
           unstable_tailwind: false,
-          unstable_vanillaExtract: false,
           v2_errorBoundary: false,
           v2_meta: false,
           v2_normalizeFormMethod: false,
diff --git a/packages/remix-testing/package.json b/packages/remix-testing/package.json
index 229913e57..f99039cbe 100644
--- a/packages/remix-testing/package.json
+++ b/packages/remix-testing/package.json
@@ -19,16 +19,19 @@
     "@remix-run/node": "1.15.0",
     "@remix-run/react": "1.15.0",
     "@remix-run/router": "1.5.0",
-    "react": "^18.2.0",
-    "react-dom": "^18.2.0",
     "react-router-dom": "6.10.0"
   },
   "devDependencies": {
     "@types/node": "^18.11.9",
     "@types/react": "^18.0.24",
     "@types/react-dom": "^18.0.8",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
     "typescript": "^5.0.4"
   },
+  "peerDependencies": {
+    "react": "^17.0.0 || ^18.0.0"
+  },
   "engines": {
     "node": ">=14"
   },