Skip to content

Commit

Permalink
Merge branch 'dev' into fix-vite-empty-named-exports
Browse files Browse the repository at this point in the history
  • Loading branch information
hi-ogawa authored Nov 14, 2023
2 parents d2779ac + 8eb9621 commit 661e858
Show file tree
Hide file tree
Showing 8 changed files with 337 additions and 57 deletions.
2 changes: 1 addition & 1 deletion .changeset/green-buses-suffer.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"@remix-run/dev": patch
---

Handle Vite v5's updated manifest path
Support Vite 5
5 changes: 5 additions & 0 deletions .changeset/hungry-tables-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/dev": patch
---

Allow `process.env.NODE_ENV` values other than `"development"` in Vite dev
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- akamfoad
- alanhoskins
- Alarid
- alcpereira
- alex-ketch
- alexmgrant
- alextes
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@
"unified": "^10.1.2",
"unist-util-remove": "^3.1.0",
"unist-util-visit": "^4.1.1",
"vite": "^4.4.9",
"vite": "5.0.0-beta.18",
"wait-on": "^7.0.1"
},
"engines": {
Expand Down
4 changes: 2 additions & 2 deletions packages/remix-dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,12 @@
"msw": "^1.2.3",
"strip-ansi": "^6.0.1",
"tiny-invariant": "^1.2.0",
"vite": "^4.4.9"
"vite": "5.0.0-beta.18"
},
"peerDependencies": {
"@remix-run/serve": "^2.2.0",
"typescript": "^5.1.0",
"vite": "^4.4.9"
"vite": "^4.4.9 || ^5.0.0"
},
"peerDependenciesMeta": {
"@remix-run/serve": {
Expand Down
130 changes: 82 additions & 48 deletions packages/remix-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
// We can only import types from Vite at the top level since we're in a CJS
// context but want to use Vite's ESM build to avoid deprecation warnings
import type * as Vite from "vite";
import { type BinaryLike, createHash } from "node:crypto";
import * as path from "node:path";
import * as fs from "node:fs/promises";
import babel from "@babel/core";
import { type ServerBuild } from "@remix-run/server-runtime";
import {
type Connect,
type Plugin as VitePlugin,
type Manifest as ViteManifest,
type ManifestChunk,
type ResolvedConfig as ResolvedViteConfig,
type ViteDevServer,
type UserConfig as ViteUserConfig,
loadEnv as viteLoadEnv,
normalizePath as viteNormalizePath,
createServer as createViteDevServer,
} from "vite";
import {
init as initEsModuleLexer,
parse as esModuleLexer,
Expand All @@ -38,6 +29,11 @@ import { removeExports } from "./remove-exports";
import { transformLegacyCssImports } from "./legacy-css-imports";
import { replaceImportSpecifier } from "./replace-import-specifier";

// We reassign the "vite" variable from a dynamic import of Vite's ESM build
// when the Vite plugin's config hook is executed
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
let vite: typeof import("vite");

const supportedRemixConfigKeys = [
"appDirectory",
"assetsBuildDirectory",
Expand Down Expand Up @@ -81,7 +77,7 @@ let injectHmrRuntimeId = VirtualModule.id("inject-hmr-runtime");

const normalizePath = (p: string) => {
let unixPath = p.replace(/[\\/]+/g, "/").replace(/^([a-zA-Z]+:|\.\/)/, "");
return viteNormalizePath(unixPath);
return vite.normalizePath(unixPath);
};

const resolveFileUrl = (
Expand Down Expand Up @@ -122,7 +118,7 @@ const getHash = (source: BinaryLike, maxLength?: number): string => {

const resolveBuildAssetPaths = (
pluginConfig: ResolvedRemixVitePluginConfig,
viteManifest: ViteManifest,
viteManifest: Vite.Manifest,
absoluteFilePath: string
): Manifest["entry"] & { css: string[] } => {
let rootRelativeFilePath = path.relative(
Expand Down Expand Up @@ -157,12 +153,12 @@ const resolveBuildAssetPaths = (
};

function resolveDependantChunks(
viteManifest: ViteManifest,
entryChunk: ManifestChunk
): ManifestChunk[] {
let chunks = new Set<ManifestChunk>();
viteManifest: Vite.Manifest,
entryChunk: Vite.ManifestChunk
): Vite.ManifestChunk[] {
let chunks = new Set<Vite.ManifestChunk>();

function walk(chunk: ManifestChunk) {
function walk(chunk: Vite.ManifestChunk) {
if (chunks.has(chunk)) {
return;
}
Expand Down Expand Up @@ -191,7 +187,7 @@ const writeFileSafe = async (file: string, contents: string): Promise<void> => {
};

const getRouteModuleExports = async (
viteChildCompiler: ViteDevServer | null,
viteChildCompiler: Vite.ViteDevServer | null,
pluginConfig: ResolvedRemixVitePluginConfig,
routeFile: string
): Promise<string[]> => {
Expand Down Expand Up @@ -246,27 +242,25 @@ const showUnstableWarning = () => {
);
};

const getViteMajor = () => {
const getViteMajorVersion = (): number => {
let vitePkg = require("vite/package.json");

return parseInt(vitePkg.version.split(".")[0]!);
};

export type RemixVitePlugin = (
options?: RemixVitePluginOptions
) => VitePlugin[];
) => Vite.Plugin[];
export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
let isViteGTEv5 = getViteMajor() >= 5;

let viteCommand: ResolvedViteConfig["command"];
let viteUserConfig: ViteUserConfig;
let viteCommand: Vite.ResolvedConfig["command"];
let viteUserConfig: Vite.UserConfig;
let isViteV4 = getViteMajorVersion() === 4;

let cssModulesManifest: Record<string, string> = {};
let ssrBuildContext:
| { isSsrBuild: false }
| { isSsrBuild: true; getManifest: () => Promise<Manifest> };

let viteChildCompiler: ViteDevServer | null = null;
let viteChildCompiler: Vite.ViteDevServer | null = null;
let cachedPluginConfig: ResolvedRemixVitePluginConfig | undefined;

let resolvePluginConfig =
Expand Down Expand Up @@ -356,16 +350,16 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
let createBuildManifest = async (): Promise<Manifest> => {
let pluginConfig = await resolvePluginConfig();

let viteManifestPath = isViteGTEv5
? path.join(".vite", "manifest.json")
: "manifest.json";
let viteManifestPath = isViteV4
? "manifest.json"
: path.join(".vite", "manifest.json");

let viteManifest = JSON.parse(
await fs.readFile(
path.resolve(pluginConfig.assetsBuildDirectory, viteManifestPath),
"utf-8"
)
) as ViteManifest;
) as Vite.Manifest;

let entry: Manifest["entry"] = resolveBuildAssetPaths(
pluginConfig,
Expand Down Expand Up @@ -462,6 +456,9 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
{
name: "remix",
config: async (_viteUserConfig, viteConfigEnv) => {
// Load Vite's ESM build up-front as soon as we're in an async context
vite = await import("vite");

viteUserConfig = _viteUserConfig;
viteCommand = viteConfigEnv.command;

Expand All @@ -470,7 +467,7 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {

Object.assign(
process.env,
viteLoadEnv(
vite.loadEnv(
viteConfigEnv.mode,
pluginConfig.rootDirectory,
// We override default prefix of "VITE_" with a blank string since
Expand All @@ -480,6 +477,12 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
)
);

let isSsrBuild =
"ssrBuild" in viteConfigEnv &&
typeof viteConfigEnv.ssrBuild === "boolean"
? viteConfigEnv.ssrBuild // Vite v4 back compat
: viteConfigEnv.isSsrBuild;

return {
appType: "custom",
experimental: { hmrPartialAccept: true },
Expand Down Expand Up @@ -519,7 +522,7 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
base: pluginConfig.publicPath,
build: {
...viteUserConfig.build,
...(!viteConfigEnv.ssrBuild
...(!isSsrBuild
? {
manifest: true,
outDir: pluginConfig.assetsBuildDirectory,
Expand Down Expand Up @@ -555,7 +558,41 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
async configResolved(viteConfig) {
await initEsModuleLexer;

viteChildCompiler = await createViteDevServer({
ssrBuildContext =
viteConfig.build.ssr && viteCommand === "build"
? { isSsrBuild: true, getManifest: createBuildManifest }
: { isSsrBuild: false };

// We load the same Vite config file again for the child compiler so
// that both parent and child compiler's plugins have independent state.
// If we re-used the `viteUserConfig.plugins` array for the child
// compiler, it could lead to mutating shared state between plugin
// instances in unexpected ways, e.g. during `vite build` the
// `configResolved` plugin hook would be called with `command = "build"`
// by parent and then `command = "serve"` by child, which some plugins
// may respond to by updating state referenced by the parent.
if (!viteConfig.configFile) {
throw new Error(
"The Remix Vite plugin requires the use of a Vite config file"
);
}
let childCompilerConfigFile = await vite.loadConfigFromFile(
{
command: viteConfig.command,
mode: viteConfig.mode,
...(isViteV4
? { ssrBuild: ssrBuildContext.isSsrBuild }
: { isSsrBuild: ssrBuildContext.isSsrBuild }),
},
viteConfig.configFile
);

invariant(
childCompilerConfigFile,
"Vite config file was unable to be resolved for Remix child compiler"
);

viteChildCompiler = await vite.createServer({
...viteUserConfig,
mode: viteConfig.mode,
server: {
Expand All @@ -568,7 +605,7 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
configFile: false,
envFile: false,
plugins: [
...(viteUserConfig.plugins ?? [])
...(childCompilerConfigFile.config.plugins ?? [])
.flat()
// Exclude this plugin from the child compiler to prevent an
// infinite loop (plugin creates a child compiler with the same
Expand Down Expand Up @@ -597,11 +634,6 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
],
});
await viteChildCompiler.pluginContainer.buildStart({});

ssrBuildContext =
viteConfig.build.ssr && viteCommand === "build"
? { isSsrBuild: true, getManifest: createBuildManifest }
: { isSsrBuild: false };
},
transform(code, id) {
if (isCssModulesFile(id)) {
Expand All @@ -625,7 +657,7 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
let previousPluginConfig: ResolvedRemixVitePluginConfig | undefined;

let localsByRequest = new WeakMap<
Connect.IncomingMessage,
Vite.Connect.IncomingMessage,
{
build: ServerBuild;
criticalCss: string | undefined;
Expand Down Expand Up @@ -825,7 +857,9 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
return [
'import { createElement } from "react";',
'export * from "@remix-run/react";',
'export const LiveReload = process.env.NODE_ENV !== "development" ? () => null : ',
`export const LiveReload = ${
viteCommand !== "serve"
} ? () => null : `,
'() => createElement("script", {',
" dangerouslySetInnerHTML: { ",
" __html: `window.__remixLiveReloadEnabled = true`",
Expand Down Expand Up @@ -906,7 +940,7 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
allowAwaitOutsideFunction: true,
plugins: ["jsx", "typescript"],
},
plugins: [require("react-refresh/babel")],
plugins: [[require("react-refresh/babel"), { skipEnvCheck: true }]],
sourceMaps: true,
});
if (result === null) return;
Expand Down Expand Up @@ -954,7 +988,7 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
},
},
]
: []) satisfies VitePlugin[]),
: []) satisfies Vite.Plugin[]),
];
};

Expand Down Expand Up @@ -1015,8 +1049,8 @@ function getRoute(
pluginConfig: ResolvedRemixVitePluginConfig,
file: string
): Route | undefined {
if (!file.startsWith(viteNormalizePath(pluginConfig.appDirectory))) return;
let routePath = viteNormalizePath(
if (!file.startsWith(vite.normalizePath(pluginConfig.appDirectory))) return;
let routePath = vite.normalizePath(
path.relative(pluginConfig.appDirectory, file)
);
let route = Object.values(pluginConfig.routes).find(
Expand All @@ -1027,7 +1061,7 @@ function getRoute(

async function getRouteMetadata(
pluginConfig: ResolvedRemixVitePluginConfig,
viteChildCompiler: ViteDevServer | null,
viteChildCompiler: Vite.ViteDevServer | null,
route: Route
) {
let sourceExports = await getRouteModuleExports(
Expand Down
11 changes: 9 additions & 2 deletions packages/remix-react/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1041,9 +1041,16 @@ export function useFetcher<TData = AppData>(
return useFetcherRR(opts);
}

// Dead Code Elimination magic for production builds.
// This way devs don't have to worry about doing the NODE_ENV check themselves.
/**
* This component connects your app to the Remix asset server and
* automatically reloads the page when files change in development.
* In production, it renders null, so you can safely render it always in your root route.
*
* @see https://remix.run/docs/components/live-reload
*/
export const LiveReload =
// Dead Code Elimination magic for production builds.
// This way devs don't have to worry about doing the NODE_ENV check themselves.
process.env.NODE_ENV !== "development"
? () => null
: function LiveReload({
Expand Down
Loading

0 comments on commit 661e858

Please sign in to comment.