From 260f3265679dbce5b67e8452e1598052e6d860fa Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Fri, 22 Sep 2023 11:20:37 -0400 Subject: [PATCH 01/38] router globally accessible on window --- packages/remix-react/browser.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/remix-react/browser.tsx b/packages/remix-react/browser.tsx index 904b80e20ea..48c5cd199e7 100644 --- a/packages/remix-react/browser.tsx +++ b/packages/remix-react/browser.tsx @@ -26,6 +26,7 @@ declare global { hmrRuntime?: string; }; }; + var __remixRouter: Router; var __remixRouteModules: RouteModules; var __remixManifest: EntryContext["manifest"]; var __remixRevalidation: number | undefined; @@ -209,6 +210,9 @@ export function RemixBrowser(_props: RemixBrowserProps): ReactElement { v7_normalizeFormMethod: true, }, }); + // @ts-ignore + router.createRoutesForHMR = createClientRoutesWithHMRRevalidationOptOut; + window.__remixRouter = router; // Notify that the router is ready for HMR if (hmrRouterReadyResolve) { From b3e10d85149b995ee14eb7d83eebbd3d8d50fdc7 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Sat, 30 Sep 2023 10:44:53 +1000 Subject: [PATCH 02/38] support route CSS bindings and critical CSS --- packages/remix-react/browser.tsx | 2 ++ packages/remix-react/components.tsx | 3 ++- packages/remix-react/entry.ts | 1 + packages/remix-react/links.ts | 22 ++++++++++++++----- packages/remix-react/routes.tsx | 3 ++- packages/remix-react/server.tsx | 3 ++- packages/remix-server-runtime/entry.ts | 1 + packages/remix-server-runtime/routes.ts | 1 + packages/remix-server-runtime/server.ts | 17 ++++++++++---- .../remix-server-runtime/serverHandoff.ts | 1 + 10 files changed, 41 insertions(+), 13 deletions(-) diff --git a/packages/remix-react/browser.tsx b/packages/remix-react/browser.tsx index 48c5cd199e7..5b63a583c55 100644 --- a/packages/remix-react/browser.tsx +++ b/packages/remix-react/browser.tsx @@ -18,6 +18,7 @@ declare global { var __remixContext: { url: string; state: HydrationState; + criticalCss?: string; future: FutureConfig; // The number of active deferred keys rendered on the server a?: number; @@ -244,6 +245,7 @@ export function RemixBrowser(_props: RemixBrowserProps): ReactElement { manifest: window.__remixManifest, routeModules: window.__remixRouteModules, future: window.__remixContext.future, + criticalCss: window.__remixContext.criticalCss, }} > diff --git a/packages/remix-react/components.tsx b/packages/remix-react/components.tsx index c6961b0841f..0e9654eb02a 100644 --- a/packages/remix-react/components.tsx +++ b/packages/remix-react/components.tsx @@ -272,7 +272,7 @@ export function composeEventHandlers< * @see https://remix.run/components/links */ export function Links() { - let { manifest, routeModules } = useRemixContext(); + let { manifest, routeModules, criticalCss } = useRemixContext(); let { errors, matches: routerMatches } = useDataRouterStateContext(); let matches = errors @@ -289,6 +289,7 @@ export function Links() { return ( <> + {criticalCss ? : null} {keyedLinks.map(({ key, link }) => isPageLinkDescriptor(link) ? ( diff --git a/packages/remix-react/entry.ts b/packages/remix-react/entry.ts index e81d6d4227d..ded8cd267a3 100644 --- a/packages/remix-react/entry.ts +++ b/packages/remix-react/entry.ts @@ -12,6 +12,7 @@ type SerializedError = { export interface RemixContextObject { manifest: AssetsManifest; routeModules: RouteModules; + criticalCss?: string; serverHandoffString?: string; future: FutureConfig; abortDelay?: number; diff --git a/packages/remix-react/links.ts b/packages/remix-react/links.ts index e9724dfc3e5..c8d3a10e292 100644 --- a/packages/remix-react/links.ts +++ b/packages/remix-react/links.ts @@ -4,6 +4,7 @@ import { parsePath } from "react-router-dom"; import type { AssetsManifest } from "./entry"; import type { RouteModules, RouteModule } from "./routeModules"; +import type { EntryRoute } from "./routes"; import { loadRouteModule } from "./routeModules"; type Primitive = null | undefined | string | number | boolean | symbol | bigint; @@ -211,22 +212,31 @@ export function getKeyedLinksForMatches( manifest: AssetsManifest ): KeyedLinkDescriptor[] { let descriptors = matches - .map((match): LinkDescriptor[] => { + .map((match): LinkDescriptor[][] => { let module = routeModules[match.route.id]; - return module.links?.() || []; + let route = manifest.routes[match.route.id]; + return [ + route.css ? route.css.map((href) => ({ rel: "stylesheet", href })) : [], + module.links?.() || [], + ]; }) - .flat(1); + .flat(2); let preloads = getCurrentPageModulePreloadHrefs(matches, manifest); return dedupeLinkDescriptors(descriptors, preloads); } export async function prefetchStyleLinks( + route: EntryRoute, routeModule: RouteModule ): Promise { - if (!routeModule.links || !isPreloadSupported()) return; - let descriptors = routeModule.links(); - if (!descriptors) return; + if ((!route.css && !routeModule.links) || !isPreloadSupported()) return; + + let descriptors = [ + route.css?.map((href) => ({ rel: "stylesheet", href })) ?? [], + routeModule.links?.() ?? [], + ].flat(1); + if (descriptors.length === 0) return; let styleLinks: HtmlLinkDescriptor[] = []; for (let descriptor of descriptors) { diff --git a/packages/remix-react/routes.tsx b/packages/remix-react/routes.tsx index 074b56b239d..d8efa5dcb32 100644 --- a/packages/remix-react/routes.tsx +++ b/packages/remix-react/routes.tsx @@ -38,6 +38,7 @@ export interface EntryRoute extends Route { hasLoader: boolean; hasErrorBoundary: boolean; imports?: string[]; + css?: string[]; module: string; parentId?: string; } @@ -241,7 +242,7 @@ async function loadRouteModuleWithBlockingLinks( routeModules: RouteModules ) { let routeModule = await loadRouteModule(route, routeModules); - await prefetchStyleLinks(routeModule); + await prefetchStyleLinks(route, routeModule); // Resource routes are built with an empty object as the default export - // ignore those when setting the Component diff --git a/packages/remix-react/server.tsx b/packages/remix-react/server.tsx index 47c4b576116..aa6042deb62 100644 --- a/packages/remix-react/server.tsx +++ b/packages/remix-react/server.tsx @@ -30,7 +30,7 @@ export function RemixServer({ url = new URL(url); } - let { manifest, routeModules, serverHandoffString } = context; + let { manifest, routeModules, criticalCss, serverHandoffString } = context; let routes = createServerRoutes( manifest.routes, routeModules, @@ -43,6 +43,7 @@ export function RemixServer({ value={{ manifest, routeModules, + criticalCss, serverHandoffString, future: context.future, serializeError: context.serializeError, diff --git a/packages/remix-server-runtime/entry.ts b/packages/remix-server-runtime/entry.ts index 593f87f4785..90a9ba37908 100644 --- a/packages/remix-server-runtime/entry.ts +++ b/packages/remix-server-runtime/entry.ts @@ -7,6 +7,7 @@ import type { RouteModules, EntryRouteModule } from "./routeModules"; export interface EntryContext { manifest: AssetsManifest; routeModules: RouteModules; + criticalCss?: string; serverHandoffString?: string; staticHandlerContext: StaticHandlerContext; future: FutureConfig; diff --git a/packages/remix-server-runtime/routes.ts b/packages/remix-server-runtime/routes.ts index ef430fdd317..4acbcb469cf 100644 --- a/packages/remix-server-runtime/routes.ts +++ b/packages/remix-server-runtime/routes.ts @@ -29,6 +29,7 @@ export interface EntryRoute extends Route { hasLoader: boolean; hasErrorBoundary: boolean; imports?: string[]; + css?: string[]; module: string; parentId?: string; } diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index de63313644f..c405a2fc4d2 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -30,7 +30,8 @@ import { createServerHandoffString } from "./serverHandoff"; export type RequestHandler = ( request: Request, - loadContext?: AppLoadContext + loadContext?: AppLoadContext, + args?: { criticalCss?: string } ) => Promise; export type CreateRequestHandlerFunction = ( @@ -58,7 +59,11 @@ export const createRequestHandler: CreateRequestHandlerFunction = ( } }); - return async function requestHandler(request, loadContext = {}) { + return async function requestHandler( + request, + loadContext = {}, + { criticalCss } = {} + ) { let url = new URL(request.url); let matches = matchServerRoutes(routes, url.pathname); @@ -109,7 +114,8 @@ export const createRequestHandler: CreateRequestHandlerFunction = ( staticHandler, request, loadContext, - handleError + handleError, + criticalCss ); } @@ -209,7 +215,8 @@ async function handleDocumentRequestRR( staticHandler: StaticHandler, request: Request, loadContext: AppLoadContext, - handleError: (err: unknown) => void + handleError: (err: unknown) => void, + criticalCss?: string ) { let context; try { @@ -242,8 +249,10 @@ async function handleDocumentRequestRR( manifest: build.assets, routeModules: createEntryRouteModules(build.routes), staticHandlerContext: context, + criticalCss, serverHandoffString: createServerHandoffString({ url: context.location.pathname, + criticalCss, state: { loaderData: context.loaderData, actionData: context.actionData, diff --git a/packages/remix-server-runtime/serverHandoff.ts b/packages/remix-server-runtime/serverHandoff.ts index 583f2a21db1..85aed8e376f 100644 --- a/packages/remix-server-runtime/serverHandoff.ts +++ b/packages/remix-server-runtime/serverHandoff.ts @@ -19,6 +19,7 @@ export function createServerHandoffString(serverHandoff: { // Don't allow StaticHandlerContext to be passed in verbatim, since then // we'd end up including duplicate info state: ValidateShape; + criticalCss?: string; url: string; future: FutureConfig; }): string { From a03ab30372dd8f96af156efb4b9189f06302f581 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Wed, 4 Oct 2023 16:51:20 +1100 Subject: [PATCH 03/38] port revive code into remix-dev --- packages/remix-dev/cli/commands.ts | 12 + packages/remix-dev/cli/run.ts | 24 +- packages/remix-dev/package.json | 16 +- packages/remix-dev/rollup.config.js | 8 +- packages/remix-dev/vite.d.ts | 1 + packages/remix-dev/vite.js | 1 + packages/remix-dev/vite/babel.ts | 9 + packages/remix-dev/vite/build.ts | 19 + packages/remix-dev/vite/dev.ts | 38 + packages/remix-dev/vite/index.ts | 4 + packages/remix-dev/vite/legacy-css-imports.ts | 28 + packages/remix-dev/vite/node/adapter.ts | 205 ++++ packages/remix-dev/vite/plugin.ts | 935 ++++++++++++++++++ .../remix-dev/vite/remove-exports-test.ts | 399 ++++++++ packages/remix-dev/vite/remove-exports.ts | 363 +++++++ .../vite/replace-import-specifier.ts | 26 + .../remix-dev/vite/static/refresh-utils.cjs | 170 ++++ packages/remix-dev/vite/styles.ts | 183 ++++ packages/remix-dev/vite/vmod.ts | 3 + yarn.lock | 192 ++++ 20 files changed, 2630 insertions(+), 6 deletions(-) create mode 100644 packages/remix-dev/vite.d.ts create mode 100644 packages/remix-dev/vite.js create mode 100644 packages/remix-dev/vite/babel.ts create mode 100644 packages/remix-dev/vite/build.ts create mode 100644 packages/remix-dev/vite/dev.ts create mode 100644 packages/remix-dev/vite/index.ts create mode 100644 packages/remix-dev/vite/legacy-css-imports.ts create mode 100644 packages/remix-dev/vite/node/adapter.ts create mode 100644 packages/remix-dev/vite/plugin.ts create mode 100644 packages/remix-dev/vite/remove-exports-test.ts create mode 100644 packages/remix-dev/vite/remove-exports.ts create mode 100644 packages/remix-dev/vite/replace-import-specifier.ts create mode 100644 packages/remix-dev/vite/static/refresh-utils.cjs create mode 100644 packages/remix-dev/vite/styles.ts create mode 100644 packages/remix-dev/vite/vmod.ts diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index 18b7c19b603..69612ef1cab 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -11,6 +11,8 @@ import * as compiler from "../compiler"; import * as devServer from "../devServer"; import * as devServer_unstable from "../devServer_unstable"; import type { RemixConfig } from "../config"; +import { type ViteDevOptions } from "../vite/dev"; +import { type ViteBuildOptions } from "../vite/build"; import { readConfig } from "../config"; import { formatRoutes, RoutesFormat, isRoutesFormat } from "../config/format"; import { detectPackageManager } from "./detectPackageManager"; @@ -132,6 +134,11 @@ export async function build( logger.info("built" + pc.gray(` (${prettyMs(Date.now() - start)})`)); } +export async function viteBuild(options: ViteBuildOptions = {}) { + let { build } = await import("../vite/build"); + await build(options); +} + export async function watch( remixRootOrConfig: string | RemixConfig, mode?: string @@ -175,6 +182,11 @@ export async function dev( await new Promise(() => {}); } +export async function viteDev(options: ViteDevOptions = {}) { + let { dev } = await import("../vite/dev"); + await dev(options); +} + let clientEntries = ["entry.client.tsx", "entry.client.js", "entry.client.jsx"]; let serverEntries = ["entry.server.tsx", "entry.server.js", "entry.server.jsx"]; let entries = ["entry.client", "entry.server"]; diff --git a/packages/remix-dev/cli/run.ts b/packages/remix-dev/cli/run.ts index 05796f3a0a2..6b11406c95a 100644 --- a/packages/remix-dev/cli/run.ts +++ b/packages/remix-dev/cli/run.ts @@ -115,6 +115,17 @@ export async function run(argv: string[] = process.argv.slice(2)) { "-p": "--port", "--tls-key": String, "--tls-cert": String, + + // vite + ...(process.env.REMIX_EXPERIMENTAL_VITE + ? { + "--strictPort": Boolean, + "--config": String, + // TODO: Also support boolean usage? e.g. remix dev --host + // Note that arg doesn't support this: /~https://github.com/vercel/arg/issues/61 + "--host": String, + } + : {}), }, { argv, @@ -169,8 +180,12 @@ export async function run(argv: string[] = process.argv.slice(2)) { await commands.routes(input[1], flags.json ? "json" : "jsx"); break; case "build": - if (!process.env.NODE_ENV) process.env.NODE_ENV = "production"; - await commands.build(input[1], process.env.NODE_ENV, flags.sourcemap); + if (process.env.REMIX_EXPERIMENTAL_VITE) { + await commands.viteBuild(flags); + } else { + if (!process.env.NODE_ENV) process.env.NODE_ENV = "production"; + await commands.build(input[1], process.env.NODE_ENV, flags.sourcemap); + } break; case "watch": if (!process.env.NODE_ENV) process.env.NODE_ENV = "development"; @@ -185,10 +200,13 @@ export async function run(argv: string[] = process.argv.slice(2)) { break; } case "dev": - await commands.dev(input[1], flags); + await (process.env.REMIX_EXPERIMENTAL_VITE + ? commands.viteDev(flags) + : commands.dev(input[1], flags)); break; default: // `remix ./my-project` is shorthand for `remix dev ./my-project` + // TODO: Support this in Vite mode await commands.dev(input[0], flags); } } diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 7a6279be278..23e2f966596 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -25,8 +25,11 @@ "@babel/plugin-syntax-jsx": "^7.21.4", "@babel/preset-typescript": "^7.21.5", "@babel/traverse": "^7.21.5", + "@babel/types": "^7.22.5", "@mdx-js/mdx": "^2.3.0", "@npmcli/package-json": "^4.0.1", + "@remix-run/node": "2.0.1", + "@remix-run/router": "1.9.0", "@remix-run/server-runtime": "2.0.1", "@types/mdx": "^2.0.5", "@vanilla-extract/integration": "^6.2.0", @@ -35,6 +38,7 @@ "chalk": "^4.1.2", "chokidar": "^3.5.1", "dotenv": "^16.0.0", + "es-module-lexer": "^1.3.1", "esbuild": "0.17.6", "esbuild-plugins-node-modules-polyfill": "^1.6.0", "execa": "5.1.1", @@ -50,6 +54,7 @@ "minimatch": "^9.0.0", "node-fetch": "^2.6.9", "ora": "^5.4.1", + "parse-multipart-data": "^1.5.0", "picocolors": "^1.0.0", "picomatch": "^2.3.1", "pidtree": "^0.6.0", @@ -63,8 +68,10 @@ "remark-frontmatter": "4.0.1", "remark-mdx-frontmatter": "^1.0.1", "semver": "^7.3.7", + "set-cookie-parser": "^2.6.0", "tar-fs": "^2.1.1", "tsconfig-paths": "^4.0.0", + "undici": "^5.22.1", "ws": "^7.4.5" }, "devDependencies": { @@ -85,11 +92,13 @@ "fast-glob": "3.2.11", "msw": "^1.2.3", "strip-ansi": "^6.0.1", - "tiny-invariant": "^1.2.0" + "tiny-invariant": "^1.2.0", + "vite": "^4.4.9" }, "peerDependencies": { "@remix-run/serve": "^2.0.1", - "typescript": "^5.1.0" + "typescript": "^5.1.0", + "vite": "^4.4.9" }, "peerDependenciesMeta": { "@remix-run/serve": { @@ -97,6 +106,9 @@ }, "typescript": { "optional": true + }, + "vite": { + "optional": true } }, "engines": { diff --git a/packages/remix-dev/rollup.config.js b/packages/remix-dev/rollup.config.js index 9ffa2108934..4d39b7e1500 100644 --- a/packages/remix-dev/rollup.config.js +++ b/packages/remix-dev/rollup.config.js @@ -23,7 +23,7 @@ module.exports = function rollup() { external(id) { return isBareModuleId(id); }, - input: `${sourceDir}/index.ts`, + input: [`${sourceDir}/index.ts`, `${sourceDir}/vite/index.ts`], output: { banner: createBanner("@remix-run/dev", version), dir: outputDist, @@ -43,10 +43,16 @@ module.exports = function rollup() { { src: `LICENSE.md`, dest: [outputDir, sourceDir] }, { src: `${sourceDir}/package.json`, dest: [outputDir, outputDist] }, { src: `${sourceDir}/README.md`, dest: outputDir }, + { src: `${sourceDir}/vite/static`, dest: `${outputDist}/vite` }, { src: `${sourceDir}/config/defaults`, dest: [`${outputDir}/config`, `${outputDist}/config`], }, + // This needs to end up in the root of the pkg but also needs to + // reference other compiled files. Just copying these are easier + // than dealing with output configuration for sharing chunks x-builds. + { src: `${sourceDir}/vite.js`, dest: outputDir }, + { src: `${sourceDir}/vite.d.ts`, dest: outputDir }, ], }), // Allow dynamic imports in CJS code to allow us to utilize diff --git a/packages/remix-dev/vite.d.ts b/packages/remix-dev/vite.d.ts new file mode 100644 index 00000000000..5f55f621900 --- /dev/null +++ b/packages/remix-dev/vite.d.ts @@ -0,0 +1 @@ +export * from "./dist/vite"; diff --git a/packages/remix-dev/vite.js b/packages/remix-dev/vite.js new file mode 100644 index 00000000000..386c03a770f --- /dev/null +++ b/packages/remix-dev/vite.js @@ -0,0 +1 @@ +module.exports = require("./dist/vite"); diff --git a/packages/remix-dev/vite/babel.ts b/packages/remix-dev/vite/babel.ts new file mode 100644 index 00000000000..7f7467905aa --- /dev/null +++ b/packages/remix-dev/vite/babel.ts @@ -0,0 +1,9 @@ +import type { NodePath } from "@babel/traverse"; +import type { types as BabelTypes } from "@babel/core"; +import { parse } from "@babel/parser"; +import * as t from "@babel/types"; +import traverse from "@babel/traverse"; +import generate from "@babel/generator"; + +export { traverse, generate, parse, t }; +export type { BabelTypes, NodePath }; diff --git a/packages/remix-dev/vite/build.ts b/packages/remix-dev/vite/build.ts new file mode 100644 index 00000000000..c863db92ac1 --- /dev/null +++ b/packages/remix-dev/vite/build.ts @@ -0,0 +1,19 @@ +import * as vite from "vite"; + +export interface ViteBuildOptions { + config?: string; + force?: boolean; +} + +export async function build({ config: configFile, force }: ViteBuildOptions) { + async function viteBuild({ ssr }: { ssr: boolean }) { + await vite.build({ + configFile, + build: { ssr }, + optimizeDeps: { force }, + }); + } + + await viteBuild({ ssr: false }); + await viteBuild({ ssr: true }); +} diff --git a/packages/remix-dev/vite/dev.ts b/packages/remix-dev/vite/dev.ts new file mode 100644 index 00000000000..a05e595f293 --- /dev/null +++ b/packages/remix-dev/vite/dev.ts @@ -0,0 +1,38 @@ +import { spawn } from "node:child_process"; + +export interface ViteDevOptions { + config?: string; + port?: string; + strictPort?: boolean; + host?: true | string; + force?: boolean; +} + +export async function dev({ + config, + port, + strictPort, + host, + force, +}: ViteDevOptions) { + spawn( + "vite", + [ + "dev", + ...(config ? ["--config", config] : []), + ...(port ? ["--port", port] : []), + ...(strictPort ? ["--strictPort"] : []), + ...(force ? ["--force"] : []), + ...(host ? ["--host", ...(typeof host === "string" ? [host] : [])] : []), + ], + { + shell: true, + stdio: "inherit", + env: { + ...process.env, + }, + } + ); + + await new Promise(() => {}); +} diff --git a/packages/remix-dev/vite/index.ts b/packages/remix-dev/vite/index.ts new file mode 100644 index 00000000000..86093dcae64 --- /dev/null +++ b/packages/remix-dev/vite/index.ts @@ -0,0 +1,4 @@ +export { + remix as experimental_remix, + legacyCssImportSemantics as experimental_legacyCssImportSemantics, +} from "./plugin"; diff --git a/packages/remix-dev/vite/legacy-css-imports.ts b/packages/remix-dev/vite/legacy-css-imports.ts new file mode 100644 index 00000000000..b0ba00f2910 --- /dev/null +++ b/packages/remix-dev/vite/legacy-css-imports.ts @@ -0,0 +1,28 @@ +import { parse, traverse, generate, t } from "./babel"; + +export const transformLegacyCssImports = (source: string) => { + let ast = parse(source, { + sourceType: "module", + plugins: ["typescript", "jsx"], + }); + + traverse(ast, { + // Handle `import styles from "./styles.css"` + ImportDeclaration(path) { + if ( + path.node.source.value.endsWith(".css") && + // CSS Modules are bundled in the Remix compiler so they're already + // compatible with Vite's default CSS handling + !path.node.source.value.endsWith(".module.css") && + t.isImportDefaultSpecifier(path.node.specifiers[0]) + ) { + path.node.source.value += "?url"; + } + }, + }); + + return { + code: generate(ast, { retainLines: true }).code, + map: null, + }; +}; diff --git a/packages/remix-dev/vite/node/adapter.ts b/packages/remix-dev/vite/node/adapter.ts new file mode 100644 index 00000000000..3377163431d --- /dev/null +++ b/packages/remix-dev/vite/node/adapter.ts @@ -0,0 +1,205 @@ +// @ts-nocheck +// adapted from /~https://github.com/solidjs/solid-start/blob/ccff60ce75e066f6613daf0272dbb43a196235a4/packages/start/node/fetch.js +import { once } from "events"; +import { type IncomingMessage, type ServerResponse } from "http"; +import multipart from "parse-multipart-data"; +import { splitCookiesString } from "set-cookie-parser"; +import { Readable } from "stream"; +import { File, FormData, Headers, Request as BaseNodeRequest } from "undici"; +import { type ServerBuild, installGlobals } from "@remix-run/node"; +import { createRequestHandler as createBaseRequestHandler } from "@remix-run/server-runtime"; + +installGlobals(); + +function nodeToWeb(nodeStream) { + let destroyed = false; + let listeners = {}; + + function start(controller) { + listeners["data"] = onData; + listeners["end"] = onData; + listeners["end"] = onDestroy; + listeners["close"] = onDestroy; + listeners["error"] = onDestroy; + for (let name in listeners) nodeStream.on(name, listeners[name]); + + nodeStream.pause(); + + function onData(chunk) { + if (destroyed) return; + controller.enqueue(chunk); + nodeStream.pause(); + } + + function onDestroy(err) { + if (destroyed) return; + destroyed = true; + + for (let name in listeners) + nodeStream.removeListener(name, listeners[name]); + + if (err) controller.error(err); + else controller.close(); + } + } + + function pull() { + if (destroyed) return; + nodeStream.resume(); + } + + function cancel() { + destroyed = true; + + for (let name in listeners) + nodeStream.removeListener(name, listeners[name]); + + nodeStream.push(null); + nodeStream.pause(); + if (nodeStream.destroy) nodeStream.destroy(); + else if (nodeStream.close) nodeStream.close(); + } + + return new ReadableStream({ start: start, pull: pull, cancel: cancel }); +} + +function createHeaders(requestHeaders) { + let headers = new Headers(); + + for (let [key, values] of Object.entries(requestHeaders)) { + if (values) { + if (Array.isArray(values)) { + for (let value of values) { + headers.append(key, value); + } + } else { + headers.set(key, values); + } + } + } + + return headers; +} + +class NodeRequest extends BaseNodeRequest { + constructor(input, init) { + if (init && init.data && init.data.on) { + init = { + duplex: "half", + ...init, + body: init.data.headers["content-type"]?.includes("x-www") + ? init.data + : nodeToWeb(init.data), + }; + } + + super(input, init); + } + + // async json() { + // return JSON.parse(await this.text()); + // } + + async buffer() { + return Buffer.from(await super.arrayBuffer()); + } + + // async text() { + // return (await this.buffer()).toString(); + // } + + // @ts-ignore + async formData() { + if ( + this.headers.get("content-type") === "application/x-www-form-urlencoded" + ) { + return await super.formData(); + } else { + let data = await this.buffer(); + let input = multipart.parse( + data, + this.headers + .get("content-type") + .replace("multipart/form-data; boundary=", "") + ); + let form = new FormData(); + input.forEach(({ name, data, filename, type }) => { + // file fields have Content-Type set, + // whereas non-file fields must not + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart-form-data + let isFile = type !== undefined; + if (isFile) { + let value = new File([data], filename, { type }); + form.append(name, value, filename); + } else { + let value = data.toString("utf-8"); + form.append(name, value); + } + }); + return form; + } + } + + // @ts-ignore + clone() { + /** @type {BaseNodeRequest & { buffer?: () => Promise; formData?: () => Promise }} */ + let el = super.clone(); + el.buffer = this.buffer.bind(el); + el.formData = this.formData.bind(el); + return el; + } +} + +function createRequest(req: IncomingMessage): Request { + let origin = + req.headers.origin && "null" !== req.headers.origin + ? req.headers.origin + : `http://${req.headers.host}`; + let url = new URL(req.url, origin); + + let init = { + method: req.method, + headers: createHeaders(req.headers), + // POST, PUT, & PATCH will be read as body by NodeRequest + data: req.method.indexOf("P") === 0 ? req : null, + }; + + return new NodeRequest(url.href, init); +} + +async function handleNodeResponse(webRes: Response, res: ServerResponse) { + res.statusCode = webRes.status; + res.statusMessage = webRes.statusText; + + for (let [name, value] of webRes.headers) { + if (name === "set-cookie") { + res.setHeader(name, splitCookiesString(value)); + } else res.setHeader(name, value); + } + + if (webRes.body) { + let readable = Readable.from(webRes.body); + readable.pipe(res); + await once(readable, "end"); + } else { + res.end(); + } +} + +export let createRequestHandler = ( + build: ServerBuild, + { + mode = "production", + criticalCss, + }: { + mode?: string; + criticalCss?: string; + } +) => { + let handler = createBaseRequestHandler(build, mode); + return async (req: IncomingMessage, res: ServerResponse) => { + let request = createRequest(req); + let response = await handler(request, {}, { criticalCss }); + handleNodeResponse(response, res); + }; +}; diff --git a/packages/remix-dev/vite/plugin.ts b/packages/remix-dev/vite/plugin.ts new file mode 100644 index 00000000000..572f535a5cd --- /dev/null +++ b/packages/remix-dev/vite/plugin.ts @@ -0,0 +1,935 @@ +import { type BinaryLike, createHash } from "node:crypto"; +import * as path from "node:path"; +import * as fs from "node:fs/promises"; +import { existsSync as fsExistsSync } from "node:fs"; +import { execSync } from "node:child_process"; +import babel from "@babel/core"; +import PackageJson from "@npmcli/package-json"; +import { type ServerBuild } from "@remix-run/server-runtime"; +import { + type Plugin, + type Manifest as ViteManifest, + type ResolvedConfig as ResolvedViteConfig, + type ViteDevServer, + type UserConfig as ViteUserConfig, + normalizePath as viteNormalizePath, + createServer as createViteDevServer, +} from "vite"; +import { + init as initEsModuleLexer, + parse as esModuleLexer, +} from "es-module-lexer"; +import jsesc from "jsesc"; + +import { defineRoutes, type RouteManifest } from "../config/routes"; +import { + type AppConfig as RemixUserConfig, + type RemixConfig as ResolvedRemixConfig, +} from "../config"; +import { flatRoutes } from "../config/flat-routes"; +import { detectPackageManager } from "../cli/detectPackageManager"; +import { type Manifest } from "../manifest"; +import { createRequestHandler } from "./node/adapter"; +import { getStylesForUrl, isCssModulesFile } from "./styles"; +import * as VirtualModule from "./vmod"; +import { removeExports } from "./remove-exports"; +import { transformLegacyCssImports } from "./legacy-css-imports"; +import { replaceImportSpecifier } from "./replace-import-specifier"; + +export type RemixVitePluginOptions = Pick< + RemixUserConfig, + | "appDirectory" + | "assetsBuildDirectory" + | "ignoredRouteFiles" + | "publicPath" + | "routes" + | "serverBuildPath" + | "serverModuleFormat" +>; + +type ResolvedRemixVitePluginConfig = Pick< + ResolvedRemixConfig, + | "appDirectory" + | "rootDirectory" + | "assetsBuildDirectory" + | "entryClientFile" + | "entryServerFile" + | "future" + | "publicPath" + | "relativeAssetsBuildDirectory" + | "routes" + | "serverBuildPath" + | "serverModuleFormat" +>; + +let serverEntryId = VirtualModule.id("server-entry"); +let serverManifestId = VirtualModule.id("server-manifest"); +let browserManifestId = VirtualModule.id("browser-manifest"); +let remixReactProxyId = VirtualModule.id("remix-react-proxy"); +let hmrRuntimeId = VirtualModule.id("hmr-runtime"); + +const normalizePath = (p: string) => { + let unixPath = p.replace(/[\\/]+/g, "/").replace(/^([a-zA-Z]+:|\.\/)/, ""); + return viteNormalizePath(unixPath); +}; + +const resolveFsUrl = (filePath: string) => `/@fs${normalizePath(filePath)}`; + +const isJsFile = (filePath: string) => /\.[cm]?[jt]sx?$/i.test(filePath); + +type Route = RouteManifest[string]; +const resolveRelativeRouteFilePath = ( + route: Route, + pluginConfig: ResolvedRemixVitePluginConfig +) => { + let file = route.file; + let fullPath = path.resolve(pluginConfig.appDirectory, file); + + return normalizePath(fullPath); +}; + +let vmods = [serverEntryId, serverManifestId, browserManifestId]; + +const getHash = (source: BinaryLike, maxLength?: number): string => { + let hash = createHash("sha256").update(source).digest("hex"); + return typeof maxLength === "number" ? hash.slice(0, maxLength) : hash; +}; + +const resolveBuildAssetPaths = ( + pluginConfig: ResolvedRemixVitePluginConfig, + manifest: ViteManifest, + appRelativePath: string +): Manifest["entry"] & { css: string[] } => { + let appPath = path.relative(process.cwd(), pluginConfig.appDirectory); + let manifestKey = normalizePath(path.join(appPath, appRelativePath)); + let manifestEntry = manifest[manifestKey]; + return { + module: `${pluginConfig.publicPath}${manifestEntry.file}`, + imports: + manifestEntry.imports?.map((imported) => { + return `${pluginConfig.publicPath}${manifest[imported].file}`; + }) ?? [], + css: + manifestEntry.css?.map((href) => { + return `${pluginConfig.publicPath}${href}`; + }) ?? [], + }; +}; + +const writeFileSafe = async (file: string, contents: string): Promise => { + await fs.mkdir(path.dirname(file), { recursive: true }); + await fs.writeFile(file, contents); +}; + +const getRouteModuleExports = async ( + viteChildCompiler: ViteDevServer | null, + pluginConfig: ResolvedRemixVitePluginConfig, + routeFile: string +): Promise => { + if (!viteChildCompiler) { + throw new Error("Vite child compiler not found"); + } + + // We transform the route module code with the Vite child compiler so that we + // can parse the exports from non-JS files like MDX. This ensures that we can + // understand the exports from anything that Vite can compile to JS, not just + // the route file formats that the Remix compiler historically supported. + + let ssr = true; + let { pluginContainer, moduleGraph } = viteChildCompiler; + let routePath = path.join(pluginConfig.appDirectory, routeFile); + let url = resolveFsUrl(routePath); + + let resolveId = async () => { + let result = await pluginContainer.resolveId(url, undefined, { ssr }); + if (!result) throw new Error(`Could not resolve module ID for ${url}`); + return result.id; + }; + + let [id, code] = await Promise.all([ + resolveId(), + fs.readFile(routePath, "utf-8"), + // pluginContainer.transform(...) fails if we don't do this first: + moduleGraph.ensureEntryFromUrl(url, ssr), + ]); + + let transformed = await pluginContainer.transform(code, id, { ssr }); + let [, exports] = esModuleLexer(transformed.code); + let exportNames = exports.map((e) => e.n); + + return exportNames; +}; + +const entryExts = [".js", ".jsx", ".ts", ".tsx"]; +const findEntry = (dir: string, basename: string): string | undefined => { + for (let ext of entryExts) { + let file = path.resolve(dir, basename + ext); + if (fsExistsSync(file)) return path.relative(dir, file); + } + + return undefined; +}; + +const addTrailingSlash = (path: string): string => + path.endsWith("/") ? path : path + "/"; + +export let remix: (options?: RemixVitePluginOptions) => Plugin[] = ( + options = {} +) => { + let viteCommand: ResolvedViteConfig["command"]; + let viteUserConfig: ViteUserConfig; + + let cssModulesManifest: Record = {}; + let ssrBuildContext: + | { isSsrBuild: false } + | { isSsrBuild: true; getManifest: () => Promise }; + + let viteChildCompiler: ViteDevServer | null = null; + + let resolvePluginConfig = + async (): Promise => { + let rootDirectory = + viteUserConfig.root ?? process.env.REMIX_ROOT ?? process.cwd(); + let appDirectory = path.resolve( + rootDirectory, + options.appDirectory ?? "app" + ); + let serverBuildPath = path.resolve( + rootDirectory, + options.serverBuildPath ?? "build/index.js" + ); + let serverModuleFormat = options.serverModuleFormat ?? "esm"; + let relativeAssetsBuildDirectory = + options.assetsBuildDirectory ?? path.join("public", "build"); + let assetsBuildDirectory = path.resolve( + rootDirectory, + relativeAssetsBuildDirectory + ); + + let userEntryClientFile = findEntry(appDirectory, "entry.client"); + let entryClientFile = userEntryClientFile ?? "entry.client.tsx"; + + let userEntryServerFile = findEntry(appDirectory, "entry.server"); + let entryServerFile: string; + + let pkgJson = await PackageJson.load(rootDirectory); + let deps = pkgJson.content.dependencies ?? {}; + + if (userEntryServerFile) { + entryServerFile = userEntryServerFile; + } else { + let serverRuntime = deps["@remix-run/deno"] + ? "deno" + : deps["@remix-run/cloudflare"] + ? "cloudflare" + : deps["@remix-run/node"] + ? "node" + : undefined; + + if (!serverRuntime) { + let serverRuntimes = [ + "@remix-run/deno", + "@remix-run/cloudflare", + "@remix-run/node", + ]; + let disjunctionListFormat = new Intl.ListFormat("en", { + style: "long", + type: "disjunction", + }); + let formattedList = disjunctionListFormat.format(serverRuntimes); + throw new Error( + `Could not determine server runtime. Please install one of the following: ${formattedList}` + ); + } + + if (!deps["isbot"]) { + console.log( + "adding `isbot` to your package.json, you should commit this change" + ); + + pkgJson.update({ + dependencies: { + ...pkgJson.content.dependencies, + isbot: "latest", + }, + }); + + await pkgJson.save(); + + let packageManager = detectPackageManager() ?? "npm"; + + execSync(`${packageManager} install`, { + cwd: rootDirectory, + stdio: "inherit", + }); + } + + entryServerFile = `entry.server.${serverRuntime}.tsx`; + } + + let publicPath = addTrailingSlash(options.publicPath ?? "/build/"); + + let rootRouteFile = findEntry(appDirectory, "root"); + if (!rootRouteFile) { + throw new Error(`Missing "root" route file in ${appDirectory}`); + } + + let routes: RouteManifest = { + root: { path: "", id: "root", file: rootRouteFile }, + }; + + if (fsExistsSync(path.resolve(appDirectory, "routes"))) { + let fileRoutes = flatRoutes(appDirectory, options.ignoredRouteFiles); + for (let route of Object.values(fileRoutes)) { + routes[route.id] = { ...route, parentId: route.parentId || "root" }; + } + } + if (options.routes) { + let manualRoutes = await options.routes(defineRoutes); + for (let route of Object.values(manualRoutes)) { + routes[route.id] = { ...route, parentId: route.parentId || "root" }; + } + } + + return { + appDirectory, + rootDirectory, + assetsBuildDirectory, + entryClientFile, + publicPath, + routes, + entryServerFile, + serverBuildPath, + serverModuleFormat, + relativeAssetsBuildDirectory, + future: {}, + }; + }; + + let getServerEntry = async () => { + let pluginConfig = await resolvePluginConfig(); + + return ` + import * as entryServer from ${JSON.stringify( + resolveFsUrl( + path.resolve(pluginConfig.appDirectory, pluginConfig.entryServerFile) + ) + )}; + ${Object.keys(pluginConfig.routes) + .map((key, index) => { + let route = pluginConfig.routes[key]!; + return `import * as route${index} from ${JSON.stringify( + resolveFsUrl(resolveRelativeRouteFilePath(route, pluginConfig)) + )};`; + }) + .join("\n")} + export { default as assets } from ${JSON.stringify(serverManifestId)}; + export const assetsBuildDirectory = ${JSON.stringify( + pluginConfig.relativeAssetsBuildDirectory + )}; + ${ + pluginConfig.future + ? `export const future = ${JSON.stringify(pluginConfig.future)}` + : "" + }; + export const publicPath = ${JSON.stringify(pluginConfig.publicPath)}; + export const entry = { module: entryServer }; + export const routes = { + ${Object.keys(pluginConfig.routes) + .map((key, index) => { + let route = pluginConfig.routes[key]!; + return `${JSON.stringify(key)}: { + id: ${JSON.stringify(route.id)}, + parentId: ${JSON.stringify(route.parentId)}, + path: ${JSON.stringify(route.path)}, + index: ${JSON.stringify(route.index)}, + caseSensitive: ${JSON.stringify(route.caseSensitive)}, + module: route${index} + }`; + }) + .join(",\n ")} + };`; + }; + + let createBuildManifest = async (): Promise => { + let pluginConfig = await resolvePluginConfig(); + let viteManifest = JSON.parse( + await fs.readFile( + path.resolve(pluginConfig.assetsBuildDirectory, "manifest.json"), + "utf-8" + ) + ) as ViteManifest; + + let entry: Manifest["entry"] = resolveBuildAssetPaths( + pluginConfig, + viteManifest, + pluginConfig.entryClientFile + ); + + let routes: Manifest["routes"] = {}; + for (let [key, route] of Object.entries(pluginConfig.routes)) { + let sourceExports = await getRouteModuleExports( + viteChildCompiler, + pluginConfig, + route.file + ); + + routes[key] = { + id: route.id, + parentId: route.parentId, + path: route.path, + index: route.index, + caseSensitive: route.caseSensitive, + hasAction: sourceExports.includes("action"), + hasLoader: sourceExports.includes("loader"), + hasErrorBoundary: sourceExports.includes("ErrorBoundary"), + ...resolveBuildAssetPaths(pluginConfig, viteManifest, route.file), + }; + } + + let fingerprintedValues = { entry, routes }; + let version = getHash(JSON.stringify(fingerprintedValues), 8); + let manifestFilename = `manifest-${version}.js`; + let url = `${pluginConfig.publicPath}${manifestFilename}`; + let nonFingerprintedValues = { url, version }; + + let manifest: Manifest = { + ...fingerprintedValues, + ...nonFingerprintedValues, + }; + + await writeFileSafe( + path.join(pluginConfig.assetsBuildDirectory, manifestFilename), + `window.__remixManifest=${JSON.stringify(manifest)};` + ); + + return manifest; + }; + + let getDevManifest = async (): Promise => { + let pluginConfig = await resolvePluginConfig(); + let routes: Manifest["routes"] = {}; + + for (let [key, route] of Object.entries(pluginConfig.routes)) { + let sourceExports = await getRouteModuleExports( + viteChildCompiler, + pluginConfig, + route.file + ); + + routes[key] = { + id: route.id, + parentId: route.parentId, + path: route.path, + index: route.index, + caseSensitive: route.caseSensitive, + module: `${resolveFsUrl( + resolveRelativeRouteFilePath(route, pluginConfig) + )}${ + isJsFile(route.file) ? "" : "?import" // Ensure the Vite dev server responds with a JS module + }`, + hasAction: sourceExports.includes("action"), + hasLoader: sourceExports.includes("loader"), + hasErrorBoundary: sourceExports.includes("ErrorBoundary"), + imports: [], + }; + } + + return { + version: String(Math.random()), + url: VirtualModule.url(browserManifestId), + entry: { + module: resolveFsUrl( + path.resolve(pluginConfig.appDirectory, pluginConfig.entryClientFile) + ), + imports: [], + }, + routes, + }; + }; + + return [ + { + name: "remix", + config: async (_viteUserConfig, viteConfigEnv) => { + viteUserConfig = _viteUserConfig; + viteCommand = viteConfigEnv.command; + + let pluginConfig = await resolvePluginConfig(); + + return { + appType: "custom", + experimental: { hmrPartialAccept: true }, + ...(viteCommand === "build" && { + base: pluginConfig.publicPath, + build: { + ...viteUserConfig.build, + ...(!viteConfigEnv.ssrBuild + ? { + manifest: true, + outDir: pluginConfig.assetsBuildDirectory, + rollupOptions: { + ...viteUserConfig.build?.rollupOptions, + preserveEntrySignatures: "exports-only", + input: [ + path.resolve( + pluginConfig.appDirectory, + pluginConfig.entryClientFile + ), + ...Object.values(pluginConfig.routes).map((route) => + path.resolve(pluginConfig.appDirectory, route.file) + ), + ], + }, + } + : { + outDir: path.dirname(pluginConfig.serverBuildPath), + rollupOptions: { + ...viteUserConfig.build?.rollupOptions, + preserveEntrySignatures: "exports-only", + input: serverEntryId, + output: { + entryFileNames: path.basename( + pluginConfig.serverBuildPath + ), + format: pluginConfig.serverModuleFormat, + }, + }, + }), + }, + }), + }; + }, + async configResolved(viteConfig) { + await initEsModuleLexer; + + viteChildCompiler = await createViteDevServer({ + ...viteUserConfig, + configFile: false, + envFile: false, + plugins: [ + ...(viteUserConfig.plugins ?? []) + .flat() + // Exclude this plugin from the child compiler to prevent an + // infinite loop (plugin creates a child compiler with the same + // plugin that creates another child compiler, repeat ad + // infinitum), and to prevent the manifest from being written to + // disk from the child compiler. This is important in the + // production build because the child compiler is a Vite dev + // server and will generate incorrect manifests. + .filter( + (plugin) => + typeof plugin === "object" && + plugin !== null && + "name" in plugin && + plugin.name !== "remix" && + plugin.name !== "remix-hmr-updates" + ), + { + name: "no-hmr", + handleHotUpdate() { + // parent vite server is already sending HMR updates + // do not send duplicate HMR updates from child server + // which log confusing "page reloaded" messages that aren't true + return []; + }, + }, + ], + }); + await viteChildCompiler.pluginContainer.buildStart({}); + + ssrBuildContext = + viteConfig.build.ssr && viteCommand === "build" + ? { isSsrBuild: true, getManifest: createBuildManifest } + : { isSsrBuild: false }; + }, + transform(code, id) { + if (isCssModulesFile(id)) { + cssModulesManifest[id] = code; + } + }, + configureServer(vite) { + return () => { + vite.middlewares.use(async (req, res, next) => { + try { + // Invalidate all virtual modules + vmods.forEach((vmod) => { + let mod = vite.moduleGraph.getModuleById( + VirtualModule.resolve(vmod) + ); + + if (mod) { + vite.moduleGraph.invalidateModule(mod); + } + }); + + let { url } = req; + let [pluginConfig, build] = await Promise.all([ + resolvePluginConfig(), + vite.ssrLoadModule(serverEntryId) as Promise, + ]); + + let handle = createRequestHandler(build, { + mode: "development", + criticalCss: await getStylesForUrl( + vite, + pluginConfig, + cssModulesManifest, + build, + url + ), + }); + + await handle(req, res); + } catch (error) { + next(error); + } + }); + }; + }, + async buildEnd() { + await viteChildCompiler?.close(); + }, + }, + { + name: "remix-virtual-modules", + enforce: "pre", + resolveId(id) { + if (vmods.includes(id)) return VirtualModule.resolve(id); + }, + async load(id) { + switch (id) { + case VirtualModule.resolve(serverEntryId): { + return await getServerEntry(); + } + case VirtualModule.resolve(serverManifestId): { + let manifest = ssrBuildContext.isSsrBuild + ? await ssrBuildContext.getManifest() + : await getDevManifest(); + + return `export default ${jsesc(manifest, { es6: true })};`; + } + case VirtualModule.resolve(browserManifestId): { + if (viteCommand === "build") { + throw new Error("This module only exists in development"); + } + + let manifest = await getDevManifest(); + + return `window.__remixManifest=${jsesc(manifest, { es6: true })};`; + } + } + }, + }, + { + name: "remix-empty-server-modules", + enforce: "pre", + async transform(_code, id, options) { + if (!options?.ssr && /\.server(\.[cm]?[jt]sx?)?$/.test(id)) + return { + code: "export default {}", + map: null, + }; + }, + }, + { + name: "remix-empty-client-modules", + enforce: "pre", + async transform(_code, id, options) { + if (options?.ssr && /\.client(\.[cm]?[jt]sx?)?$/.test(id)) + return { + code: "export default {}", + map: null, + }; + }, + }, + { + name: "remix-remove-server-exports", + enforce: "post", // Ensure we're operating on the transformed code to support MDX etc. + async transform(code, id, options) { + if (options?.ssr) return; + + let pluginConfig = await resolvePluginConfig(); + + let route = getRoute(pluginConfig, id); + if (!route) return; + + let serverExports = ["loader", "action", "headers"]; + + let routeExports = await getRouteModuleExports( + viteChildCompiler, + pluginConfig, + route.file + ); + + // ignore resource routes that only have server exports + // note: resource routes for fullstack components don't need a `default` export + // but still need their server exports removed + let browserExports = routeExports.filter( + (x) => !serverExports.includes(x) + ); + if (browserExports.length === 0) return; + + return { + code: removeExports(code, serverExports), + map: null, + }; + }, + }, + { + name: "remix-remix-react-proxy", + enforce: "post", // Ensure we're operating on the transformed code to support MDX etc. + resolveId(id) { + if (id === remixReactProxyId) { + return VirtualModule.resolve(remixReactProxyId); + } + }, + transform(code, id) { + // Don't transform the proxy itself, otherwise it will import itself + if (id === VirtualModule.resolve(remixReactProxyId)) { + return; + } + + // Don't transform files that don't need the proxy + if ( + !code.includes("@remix-run/react") && + !code.includes("LiveReload") + ) { + return; + } + + // Rewrite imports to use the proxy + return replaceImportSpecifier({ + code, + specifier: "@remix-run/react", + replaceWith: remixReactProxyId, + }); + }, + load(id) { + if (id === VirtualModule.resolve(remixReactProxyId)) { + // TODO: ensure react refresh is initialized before `` + return [ + 'import { createElement } from "react";', + 'export * from "@remix-run/react";', + 'export const LiveReload = process.env.NODE_ENV !== "development" ? () => null : ', + '() => createElement("script", {', + ' type: "module",', + " suppressHydrationWarning: true,", + " dangerouslySetInnerHTML: { __html: `", + ` import RefreshRuntime from "${VirtualModule.url( + hmrRuntimeId + )}"`, + " RefreshRuntime.injectIntoGlobalHook(window)", + " window.$RefreshReg$ = () => {}", + " window.$RefreshSig$ = () => (type) => type", + " window.__vite_plugin_react_preamble_installed__ = true", + " `}", + "});", + ].join("\n"); + } + }, + }, + { + name: "remix-hmr-runtime", + enforce: "pre", + resolveId(id) { + if (id === hmrRuntimeId) return VirtualModule.resolve(hmrRuntimeId); + }, + async load(id) { + if (id !== VirtualModule.resolve(hmrRuntimeId)) return; + + let reactRefreshDir = path.dirname( + require.resolve("react-refresh/package.json") + ); + let reactRefreshRuntimePath = path.join( + reactRefreshDir, + "cjs/react-refresh-runtime.development.js" + ); + + return [ + "const exports = {}", + await fs.readFile(reactRefreshRuntimePath, "utf8"), + await fs.readFile( + require.resolve("./static/refresh-utils.cjs"), + "utf8" + ), + "export default exports", + ].join("\n"); + }, + }, + { + name: "remix-react-refresh-babel", + enforce: "post", + async transform(code, id, options) { + if (viteCommand !== "serve") return; + if (id.includes("/node_modules/")) return; + + let [filepath] = id.split("?"); + if (!/.[tj]sx?$/.test(filepath)) return; + + let devRuntime = "react/jsx-dev-runtime"; + let ssr = options?.ssr === true; + let isJSX = filepath.endsWith("x"); + let useFastRefresh = !ssr && (isJSX || code.includes(devRuntime)); + if (!useFastRefresh) return; + + let result = await babel.transformAsync(code, { + filename: id, + sourceFileName: filepath, + parserOpts: { + sourceType: "module", + allowAwaitOutsideFunction: true, + plugins: ["jsx", "typescript"], + }, + plugins: ["react-refresh/babel"], + sourceMaps: true, + }); + if (result === null) return; + + code = result.code!; + let refreshContentRE = /\$Refresh(?:Reg|Sig)\$\(/; + if (refreshContentRE.test(code)) { + let pluginConfig = await resolvePluginConfig(); + code = addRefreshWrapper(pluginConfig, code, id); + } + return { code, map: result.map }; + }, + }, + { + name: "remix-hmr-updates", + async handleHotUpdate({ server, file, modules }) { + let pluginConfig = await resolvePluginConfig(); + let route = getRoute(pluginConfig, file); + if (route) { + server.ws.send({ + type: "custom", + event: "remix:hmr-route", + data: { + route: await getRouteMetadata( + pluginConfig, + viteChildCompiler, + route + ), + }, + }); + return modules; + } + return modules; + }, + }, + ]; +}; + +function addRefreshWrapper( + pluginConfig: ResolvedRemixVitePluginConfig, + code: string, + id: string +): string { + let isRoute = getRoute(pluginConfig, id); + let acceptExports = isRoute ? ["meta", "links", "shouldRevalidate"] : []; + return ( + REACT_REFRESH_HEADER.replace("__SOURCE__", JSON.stringify(id)) + + code + + REACT_REFRESH_FOOTER.replace("__SOURCE__", JSON.stringify(id)).replace( + "__ACCEPT_EXPORTS__", + JSON.stringify(acceptExports) + ) + ); +} + +const REACT_REFRESH_HEADER = ` +import RefreshRuntime from "${hmrRuntimeId}"; + +const inWebWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope; +let prevRefreshReg; +let prevRefreshSig; + +if (import.meta.hot && !inWebWorker) { + if (!window.__vite_plugin_react_preamble_installed__) { + throw new Error( + "@vitejs/plugin-react can't detect preamble. Something is wrong. " + + "See /~https://github.com/vitejs/vite-plugin-react/pull/11#discussion_r430879201" + ); + } + + prevRefreshReg = window.$RefreshReg$; + prevRefreshSig = window.$RefreshSig$; + window.$RefreshReg$ = (type, id) => { + RefreshRuntime.register(type, __SOURCE__ + " " + id) + }; + window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform; +}`.replace(/\n+/g, ""); + +const REACT_REFRESH_FOOTER = ` +if (import.meta.hot && !inWebWorker) { + window.$RefreshReg$ = prevRefreshReg; + window.$RefreshSig$ = prevRefreshSig; + RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => { + RefreshRuntime.registerExportsForReactRefresh(__SOURCE__, currentExports); + import.meta.hot.accept((nextExports) => { + if (!nextExports) return; + const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate(currentExports, nextExports, __ACCEPT_EXPORTS__); + if (invalidateMessage) import.meta.hot.invalidate(invalidateMessage); + }); + }); +}`; + +export let legacyCssImportSemantics: () => Plugin[] = () => { + return [ + { + name: "remix-legacy-css-import-semantics", + enforce: "pre", + transform(code) { + if (code.includes('.css"') || code.includes(".css'")) { + return transformLegacyCssImports(code); + } + }, + }, + ]; +}; + +function getRoute( + pluginConfig: ResolvedRemixVitePluginConfig, + file: string +): Route | undefined { + if (!file.startsWith(pluginConfig.appDirectory)) return; + let routePath = path.relative(pluginConfig.appDirectory, file); + let route = Object.values(pluginConfig.routes).find( + (r) => r.file === routePath + ); + return route; +} + +async function getRouteMetadata( + pluginConfig: ResolvedRemixVitePluginConfig, + viteChildCompiler: ViteDevServer | null, + route: Route +) { + let sourceExports = await getRouteModuleExports( + viteChildCompiler, + pluginConfig, + route.file + ); + + let info = { + id: route.id, + parentId: route.parentId, + path: route.path, + index: route.index, + caseSensitive: route.caseSensitive, + url: + "/" + + path.relative( + pluginConfig.rootDirectory, + resolveRelativeRouteFilePath(route, pluginConfig) + ), + module: `${resolveFsUrl( + resolveRelativeRouteFilePath(route, pluginConfig) + )}?import`, // Ensure the Vite dev server responds with a JS module + hasAction: sourceExports.includes("action"), + hasLoader: sourceExports.includes("loader"), + hasErrorBoundary: sourceExports.includes("ErrorBoundary"), + imports: [], + }; + return info; +} diff --git a/packages/remix-dev/vite/remove-exports-test.ts b/packages/remix-dev/vite/remove-exports-test.ts new file mode 100644 index 00000000000..bb52e1f8822 --- /dev/null +++ b/packages/remix-dev/vite/remove-exports-test.ts @@ -0,0 +1,399 @@ +import { removeExports } from "./remove-exports"; + +describe("removeExports", () => { + test("arrow function", () => { + let result = removeExports( + ` + export const serverExport_1 = () => {} + export const serverExport_2 = () => {} + + export const clientExport_1 = () => {} + export const clientExport_2 = () => {} + `, + ["serverExport_1", "serverExport_2"] + ); + expect(result).toMatchInlineSnapshot(` + "export const clientExport_1 = () => {}; + export const clientExport_2 = () => {};" + `); + expect(result).not.toMatch(/server/i); + }); + + test("arrow function with dependencies", () => { + let result = removeExports( + ` + import { serverLib } from 'server-lib' + import { clientLib } from 'client-lib' + import { sharedLib } from 'shared-lib' + + const SERVER_STRING = 'SERVER_STRING' + + const sharedUtil = () => sharedLib() + const serverUtil = () => sharedUtil(serverLib(SERVER_STRING)) + const clientUtil = () => sharedUtil(clientLib()) + + export const serverExport_1 = () => serverUtil() + export const serverExport_2 = () => serverUtil() + + export const clientExport_1 = () => clientUtil() + export const clientExport_2 = () => clientUtil() + `, + ["serverExport_1", "serverExport_2"] + ); + expect(result).toMatchInlineSnapshot(` + "import { clientLib } from 'client-lib'; + import { sharedLib } from 'shared-lib'; + const sharedUtil = () => sharedLib(); + const clientUtil = () => sharedUtil(clientLib()); + export const clientExport_1 = () => clientUtil(); + export const clientExport_2 = () => clientUtil();" + `); + expect(result).not.toMatch(/server/i); + }); + + test("function statement", () => { + let result = removeExports( + ` + export function serverExport_1(){} + export function serverExport_2(){} + + export function clientExport_1(){} + export function clientExport_2(){} + `, + ["serverExport_1", "serverExport_2"] + ); + expect(result).toMatchInlineSnapshot(` + "export function clientExport_1() {} + export function clientExport_2() {}" + `); + expect(result).not.toMatch(/server/i); + }); + + test("function statement with dependencies", () => { + let result = removeExports( + ` + import { serverLib } from 'server-lib' + import { clientLib } from 'client-lib' + import { sharedLib } from 'shared-lib' + + const SERVER_STRING = 'SERVER_STRING' + + function sharedUtil() { return sharedLib() } + function serverUtil() { return sharedUtil(serverLib(SERVER_STRING)) } + function clientUtil() { return sharedUtil(clientLib()) } + + export function serverExport_1() { return serverUtil() } + export function serverExport_2() { return serverUtil() } + + export function clientExport_1() { return clientUtil() } + export function clientExport_2() { return clientUtil() } + `, + ["serverExport_1", "serverExport_2"] + ); + expect(result).toMatchInlineSnapshot(` + "import { clientLib } from 'client-lib'; + import { sharedLib } from 'shared-lib'; + function sharedUtil() { + return sharedLib(); + } + function clientUtil() { + return sharedUtil(clientLib()); + } + export function clientExport_1() { + return clientUtil(); + } + export function clientExport_2() { + return clientUtil(); + }" + `); + expect(result).not.toMatch(/server/i); + }); + + test("object", () => { + let result = removeExports( + ` + export const serverExport_1 = {} + export const serverExport_2 = {} + + export const clientExport_1 = {} + export const clientExport_2 = {} + `, + ["serverExport_1", "serverExport_2"] + ); + expect(result).toMatchInlineSnapshot(` + "export const clientExport_1 = {}; + export const clientExport_2 = {};" + `); + expect(result).not.toMatch(/server/i); + }); + + test("object with dependencies", () => { + let result = removeExports( + ` + import { serverLib } from 'server-lib' + import { clientLib } from 'client-lib' + import { sharedLib } from 'shared-lib' + + const SERVER_STRING = 'SERVER_STRING' + + const sharedUtil = () => sharedLib() + const serverUtil = () => sharedUtil(serverLib(SERVER_STRING)) + const clientUtil = () => sharedUtil(clientLib()) + + export const serverExport_1 = { value: serverUtil() } + export const serverExport_2 = { value: serverUtil() } + + export const clientExport_1 = { value: clientUtil() } + export const clientExport_2 = { value: clientUtil() } + `, + ["serverExport_1", "serverExport_2"] + ); + expect(result).toMatchInlineSnapshot(` + "import { clientLib } from 'client-lib'; + import { sharedLib } from 'shared-lib'; + const sharedUtil = () => sharedLib(); + const clientUtil = () => sharedUtil(clientLib()); + export const clientExport_1 = { + value: clientUtil() + }; + export const clientExport_2 = { + value: clientUtil() + };" + `); + expect(result).not.toMatch(/server/i); + }); + + test("function call", () => { + let result = removeExports( + ` + export const serverExport_1 = globalFunction() + export const serverExport_2 = globalFunction() + + export const clientExport_1 = globalFunction() + export const clientExport_2 = globalFunction() + `, + ["serverExport_1", "serverExport_2"] + ); + expect(result).toMatchInlineSnapshot(` + "export const clientExport_1 = globalFunction(); + export const clientExport_2 = globalFunction();" + `); + expect(result).not.toMatch(/server/i); + }); + + test("function call with dependencies", () => { + let result = removeExports( + ` + import { serverLib } from 'server-lib' + import { clientLib } from 'client-lib' + import { sharedLib } from 'shared-lib' + + const SERVER_STRING = 'SERVER_STRING' + + const sharedUtil = () => sharedLib() + const serverUtil = () => sharedUtil(serverLib(SERVER_STRING)) + const clientUtil = () => sharedUtil(clientLib()) + + export const serverExport_1 = serverUtil() + export const serverExport_2 = serverUtil() + + export const clientExport_1 = clientUtil() + export const clientExport_2 = clientUtil() + `, + ["serverExport_1", "serverExport_2"] + ); + expect(result).toMatchInlineSnapshot(` + "import { clientLib } from 'client-lib'; + import { sharedLib } from 'shared-lib'; + const sharedUtil = () => sharedLib(); + const clientUtil = () => sharedUtil(clientLib()); + export const clientExport_1 = clientUtil(); + export const clientExport_2 = clientUtil();" + `); + expect(result).not.toMatch(/server/i); + }); + + test("iife", () => { + let result = removeExports( + ` + export const serverExport_1 = (() => {})() + export const serverExport_2 = (() => {})() + + export const clientExport_1 = (() => {})() + export const clientExport_2 = (() => {})() + `, + ["serverExport_1", "serverExport_2"] + ); + expect(result).toMatchInlineSnapshot(` + "export const clientExport_1 = (() => {})(); + export const clientExport_2 = (() => {})();" + `); + expect(result).not.toMatch(/server/i); + }); + + test("iife with dependencies", () => { + let result = removeExports( + ` + import { serverLib } from 'server-lib' + import { clientLib } from 'client-lib' + import { sharedLib } from 'shared-lib' + + const SERVER_STRING = 'SERVER_STRING' + + const sharedUtil = () => sharedLib() + const serverUtil = () => sharedUtil(serverLib(SERVER_STRING)) + const clientUtil = () => sharedUtil(clientLib()) + + export const serverExport_1 = (() => serverUtil())() + export const serverExport_2 = (() => serverUtil())() + + export const clientExport_1 = (() => clientUtil())() + export const clientExport_2 = (() => clientUtil())() + `, + ["serverExport_1", "serverExport_2"] + ); + expect(result).toMatchInlineSnapshot(` + "import { clientLib } from 'client-lib'; + import { sharedLib } from 'shared-lib'; + const sharedUtil = () => sharedLib(); + const clientUtil = () => sharedUtil(clientLib()); + export const clientExport_1 = (() => clientUtil())(); + export const clientExport_2 = (() => clientUtil())();" + `); + expect(result).not.toMatch(/server/i); + }); + + test("re-export", () => { + let result = removeExports( + ` + export { serverExport_1 } from './server/1' + export { serverExport_2 } from './server/2' + + export { clientExport_1 } from './client/1' + export { clientExport_2 } from './client/2' + `, + ["serverExport_1", "serverExport_2"] + ); + expect(result).toMatchInlineSnapshot(` + "export { clientExport_1 } from './client/1'; + export { clientExport_2 } from './client/2';" + `); + expect(result).not.toMatch(/server/i); + }); + + test("re-export multiple", () => { + let result = removeExports( + ` + export { serverExport_1, serverExport_2 } from './server' + + export { clientExport_1, clientExport_2 } from './client' + `, + ["serverExport_1", "serverExport_2"] + ); + expect(result).toMatchInlineSnapshot( + "\"export { clientExport_1, clientExport_2 } from './client';\"" + ); + expect(result).not.toMatch(/server/i); + }); + + test("re-export manual", () => { + let result = removeExports( + ` + import { serverExport_1 } from './server/1' + import { serverExport_2 } from './server/2' + import { clientExport_1 } from './client/1' + import { clientExport_2 } from './client/2' + + export { serverExport_1 } + export { serverExport_2 } + + export { clientExport_1 } + export { clientExport_2 } + `, + ["serverExport_1", "serverExport_2"] + ); + expect(result).toMatchInlineSnapshot(` + "import { clientExport_1 } from './client/1'; + import { clientExport_2 } from './client/2'; + export { clientExport_1 }; + export { clientExport_2 };" + `); + expect(result).not.toMatch(/server/i); + }); + + test("number", () => { + let result = removeExports( + ` + export const serverExport_1 = 123 + export const serverExport_2 = 123 + + export const clientExport_1 = 123 + export const clientExport_2 = 123 + `, + ["serverExport_1", "serverExport_2"] + ); + expect(result).toMatchInlineSnapshot(` + "export const clientExport_1 = 123; + export const clientExport_2 = 123;" + `); + expect(result).not.toMatch(/server/i); + }); + + test("string", () => { + let result = removeExports( + ` + export const serverExport_1 = 'string' + export const serverExport_2 = 'string' + + export const clientExport_1 = 'string' + export const clientExport_2 = 'string' + `, + ["serverExport_1", "serverExport_2"] + ); + expect(result).toMatchInlineSnapshot(` + "export const clientExport_1 = 'string'; + export const clientExport_2 = 'string';" + `); + expect(result).not.toMatch(/server/i); + }); + + test("string reference", () => { + let result = removeExports( + ` + const SERVER_STRING = 'SERVER_STRING'; + const CLIENT_STRING = 'CLIENT_STRING'; + + export const serverExport_1 = SERVER_STRING + export const serverExport_2 = SERVER_STRING + + export const clientExport_1 = CLIENT_STRING + export const clientExport_2 = CLIENT_STRING + `, + ["serverExport_1", "serverExport_2"] + ); + expect(result).toMatchInlineSnapshot(` + "const CLIENT_STRING = 'CLIENT_STRING'; + export const clientExport_1 = CLIENT_STRING; + export const clientExport_2 = CLIENT_STRING;" + `); + expect(result).not.toMatch(/server/i); + }); + + test("null", () => { + let result = removeExports( + ` + export const serverExport_1 = null + export const serverExport_2 = null + + export const clientExport_1 = null + export const clientExport_2 = null + `, + ["serverExport_1", "serverExport_2"] + ); + expect(result).toMatchInlineSnapshot(` + "export const clientExport_1 = null; + export const clientExport_2 = null;" + `); + expect(result).not.toMatch(/server/i); + }); +}); diff --git a/packages/remix-dev/vite/remove-exports.ts b/packages/remix-dev/vite/remove-exports.ts new file mode 100644 index 00000000000..cb5da87b9a6 --- /dev/null +++ b/packages/remix-dev/vite/remove-exports.ts @@ -0,0 +1,363 @@ +// Adapted from /~https://github.com/egoist/babel-plugin-eliminator/blob/d29859396b7708b7f7abbacdd951cbbc80902f00/src/index.ts +// Which was originally adapted from /~https://github.com/vercel/next.js/blob/574fe0b582d5cc1b13663121fd47a3d82deaaa17/packages/next/build/babel/plugins/next-ssg-transform.ts +import { + type BabelTypes, + type NodePath, + parse, + traverse, + generate, + t, +} from "./babel"; + +function getIdentifier( + path: NodePath< + | BabelTypes.FunctionDeclaration + | BabelTypes.FunctionExpression + | BabelTypes.ArrowFunctionExpression + > +): NodePath | null { + let parentPath = path.parentPath; + if (parentPath.type === "VariableDeclarator") { + let variablePath = parentPath as NodePath; + let name = variablePath.get("id"); + return name.node.type === "Identifier" + ? (name as NodePath) + : null; + } + + if (parentPath.type === "AssignmentExpression") { + let variablePath = parentPath as NodePath; + let name = variablePath.get("left"); + return name.node.type === "Identifier" + ? (name as NodePath) + : null; + } + + if (path.node.type === "ArrowFunctionExpression") { + return null; + } + + return path.node.id && path.node.id.type === "Identifier" + ? (path.get("id") as NodePath) + : null; +} + +function isIdentifierReferenced( + ident: NodePath +): boolean { + let binding = ident.scope.getBinding(ident.node.name); + if (binding?.referenced) { + // Functions can reference themselves, so we need to check if there's a + // binding outside the function scope or not. + if (binding.path.type === "FunctionDeclaration") { + return !binding.constantViolations + .concat(binding.referencePaths) + // Check that every reference is contained within the function: + .every((ref) => ref.findParent((parent) => parent === binding?.path)); + } + + return true; + } + return false; +} + +export const removeExports = (source: string, exportsToRemove: string[]) => { + let document = parse(source, { sourceType: "module" }); + let generateCode = () => generate(document).code; + + let referencedIdentifiers = new Set>(); + let removedExports = new Set(); + + let markImport = ( + path: NodePath< + | BabelTypes.ImportSpecifier + | BabelTypes.ImportDefaultSpecifier + | BabelTypes.ImportNamespaceSpecifier + > + ) => { + let local = path.get("local"); + if (isIdentifierReferenced(local)) { + referencedIdentifiers.add(local); + } + }; + + let markFunction = ( + path: NodePath< + | BabelTypes.FunctionDeclaration + | BabelTypes.FunctionExpression + | BabelTypes.ArrowFunctionExpression + > + ) => { + let identifier = getIdentifier(path); + if (identifier?.node && isIdentifierReferenced(identifier)) { + referencedIdentifiers.add(identifier); + } + }; + + traverse(document, { + VariableDeclarator(variablePath) { + if (variablePath.node.id.type === "Identifier") { + let local = variablePath.get("id") as NodePath; + if (isIdentifierReferenced(local)) { + referencedIdentifiers.add(local); + } + } else if (variablePath.node.id.type === "ObjectPattern") { + let pattern = variablePath.get( + "id" + ) as NodePath; + + let properties = pattern.get("properties"); + properties.forEach((p) => { + let local = p.get( + p.node.type === "ObjectProperty" + ? "value" + : p.node.type === "RestElement" + ? "argument" + : (function () { + throw new Error("invariant"); + })() + ) as NodePath; + if (isIdentifierReferenced(local)) { + referencedIdentifiers.add(local); + } + }); + } else if (variablePath.node.id.type === "ArrayPattern") { + let pattern = variablePath.get( + "id" + ) as NodePath; + + let elements = pattern.get("elements"); + elements.forEach((element) => { + let local: NodePath; + if (element.node?.type === "Identifier") { + local = element as NodePath; + } else if (element.node?.type === "RestElement") { + local = element.get("argument") as NodePath; + } else { + return; + } + + if (isIdentifierReferenced(local)) { + referencedIdentifiers.add(local); + } + }); + } + }, + + FunctionDeclaration: markFunction, + FunctionExpression: markFunction, + ArrowFunctionExpression: markFunction, + ImportSpecifier: markImport, + ImportDefaultSpecifier: markImport, + ImportNamespaceSpecifier: markImport, + + ExportNamedDeclaration(path) { + let shouldRemove = false; + + // Handle re-exports: export { preload } from './foo' + path.node.specifiers = path.node.specifiers.filter((spec) => { + if (spec.exported.type !== "Identifier") { + return true; + } + + let { name } = spec.exported; + for (let namedExport of exportsToRemove) { + if (name === namedExport) { + removedExports.add(namedExport); + return false; + } + } + + return true; + }); + + let { declaration } = path.node; + + // When no re-exports are left, remove the path + if (!declaration && path.node.specifiers.length === 0) { + shouldRemove = true; + } + + if (declaration && declaration.type === "VariableDeclaration") { + declaration.declarations = declaration.declarations.filter( + (declarator: BabelTypes.VariableDeclarator) => { + for (let name of exportsToRemove) { + if ((declarator.id as BabelTypes.Identifier).name === name) { + removedExports.add(name); + return false; + } + } + return true; + } + ); + if (declaration.declarations.length === 0) { + shouldRemove = true; + } + } + + if (declaration && declaration.type === "FunctionDeclaration") { + for (let name of exportsToRemove) { + if (declaration.id?.name === name) { + shouldRemove = true; + removedExports.add(name); + } + } + } + + if (shouldRemove) { + path.remove(); + } + }, + }); + + if (removedExports.size === 0) { + // No server-specific exports found so there's + // no need to remove unused references + return generateCode(); + } + + let referencesRemovedInThisPass: number; + + let sweepFunction = ( + path: NodePath< + | BabelTypes.FunctionDeclaration + | BabelTypes.FunctionExpression + | BabelTypes.ArrowFunctionExpression + > + ) => { + let identifier = getIdentifier(path); + if ( + identifier?.node && + referencedIdentifiers.has(identifier) && + !isIdentifierReferenced(identifier) + ) { + ++referencesRemovedInThisPass; + + if ( + t.isAssignmentExpression(path.parentPath.node) || + t.isVariableDeclarator(path.parentPath.node) + ) { + path.parentPath.remove(); + } else { + path.remove(); + } + } + }; + + let sweepImport = ( + path: NodePath< + | BabelTypes.ImportSpecifier + | BabelTypes.ImportDefaultSpecifier + | BabelTypes.ImportNamespaceSpecifier + > + ) => { + let local = path.get("local"); + if (referencedIdentifiers.has(local) && !isIdentifierReferenced(local)) { + ++referencesRemovedInThisPass; + path.remove(); + if ( + (path.parent as BabelTypes.ImportDeclaration).specifiers.length === 0 + ) { + path.parentPath.remove(); + } + } + }; + + // Traverse again to remove unused references. This happens at least once, + // then repeats until no more references are removed. + do { + referencesRemovedInThisPass = 0; + + traverse(document, { + Program(path) { + path.scope.crawl(); + }, + // eslint-disable-next-line no-loop-func + VariableDeclarator(variablePath) { + if (variablePath.node.id.type === "Identifier") { + let local = variablePath.get("id") as NodePath; + if ( + referencedIdentifiers.has(local) && + !isIdentifierReferenced(local) + ) { + ++referencesRemovedInThisPass; + variablePath.remove(); + } + } else if (variablePath.node.id.type === "ObjectPattern") { + let pattern = variablePath.get( + "id" + ) as NodePath; + + let beforeCount = referencesRemovedInThisPass; + let properties = pattern.get("properties"); + properties.forEach((property) => { + let local = property.get( + property.node.type === "ObjectProperty" + ? "value" + : property.node.type === "RestElement" + ? "argument" + : (function () { + throw new Error("invariant"); + })() + ) as NodePath; + + if ( + referencedIdentifiers.has(local) && + !isIdentifierReferenced(local) + ) { + ++referencesRemovedInThisPass; + property.remove(); + } + }); + + if ( + beforeCount !== referencesRemovedInThisPass && + pattern.get("properties").length < 1 + ) { + variablePath.remove(); + } + } else if (variablePath.node.id.type === "ArrayPattern") { + let pattern = variablePath.get( + "id" + ) as NodePath; + + let beforeCount = referencesRemovedInThisPass; + let elements = pattern.get("elements"); + elements.forEach((e) => { + let local: NodePath; + if (e.node?.type === "Identifier") { + local = e as NodePath; + } else if (e.node?.type === "RestElement") { + local = e.get("argument") as NodePath; + } else { + return; + } + + if ( + referencedIdentifiers.has(local) && + !isIdentifierReferenced(local) + ) { + ++referencesRemovedInThisPass; + e.remove(); + } + }); + + if ( + beforeCount !== referencesRemovedInThisPass && + pattern.get("elements").length < 1 + ) { + variablePath.remove(); + } + } + }, + FunctionDeclaration: sweepFunction, + FunctionExpression: sweepFunction, + ArrowFunctionExpression: sweepFunction, + ImportSpecifier: sweepImport, + ImportDefaultSpecifier: sweepImport, + ImportNamespaceSpecifier: sweepImport, + }); + } while (referencesRemovedInThisPass); + + return generateCode(); +}; diff --git a/packages/remix-dev/vite/replace-import-specifier.ts b/packages/remix-dev/vite/replace-import-specifier.ts new file mode 100644 index 00000000000..8f1fd59c07c --- /dev/null +++ b/packages/remix-dev/vite/replace-import-specifier.ts @@ -0,0 +1,26 @@ +import { parse, traverse, generate } from "./babel"; + +export const replaceImportSpecifier = ({ + code, + specifier, + replaceWith, +}: { + code: string; + specifier: string; + replaceWith: string; +}) => { + let ast = parse(code, { sourceType: "module" }); + + traverse(ast, { + ImportDeclaration(path) { + if (path.node.source.value === specifier) { + path.node.source.value = replaceWith; + } + }, + }); + + return { + code: generate(ast, { retainLines: true }).code, + map: null, + }; +}; diff --git a/packages/remix-dev/vite/static/refresh-utils.cjs b/packages/remix-dev/vite/static/refresh-utils.cjs new file mode 100644 index 00000000000..bb0d2721cdf --- /dev/null +++ b/packages/remix-dev/vite/static/refresh-utils.cjs @@ -0,0 +1,170 @@ +// adapted from /~https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/src/refreshUtils.js +// This file gets injected into the browser as a part of the HMR runtime + +function debounce(fn, delay) { + let handle; + return () => { + clearTimeout(handle); + handle = setTimeout(fn, delay); + }; +} + +/* eslint-disable no-undef */ +const enqueueUpdate = debounce(async () => { + let manifest; + if (routeUpdates.size > 0) { + manifest = JSON.parse(JSON.stringify(__remixManifest)); + + routeUpdates.forEach(async (route) => { + manifest[route.id] = route; + + let imported = await __hmr_import(route.url + "?t=" + Date.now()); + let routeModule = { + ...imported, + // react-refresh takes care of updating these in-place, + // if we don't preserve existing values we'll loose state. + default: imported.default + ? window.__remixRouteModules[route.id]?.default ?? imported.default + : imported.default, + ErrorBoundary: imported.ErrorBoundary + ? window.__remixRouteModules[route.id]?.ErrorBoundary ?? + imported.ErrorBoundary + : imported.ErrorBoundary, + }; + window.__remixRouteModules[route.id] = routeModule; + }); + + let needsRevalidation = new Set( + Array.from(routeUpdates.values()) + .filter((route) => route.hasLoader) + .map((route) => route.id) + ); + + let routes = __remixRouter.createRoutesForHMR( + needsRevalidation, + manifest.routes, + window.__remixRouteModules, + window.__remixContext.future + ); + __remixRouter._internalSetRoutes(routes); + routeUpdates.clear(); + } + + await revalidate(); + if (manifest) { + Object.assign(window.__remixManifest, manifest); + } + exports.performReactRefresh(); +}, 16); + +// Taken from /~https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/lib/runtime/RefreshUtils.js#L141 +// This allows to resister components not detected by SWC like styled component +function registerExportsForReactRefresh(filename, moduleExports) { + for (let key in moduleExports) { + if (key === "__esModule") continue; + let exportValue = moduleExports[key]; + if (exports.isLikelyComponentType(exportValue)) { + // 'export' is required to avoid key collision when renamed exports that + // shadow a local component name: /~https://github.com/vitejs/vite-plugin-react/issues/116 + // The register function has an identity check to not register twice the same component, + // so this is safe to not used the same key here. + exports.register(exportValue, filename + " export " + key); + } + } +} + +function validateRefreshBoundaryAndEnqueueUpdate( + prevExports, + nextExports, + // non-component exports that are handled by the framework (e.g. `meta` and `links` for route modules) + acceptExports = [] +) { + if ( + !predicateOnExport( + prevExports, + (key) => key in nextExports || acceptExports.includes(key) + ) + ) { + return "Could not Fast Refresh (export removed)"; + } + if ( + !predicateOnExport( + nextExports, + (key) => key in prevExports || acceptExports.includes(key) + ) + ) { + return "Could not Fast Refresh (new export)"; + } + + let hasExports = false; + let allExportsAreHandledOrUnchanged = predicateOnExport( + nextExports, + (key, value) => { + hasExports = true; + // Remix can handle Remix-specific exports (e.g. `meta` and `links`) + if (acceptExports.includes(key)) return true; + // React Fast Refresh can handle component exports + if (exports.isLikelyComponentType(value)) return true; + // Unchanged exports are implicitly handled + return prevExports[key] === nextExports[key]; + } + ); + if (hasExports && allExportsAreHandledOrUnchanged) { + enqueueUpdate(); + } else { + return "Could not Fast Refresh. Learn more at /~https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports"; + } +} + +function predicateOnExport(moduleExports, predicate) { + for (let key in moduleExports) { + if (key === "__esModule") continue; + let desc = Object.getOwnPropertyDescriptor(moduleExports, key); + if (desc && desc.get) return false; + if (!predicate(key, moduleExports[key])) return false; + } + return true; +} + +// Hides vite-ignored dynamic import so that Vite can skip analysis if no other +// dynamic import is present (/~https://github.com/vitejs/vite/pull/12732) +function __hmr_import(module) { + return import(/* @vite-ignore */ module); +} + +const routeUpdates = new Map(); + +async function revalidate() { + let { promise, resolve } = channel(); + let unsub = __remixRouter.subscribe((state) => { + if (state.revalidation === "idle") { + unsub(); + // Ensure RouterProvider setState has flushed before re-rendering + resolve(); + } + }); + window.__remixRevalidation = (window.__remixRevalidation || 0) + 1; + __remixRouter.revalidate(); + return promise; +} + +function channel() { + let resolve; + let reject; + + let promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + return { promise, resolve, reject }; +} + +import.meta.hot.on("remix:hmr-route", async ({ route }) => { + routeUpdates.set(route.id, route); +}); + +exports.__hmr_import = __hmr_import; +exports.registerExportsForReactRefresh = registerExportsForReactRefresh; +exports.validateRefreshBoundaryAndEnqueueUpdate = + validateRefreshBoundaryAndEnqueueUpdate; +exports.enqueueUpdate = enqueueUpdate; diff --git a/packages/remix-dev/vite/styles.ts b/packages/remix-dev/vite/styles.ts new file mode 100644 index 00000000000..c2421a545ee --- /dev/null +++ b/packages/remix-dev/vite/styles.ts @@ -0,0 +1,183 @@ +import * as path from "node:path"; +import { type ServerBuild } from "@remix-run/server-runtime"; +import { matchRoutes } from "@remix-run/router"; +import { type ModuleNode, type ViteDevServer } from "vite"; + +import { type RemixConfig as ResolvedRemixConfig } from "../config"; + +type ServerRouteManifest = ServerBuild["routes"]; +type ServerRoute = ServerRouteManifest[string]; + +// Style collection logic adapted from solid-start: /~https://github.com/solidjs/solid-start + +// Vite doesn't expose these so we just copy the list for now +// /~https://github.com/vitejs/vite/blob/d6bde8b03d433778aaed62afc2be0630c8131908/packages/vite/src/node/constants.ts#L49C23-L50 +const cssFileRegExp = + /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/; +// /~https://github.com/vitejs/vite/blob/d6bde8b03d433778aaed62afc2be0630c8131908/packages/vite/src/node/plugins/css.ts#L160 +const cssModulesRegExp = new RegExp(`\\.module${cssFileRegExp.source}`); + +const isCssFile = (file: string) => cssFileRegExp.test(file); +export const isCssModulesFile = (file: string) => cssModulesRegExp.test(file); + +const getStylesForFiles = async ( + viteServer: ViteDevServer, + cssModulesManifest: Record, + files: string[] +): Promise => { + let styles: Record = {}; + let deps = new Set(); + + try { + for (let file of files) { + let normalizedPath = path.resolve(file).replace(/\\/g, "/"); + let node = await viteServer.moduleGraph.getModuleById(normalizedPath); + if (!node) { + let absolutePath = path.resolve(file); + await viteServer.ssrLoadModule(absolutePath); + node = await viteServer.moduleGraph.getModuleByUrl(absolutePath); + + if (!node) { + console.log(`Could not resolve module for file: ${file}`); + continue; + } + } + + await findDeps(viteServer, node, deps); + } + } catch (e) { + console.error(e); + } + + for (let dep of deps) { + if ( + dep.file && + isCssFile(dep.file) && + !dep.url.endsWith("?url") // Ignore styles that resolved as URLs, otherwise we'll end up injecting URLs into the style tag contents + ) { + try { + let css = isCssModulesFile(dep.file) + ? cssModulesManifest[dep.file] + : (await viteServer.ssrLoadModule(dep.url)).default; + + if (css === undefined) { + throw new Error(); + } + + styles[dep.url] = css; + } catch { + console.warn(`Could not load ${dep.file}`); + // this can happen with dynamically imported modules, I think + // because the Vite module graph doesn't distinguish between + // static and dynamic imports? TODO investigate, submit fix + } + } + } + + return ( + Object.entries(styles) + .map(([fileName, css], i) => [ + `\n/* ${fileName + // Escape comment syntax in file paths + .replace(/\/\*/g, "/\\*") + .replace(/\*\//g, "*\\/")} */`, + css, + ]) + .flat() + .join("\n") || undefined + ); +}; + +const findDeps = async ( + vite: ViteDevServer, + node: ModuleNode, + deps: Set +) => { + // since `ssrTransformResult.deps` contains URLs instead of `ModuleNode`s, this process is asynchronous. + // instead of using `await`, we resolve all branches in parallel. + let branches: Promise[] = []; + + async function addFromNode(node: ModuleNode) { + if (!deps.has(node)) { + deps.add(node); + await findDeps(vite, node, deps); + } + } + + async function addFromUrl(url: string) { + let node = await vite.moduleGraph.getModuleByUrl(url); + + if (node) { + await addFromNode(node); + } + } + + if (node.ssrTransformResult) { + if (node.ssrTransformResult.deps) { + node.ssrTransformResult.deps.forEach((url) => + branches.push(addFromUrl(url)) + ); + } + } else { + node.importedModules.forEach((node) => branches.push(addFromNode(node))); + } + + await Promise.all(branches); +}; + +const groupRoutesByParentId = (manifest: ServerRouteManifest) => { + let routes: Record[]> = {}; + + Object.values(manifest).forEach((route) => { + let parentId = route.parentId || ""; + if (!routes[parentId]) { + routes[parentId] = []; + } + routes[parentId].push(route); + }); + + return routes; +}; + +// Create a map of routes by parentId to use recursively instead of +// repeatedly filtering the manifest. +const createRoutes = ( + manifest: ServerRouteManifest, + parentId: string = "", + routesByParentId: Record< + string, + Omit[] + > = groupRoutesByParentId(manifest) +): ServerRoute[] => { + return (routesByParentId[parentId] || []).map((route) => ({ + ...route, + children: createRoutes(manifest, route.id, routesByParentId), + })); +}; + +export const getStylesForUrl = async ( + vite: ViteDevServer, + config: Pick, + cssModulesManifest: Record, + build: ServerBuild, + url: string | undefined +): Promise => { + if (url === undefined || url.includes("?_data=")) { + return undefined; + } + + let routes = createRoutes(build.routes); + let appPath = path.relative(process.cwd(), config.appDirectory); + let documentRouteFiles = + matchRoutes(routes, url)?.map((match) => + path.join(appPath, config.routes[match.route.id].file) + ) ?? []; + + let styles = await getStylesForFiles( + vite, + cssModulesManifest, + documentRouteFiles + ); + + return styles; +}; diff --git a/packages/remix-dev/vite/vmod.ts b/packages/remix-dev/vite/vmod.ts new file mode 100644 index 00000000000..7f7c9ba736f --- /dev/null +++ b/packages/remix-dev/vite/vmod.ts @@ -0,0 +1,3 @@ +export let id = (name: string) => `virtual:${name}` +export let resolve = (id: string) => `\0${id}` +export let url = (id: string) => `/@id/__x00__${id}` diff --git a/yarn.lock b/yarn.lock index 77b15d340d7..ed10f4a9ea2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1322,6 +1322,11 @@ resolved "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.6.tgz#b11bd4e4d031bb320c93c83c137797b2be5b403b" integrity sha512-YnYSCceN/dUzUr5kdtUzB+wZprCafuD89Hs0Aqv9QSdwhYQybhXTaSTcrl6X/aWThn1a/j0eEpUBGOE7269REg== +"@esbuild/android-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" + integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ== + "@esbuild/android-arm@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz#5898f7832c2298bc7d0ab53701c57beb74d78b4d" @@ -1332,6 +1337,11 @@ resolved "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.6.tgz#ac6b5674da2149997f6306b3314dae59bbe0ac26" integrity sha512-bSC9YVUjADDy1gae8RrioINU6e1lCkg3VGVwm0QQ2E1CWcC4gnMce9+B6RpxuSsrsXsk1yojn7sp1fnG8erE2g== +"@esbuild/android-arm@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682" + integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw== + "@esbuild/android-x64@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz#658368ef92067866d95fb268719f98f363d13ae1" @@ -1342,6 +1352,11 @@ resolved "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.6.tgz#18c48bf949046638fc209409ff684c6bb35a5462" integrity sha512-MVcYcgSO7pfu/x34uX9u2QIZHmXAB7dEiLQC5bBl5Ryqtpj9lT2sg3gNDEsrPEmimSJW2FXIaxqSQ501YLDsZQ== +"@esbuild/android-x64@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2" + integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg== + "@esbuild/darwin-arm64@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz#584c34c5991b95d4d48d333300b1a4e2ff7be276" @@ -1352,6 +1367,11 @@ resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.6.tgz#b3fe19af1e4afc849a07c06318124e9c041e0646" integrity sha512-bsDRvlbKMQMt6Wl08nHtFz++yoZHsyTOxnjfB2Q95gato+Yi4WnRl13oC2/PJJA9yLCoRv9gqT/EYX0/zDsyMA== +"@esbuild/darwin-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1" + integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA== + "@esbuild/darwin-x64@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz#7751d236dfe6ce136cce343dce69f52d76b7f6cb" @@ -1362,6 +1382,11 @@ resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.6.tgz#f4dacd1ab21e17b355635c2bba6a31eba26ba569" integrity sha512-xh2A5oPrYRfMFz74QXIQTQo8uA+hYzGWJFoeTE8EvoZGHb+idyV4ATaukaUvnnxJiauhs/fPx3vYhU4wiGfosg== +"@esbuild/darwin-x64@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d" + integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ== + "@esbuild/freebsd-arm64@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz#cacd171665dd1d500f45c167d50c6b7e539d5fd2" @@ -1372,6 +1397,11 @@ resolved "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.6.tgz#ea4531aeda70b17cbe0e77b0c5c36298053855b4" integrity sha512-EnUwjRc1inT4ccZh4pB3v1cIhohE2S4YXlt1OvI7sw/+pD+dIE4smwekZlEPIwY6PhU6oDWwITrQQm5S2/iZgg== +"@esbuild/freebsd-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54" + integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw== + "@esbuild/freebsd-x64@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz#0769456eee2a08b8d925d7c00b79e861cb3162e4" @@ -1382,6 +1412,11 @@ resolved "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.6.tgz#1896170b3c9f63c5e08efdc1f8abc8b1ed7af29f" integrity sha512-Uh3HLWGzH6FwpviUcLMKPCbZUAFzv67Wj5MTwK6jn89b576SR2IbEp+tqUHTr8DIl0iDmBAf51MVaP7pw6PY5Q== +"@esbuild/freebsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e" + integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ== + "@esbuild/linux-arm64@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz#38e162ecb723862c6be1c27d6389f48960b68edb" @@ -1392,6 +1427,11 @@ resolved "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.6.tgz#967dfb951c6b2de6f2af82e96e25d63747f75079" integrity sha512-bUR58IFOMJX523aDVozswnlp5yry7+0cRLCXDsxnUeQYJik1DukMY+apBsLOZJblpH+K7ox7YrKrHmJoWqVR9w== +"@esbuild/linux-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0" + integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA== + "@esbuild/linux-arm@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz#1a2cd399c50040184a805174a6d89097d9d1559a" @@ -1402,6 +1442,11 @@ resolved "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.6.tgz#097a0ee2be39fed3f37ea0e587052961e3bcc110" integrity sha512-7YdGiurNt7lqO0Bf/U9/arrPWPqdPqcV6JCZda4LZgEn+PTQ5SMEI4MGR52Bfn3+d6bNEGcWFzlIxiQdS48YUw== +"@esbuild/linux-arm@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0" + integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg== + "@esbuild/linux-ia32@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz#e28c25266b036ce1cabca3c30155222841dc035a" @@ -1412,6 +1457,11 @@ resolved "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.6.tgz#a38a789d0ed157495a6b5b4469ec7868b59e5278" integrity sha512-ujp8uoQCM9FRcbDfkqECoARsLnLfCUhKARTP56TFPog8ie9JG83D5GVKjQ6yVrEVdMie1djH86fm98eY3quQkQ== +"@esbuild/linux-ia32@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7" + integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA== + "@esbuild/linux-loong64@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz#0f887b8bb3f90658d1a0117283e55dbd4c9dcf72" @@ -1422,6 +1472,11 @@ resolved "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.6.tgz#ae3983d0fb4057883c8246f57d2518c2af7cf2ad" integrity sha512-y2NX1+X/Nt+izj9bLoiaYB9YXT/LoaQFYvCkVD77G/4F+/yuVXYCWz4SE9yr5CBMbOxOfBcy/xFL4LlOeNlzYQ== +"@esbuild/linux-loong64@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d" + integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg== + "@esbuild/linux-mips64el@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz#f5d2a0b8047ea9a5d9f592a178ea054053a70289" @@ -1432,6 +1487,11 @@ resolved "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.6.tgz#15fbbe04648d944ec660ee5797febdf09a9bd6af" integrity sha512-09AXKB1HDOzXD+j3FdXCiL/MWmZP0Ex9eR8DLMBVcHorrWJxWmY8Nms2Nm41iRM64WVx7bA/JVHMv081iP2kUA== +"@esbuild/linux-mips64el@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231" + integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ== + "@esbuild/linux-ppc64@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz#876590e3acbd9fa7f57a2c7d86f83717dbbac8c7" @@ -1442,6 +1502,11 @@ resolved "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.6.tgz#38210094e8e1a971f2d1fd8e48462cc65f15ef19" integrity sha512-AmLhMzkM8JuqTIOhxnX4ubh0XWJIznEynRnZAVdA2mMKE6FAfwT2TWKTwdqMG+qEaeyDPtfNoZRpJbD4ZBv0Tg== +"@esbuild/linux-ppc64@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb" + integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA== + "@esbuild/linux-riscv64@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz#7f49373df463cd9f41dc34f9b2262d771688bf09" @@ -1452,6 +1517,11 @@ resolved "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.6.tgz#bc3c66d5578c3b9951a6ed68763f2a6856827e4a" integrity sha512-Y4Ri62PfavhLQhFbqucysHOmRamlTVK10zPWlqjNbj2XMea+BOs4w6ASKwQwAiqf9ZqcY9Ab7NOU4wIgpxwoSQ== +"@esbuild/linux-riscv64@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6" + integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A== + "@esbuild/linux-s390x@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz#e2afd1afcaf63afe2c7d9ceacd28ec57c77f8829" @@ -1462,6 +1532,11 @@ resolved "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.6.tgz#d7ba7af59285f63cfce6e5b7f82a946f3e6d67fc" integrity sha512-SPUiz4fDbnNEm3JSdUW8pBJ/vkop3M1YwZAVwvdwlFLoJwKEZ9L98l3tzeyMzq27CyepDQ3Qgoba44StgbiN5Q== +"@esbuild/linux-s390x@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071" + integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ== + "@esbuild/linux-x64@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz#8a0e9738b1635f0c53389e515ae83826dec22aa4" @@ -1472,6 +1547,11 @@ resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.6.tgz#ba51f8760a9b9370a2530f98964be5f09d90fed0" integrity sha512-a3yHLmOodHrzuNgdpB7peFGPx1iJ2x6m+uDvhP2CKdr2CwOaqEFMeSqYAHU7hG+RjCq8r2NFujcd/YsEsFgTGw== +"@esbuild/linux-x64@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338" + integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w== + "@esbuild/netbsd-x64@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz#c29fb2453c6b7ddef9a35e2c18b37bda1ae5c462" @@ -1482,6 +1562,11 @@ resolved "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.6.tgz#e84d6b6fdde0261602c1e56edbb9e2cb07c211b9" integrity sha512-EanJqcU/4uZIBreTrnbnre2DXgXSa+Gjap7ifRfllpmyAU7YMvaXmljdArptTHmjrkkKm9BK6GH5D5Yo+p6y5A== +"@esbuild/netbsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1" + integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A== + "@esbuild/openbsd-x64@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz#95e75a391403cb10297280d524d66ce04c920691" @@ -1492,6 +1577,11 @@ resolved "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.6.tgz#cf4b9fb80ce6d280a673d54a731d9c661f88b083" integrity sha512-xaxeSunhQRsTNGFanoOkkLtnmMn5QbA0qBhNet/XLVsc+OVkpIWPHcr3zTW2gxVU5YOHFbIHR9ODuaUdNza2Vw== +"@esbuild/openbsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae" + integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg== + "@esbuild/sunos-x64@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz#722eaf057b83c2575937d3ffe5aeb16540da7273" @@ -1502,6 +1592,11 @@ resolved "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.6.tgz#a6838e246079b24d962b9dcb8d208a3785210a73" integrity sha512-gnMnMPg5pfMkZvhHee21KbKdc6W3GR8/JuE0Da1kjwpK6oiFU3nqfHuVPgUX2rsOx9N2SadSQTIYV1CIjYG+xw== +"@esbuild/sunos-x64@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d" + integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ== + "@esbuild/win32-arm64@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz#9aa9dc074399288bdcdd283443e9aeb6b9552b6f" @@ -1512,6 +1607,11 @@ resolved "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.6.tgz#ace0186e904d109ea4123317a3ba35befe83ac21" integrity sha512-G95n7vP1UnGJPsVdKXllAJPtqjMvFYbN20e8RK8LVLhlTiSOH1sd7+Gt7rm70xiG+I5tM58nYgwWrLs6I1jHqg== +"@esbuild/win32-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9" + integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg== + "@esbuild/win32-ia32@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz#95ad43c62ad62485e210f6299c7b2571e48d2b03" @@ -1522,6 +1622,11 @@ resolved "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.6.tgz#7fb3f6d4143e283a7f7dffc98a6baf31bb365c7e" integrity sha512-96yEFzLhq5bv9jJo5JhTs1gI+1cKQ83cUpyxHuGqXVwQtY5Eq54ZEsKs8veKtiKwlrNimtckHEkj4mRh4pPjsg== +"@esbuild/win32-ia32@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102" + integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g== + "@esbuild/win32-x64@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061" @@ -1532,6 +1637,11 @@ resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.6.tgz#563ff4277f1230a006472664fa9278a83dd124da" integrity sha512-n6d8MOyUrNp6G4VSpRcgjs5xj4A91svJSaiwLIDWVWEsZtpN5FA9NlBbZHDmAJc2e8e6SF4tkBD3HAvPF+7igA== +"@esbuild/win32-x64@0.18.20": + version "0.18.20" + resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" + integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ== + "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -1569,6 +1679,11 @@ resolved "https://registry.npmjs.org/@extra-number/significant-digits/-/significant-digits-1.3.9.tgz" integrity sha512-E5PY/bCwrNqEHh4QS6AQBinLZ+sxM1lT8tsSVYk8VwhWIPp6fCU/BMRVq0V8iJ8LwS3FHmaA4vUzb78s4BIIyA== +"@fastify/busboy@^2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz#f22824caff3ae506b18207bad4126dbc6ccdb6b8" + integrity sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ== + "@humanwhocodes/config-array@^0.11.8": version "0.11.8" resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" @@ -5116,6 +5231,11 @@ es-get-iterator@^1.1.2: isarray "^2.0.5" stop-iteration-iterator "^1.0.0" +es-module-lexer@^1.3.1: + version "1.3.1" + resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz#c1b0dd5ada807a3b3155315911f364dc4e909db1" + integrity sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q== + es-set-tostringtag@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" @@ -5211,6 +5331,34 @@ esbuild@^0.17.5: "@esbuild/win32-ia32" "0.17.19" "@esbuild/win32-x64" "0.17.19" +esbuild@^0.18.10: + version "0.18.20" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6" + integrity sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA== + optionalDependencies: + "@esbuild/android-arm" "0.18.20" + "@esbuild/android-arm64" "0.18.20" + "@esbuild/android-x64" "0.18.20" + "@esbuild/darwin-arm64" "0.18.20" + "@esbuild/darwin-x64" "0.18.20" + "@esbuild/freebsd-arm64" "0.18.20" + "@esbuild/freebsd-x64" "0.18.20" + "@esbuild/linux-arm" "0.18.20" + "@esbuild/linux-arm64" "0.18.20" + "@esbuild/linux-ia32" "0.18.20" + "@esbuild/linux-loong64" "0.18.20" + "@esbuild/linux-mips64el" "0.18.20" + "@esbuild/linux-ppc64" "0.18.20" + "@esbuild/linux-riscv64" "0.18.20" + "@esbuild/linux-s390x" "0.18.20" + "@esbuild/linux-x64" "0.18.20" + "@esbuild/netbsd-x64" "0.18.20" + "@esbuild/openbsd-x64" "0.18.20" + "@esbuild/sunos-x64" "0.18.20" + "@esbuild/win32-arm64" "0.18.20" + "@esbuild/win32-ia32" "0.18.20" + "@esbuild/win32-x64" "0.18.20" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" @@ -9521,6 +9669,11 @@ parse-ms@^2.1.0: resolved "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz" integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA== +parse-multipart-data@^1.5.0: + version "1.5.0" + resolved "https://registry.npmjs.org/parse-multipart-data/-/parse-multipart-data-1.5.0.tgz#ab894cc6c40229d0a2042500e120df7562d94b87" + integrity sha512-ck5zaMF0ydjGfejNMnlo5YU2oJ+pT+80Jb1y4ybanT27j+zbVP/jkYmCrUGsEln0Ox/hZmuvgy8Ra7AxbXP2Mw== + parse5-htmlparser2-tree-adapter@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1" @@ -9831,6 +9984,15 @@ postcss@^8.0.9, postcss@^8.4.19, postcss@^8.4.23: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.27: + version "8.4.31" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + preferred-pm@^3.0.0: version "3.0.3" resolved "https://registry.npmjs.org/preferred-pm/-/preferred-pm-3.0.3.tgz" @@ -10599,6 +10761,13 @@ rollup@^3.21.0: optionalDependencies: fsevents "~2.3.2" +rollup@^3.27.1: + version "3.29.4" + resolved "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" + integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw== + optionalDependencies: + fsevents "~2.3.2" + rrweb-cssom@^0.6.0: version "0.6.0" resolved "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1" @@ -10743,6 +10912,11 @@ set-cookie-parser@^2.4.6, set-cookie-parser@^2.4.8: resolved "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz" integrity sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg== +set-cookie-parser@^2.6.0: + version "2.6.0" + resolved "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz#131921e50f62ff1a66a461d7d62d7b21d5d15a51" + integrity sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ== + set-getter@^0.1.0: version "0.1.1" resolved "https://registry.npmjs.org/set-getter/-/set-getter-0.1.1.tgz" @@ -11803,6 +11977,13 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undici@^5.22.1: + version "5.25.4" + resolved "https://registry.npmjs.org/undici/-/undici-5.25.4.tgz#7d8ef81d94f84cd384986271e5e5599b6dff4296" + integrity sha512-450yJxT29qKMf3aoudzFpIciqpx6Pji3hEWaXqXmanbXF58LTAGCKxcJjxMXWu3iG+Mudgo3ZUfDB6YDFd/dAw== + dependencies: + "@fastify/busboy" "^2.0.0" + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz" @@ -12133,6 +12314,17 @@ vite-node@^0.28.5: optionalDependencies: fsevents "~2.3.2" +vite@^4.4.9: + version "4.4.10" + resolved "https://registry.npmjs.org/vite/-/vite-4.4.10.tgz#3794639cc433f7cb33ad286930bf0378c86261c8" + integrity sha512-TzIjiqx9BEXF8yzYdF2NTf1kFFbjMjUSV0LFZ3HyHoI3SGSPLnnFUKiIQtL3gl2AjHvMrprOvQ3amzaHgQlAxw== + dependencies: + esbuild "^0.18.10" + postcss "^8.4.27" + rollup "^3.27.1" + optionalDependencies: + fsevents "~2.3.2" + w3c-xmlserializer@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073" From b049de2ba2c3d9adb9b3cf8b270bb0b1440ca0db Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Thu, 5 Oct 2023 09:35:43 +1100 Subject: [PATCH 04/38] support default entry files, add build smoke test --- integration/helpers/create-fixture.ts | 7 ++- integration/vite-test.ts | 87 +++++++++++++++++++++++++++ packages/remix-dev/vite/plugin.ts | 60 ++++++++++++------ 3 files changed, 133 insertions(+), 21 deletions(-) create mode 100644 integration/vite-test.ts diff --git a/integration/helpers/create-fixture.ts b/integration/helpers/create-fixture.ts index 39061195064..95963f5d269 100644 --- a/integration/helpers/create-fixture.ts +++ b/integration/helpers/create-fixture.ts @@ -28,6 +28,7 @@ export interface FixtureInit { template?: "cf-template" | "deno-template" | "node-template"; config?: Partial; useRemixServe?: boolean; + env?: Record; } export type Fixture = Awaited>; @@ -272,7 +273,7 @@ export async function createFixtureProject( ); fse.writeFileSync(path.join(projectDir, "remix.config.js"), contents); - build(projectDir, init.buildStdio, init.sourcemap, mode); + build(projectDir, init.buildStdio, init.sourcemap, mode, init.env); return projectDir; } @@ -281,7 +282,8 @@ function build( projectDir: string, buildStdio?: Writable, sourcemap?: boolean, - mode?: ServerMode + mode?: ServerMode, + env?: Record ) { // We have a "require" instead of a dynamic import in readConfig gated // behind mode === ServerMode.Test to make jest happy, but that doesn't @@ -298,6 +300,7 @@ function build( cwd: projectDir, env: { ...process.env, + ...env, NODE_ENV: mode || ServerMode.Production, }, }); diff --git a/integration/vite-test.ts b/integration/vite-test.ts new file mode 100644 index 00000000000..8ffd3501399 --- /dev/null +++ b/integration/vite-test.ts @@ -0,0 +1,87 @@ +import { test, expect } from "@playwright/test"; + +import { + createAppFixture, + createFixture, + js, +} from "./helpers/create-fixture.js"; +import type { Fixture, AppFixture } from "./helpers/create-fixture.js"; +import { PlaywrightFixture, selectHtml } from "./helpers/playwright-fixture.js"; + +test.describe("Vite", () => { + let fixture: Fixture; + let appFixture: AppFixture; + + test.beforeAll(async () => { + fixture = await createFixture({ + env: { + REMIX_EXPERIMENTAL_VITE: "1", + }, + files: { + "remix.config.js": js` + throw new Error("Remix should not access remix.config.js when using Vite"); + export default {}; + `, + "vite.config.mjs": js` + import { defineConfig } from "vite"; + import { experimental_remix } from "@remix-run/dev/vite"; + + export default defineConfig({ + plugins: [experimental_remix()], + }); + `, + "app/root.tsx": js` + import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; + + export default function Root() { + return ( + + + + + + +
+

Root

+ +
+ + + + ); + } + `, + + "app/routes/_index.tsx": js` + export default function() { + return

Index

; + } + `, + }, + }); + + appFixture = await createAppFixture(fixture); + }); + + test.afterAll(() => { + appFixture.close(); + }); + + test("server renders matching routes", async () => { + let res = await fixture.requestDocument("/"); + expect(res.status).toBe(200); + expect(selectHtml(await res.text(), "#content")).toBe(`
+

Root

+

Index

+
`); + }); + + test("hydrates", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + expect(await app.getHtml("#content")).toBe(`
+

Root

+

Index

+
`); + }); +}); diff --git a/packages/remix-dev/vite/plugin.ts b/packages/remix-dev/vite/plugin.ts index 572f535a5cd..e4697dccb22 100644 --- a/packages/remix-dev/vite/plugin.ts +++ b/packages/remix-dev/vite/plugin.ts @@ -52,8 +52,8 @@ type ResolvedRemixVitePluginConfig = Pick< | "appDirectory" | "rootDirectory" | "assetsBuildDirectory" - | "entryClientFile" - | "entryServerFile" + | "entryClientFilePath" + | "entryServerFilePath" | "future" | "publicPath" | "relativeAssetsBuildDirectory" @@ -98,11 +98,24 @@ const getHash = (source: BinaryLike, maxLength?: number): string => { const resolveBuildAssetPaths = ( pluginConfig: ResolvedRemixVitePluginConfig, manifest: ViteManifest, - appRelativePath: string + absoluteFilePath: string ): Manifest["entry"] & { css: string[] } => { - let appPath = path.relative(process.cwd(), pluginConfig.appDirectory); - let manifestKey = normalizePath(path.join(appPath, appRelativePath)); + let rootRelativeFilePath = path.relative( + pluginConfig.rootDirectory, + absoluteFilePath + ); + let manifestKey = normalizePath(rootRelativeFilePath); let manifestEntry = manifest[manifestKey]; + + if (!manifestEntry) { + let knownManifestKeys = Object.keys(manifest) + .map((key) => '"' + key + '"') + .join(", "); + throw new Error( + `No manifest entry found for "${manifestKey}". Known manifest keys: ${knownManifestKeys}` + ); + } + return { module: `${pluginConfig.publicPath}${manifestEntry.file}`, imports: @@ -206,6 +219,13 @@ export let remix: (options?: RemixVitePluginOptions) => Plugin[] = ( relativeAssetsBuildDirectory ); + let defaultsDirectory = path.resolve( + __dirname, + "..", + "config", + "defaults" + ); + let userEntryClientFile = findEntry(appDirectory, "entry.client"); let entryClientFile = userEntryClientFile ?? "entry.client.tsx"; @@ -267,6 +287,14 @@ export let remix: (options?: RemixVitePluginOptions) => Plugin[] = ( entryServerFile = `entry.server.${serverRuntime}.tsx`; } + let entryClientFilePath = userEntryClientFile + ? path.resolve(appDirectory, userEntryClientFile) + : path.resolve(defaultsDirectory, entryClientFile); + + let entryServerFilePath = userEntryServerFile + ? path.resolve(appDirectory, userEntryServerFile) + : path.resolve(defaultsDirectory, entryServerFile); + let publicPath = addTrailingSlash(options.publicPath ?? "/build/"); let rootRouteFile = findEntry(appDirectory, "root"); @@ -295,10 +323,10 @@ export let remix: (options?: RemixVitePluginOptions) => Plugin[] = ( appDirectory, rootDirectory, assetsBuildDirectory, - entryClientFile, + entryClientFilePath, publicPath, routes, - entryServerFile, + entryServerFilePath, serverBuildPath, serverModuleFormat, relativeAssetsBuildDirectory, @@ -311,9 +339,7 @@ export let remix: (options?: RemixVitePluginOptions) => Plugin[] = ( return ` import * as entryServer from ${JSON.stringify( - resolveFsUrl( - path.resolve(pluginConfig.appDirectory, pluginConfig.entryServerFile) - ) + resolveFsUrl(pluginConfig.entryServerFilePath) )}; ${Object.keys(pluginConfig.routes) .map((key, index) => { @@ -363,11 +389,12 @@ export let remix: (options?: RemixVitePluginOptions) => Plugin[] = ( let entry: Manifest["entry"] = resolveBuildAssetPaths( pluginConfig, viteManifest, - pluginConfig.entryClientFile + pluginConfig.entryClientFilePath ); let routes: Manifest["routes"] = {}; for (let [key, route] of Object.entries(pluginConfig.routes)) { + let routeFilePath = path.join(pluginConfig.appDirectory, route.file); let sourceExports = await getRouteModuleExports( viteChildCompiler, pluginConfig, @@ -383,7 +410,7 @@ export let remix: (options?: RemixVitePluginOptions) => Plugin[] = ( hasAction: sourceExports.includes("action"), hasLoader: sourceExports.includes("loader"), hasErrorBoundary: sourceExports.includes("ErrorBoundary"), - ...resolveBuildAssetPaths(pluginConfig, viteManifest, route.file), + ...resolveBuildAssetPaths(pluginConfig, viteManifest, routeFilePath), }; } @@ -439,9 +466,7 @@ export let remix: (options?: RemixVitePluginOptions) => Plugin[] = ( version: String(Math.random()), url: VirtualModule.url(browserManifestId), entry: { - module: resolveFsUrl( - path.resolve(pluginConfig.appDirectory, pluginConfig.entryClientFile) - ), + module: resolveFsUrl(pluginConfig.entryClientFilePath), imports: [], }, routes, @@ -472,10 +497,7 @@ export let remix: (options?: RemixVitePluginOptions) => Plugin[] = ( ...viteUserConfig.build?.rollupOptions, preserveEntrySignatures: "exports-only", input: [ - path.resolve( - pluginConfig.appDirectory, - pluginConfig.entryClientFile - ), + pluginConfig.entryClientFilePath, ...Object.values(pluginConfig.routes).map((route) => path.resolve(pluginConfig.appDirectory, route.file) ), From 287c3529bd183dd843f93f9e8f2baddee55ecd56 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Thu, 5 Oct 2023 09:42:01 +1100 Subject: [PATCH 05/38] add vite to root dependencies --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 8087cf155d3..f1e3278d338 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,8 @@ "typescript": "^5.1.0", "unified": "^10.1.2", "unist-util-remove": "^3.1.0", - "unist-util-visit": "^4.1.1" + "unist-util-visit": "^4.1.1", + "vite": "^4.4.9" }, "engines": { "node": ">=18.0.0" From cff862635df59288ec51c28799d7d2cc0225732f Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Thu, 5 Oct 2023 13:44:50 +1100 Subject: [PATCH 06/38] fix vite bin resolution, add vite dev smoke test --- .../{vite-test.ts => vite-build-test.ts} | 26 ++- integration/vite-dev-test.ts | 166 ++++++++++++++++++ package.json | 4 +- packages/remix-dev/package.json | 2 + packages/remix-dev/vite/dev.ts | 5 +- yarn.lock | 100 +++++++++++ 6 files changed, 294 insertions(+), 9 deletions(-) rename integration/{vite-test.ts => vite-build-test.ts} (76%) create mode 100644 integration/vite-dev-test.ts diff --git a/integration/vite-test.ts b/integration/vite-build-test.ts similarity index 76% rename from integration/vite-test.ts rename to integration/vite-build-test.ts index 8ffd3501399..7908c1d9c8c 100644 --- a/integration/vite-test.ts +++ b/integration/vite-build-test.ts @@ -8,7 +8,7 @@ import { import type { Fixture, AppFixture } from "./helpers/create-fixture.js"; import { PlaywrightFixture, selectHtml } from "./helpers/playwright-fixture.js"; -test.describe("Vite", () => { +test.describe("Vite build", () => { let fixture: Fixture; let appFixture: AppFixture; @@ -51,10 +51,21 @@ test.describe("Vite", () => { ); } `, - "app/routes/_index.tsx": js` + import { useState, useEffect } from "react"; + export default function() { - return

Index

; + const [mounted, setMounted] = useState(false); + useEffect(() => { + setMounted(true); + }, []); + + return ( + <> +

Index

+ {!mounted ?

Loading...

:

Mounted

} + + ); } `, }, @@ -73,15 +84,16 @@ test.describe("Vite", () => { expect(selectHtml(await res.text(), "#content")).toBe(`

Root

Index

+

Loading...

`); }); test("hydrates", async ({ page }) => { let app = new PlaywrightFixture(appFixture, page); await app.goto("/"); - expect(await app.getHtml("#content")).toBe(`
-

Root

-

Index

-
`); + expect(await page.locator("#content h2").textContent()).toBe("Index"); + expect(await page.locator("#content h3[data-mounted]").textContent()).toBe( + "Mounted" + ); }); }); diff --git a/integration/vite-dev-test.ts b/integration/vite-dev-test.ts new file mode 100644 index 00000000000..2d929149361 --- /dev/null +++ b/integration/vite-dev-test.ts @@ -0,0 +1,166 @@ +import { test, expect } from "@playwright/test"; +import execa, { type ExecaChildProcess } from "execa"; +import pidtree from "pidtree"; +import getPort from "get-port"; +import waitOn from "wait-on"; + +import { createFixtureProject, js } from "./helpers/create-fixture.js"; + +test.describe("Vite dev", () => { + let projectDir: string; + let devProc: ExecaChildProcess; + let devPort: number; + + test.beforeAll(async () => { + devPort = await getPort(); + projectDir = await createFixtureProject({ + env: { + REMIX_EXPERIMENTAL_VITE: "1", + }, + files: { + "remix.config.js": js` + throw new Error("Remix should not access remix.config.js when using Vite"); + export default {}; + `, + "vite.config.mjs": js` + import { defineConfig } from "vite"; + import { experimental_remix } from "@remix-run/dev/vite"; + + export default defineConfig({ + optimizeDeps: { + include: ["react", "react-dom/client"], + }, + server: { + port: ${devPort}, + strictPort: true, + }, + plugins: [experimental_remix()], + }); + `, + "app/root.tsx": js` + import { Links, Meta, Outlet, Scripts, LiveReload } from "@remix-run/react"; + + export default function Root() { + return ( + + + + + + +
+

Root

+ +
+ + + + + ); + } + `, + "app/routes/_index.tsx": js` + import { useState, useEffect } from "react"; + + export default function() { + const [mounted, setMounted] = useState(false); + useEffect(() => { + setMounted(true); + }, []); + + return ( + <> +

Index

+ {!mounted ?

Loading...

:

Mounted

} + + ); + } + `, + }, + }); + + let nodeBin = process.argv[0]; + devProc = execa( + nodeBin, + ["node_modules/@remix-run/dev/dist/cli.js", "dev"], + { + cwd: projectDir, + env: { + ...process.env, + REMIX_EXPERIMENTAL_VITE: "1", + }, + } + ); + + await waitOn({ resources: [`http://localhost:${devPort}/`] }); + }); + + test.afterAll(async () => { + devProc.pid && (await killtree(devProc.pid)); + }); + + test("renders matching routes", async ({ page }) => { + await page.goto(`http://localhost:${devPort}/`, { + waitUntil: "networkidle", + }); + expect(await page.locator("#content h2").textContent()).toBe("Index"); + expect(await page.locator("#content h3[data-mounted]").textContent()).toBe( + "Mounted" + ); + }); +}); + +let isWindows = process.platform === "win32"; + +let kill = async (pid: number) => { + if (!isAlive(pid)) return; + if (isWindows) { + await execa("taskkill", ["/F", "/PID", pid.toString()]).catch((error) => { + // taskkill 128 -> the process is already dead + if (error.exitCode === 128) return; + if (/There is no running instance of the task./.test(error.message)) + return; + console.warn(error.message); + }); + return; + } + await execa("kill", ["-9", pid.toString()]).catch((error) => { + // process is already dead + if (/No such process/.test(error.message)) return; + console.warn(error.message); + }); +}; + +let isAlive = (pid: number) => { + try { + process.kill(pid, 0); + return true; + } catch (error) { + return false; + } +}; + +let killtree = async (pid: number) => { + let descendants = await pidtree(pid).catch(() => undefined); + if (descendants === undefined) return; + let pids = [pid, ...descendants]; + + await Promise.all(pids.map(kill)); + + return new Promise((resolve, reject) => { + let check = setInterval(() => { + pids = pids.filter(isAlive); + if (pids.length === 0) { + clearInterval(check); + resolve(); + } + }, 50); + + setTimeout(() => { + clearInterval(check); + reject( + new Error("Timeout: Processes did not exit within the specified time.") + ); + }, 2000); + }); +}; diff --git a/package.json b/package.json index f1e3278d338..61c44294391 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@types/semver": "^7.3.4", "@types/serialize-javascript": "^5.0.2", "@types/ssri": "^7.1.0", + "@types/wait-on": "^5.3.2", "babel-jest": "^29.6.4", "babel-plugin-transform-remove-console": "^6.9.4", "chalk": "^4.1.2", @@ -124,7 +125,8 @@ "unified": "^10.1.2", "unist-util-remove": "^3.1.0", "unist-util-visit": "^4.1.1", - "vite": "^4.4.9" + "vite": "^4.4.9", + "wait-on": "^7.0.1" }, "engines": { "node": ">=18.0.0" diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 23e2f966596..2246030bc70 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -67,6 +67,7 @@ "react-refresh": "^0.14.0", "remark-frontmatter": "4.0.1", "remark-mdx-frontmatter": "^1.0.1", + "resolve-bin": "^1.0.1", "semver": "^7.3.7", "set-cookie-parser": "^2.6.0", "tar-fs": "^2.1.1", @@ -85,6 +86,7 @@ "@types/npmcli__package-json": "^4.0.0", "@types/picomatch": "^2.3.0", "@types/prettier": "^2.7.3", + "@types/resolve-bin": "^0.4.1", "@types/shelljs": "^0.8.11", "@types/tar-fs": "^2.0.1", "@types/ws": "^7.4.1", diff --git a/packages/remix-dev/vite/dev.ts b/packages/remix-dev/vite/dev.ts index a05e595f293..c8dee707633 100644 --- a/packages/remix-dev/vite/dev.ts +++ b/packages/remix-dev/vite/dev.ts @@ -1,4 +1,5 @@ import { spawn } from "node:child_process"; +import resolveBin from "resolve-bin"; export interface ViteDevOptions { config?: string; @@ -15,8 +16,10 @@ export async function dev({ host, force, }: ViteDevOptions) { + let viteBin = resolveBin.sync("vite"); + spawn( - "vite", + viteBin, [ "dev", ...(config ? ["--config", config] : []), diff --git a/yarn.lock b/yarn.lock index ed10f4a9ea2..eca542c858c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1684,6 +1684,18 @@ resolved "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz#f22824caff3ae506b18207bad4126dbc6ccdb6b8" integrity sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ== +"@hapi/hoek@^9.0.0": + version "9.3.0" + resolved "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" + integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== + +"@hapi/topo@^5.0.0": + version "5.1.0" + resolved "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" + integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== + dependencies: + "@hapi/hoek" "^9.0.0" + "@humanwhocodes/config-array@^0.11.8": version "0.11.8" resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" @@ -2374,6 +2386,23 @@ resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz" integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg== +"@sideway/address@^4.1.3": + version "4.1.4" + resolved "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" + integrity sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@sideway/formula@^3.0.1": + version "3.0.1" + resolved "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== + +"@sideway/pinpoint@^2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" + integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== + "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" @@ -2913,6 +2942,11 @@ dependencies: "@types/node" "*" +"@types/resolve-bin@^0.4.1": + version "0.4.1" + resolved "https://registry.npmjs.org/@types/resolve-bin/-/resolve-bin-0.4.1.tgz#e5f2c2de1de8b77f2222a9e28e56ecc37028116c" + integrity sha512-5bWIuv+28N2+WxIQNdLwkzD+THqH221tR1rsjHV23CiM46+AZZ/U5V5KsuoBywkB/x7gK/xEBW1l9R7Ucdkcvg== + "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz" @@ -3044,6 +3078,13 @@ resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/wait-on@^5.3.2": + version "5.3.2" + resolved "https://registry.npmjs.org/@types/wait-on/-/wait-on-5.3.2.tgz#f3722017a2bfacdc9bee095185a312befecea28c" + integrity sha512-7NBSJs/YvbHlaYCJ7wIUF6t7ct3OMt525NmZ+US73pPlkmpxd9ADwfNxrRAmg8nWlcTMqR0PkhW7aYk3FLlvrQ== + dependencies: + "@types/node" "*" + "@types/ws@^7.4.1": version "7.4.7" resolved "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz" @@ -3772,6 +3813,14 @@ axe-core@^4.6.2: resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.6.3.tgz#fc0db6fdb65cc7a80ccf85286d91d64ababa3ece" integrity sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg== +axios@^0.27.2: + version "0.27.2" + resolved "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== + dependencies: + follow-redirects "^1.14.9" + form-data "^4.0.0" + axobject-query@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1" @@ -6014,6 +6063,11 @@ finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" +find-parent-dir@~0.3.0: + version "0.3.1" + resolved "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.1.tgz#c5c385b96858c3351f95d446cab866cbf9f11125" + integrity sha512-o4UcykWV/XN9wm+jMEtWLPlV8RXCZnMhQI6F6OdHeSez7iiJWePw8ijOlskJZMsaQoGR/b7dH6lO02HhaTN7+A== + find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" @@ -6058,6 +6112,11 @@ flatted@^3.1.0: resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz" integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA== +follow-redirects@^1.14.9: + version "1.15.3" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" + integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -7786,6 +7845,17 @@ jiti@^1.17.2: resolved "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz#80c3ef3d486ebf2450d9335122b32d121f2a83cd" integrity sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg== +joi@^17.7.0: + version "17.11.0" + resolved "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz#aa9da753578ec7720e6f0ca2c7046996ed04fc1a" + integrity sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ== + dependencies: + "@hapi/hoek" "^9.0.0" + "@hapi/topo" "^5.0.0" + "@sideway/address" "^4.1.3" + "@sideway/formula" "^3.0.1" + "@sideway/pinpoint" "^2.0.0" + js-levenshtein@^1.1.6: version "1.1.6" resolved "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz" @@ -9014,6 +9084,11 @@ minimist@^1.2.0, minimist@^1.2.6: resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +minimist@^1.2.7: + version "1.2.8" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + minipass-collect@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz" @@ -10639,6 +10714,13 @@ requires-port@^1.0.0: resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== +resolve-bin@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/resolve-bin/-/resolve-bin-1.0.1.tgz#795255591443e7007b21f2eadd8baa39b7378e50" + integrity sha512-4G9C3udcDB1c9qaopB+9dygm2bMyF2LeJ2JHBIc24N7ob+UuSSwX3ID1hQwpDEQep9ZRNdhT//rgEd6xbWA/SA== + dependencies: + find-parent-dir "~0.3.0" + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" @@ -10804,6 +10886,13 @@ rxjs@^7.5.1, rxjs@^7.5.5: dependencies: tslib "^2.1.0" +rxjs@^7.8.0: + version "7.8.1" + resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== + dependencies: + tslib "^2.1.0" + sade@^1.7.3: version "1.8.1" resolved "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz" @@ -12332,6 +12421,17 @@ w3c-xmlserializer@^4.0.0: dependencies: xml-name-validator "^4.0.0" +wait-on@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/wait-on/-/wait-on-7.0.1.tgz#5cff9f8427e94f4deacbc2762e6b0a489b19eae9" + integrity sha512-9AnJE9qTjRQOlTZIldAaf/da2eW0eSRSgcqq85mXQja/DW3MriHxkpODDSUEg+Gri/rKEcXUZHe+cevvYItaog== + dependencies: + axios "^0.27.2" + joi "^17.7.0" + lodash "^4.17.21" + minimist "^1.2.7" + rxjs "^7.8.0" + walker@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" From 7ee5a0d8b6b7ea301aa749c616d19d8fedf1eba8 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Thu, 5 Oct 2023 14:39:32 +1100 Subject: [PATCH 07/38] use cross-spawn --- packages/remix-dev/package.json | 2 ++ packages/remix-dev/vite/dev.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 2246030bc70..4ffd8be0f38 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -37,6 +37,7 @@ "cacache": "^17.1.3", "chalk": "^4.1.2", "chokidar": "^3.5.1", + "cross-spawn": "^7.0.3", "dotenv": "^16.0.0", "es-module-lexer": "^1.3.1", "esbuild": "0.17.6", @@ -78,6 +79,7 @@ "devDependencies": { "@remix-run/serve": "2.0.1", "@types/cacache": "^17.0.0", + "@types/cross-spawn": "^6.0.2", "@types/gunzip-maybe": "^1.4.0", "@types/jsesc": "^3.0.1", "@types/lodash.debounce": "^4.0.6", diff --git a/packages/remix-dev/vite/dev.ts b/packages/remix-dev/vite/dev.ts index c8dee707633..11376ea2e78 100644 --- a/packages/remix-dev/vite/dev.ts +++ b/packages/remix-dev/vite/dev.ts @@ -1,4 +1,4 @@ -import { spawn } from "node:child_process"; +import { spawn } from "cross-spawn"; import resolveBin from "resolve-bin"; export interface ViteDevOptions { From 0c692d6fe0284ae0221e773d506b17d22b1755d7 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Thu, 5 Oct 2023 14:39:58 +1100 Subject: [PATCH 08/38] log terminal output on vite dev timeout --- integration/vite-dev-test.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/integration/vite-dev-test.ts b/integration/vite-dev-test.ts index 2d929149361..cd49e6eb766 100644 --- a/integration/vite-dev-test.ts +++ b/integration/vite-dev-test.ts @@ -1,4 +1,5 @@ import { test, expect } from "@playwright/test"; +import type { Readable } from "node:stream"; import execa, { type ExecaChildProcess } from "execa"; import pidtree from "pidtree"; import getPort from "get-port"; @@ -91,8 +92,17 @@ test.describe("Vite dev", () => { }, } ); - - await waitOn({ resources: [`http://localhost:${devPort}/`] }); + let devStdout = bufferize(devProc.stdout!); + let devStderr = bufferize(devProc.stderr!); + + await waitOn({ + resources: [`http://localhost:${devPort}/`], + timeout: 10000, + }).catch((err) => { + console.log(devStdout()); + console.log(devStderr()); + throw err; + }); }); test.afterAll(async () => { @@ -110,6 +120,12 @@ test.describe("Vite dev", () => { }); }); +let bufferize = (stream: Readable): (() => string) => { + let buffer = ""; + stream.on("data", (data) => (buffer += data.toString())); + return () => buffer; +}; + let isWindows = process.platform === "win32"; let kill = async (pid: number) => { From 275dd37ffaaa21ceb42da7a89c959e1ef3b85fc8 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Thu, 5 Oct 2023 15:10:15 +1100 Subject: [PATCH 09/38] include stdout in timeout error message --- integration/vite-dev-test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/integration/vite-dev-test.ts b/integration/vite-dev-test.ts index cd49e6eb766..391a98bb95e 100644 --- a/integration/vite-dev-test.ts +++ b/integration/vite-dev-test.ts @@ -99,9 +99,15 @@ test.describe("Vite dev", () => { resources: [`http://localhost:${devPort}/`], timeout: 10000, }).catch((err) => { - console.log(devStdout()); - console.log(devStderr()); - throw err; + let stdout = devStdout(); + let stderr = devStderr(); + throw new Error( + [ + err.message, + ...(stdout ? ["", "stdout:", stdout] : []), + ...(stderr ? ["", "stderr:", stderr] : []), + ].join("\n") + ); }); }); From cde0bfba7e5bb7c1061c8109943efec53c735aa7 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 6 Oct 2023 06:56:25 +1100 Subject: [PATCH 10/38] improve vite dev test error message --- integration/vite-dev-test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/integration/vite-dev-test.ts b/integration/vite-dev-test.ts index 391a98bb95e..0c19805e8ae 100644 --- a/integration/vite-dev-test.ts +++ b/integration/vite-dev-test.ts @@ -104,8 +104,14 @@ test.describe("Vite dev", () => { throw new Error( [ err.message, - ...(stdout ? ["", "stdout:", stdout] : []), - ...(stderr ? ["", "stderr:", stderr] : []), + "", + "exit code: " + devProc.exitCode, + "", + "stdout:", + stdout || "", + "", + "stderr:", + stderr || "", ].join("\n") ); }); From efeefa8e4d98eb6f26cd334c92c9762810409597 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 6 Oct 2023 07:22:07 +1100 Subject: [PATCH 11/38] fix new prefetchStyleLinks usage --- packages/remix-react/routes.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/remix-react/routes.tsx b/packages/remix-react/routes.tsx index d8efa5dcb32..3aee8c494c0 100644 --- a/packages/remix-react/routes.tsx +++ b/packages/remix-react/routes.tsx @@ -136,7 +136,7 @@ export function createClientRoutes( // Only prefetch links if we've been loaded into the cache, route.lazy // will handle initial loads let routeModulePromise = routeModulesCache[route.id] - ? prefetchStyleLinks(routeModulesCache[route.id]) + ? prefetchStyleLinks(route, routeModulesCache[route.id]) : Promise.resolve(); try { if (!route.hasLoader) return null; @@ -149,7 +149,7 @@ export function createClientRoutes( // Only prefetch links if we've been loaded into the cache, route.lazy // will handle initial loads let routeModulePromise = routeModulesCache[route.id] - ? prefetchStyleLinks(routeModulesCache[route.id]) + ? prefetchStyleLinks(route, routeModulesCache[route.id]) : Promise.resolve(); try { if (!route.hasAction) { From 450696a957d6bf8bf9b9d45f09c708c1263f7151 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 6 Oct 2023 08:55:56 +1100 Subject: [PATCH 12/38] use require.resolve to get cli path in test --- integration/vite-dev-test.ts | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/integration/vite-dev-test.ts b/integration/vite-dev-test.ts index 0c19805e8ae..a5710520bab 100644 --- a/integration/vite-dev-test.ts +++ b/integration/vite-dev-test.ts @@ -1,5 +1,6 @@ import { test, expect } from "@playwright/test"; import type { Readable } from "node:stream"; +import { createRequire } from "node:module"; import execa, { type ExecaChildProcess } from "execa"; import pidtree from "pidtree"; import getPort from "get-port"; @@ -7,6 +8,8 @@ import waitOn from "wait-on"; import { createFixtureProject, js } from "./helpers/create-fixture.js"; +const require = createRequire(import.meta.url); + test.describe("Vite dev", () => { let projectDir: string; let devProc: ExecaChildProcess; @@ -81,17 +84,14 @@ test.describe("Vite dev", () => { }); let nodeBin = process.argv[0]; - devProc = execa( - nodeBin, - ["node_modules/@remix-run/dev/dist/cli.js", "dev"], - { - cwd: projectDir, - env: { - ...process.env, - REMIX_EXPERIMENTAL_VITE: "1", - }, - } - ); + let cliPath = require.resolve("@remix-run/dev/dist/cli.js", { + paths: [projectDir], + }); + let cliArgs = [cliPath, "dev"]; + devProc = execa(nodeBin, cliArgs, { + cwd: projectDir, + env: { ...process.env, REMIX_EXPERIMENTAL_VITE: "1" }, + }); let devStdout = bufferize(devProc.stdout!); let devStderr = bufferize(devProc.stderr!); @@ -105,13 +105,12 @@ test.describe("Vite dev", () => { [ err.message, "", + "command: " + [nodeBin, ...cliArgs].join(" "), + "pid: " + (devProc.pid ?? "undefined"), + "connected: " + devProc.connected, "exit code: " + devProc.exitCode, - "", - "stdout:", - stdout || "", - "", - "stderr:", - stderr || "", + "stdout: " + stdout ? `\n${stdout}\n` : "", + "stderr: " + stderr ? `\n${stderr}\n` : "", ].join("\n") ); }); From d969742b9fba20e7927e0c64d69409f8f3b8d4ef Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 6 Oct 2023 10:32:55 +1100 Subject: [PATCH 13/38] spawn dev cli in test --- integration/vite-dev-test.ts | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/integration/vite-dev-test.ts b/integration/vite-dev-test.ts index a5710520bab..b63ad5ccc76 100644 --- a/integration/vite-dev-test.ts +++ b/integration/vite-dev-test.ts @@ -1,18 +1,16 @@ import { test, expect } from "@playwright/test"; import type { Readable } from "node:stream"; -import { createRequire } from "node:module"; -import execa, { type ExecaChildProcess } from "execa"; +import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; +import execa from "execa"; import pidtree from "pidtree"; import getPort from "get-port"; import waitOn from "wait-on"; import { createFixtureProject, js } from "./helpers/create-fixture.js"; -const require = createRequire(import.meta.url); - test.describe("Vite dev", () => { let projectDir: string; - let devProc: ExecaChildProcess; + let devProc: ChildProcessWithoutNullStreams; let devPort: number; test.beforeAll(async () => { @@ -83,17 +81,18 @@ test.describe("Vite dev", () => { }, }); - let nodeBin = process.argv[0]; - let cliPath = require.resolve("@remix-run/dev/dist/cli.js", { - paths: [projectDir], - }); - let cliArgs = [cliPath, "dev"]; - devProc = execa(nodeBin, cliArgs, { - cwd: projectDir, - env: { ...process.env, REMIX_EXPERIMENTAL_VITE: "1" }, - }); - let devStdout = bufferize(devProc.stdout!); - let devStderr = bufferize(devProc.stderr!); + let nodebin = process.argv[0]; + devProc = spawn( + nodebin, + ["./node_modules/@remix-run/dev/dist/cli.js", "dev"], + { + cwd: projectDir, + env: { ...process.env, REMIX_EXPERIMENTAL_VITE: "1" }, + stdio: "pipe", + } + ); + let devStdout = bufferize(devProc.stdout); + let devStderr = bufferize(devProc.stderr); await waitOn({ resources: [`http://localhost:${devPort}/`], @@ -105,9 +104,6 @@ test.describe("Vite dev", () => { [ err.message, "", - "command: " + [nodeBin, ...cliArgs].join(" "), - "pid: " + (devProc.pid ?? "undefined"), - "connected: " + devProc.connected, "exit code: " + devProc.exitCode, "stdout: " + stdout ? `\n${stdout}\n` : "", "stderr: " + stderr ? `\n${stderr}\n` : "", From 4fef4bd3eb61241bb9bbbec1a2656f997cb19558 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 6 Oct 2023 12:05:34 +1100 Subject: [PATCH 14/38] run vite bin via node --- packages/remix-dev/vite/dev.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/remix-dev/vite/dev.ts b/packages/remix-dev/vite/dev.ts index 11376ea2e78..ad1689ccc0f 100644 --- a/packages/remix-dev/vite/dev.ts +++ b/packages/remix-dev/vite/dev.ts @@ -19,8 +19,9 @@ export async function dev({ let viteBin = resolveBin.sync("vite"); spawn( - viteBin, + "node", [ + viteBin, "dev", ...(config ? ["--config", config] : []), ...(port ? ["--port", port] : []), From c1f55da49a73bc19f8a6518517f7b1eb119de890 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Mon, 9 Oct 2023 08:49:40 +1100 Subject: [PATCH 15/38] avoid fs URLs for local project files --- packages/remix-dev/vite/plugin.ts | 32 ++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/remix-dev/vite/plugin.ts b/packages/remix-dev/vite/plugin.ts index e4697dccb22..1035d9c1a81 100644 --- a/packages/remix-dev/vite/plugin.ts +++ b/packages/remix-dev/vite/plugin.ts @@ -73,7 +73,20 @@ const normalizePath = (p: string) => { return viteNormalizePath(unixPath); }; -const resolveFsUrl = (filePath: string) => `/@fs${normalizePath(filePath)}`; +const resolveFileUrl = ( + { rootDirectory }: Pick, + filePath: string +) => { + let relativePath = path.relative(rootDirectory, filePath); + + if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) { + throw new Error( + `Cannot resolve asset path "${filePath}" outside of root directory "${rootDirectory}".` + ); + } + + return `/${normalizePath(relativePath)}`; +}; const isJsFile = (filePath: string) => /\.[cm]?[jt]sx?$/i.test(filePath); @@ -151,7 +164,7 @@ const getRouteModuleExports = async ( let ssr = true; let { pluginContainer, moduleGraph } = viteChildCompiler; let routePath = path.join(pluginConfig.appDirectory, routeFile); - let url = resolveFsUrl(routePath); + let url = resolveFileUrl(pluginConfig, routePath); let resolveId = async () => { let result = await pluginContainer.resolveId(url, undefined, { ssr }); @@ -339,13 +352,16 @@ export let remix: (options?: RemixVitePluginOptions) => Plugin[] = ( return ` import * as entryServer from ${JSON.stringify( - resolveFsUrl(pluginConfig.entryServerFilePath) + resolveFileUrl(pluginConfig, pluginConfig.entryServerFilePath) )}; ${Object.keys(pluginConfig.routes) .map((key, index) => { let route = pluginConfig.routes[key]!; return `import * as route${index} from ${JSON.stringify( - resolveFsUrl(resolveRelativeRouteFilePath(route, pluginConfig)) + resolveFileUrl( + pluginConfig, + resolveRelativeRouteFilePath(route, pluginConfig) + ) )};`; }) .join("\n")} @@ -450,7 +466,8 @@ export let remix: (options?: RemixVitePluginOptions) => Plugin[] = ( path: route.path, index: route.index, caseSensitive: route.caseSensitive, - module: `${resolveFsUrl( + module: `${resolveFileUrl( + pluginConfig, resolveRelativeRouteFilePath(route, pluginConfig) )}${ isJsFile(route.file) ? "" : "?import" // Ensure the Vite dev server responds with a JS module @@ -466,7 +483,7 @@ export let remix: (options?: RemixVitePluginOptions) => Plugin[] = ( version: String(Math.random()), url: VirtualModule.url(browserManifestId), entry: { - module: resolveFsUrl(pluginConfig.entryClientFilePath), + module: resolveFileUrl(pluginConfig, pluginConfig.entryClientFilePath), imports: [], }, routes, @@ -945,7 +962,8 @@ async function getRouteMetadata( pluginConfig.rootDirectory, resolveRelativeRouteFilePath(route, pluginConfig) ), - module: `${resolveFsUrl( + module: `${resolveFileUrl( + pluginConfig, resolveRelativeRouteFilePath(route, pluginConfig) )}?import`, // Ensure the Vite dev server responds with a JS module hasAction: sourceExports.includes("action"), From 42c15ef8e95a34958bd702008e0aae0e6bdc498d Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Mon, 9 Oct 2023 09:26:26 +1100 Subject: [PATCH 16/38] stringify vite bin path --- packages/remix-dev/vite/dev.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-dev/vite/dev.ts b/packages/remix-dev/vite/dev.ts index ad1689ccc0f..bec43ce9572 100644 --- a/packages/remix-dev/vite/dev.ts +++ b/packages/remix-dev/vite/dev.ts @@ -21,7 +21,7 @@ export async function dev({ spawn( "node", [ - viteBin, + JSON.stringify(viteBin), "dev", ...(config ? ["--config", config] : []), ...(port ? ["--port", port] : []), From 70afeae861fca4677935a60fff0ae0d1ee67c4c7 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Mon, 9 Oct 2023 11:32:50 +1100 Subject: [PATCH 17/38] add hmr smoke test --- integration/vite-dev-test.ts | 40 ++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/integration/vite-dev-test.ts b/integration/vite-dev-test.ts index b63ad5ccc76..a0bdf5f7bb5 100644 --- a/integration/vite-dev-test.ts +++ b/integration/vite-dev-test.ts @@ -1,6 +1,8 @@ import { test, expect } from "@playwright/test"; import type { Readable } from "node:stream"; import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; +import fs from "node:fs/promises"; +import path from "node:path"; import execa from "execa"; import pidtree from "pidtree"; import getPort from "get-port"; @@ -64,17 +66,19 @@ test.describe("Vite dev", () => { "app/routes/_index.tsx": js` import { useState, useEffect } from "react"; - export default function() { + export default function IndexRoute() { const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); return ( - <> -

Index

- {!mounted ?

Loading...

:

Mounted

} - +
+

Index

+ +

Mounted: {mounted ? "yes" : "no"}

+

HMR updated: no

+
); } `, @@ -120,10 +124,30 @@ test.describe("Vite dev", () => { await page.goto(`http://localhost:${devPort}/`, { waitUntil: "networkidle", }); - expect(await page.locator("#content h2").textContent()).toBe("Index"); - expect(await page.locator("#content h3[data-mounted]").textContent()).toBe( - "Mounted" + await expect(page.locator("#index [data-title]")).toHaveText("Index"); + await expect(page.locator("#index [data-mounted]")).toHaveText( + "Mounted: yes" ); + + let hmrStatus = page.locator("#index [data-hmr]"); + await expect(hmrStatus).toHaveText("HMR updated: no"); + + let input = page.locator("#index input"); + await expect(input).toBeVisible(); + await input.type("stateful"); + + let indexRouteContents = await fs.readFile( + path.join(projectDir, "app/routes/_index.tsx"), + "utf8" + ); + await fs.writeFile( + path.join(projectDir, "app/routes/_index.tsx"), + indexRouteContents.replace("HMR updated: no", "HMR updated: yes"), + "utf8" + ); + await page.waitForLoadState("networkidle"); + await expect(hmrStatus).toHaveText("HMR updated: yes"); + await expect(input).toHaveValue("stateful"); }); }); From 569b327b98b1232aaf97092760a0052be7b14c67 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Mon, 9 Oct 2023 14:06:19 +1100 Subject: [PATCH 18/38] add remark-remix-mdx-frontmatter --- .changeset/config.json | 1 + jest.config.js | 1 + package.json | 1 + .../remark-remix-mdx-frontmatter/index.ts | 107 ++++++++++++++++++ .../jest.config.js | 6 + .../remark-remix-mdx-frontmatter/package.json | 39 +++++++ .../rollup.config.js | 71 ++++++++++++ .../tsconfig.json | 19 ++++ scripts/publish.js | 1 + scripts/utils.js | 1 + tsconfig.json | 1 + yarn.lock | 30 +++++ 12 files changed, 278 insertions(+) create mode 100644 packages/remark-remix-mdx-frontmatter/index.ts create mode 100644 packages/remark-remix-mdx-frontmatter/jest.config.js create mode 100644 packages/remark-remix-mdx-frontmatter/package.json create mode 100644 packages/remark-remix-mdx-frontmatter/rollup.config.js create mode 100644 packages/remark-remix-mdx-frontmatter/tsconfig.json diff --git a/.changeset/config.json b/.changeset/config.json index 12d246d66c0..c7573ed3778 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -17,6 +17,7 @@ "@remix-run/express", "@remix-run/node", "@remix-run/react", + "@remix-run/remark-remix-mdx-frontmatter", "@remix-run/serve", "@remix-run/server-runtime", "@remix-run/testing" diff --git a/jest.config.js b/jest.config.js index 5ffd3c56699..f880f9a4bbc 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,6 +7,7 @@ module.exports = { ], projects: [ "packages/create-remix", + "packages/remark-remix-mdx-frontmatter", "packages/remix", "packages/remix-architect", "packages/remix-cloudflare", diff --git a/package.json b/package.json index 61c44294391..a30b1dd27d8 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "workspaces": [ "integration", "packages/create-remix", + "packages/remark-remix-mdx-frontmatter", "packages/remix", "packages/remix-architect", "packages/remix-cloudflare", diff --git a/packages/remark-remix-mdx-frontmatter/index.ts b/packages/remark-remix-mdx-frontmatter/index.ts new file mode 100644 index 00000000000..38f5c04cd71 --- /dev/null +++ b/packages/remark-remix-mdx-frontmatter/index.ts @@ -0,0 +1,107 @@ +import type { Plugin } from "unified"; +import type { Literal, Root } from "mdast"; +import type { ExportNamedDeclaration } from "estree"; +import { valueToEstree } from "estree-util-value-to-estree"; +import { parse as parseToml } from "toml"; +import { parse as parseYaml } from "yaml"; + +type FrontmatterParsers = Record unknown>; + +export interface RemarkRemixMdxFrontmatterOptions { + name?: string; + parsers?: FrontmatterParsers; +} + +export const remarkRemixMdxFrontmatter: Plugin< + [RemarkRemixMdxFrontmatterOptions?], + any +> = ({ name: frontmatterExportName = "frontmatter", parsers } = {}) => { + let allParsers: FrontmatterParsers = { + toml: parseToml, + yaml: parseYaml, + ...parsers, + }; + + return (rootNode: Root, { basename = "" }) => { + let frontmatter: unknown; + + let node = rootNode.children.find(({ type }) => + Object.hasOwnProperty.call(allParsers, type) + ); + + if (node) { + let parser = allParsers[node.type]; + frontmatter = parser((node as Literal).value); + } + + let frontmatterHasKey = (key: string): boolean => + typeof frontmatter === "object" && + frontmatter !== null && + key in frontmatter; + + rootNode.children.unshift({ + type: "mdxjsEsm", + value: "", + data: { + estree: { + type: "Program", + sourceType: "module", + body: [ + { + type: "ExportNamedDeclaration", + specifiers: [], + declaration: { + type: "VariableDeclaration", + kind: "const", + declarations: [ + { + type: "VariableDeclarator", + id: { + type: "Identifier", + name: frontmatterExportName, + }, + init: valueToEstree(frontmatter), + }, + ], + }, + }, + ...["headers", "meta", "handle"].filter(frontmatterHasKey).map( + (remixExportName: string): ExportNamedDeclaration => ({ + type: "ExportNamedDeclaration", + specifiers: [], + declaration: { + type: "VariableDeclaration", + kind: "const", + declarations: [ + { + type: "VariableDeclarator", + id: { + type: "Identifier", + name: remixExportName, + }, + init: { + type: "MemberExpression", + optional: false, + computed: false, + object: { + type: "Identifier", + name: frontmatterExportName, + }, + property: { + type: "Identifier", + name: remixExportName, + }, + }, + }, + ], + }, + }) + ), + ], + }, + }, + }); + }; +}; + +export default remarkRemixMdxFrontmatter; diff --git a/packages/remark-remix-mdx-frontmatter/jest.config.js b/packages/remark-remix-mdx-frontmatter/jest.config.js new file mode 100644 index 00000000000..5ed8a4f1c93 --- /dev/null +++ b/packages/remark-remix-mdx-frontmatter/jest.config.js @@ -0,0 +1,6 @@ +/** @type {import('jest').Config} */ +module.exports = { + ...require("../../jest/jest.config.shared"), + displayName: "remark-remix-mdx-frontmatter", + setupFiles: [], +}; diff --git a/packages/remark-remix-mdx-frontmatter/package.json b/packages/remark-remix-mdx-frontmatter/package.json new file mode 100644 index 00000000000..bef8ff2cf0e --- /dev/null +++ b/packages/remark-remix-mdx-frontmatter/package.json @@ -0,0 +1,39 @@ +{ + "name": "@remix-run/remark-remix-mdx-frontmatter", + "version": "2.0.1", + "description": "Remark plugin to parse Remix-specific frontmatter in MDX files", + "bugs": { + "url": "/~https://github.com/remix-run/remix/issues" + }, + "repository": { + "type": "git", + "url": "/~https://github.com/remix-run/remix", + "directory": "packages/remark-remix-mdx-frontmatter" + }, + "license": "MIT", + "main": "./dist/index.js", + "module": "./dist/esm/index.js", + "typings": "./dist/index.d.ts", + "files": [ + "dist/", + "CHANGELOG.md", + "LICENSE.md", + "README.md" + ], + "dependencies": { + "@types/estree": "^1.0.2", + "@types/mdast": "^3.0.0", + "estree-util-value-to-estree": "^3.0.1", + "toml": "^3.0.0", + "unified": "^10.0.0", + "yaml": "^2.0.0" + }, + "devDependencies": { + "@mdx-js/mdx": "^2.3.0", + "estree-jsx": "^0.0.1", + "mdast-util-mdx": "^2.0.0" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/packages/remark-remix-mdx-frontmatter/rollup.config.js b/packages/remark-remix-mdx-frontmatter/rollup.config.js new file mode 100644 index 00000000000..370db3e0184 --- /dev/null +++ b/packages/remark-remix-mdx-frontmatter/rollup.config.js @@ -0,0 +1,71 @@ +const path = require("node:path"); +const babel = require("@rollup/plugin-babel").default; +const nodeResolve = require("@rollup/plugin-node-resolve").default; +const copy = require("rollup-plugin-copy"); + +const { + copyToPlaygrounds, + createBanner, + getOutputDir, + isBareModuleId, +} = require("../../rollup.utils"); +const { name: packageName, version } = require("./package.json"); + +/** @returns {import("rollup").RollupOptions[]} */ +module.exports = function rollup() { + let sourceDir = "packages/remark-remix-mdx-frontmatter"; + let outputDir = getOutputDir(packageName); + let outputDist = path.join(outputDir, "dist"); + + return [ + { + external(id) { + return isBareModuleId(id); + }, + input: `${sourceDir}/index.ts`, + output: { + banner: createBanner(packageName, version), + dir: outputDist, + format: "cjs", + preserveModules: true, + exports: "named", + }, + plugins: [ + babel({ + babelHelpers: "bundled", + exclude: /node_modules/, + extensions: [".ts"], + }), + nodeResolve({ extensions: [".ts"] }), + copy({ + targets: [ + { src: `LICENSE.md`, dest: outputDir }, + { src: `${sourceDir}/package.json`, dest: outputDir }, + { src: `${sourceDir}/README.md`, dest: outputDir }, + ], + }), + ], + }, + { + external(id) { + return isBareModuleId(id); + }, + input: `${sourceDir}/index.ts`, + output: { + banner: createBanner(packageName, version), + dir: `${outputDist}/esm`, + format: "esm", + preserveModules: true, + }, + plugins: [ + babel({ + babelHelpers: "bundled", + exclude: /node_modules/, + extensions: [".ts"], + }), + nodeResolve({ extensions: [".ts"] }), + copyToPlaygrounds(), + ], + }, + ]; +}; diff --git a/packages/remark-remix-mdx-frontmatter/tsconfig.json b/packages/remark-remix-mdx-frontmatter/tsconfig.json new file mode 100644 index 00000000000..659f2e5245c --- /dev/null +++ b/packages/remark-remix-mdx-frontmatter/tsconfig.json @@ -0,0 +1,19 @@ +{ + "include": ["**/*.ts", "package.json"], + "exclude": ["dist", "__tests__", "node_modules"], + "compilerOptions": { + "lib": ["ES2022"], + "target": "ES2022", + "module": "ES2022", + "types": ["mdast-util-mdx"], + "moduleResolution": "Bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "declaration": true, + "emitDeclarationOnly": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "outDir": "../../build/node_modules/@remix-run/remark-remix-mdx-frontmatter/dist", + "rootDir": "." + } +} diff --git a/scripts/publish.js b/scripts/publish.js index dfb87429cc4..fb250203e36 100644 --- a/scripts/publish.js +++ b/scripts/publish.js @@ -56,6 +56,7 @@ async function run() { "serve", "css-bundle", "testing", + "remark-remix-mdx-frontmatter", ]) { publish(path.join(buildDir, "@remix-run", name), tag); } diff --git a/scripts/utils.js b/scripts/utils.js index 21b9655b44c..10ff865161f 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -17,6 +17,7 @@ let remixPackages = { "eslint-config", "css-bundle", "testing", + "remark-remix-mdx-frontmatter", ], get all() { return [...this.adapters, ...this.runtimes, ...this.core, "serve"]; diff --git a/tsconfig.json b/tsconfig.json index fe138b06305..5fd499a5d16 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "references": [ { "path": "integration" }, { "path": "packages/create-remix" }, + { "path": "packages/remark-remix-mdx-frontmatter" }, { "path": "packages/remix" }, { "path": "packages/remix-architect" }, { "path": "packages/remix-cloudflare" }, diff --git a/yarn.lock b/yarn.lock index eca542c858c..448e77ab37a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2651,6 +2651,11 @@ resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== +"@types/estree@0.0.42": + version "0.0.42" + resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.42.tgz#8d0c1f480339efedb3e46070e22dd63e0430dd11" + integrity sha512-K1DPVvnBCPxzD+G51/cxVIoc2X8uUVl1zpJeE6iKcgHMj4+tbat5Xu4TjV7v2QSDbIeAfLi2hIk+u2+s0MlpUQ== + "@types/estree@^0.0.46": version "0.0.46" resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.46.tgz" @@ -2661,6 +2666,11 @@ resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz" integrity sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew== +"@types/estree@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.2.tgz#ff02bc3dc8317cd668dfec247b750ba1f1d62453" + integrity sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA== + "@types/express-serve-static-core@^4.17.18": version "4.17.24" resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz" @@ -5726,6 +5736,13 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== +estree-jsx@^0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/estree-jsx/-/estree-jsx-0.0.1.tgz#14ac8a16de858a15677764428ab7929c08381988" + integrity sha512-M/bgmzaDP/aPZJoE5znuCsa8gleRMj33eeioZHpnc1SM5lJk7V1ZH7Tp+y4BeBYuWi73XiGPwPmMmzpz5HCS7w== + dependencies: + "@types/estree" "0.0.42" + estree-util-attach-comments@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-2.0.0.tgz" @@ -5768,6 +5785,14 @@ estree-util-value-to-estree@^1.0.0: dependencies: is-plain-obj "^3.0.0" +estree-util-value-to-estree@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.0.1.tgz#0b7b5d6b6a4aaad5c60999ffbc265a985df98ac5" + integrity sha512-b2tdzTurEIbwRh+mKrEcaWfu1wgb8J1hVsgREg7FFiecWwK/PhO8X0kyc+0bIcKNtD4sqxIdNoRy6/p/TvECEA== + dependencies: + "@types/estree" "^1.0.0" + is-plain-obj "^4.0.0" + estree-util-visit@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-1.1.0.tgz" @@ -12687,6 +12712,11 @@ yaml@^1.10.2: resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.0.0: + version "2.3.2" + resolved "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz#f522db4313c671a0ca963a75670f1c12ea909144" + integrity sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg== + yaml@^2.1.1: version "2.2.1" resolved "https://registry.npmjs.org/yaml/-/yaml-2.2.1.tgz" From 2b6914353ab94cc9166861d0569af7bbd7600dfd Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Tue, 10 Oct 2023 13:30:48 +1100 Subject: [PATCH 19/38] add initial vite docs, tweak API --- docs/guides/vite.md | 346 ++++++++++++++++++ docs/styling/bundling.md | 5 +- .../remark-remix-mdx-frontmatter/index.ts | 4 +- packages/remix-dev/vite/index.ts | 5 +- packages/remix-dev/vite/plugin.ts | 35 +- 5 files changed, 370 insertions(+), 25 deletions(-) create mode 100644 docs/guides/vite.md diff --git a/docs/guides/vite.md b/docs/guides/vite.md new file mode 100644 index 00000000000..194a23aea96 --- /dev/null +++ b/docs/guides/vite.md @@ -0,0 +1,346 @@ +--- +title: Vite (Experimental) +toc: false +--- + +# Vite (Experimental) + +Vite support is currently experimental and only intended to gather early feedback. We don't yet recommend using this for production apps. + +[Vite] is a powerful, performant and extensible development environment for JavaScript projects. In order to improve and extend Remix's bundling capabilities, we're currently exploring the use of Vite as an alternative compiler to esbuild. + +To get started with Vite in an existing Remix project (or a new one created with [create-remix]), first install Vite as a dev dependency: + +```shellscript nonumber +npm install -D vite +``` + +Then add `vite.config.mjs` to the project root, providing the Remix plugin to the `plugins` array: + +```js filename=vite.config.mjs +import { experimental_remix } from "@remix-run/dev/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [experimental_remix()], +}); +``` + +The Vite plugin accepts the following subset of Remix config options: + +Note that `remix.config.js` is not used by the Remix Vite plugin unless you manually import it in your Vite config and pass it to the plugin. + +- [appDirectory][appdirectory] +- [assetsBuildDirectory][assetsbuilddirectory] +- [ignoredRouteFiles][ignoredroutefiles] +- [publicPath][publicpath] +- [routes][routes] +- [serverBuildPath][serverbuildpath] +- [serverModuleFormat][servermoduleformat] + +For example: + +```js filename=vite.config.mjs +import { experimental_remix } from "@remix-run/dev/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + experimental_remix({ + ignoredRouteFiles: ["**/.*"], + }), + ], +}); +``` + +All other bundling-related options are now [configured with Vite][vite-config]. This means you have much greater control over the bundling process. + +To start a development server or run a production build using Vite, set the `REMIX_EXPERIMENTAL_VITE` environment variable when running Remix's `dev` and `build` commands: + +You can use [cross-env](https://www.npmjs.com/package/cross-env) to set this environment variable in a cross-platform manner. + +```shellscript nonumber +# Start a development server: +cross-env REMIX_EXPERIMENTAL_VITE=1 remix dev + +# Run a production build: +cross-env REMIX_EXPERIMENTAL_VITE=1 remix build +``` + +## Differences When Using Vite + +Since Vite is now responsible for bundling your app, there are some differences between Vite and the Remix compiler that you'll need to be aware of. + +### New Bundling Features + +Vite has many [features][vite-features] and [plugins][vite-plugins] that are not built into the Remix compiler. Any use of these features will break backwards compatibility with the Remix compiler and should only be used if you intend to use Vite exclusively. + +### Path Aliases + +The Remix compiler leverages the `paths` option in your `tsconfig.json` to resolve path aliases. This is commonly used in the Remix community to define `~` as an alias for the `app` directory. + +Vite does not provide any path aliases by default. You can install the [vite-tsconfig-paths][vite-tsconfig-paths] plugin to automatically resolve path aliases from your `tsconfig.json` in Vite, matching the behavior of the Remix compiler: + +```shellscript nonumber +npm install -D vite-tsconfig-paths +``` + +Then add it to your Vite config: + +```js filename=vite.config.mjs +import { experimental_remix } from "@remix-run/dev/vite"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [experimental_remix(), tsconfigPaths()], +}); +``` + +Alternatively, you can define path aliases without referencing `tsconfig.json` by using Vite's [`resolve.alias`][vite-resolve-alias] option directly: + +```js filename=vite.config.mjs +import { fileURLToPath, URL } from "node:url"; + +import { experimental_remix } from "@remix-run/dev/vite"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + resolve: { + alias: { + "~": fileURLToPath(new URL("./app", import.meta.url)), + }, + }, + plugins: [experimental_remix()], +}); +``` + +### Regular CSS Imports + +When importing a CSS file in Vite, its default export is its file contents as a string. This differs from the Remix compiler which provides the file's URL. To import the URL of a CSS file in Vite, you'll need to explicitly add `?url` to the end of the import path: + +```diff +-import styles from "./styles.css"; ++import styles from "./styles.css?url"; +``` + +For example: + +```ts filename=app/dashboard/route.tsx +import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno + +import styles from "./dashboard.css?url"; + +export const links: LinksFunction = () => [ + { rel: "stylesheet", href: styles }, +]; +``` + +If you're using Vite and the Remix compiler in the same project, you can enable `legacyCssImports` in the Remix Vite plugin which will automatically append `?url` to all relevant CSS imports: + +This option is only intended for use during the transition to Vite and will be removed in the future. + +```js filename=vite.config.mjs +import { + experimental_remix, + experimental_remixLegacyCssImports, +} from "@remix-run/dev/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + experimental_remix({ + legacyCssImports: true, + }), + ], +}); +``` + +### CSS Bundling + +Vite has built-in support for CSS side-effect imports, PostCSS and CSS Modules, among other CSS bundling features. The Remix Vite plugin automatically attaches bundled CSS to the relevant routes so the [`@remix-run/css-bundle`][css-bundling] package is no longer required. + +If you're using Vite and the Remix compiler in the same project, you can continue to use `@remix-run/css-bundle` as long as you check for the existence of `cssBundleHref` before using it: + +```ts +import { cssBundleHref } from "@remix-run/css-bundle"; +import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno + +export const links: LinksFunction = () => [ + ...(cssBundleHref + ? [{ rel: "stylesheet", href: cssBundleHref }] + : []), + // ... +]; +``` + +### Tailwind + +To use [Tailwind] in Vite, first install the required dependencies: + +```shellscript nonumber +npm install -D tailwindcss postcss autoprefixer +``` + +Then generate config files for both Tailwind and PostCSS: + +```shellscript nonumber +npx tailwindcss init --ts -p +``` + +If your Remix project already has a PostCSS config file, you'll need to ensure that the `tailwindcss` plugin has been configured. This plugin was previously being injected by the Remix compiler if it was missing. + +Now we can tell it which files to generate classes from: + +```ts filename=tailwind.config.ts lines=[4] +import type { Config } from "tailwindcss"; + +export default { + content: ["./app/**/*.{js,jsx,ts,tsx}"], + theme: { + extend: {}, + }, + plugins: [], +} satisfies Config; +``` + +Then include the `@tailwind` directives somewhere in your app CSS. For example, you could create a `tailwind.css` file at the root of your app: + +```css filename=app/tailwind.css +@tailwind base; +@tailwind components; +@tailwind utilities; +``` + +### Vanilla Extract + +To use [Vanilla Extract][vanilla-extract] in Vite, install the official [Vite plugin][vanilla-extract-vite-plugin]. + +```shellscript nonumber +npm install -D @vanilla-extract/vite-plugin +``` + +Then add the plugin to your Vite config: + +```js filename=vite.config.mjs +import { experimental_remix } from "@remix-run/dev/vite"; +import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [experimental_remix(), vanillaExtractPlugin()], +}); +``` + +### MDX + +Since Vite's plugin API is an extension of the Rollup plugin API, you can use the official MDX Rollup plugin in Vite: + +```shellscript nonumber +npm install -D @mdx-js/rollup +``` + +Then add the Rollup plugin to your Vite config: + +```js filename=vite.config.mjs +import mdx from "@mdx-js/rollup"; +import { experimental_remix } from "@remix-run/dev/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [experimental_remix(), mdx()], +}); +``` + +The Remix compiler allowed you to define [frontmatter in MDX][mdx-frontmatter] including `headers`, `meta` and `handle` route exports. To reinstate this feature, you can install the following [Remark][remark] plugins: + +```shellscript nonumber +npm install -D remark-frontmatter @remix-run/remix-remark-mdx-frontmatter +``` + +These plugins are entirely optional. You can use [remark-mdx-frontmatter] if you don't need your frontmatter to contain Remix route exports (these can be defined with regular export statements if you prefer), or you can skip using frontmatter entirely. + +Then provide these plugins to the MDX Rollup plugin: + +```js filename=vite.config.mjs +import mdx from "@mdx-js/rollup"; +import { experimental_remix } from "@remix-run/dev/vite"; +import { experimental_remarkRemixMdxFrontmatter } from "@remix-run/remix-remark-mdx-frontmatter"; +import remarkFrontmatter from "remark-frontmatter"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + experimental_remix(), + mdx({ + remarkPlugins: [ + remarkFrontmatter, + experimental_remarkRemixMdxFrontmatter, + ], + }), + ], +}); +``` + +By default the `@remix-run/remark-remix-mdx-frontmatter` plugin provides frontmatter via the `frontmatter` export. This differs from the Remix compiler's frontmatter export name of `attributes`. + +To maintain backwards compatibility with the Remix compiler, you can override this via the `name` option and revert it to its original `attributes` export: + +```js filename=vite.config.mjs +import mdx from "@mdx-js/rollup"; +import { experimental_remix } from "@remix-run/dev/vite"; +import { experimental_remarkRemixMdxFrontmatter } from "@remix-run/remix-remark-mdx-frontmatter"; +import remarkFrontmatter from "remark-frontmatter"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + experimental_remix(), + mdx({ + remarkPlugins: [ + remarkFrontmatter, + [ + experimental_remarkRemixMdxFrontmatter, + { name: "attributes" }, + ], + ], + }), + ], +}); +``` + +### `*.server.ts` and `*.client.ts` Extensions + +The Remix compiler allowed you to define server/client-only files using the `*.server.ts`/`*.client.ts` extensions. The Remix Vite plugin supports this pattern with one notable difference. + +Since Vite leverages ESM at runtime to load modules, you may need to use `import *` when importing server/client-only files if the import statement hasn't been removed by dead code elimination. + +```diff +-import { something } from "./file.server.ts"; ++import * as serverOnly from "./file.server.ts"; +``` + +[vite]: https://vitejs.dev +[create-remix]: ../other-api/create-remix +[remix_config]: ../file-conventions/remix-config +[appdirectory]: ../file-conventions/remix-config#appdirectory +[assetsbuilddirectory]: ../file-conventions/remix-config#assetsbuilddirectory +[ignoredroutefiles]: ../file-conventions/remix-config#ignoredroutefiles +[publicpath]: ../file-conventions/remix-config#publicpath +[routes]: ../file-conventions/remix-config#routes +[serverbuildpath]: ../file-conventions/remix-config#serverbuildpath +[servermoduleformat]: ../file-conventions/remix-config#servermoduleformat +[vite-config]: https://vitejs.dev/config +[vite-features]: https://vitejs.dev/guide/features.html +[vite-plugins]: https://vitejs.dev/plugins +[vite-tsconfig-paths]: /~https://github.com/aleclarson/vite-tsconfig-paths +[vite-resolve-alias]: https://vitejs.dev/config/shared-options.html#resolve-alias +[css-bundling]: ../styling/bundling +[tailwind]: https://tailwindcss.com +[tailwind-postcss]: https://tailwindcss.com/docs/installation/using-postcss +[vanilla-extract]: https://vanilla-extract.style +[vanilla-extract-vite-plugin]: https://vanilla-extract.style/documentation/integrations/vite +[mdx-frontmatter]: https://mdxjs.com/guides/frontmatter +[remark-mdx-frontmatter]: /~https://github.com/remcohaszing/remark-mdx-frontmatter +[remark]: https://remark.js.org diff --git a/docs/styling/bundling.md b/docs/styling/bundling.md index 6dc45a70369..cb9ccc3a5ab 100644 --- a/docs/styling/bundling.md +++ b/docs/styling/bundling.md @@ -29,7 +29,10 @@ import { cssBundleHref } from "@remix-run/css-bundle"; import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno export const links: LinksFunction = () => [ - { rel: "stylesheet", href: cssBundleHref }, + ...(cssBundleHref + ? [{ rel: "stylesheet", href: cssBundleHref }] + : []), + // ... ]; ``` diff --git a/packages/remark-remix-mdx-frontmatter/index.ts b/packages/remark-remix-mdx-frontmatter/index.ts index 38f5c04cd71..11f66af12b4 100644 --- a/packages/remark-remix-mdx-frontmatter/index.ts +++ b/packages/remark-remix-mdx-frontmatter/index.ts @@ -12,7 +12,7 @@ export interface RemarkRemixMdxFrontmatterOptions { parsers?: FrontmatterParsers; } -export const remarkRemixMdxFrontmatter: Plugin< +export const experimental_remarkRemixMdxFrontmatter: Plugin< [RemarkRemixMdxFrontmatterOptions?], any > = ({ name: frontmatterExportName = "frontmatter", parsers } = {}) => { @@ -103,5 +103,3 @@ export const remarkRemixMdxFrontmatter: Plugin< }); }; }; - -export default remarkRemixMdxFrontmatter; diff --git a/packages/remix-dev/vite/index.ts b/packages/remix-dev/vite/index.ts index 86093dcae64..4bc7e1279c9 100644 --- a/packages/remix-dev/vite/index.ts +++ b/packages/remix-dev/vite/index.ts @@ -1,4 +1 @@ -export { - remix as experimental_remix, - legacyCssImportSemantics as experimental_legacyCssImportSemantics, -} from "./plugin"; +export { remix as experimental_remix } from "./plugin"; diff --git a/packages/remix-dev/vite/plugin.ts b/packages/remix-dev/vite/plugin.ts index 1035d9c1a81..1227e38bdee 100644 --- a/packages/remix-dev/vite/plugin.ts +++ b/packages/remix-dev/vite/plugin.ts @@ -7,7 +7,7 @@ import babel from "@babel/core"; import PackageJson from "@npmcli/package-json"; import { type ServerBuild } from "@remix-run/server-runtime"; import { - type Plugin, + type Plugin as VitePlugin, type Manifest as ViteManifest, type ResolvedConfig as ResolvedViteConfig, type ViteDevServer, @@ -45,7 +45,9 @@ export type RemixVitePluginOptions = Pick< | "routes" | "serverBuildPath" | "serverModuleFormat" ->; +> & { + legacyCssImports?: boolean; +}; type ResolvedRemixVitePluginConfig = Pick< ResolvedRemixConfig, @@ -199,7 +201,7 @@ const findEntry = (dir: string, basename: string): string | undefined => { const addTrailingSlash = (path: string): string => path.endsWith("/") ? path : path + "/"; -export let remix: (options?: RemixVitePluginOptions) => Plugin[] = ( +export let remix: (options?: RemixVitePluginOptions) => VitePlugin[] = ( options = {} ) => { let viteCommand: ResolvedViteConfig["command"]; @@ -856,6 +858,19 @@ export let remix: (options?: RemixVitePluginOptions) => Plugin[] = ( return modules; }, }, + ...((options.legacyCssImports + ? [ + { + name: "remix-legacy-css-imports", + enforce: "pre", + transform(code) { + if (code.includes('.css"') || code.includes(".css'")) { + return transformLegacyCssImports(code); + } + }, + }, + ] + : []) satisfies VitePlugin[]), ]; }; @@ -913,20 +928,6 @@ if (import.meta.hot && !inWebWorker) { }); }`; -export let legacyCssImportSemantics: () => Plugin[] = () => { - return [ - { - name: "remix-legacy-css-import-semantics", - enforce: "pre", - transform(code) { - if (code.includes('.css"') || code.includes(".css'")) { - return transformLegacyCssImports(code); - } - }, - }, - ]; -}; - function getRoute( pluginConfig: ResolvedRemixVitePluginConfig, file: string From ecfabcee8d0735e651bb4c51b83a19838b5c3eb4 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Wed, 11 Oct 2023 13:07:46 +1100 Subject: [PATCH 20/38] mark plugin unstable, make top-level dev export --- docs/future/index.md | 3 ++ docs/{guides => future}/vite.md | 48 ++++++++++++++--------------- integration/vite-build-test.ts | 4 +-- integration/vite-dev-test.ts | 4 +-- packages/remix-dev/index.ts | 1 + packages/remix-dev/rollup.config.js | 12 ++++---- packages/remix-dev/vite.d.ts | 1 - packages/remix-dev/vite.js | 1 - packages/remix-dev/vite/index.ts | 11 ++++++- packages/remix-dev/vite/plugin.ts | 7 +++-- 10 files changed, 52 insertions(+), 40 deletions(-) create mode 100644 docs/future/index.md rename docs/{guides => future}/vite.md (90%) delete mode 100644 packages/remix-dev/vite.d.ts delete mode 100644 packages/remix-dev/vite.js diff --git a/docs/future/index.md b/docs/future/index.md new file mode 100644 index 00000000000..ff789c6d3c4 --- /dev/null +++ b/docs/future/index.md @@ -0,0 +1,3 @@ +--- +title: Future +--- diff --git a/docs/guides/vite.md b/docs/future/vite.md similarity index 90% rename from docs/guides/vite.md rename to docs/future/vite.md index 194a23aea96..3571cd6ecaa 100644 --- a/docs/guides/vite.md +++ b/docs/future/vite.md @@ -1,11 +1,11 @@ --- -title: Vite (Experimental) +title: Vite (Unstable) toc: false --- -# Vite (Experimental) +# Vite (Unstable) -Vite support is currently experimental and only intended to gather early feedback. We don't yet recommend using this for production apps. +Vite support is currently unstable and only intended to gather early feedback. We don't yet recommend using this in production. [Vite] is a powerful, performant and extensible development environment for JavaScript projects. In order to improve and extend Remix's bundling capabilities, we're currently exploring the use of Vite as an alternative compiler to esbuild. @@ -18,11 +18,11 @@ npm install -D vite Then add `vite.config.mjs` to the project root, providing the Remix plugin to the `plugins` array: ```js filename=vite.config.mjs -import { experimental_remix } from "@remix-run/dev/vite"; +import { unstable_remixVitePlugin } from "@remix-run/dev"; import { defineConfig } from "vite"; export default defineConfig({ - plugins: [experimental_remix()], + plugins: [unstable_remixVitePlugin()], }); ``` @@ -41,12 +41,12 @@ The Vite plugin accepts the following subset of Remix config options: For example: ```js filename=vite.config.mjs -import { experimental_remix } from "@remix-run/dev/vite"; +import { unstable_remixVitePlugin } from "@remix-run/dev"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [ - experimental_remix({ + unstable_remixVitePlugin({ ignoredRouteFiles: ["**/.*"], }), ], @@ -88,12 +88,12 @@ npm install -D vite-tsconfig-paths Then add it to your Vite config: ```js filename=vite.config.mjs -import { experimental_remix } from "@remix-run/dev/vite"; +import { unstable_remixVitePlugin } from "@remix-run/dev"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ - plugins: [experimental_remix(), tsconfigPaths()], + plugins: [unstable_remixVitePlugin(), tsconfigPaths()], }); ``` @@ -102,7 +102,7 @@ Alternatively, you can define path aliases without referencing `tsconfig.json` b ```js filename=vite.config.mjs import { fileURLToPath, URL } from "node:url"; -import { experimental_remix } from "@remix-run/dev/vite"; +import { unstable_remixVitePlugin } from "@remix-run/dev"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; @@ -112,7 +112,7 @@ export default defineConfig({ "~": fileURLToPath(new URL("./app", import.meta.url)), }, }, - plugins: [experimental_remix()], + plugins: [unstable_remixVitePlugin()], }); ``` @@ -142,15 +142,12 @@ If you're using Vite and the Remix compiler in the same project, you can enable This option is only intended for use during the transition to Vite and will be removed in the future. ```js filename=vite.config.mjs -import { - experimental_remix, - experimental_remixLegacyCssImports, -} from "@remix-run/dev/vite"; +import { unstable_remixVitePlugin } from "@remix-run/dev"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [ - experimental_remix({ + unstable_remixVitePlugin({ legacyCssImports: true, }), ], @@ -224,12 +221,15 @@ npm install -D @vanilla-extract/vite-plugin Then add the plugin to your Vite config: ```js filename=vite.config.mjs -import { experimental_remix } from "@remix-run/dev/vite"; +import { unstable_remixVitePlugin } from "@remix-run/dev"; import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin"; import { defineConfig } from "vite"; export default defineConfig({ - plugins: [experimental_remix(), vanillaExtractPlugin()], + plugins: [ + unstable_remixVitePlugin(), + vanillaExtractPlugin(), + ], }); ``` @@ -245,11 +245,11 @@ Then add the Rollup plugin to your Vite config: ```js filename=vite.config.mjs import mdx from "@mdx-js/rollup"; -import { experimental_remix } from "@remix-run/dev/vite"; +import { unstable_remixVitePlugin } from "@remix-run/dev"; import { defineConfig } from "vite"; export default defineConfig({ - plugins: [experimental_remix(), mdx()], + plugins: [unstable_remixVitePlugin(), mdx()], }); ``` @@ -265,14 +265,14 @@ Then provide these plugins to the MDX Rollup plugin: ```js filename=vite.config.mjs import mdx from "@mdx-js/rollup"; -import { experimental_remix } from "@remix-run/dev/vite"; +import { unstable_remixVitePlugin } from "@remix-run/dev"; import { experimental_remarkRemixMdxFrontmatter } from "@remix-run/remix-remark-mdx-frontmatter"; import remarkFrontmatter from "remark-frontmatter"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [ - experimental_remix(), + unstable_remixVitePlugin(), mdx({ remarkPlugins: [ remarkFrontmatter, @@ -289,14 +289,14 @@ To maintain backwards compatibility with the Remix compiler, you can override th ```js filename=vite.config.mjs import mdx from "@mdx-js/rollup"; -import { experimental_remix } from "@remix-run/dev/vite"; +import { unstable_remixVitePlugin } from "@remix-run/dev"; import { experimental_remarkRemixMdxFrontmatter } from "@remix-run/remix-remark-mdx-frontmatter"; import remarkFrontmatter from "remark-frontmatter"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [ - experimental_remix(), + unstable_remixVitePlugin(), mdx({ remarkPlugins: [ remarkFrontmatter, diff --git a/integration/vite-build-test.ts b/integration/vite-build-test.ts index 7908c1d9c8c..4bbd1bc220c 100644 --- a/integration/vite-build-test.ts +++ b/integration/vite-build-test.ts @@ -24,10 +24,10 @@ test.describe("Vite build", () => { `, "vite.config.mjs": js` import { defineConfig } from "vite"; - import { experimental_remix } from "@remix-run/dev/vite"; + import { unstable_remixVitePlugin } from "@remix-run/dev"; export default defineConfig({ - plugins: [experimental_remix()], + plugins: [unstable_remixVitePlugin()], }); `, "app/root.tsx": js` diff --git a/integration/vite-dev-test.ts b/integration/vite-dev-test.ts index a0bdf5f7bb5..88c04cf0b8c 100644 --- a/integration/vite-dev-test.ts +++ b/integration/vite-dev-test.ts @@ -28,7 +28,7 @@ test.describe("Vite dev", () => { `, "vite.config.mjs": js` import { defineConfig } from "vite"; - import { experimental_remix } from "@remix-run/dev/vite"; + import { unstable_remixVitePlugin } from "@remix-run/dev"; export default defineConfig({ optimizeDeps: { @@ -38,7 +38,7 @@ test.describe("Vite dev", () => { port: ${devPort}, strictPort: true, }, - plugins: [experimental_remix()], + plugins: [unstable_remixVitePlugin()], }); `, "app/root.tsx": js` diff --git a/packages/remix-dev/index.ts b/packages/remix-dev/index.ts index 4c369df9dd5..acc5229f671 100644 --- a/packages/remix-dev/index.ts +++ b/packages/remix-dev/index.ts @@ -6,3 +6,4 @@ export * as cli from "./cli/index"; export type { Manifest as AssetsManifest } from "./manifest"; export { getDependenciesToBundle } from "./dependencies"; +export { unstable_remixVitePlugin } from "./vite"; diff --git a/packages/remix-dev/rollup.config.js b/packages/remix-dev/rollup.config.js index 4d39b7e1500..b1d5fd10951 100644 --- a/packages/remix-dev/rollup.config.js +++ b/packages/remix-dev/rollup.config.js @@ -23,7 +23,12 @@ module.exports = function rollup() { external(id) { return isBareModuleId(id); }, - input: [`${sourceDir}/index.ts`, `${sourceDir}/vite/index.ts`], + input: [ + `${sourceDir}/index.ts`, + // Since we're using a dynamic require for the Vite plugin, we + // need to tell Rollup it's an entry point + `${sourceDir}/vite/plugin.ts`, + ], output: { banner: createBanner("@remix-run/dev", version), dir: outputDist, @@ -48,11 +53,6 @@ module.exports = function rollup() { src: `${sourceDir}/config/defaults`, dest: [`${outputDir}/config`, `${outputDist}/config`], }, - // This needs to end up in the root of the pkg but also needs to - // reference other compiled files. Just copying these are easier - // than dealing with output configuration for sharing chunks x-builds. - { src: `${sourceDir}/vite.js`, dest: outputDir }, - { src: `${sourceDir}/vite.d.ts`, dest: outputDir }, ], }), // Allow dynamic imports in CJS code to allow us to utilize diff --git a/packages/remix-dev/vite.d.ts b/packages/remix-dev/vite.d.ts deleted file mode 100644 index 5f55f621900..00000000000 --- a/packages/remix-dev/vite.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./dist/vite"; diff --git a/packages/remix-dev/vite.js b/packages/remix-dev/vite.js deleted file mode 100644 index 386c03a770f..00000000000 --- a/packages/remix-dev/vite.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./dist/vite"); diff --git a/packages/remix-dev/vite/index.ts b/packages/remix-dev/vite/index.ts index 4bc7e1279c9..111949cedd8 100644 --- a/packages/remix-dev/vite/index.ts +++ b/packages/remix-dev/vite/index.ts @@ -1 +1,10 @@ -export { remix as experimental_remix } from "./plugin"; +// This file allows us to dynamically require the plugin so non-Vite consumers +// don't need to have Vite installed as a peer dependency. Only types should +// be imported at the top level. +import type { RemixVitePlugin } from "./plugin"; + +export const unstable_remixVitePlugin: RemixVitePlugin = (...args) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + let { remixVitePlugin } = require("./plugin") as typeof import("./plugin"); + return remixVitePlugin(...args); +}; diff --git a/packages/remix-dev/vite/plugin.ts b/packages/remix-dev/vite/plugin.ts index 1227e38bdee..8b48be2a7e9 100644 --- a/packages/remix-dev/vite/plugin.ts +++ b/packages/remix-dev/vite/plugin.ts @@ -201,9 +201,10 @@ const findEntry = (dir: string, basename: string): string | undefined => { const addTrailingSlash = (path: string): string => path.endsWith("/") ? path : path + "/"; -export let remix: (options?: RemixVitePluginOptions) => VitePlugin[] = ( - options = {} -) => { +export type RemixVitePlugin = ( + options?: RemixVitePluginOptions +) => VitePlugin[]; +export const remixVitePlugin: RemixVitePlugin = (options = {}) => { let viteCommand: ResolvedViteConfig["command"]; let viteUserConfig: ViteUserConfig; From 26cd620690a23df58112abee3d47bd7eeaf61c21 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Wed, 11 Oct 2023 13:15:58 -0400 Subject: [PATCH 21/38] docs: feature matrix, HMR limitations, and acknowledgements --- docs/future/vite.md | 104 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/docs/future/vite.md b/docs/future/vite.md index 3571cd6ecaa..330ed60ba02 100644 --- a/docs/future/vite.md +++ b/docs/future/vite.md @@ -9,6 +9,14 @@ toc: false [Vite] is a powerful, performant and extensible development environment for JavaScript projects. In order to improve and extend Remix's bundling capabilities, we're currently exploring the use of Vite as an alternative compiler to esbuild. +| Feature | Node | Deno | Cloudflare | Notes | +| ---------------------------- | ---- | ---- | ---------- | ----------------------------------------- | +| Built-in dev server | ✅ | ❓ | ⏳ | | +| Other servers (e.g. Express) | ⏳ | ⏳ | ⏳ | | +| HMR | ✅ | ❓ | ⏳ | | +| HDR | ✅ | ❓ | ⏳ | | +| MDX | ⏳ | ⏳ | ⏳ | /~https://github.com/vitejs/vite/pull/14560 | + To get started with Vite in an existing Remix project (or a new one created with [create-remix]), first install Vite as a dev dependency: ```shellscript nonumber @@ -321,6 +329,92 @@ Since Vite leverages ESM at runtime to load modules, you may need to use `import +import * as serverOnly from "./file.server.ts"; ``` +## HMR & HDR + +### React Fast Refresh limitations + +[React Fast Refresh][react_refresh] does not preserve state for class components. +This includes higher-order components that internally return classes: + +```ts +export class ComponentA extends Component {} // ❌ + +export const ComponentB = HOC(ComponentC); // ❌ won't work if HOC returns a class component + +export function ComponentD() {} // ✅ +export const ComponentE = () => {}; // ✅ +export default function ComponentF() {} // ✅ +``` + +Function components must be named, not anonymous, for React Fast Refresh to track changes: + +```ts +export default () => {}; // ❌ +export default function () {} // ❌ + +const ComponentA = () => {}; +export default ComponentA; // ✅ + +export default function ComponentB() {} // ✅ +``` + +React Fast Refresh can only handle component exports. While Remix manages special route exports like `meta`, `links`, and `header` for you, any user-defined, will cause full reloads: + +```ts +// these exports are specially handled by Remix to be HMR-compatible +export const meta = { title: "Home" }; // ✅ +export const links = [ + { rel: "stylesheet", href: "style.css" }, +]; // ✅ +export const headers = { "Cache-Control": "max-age=3600" }; // ✅ + +// these exports are treeshaken by Remix, so they never affect HMR +export const loader = () => {}; // ✅ +export const action = () => {}; // ✅ + +// This is not a Remix export, nor a component export +// so it will cause a full reloads for this route +export const myValue = "some value"; // ❌ + +export default function Route() {} // ✅ +``` + +👆 Routes probably shouldn't be exporting random values like that anyway. +If you want to reuse values across routes, stick them in their own non-route module: + +```ts filename=my-custom-value.ts +export const myValue = "some value"; +``` + +React Fast Refresh cannot track changes for a component when hooks are being added or removed from it, +causing full reloads just for the next render. After the hooks has been added, changes should result in hot updates again. +For example, if you add [`useLoaderData`][use_loader_data] to your component, you may lose state local to that component for that render. + +In some cases React cannot distinguish between existing components being changed and new components being added. +[React needs `key`s][react_keys] to disambiguate these cases and track changes when sibling elements are modified. + +These are all limitations of React and [React Refresh][react_refresh], not Remix. + +## Acknowledgements + +Vite is an amazing project and we're grateful to the Vite team for their work. +Special thanks to [Matias Capeletto, Arnaud Barré, and Bjorn Lu from the Vite team][vite-team] for their guidance. + +The Remix community was quick to explore Vite support and we're grateful for their contributions: + +- [Discussion: Consider using Vite][consider-using-vite] +- [remix-kit][remix-kit] +- [remix-vite][remix-vite] +- [vite-plugin-remix][vite-plugin-remix] + +Finally, we were inspired by how other frameworks implemented Vite support: + +- [Astro][astro] +- [SolidStart][solidstart] +- [SvelteKit][svletekit] + +We're definitely late to the Vite party, but we're excited to be here now! + [vite]: https://vitejs.dev [create-remix]: ../other-api/create-remix [remix_config]: ../file-conventions/remix-config @@ -344,3 +438,13 @@ Since Vite leverages ESM at runtime to load modules, you may need to use `import [mdx-frontmatter]: https://mdxjs.com/guides/frontmatter [remark-mdx-frontmatter]: /~https://github.com/remcohaszing/remark-mdx-frontmatter [remark]: https://remark.js.org +[use_loader_data]: ../hooks/use-loader-data +[react_refresh]: /~https://github.com/facebook/react/tree/main/packages/react-refresh +[vite-team]: https://vitejs.dev/team.html +[consider-using-vite]: /~https://github.com/remix-run/remix/discussions/2427 +[remix-kit]: /~https://github.com/jrestall/remix-kit +[remix-vite]: /~https://github.com/sudomf/remix-vite +[vite-plugin-remix]: /~https://github.com/yracnet/vite-plugin-remix +[astro]: https://astro.build/ +[solidstart]: https://start.solidjs.com/getting-started/what-is-solidstart +[sveltekit]: https://kit.svelte.dev/ From 9b8b807e5bbb705cba52cbb03943b01e375a32ad Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Wed, 11 Oct 2023 13:49:28 -0400 Subject: [PATCH 22/38] docs: feature matrix legend, getting started header --- docs/future/vite.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/future/vite.md b/docs/future/vite.md index 330ed60ba02..6017c5fdc70 100644 --- a/docs/future/vite.md +++ b/docs/future/vite.md @@ -9,6 +9,8 @@ toc: false [Vite] is a powerful, performant and extensible development environment for JavaScript projects. In order to improve and extend Remix's bundling capabilities, we're currently exploring the use of Vite as an alternative compiler to esbuild. +**Legend**: ✅ (Tested),❓ (Untested), ⏳ (Not Supported) + | Feature | Node | Deno | Cloudflare | Notes | | ---------------------------- | ---- | ---- | ---------- | ----------------------------------------- | | Built-in dev server | ✅ | ❓ | ⏳ | | @@ -17,6 +19,8 @@ toc: false | HDR | ✅ | ❓ | ⏳ | | | MDX | ⏳ | ⏳ | ⏳ | /~https://github.com/vitejs/vite/pull/14560 | +## Getting started + To get started with Vite in an existing Remix project (or a new one created with [create-remix]), first install Vite as a dev dependency: ```shellscript nonumber From 45f7dd28fc9542138ce5f6775482f301e56bf307 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Wed, 11 Oct 2023 13:51:22 -0400 Subject: [PATCH 23/38] docs: clarify that hourglass means "not yet", not "no" --- docs/future/vite.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/future/vite.md b/docs/future/vite.md index 6017c5fdc70..1e2da6674d3 100644 --- a/docs/future/vite.md +++ b/docs/future/vite.md @@ -9,7 +9,7 @@ toc: false [Vite] is a powerful, performant and extensible development environment for JavaScript projects. In order to improve and extend Remix's bundling capabilities, we're currently exploring the use of Vite as an alternative compiler to esbuild. -**Legend**: ✅ (Tested),❓ (Untested), ⏳ (Not Supported) +**Legend**: ✅ (Tested),❓ (Untested), ⏳ (Not Yet Supported) | Feature | Node | Deno | Cloudflare | Notes | | ---------------------------- | ---- | ---- | ---------- | ----------------------------------------- | From 219c32fa2bc9bf0ffb29691cd9df33844dae310e Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Wed, 11 Oct 2023 16:42:23 -0400 Subject: [PATCH 24/38] add warning message when using unstable vite support --- packages/remix-dev/cli/commands.ts | 6 ++++++ packages/remix-dev/vite/plugin.ts | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index 69612ef1cab..cbbeef75ca1 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -135,6 +135,12 @@ export async function build( } export async function viteBuild(options: ViteBuildOptions = {}) { + console.warn( + pc.yellow( + " ⚠️ Remix support for Vite is unstable\n and not recommended for production\n" + ) + ); + let { build } = await import("../vite/build"); await build(options); } diff --git a/packages/remix-dev/vite/plugin.ts b/packages/remix-dev/vite/plugin.ts index 8b48be2a7e9..8d244c37ac6 100644 --- a/packages/remix-dev/vite/plugin.ts +++ b/packages/remix-dev/vite/plugin.ts @@ -20,6 +20,7 @@ import { parse as esModuleLexer, } from "es-module-lexer"; import jsesc from "jsesc"; +import colors from "picocolors"; import { defineRoutes, type RouteManifest } from "../config/routes"; import { @@ -591,6 +592,15 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => { } }, configureServer(vite) { + vite.httpServer?.on("listening", () => { + setTimeout(() => { + vite.config.logger.warn( + colors.yellow( + "\n ⚠️ Remix support for Vite is unstable\n and not recommended for production\n" + ) + ); + }, 50); + }); return () => { vite.middlewares.use(async (req, res, next) => { try { From a77ca27495d13a3bbad17a0404cac3964d8704e8 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Thu, 12 Oct 2023 10:02:37 +1100 Subject: [PATCH 25/38] rename experimental Vite env var to unstable --- docs/future/vite.md | 6 +++--- integration/vite-build-test.ts | 2 +- integration/vite-dev-test.ts | 4 ++-- packages/remix-dev/cli/run.ts | 7 +++---- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/future/vite.md b/docs/future/vite.md index 1e2da6674d3..9de4d7705db 100644 --- a/docs/future/vite.md +++ b/docs/future/vite.md @@ -67,16 +67,16 @@ export default defineConfig({ All other bundling-related options are now [configured with Vite][vite-config]. This means you have much greater control over the bundling process. -To start a development server or run a production build using Vite, set the `REMIX_EXPERIMENTAL_VITE` environment variable when running Remix's `dev` and `build` commands: +To start a development server or run a production build using Vite, set the `REMIX_UNSTABLE_VITE` environment variable when running Remix's `dev` and `build` commands: You can use [cross-env](https://www.npmjs.com/package/cross-env) to set this environment variable in a cross-platform manner. ```shellscript nonumber # Start a development server: -cross-env REMIX_EXPERIMENTAL_VITE=1 remix dev +cross-env REMIX_UNSTABLE_VITE=1 remix dev # Run a production build: -cross-env REMIX_EXPERIMENTAL_VITE=1 remix build +cross-env REMIX_UNSTABLE_VITE=1 remix build ``` ## Differences When Using Vite diff --git a/integration/vite-build-test.ts b/integration/vite-build-test.ts index 4bbd1bc220c..bdef82a0612 100644 --- a/integration/vite-build-test.ts +++ b/integration/vite-build-test.ts @@ -15,7 +15,7 @@ test.describe("Vite build", () => { test.beforeAll(async () => { fixture = await createFixture({ env: { - REMIX_EXPERIMENTAL_VITE: "1", + REMIX_UNSTABLE_VITE: "1", }, files: { "remix.config.js": js` diff --git a/integration/vite-dev-test.ts b/integration/vite-dev-test.ts index 88c04cf0b8c..8441dba3b77 100644 --- a/integration/vite-dev-test.ts +++ b/integration/vite-dev-test.ts @@ -19,7 +19,7 @@ test.describe("Vite dev", () => { devPort = await getPort(); projectDir = await createFixtureProject({ env: { - REMIX_EXPERIMENTAL_VITE: "1", + REMIX_UNSTABLE_VITE: "1", }, files: { "remix.config.js": js` @@ -91,7 +91,7 @@ test.describe("Vite dev", () => { ["./node_modules/@remix-run/dev/dist/cli.js", "dev"], { cwd: projectDir, - env: { ...process.env, REMIX_EXPERIMENTAL_VITE: "1" }, + env: { ...process.env, REMIX_UNSTABLE_VITE: "1" }, stdio: "pipe", } ); diff --git a/packages/remix-dev/cli/run.ts b/packages/remix-dev/cli/run.ts index 6b11406c95a..6951e3f7982 100644 --- a/packages/remix-dev/cli/run.ts +++ b/packages/remix-dev/cli/run.ts @@ -116,8 +116,7 @@ export async function run(argv: string[] = process.argv.slice(2)) { "--tls-key": String, "--tls-cert": String, - // vite - ...(process.env.REMIX_EXPERIMENTAL_VITE + ...(process.env.REMIX_UNSTABLE_VITE ? { "--strictPort": Boolean, "--config": String, @@ -180,7 +179,7 @@ export async function run(argv: string[] = process.argv.slice(2)) { await commands.routes(input[1], flags.json ? "json" : "jsx"); break; case "build": - if (process.env.REMIX_EXPERIMENTAL_VITE) { + if (process.env.REMIX_UNSTABLE_VITE) { await commands.viteBuild(flags); } else { if (!process.env.NODE_ENV) process.env.NODE_ENV = "production"; @@ -200,7 +199,7 @@ export async function run(argv: string[] = process.argv.slice(2)) { break; } case "dev": - await (process.env.REMIX_EXPERIMENTAL_VITE + await (process.env.REMIX_UNSTABLE_VITE ? commands.viteDev(flags) : commands.dev(input[1], flags)); break; From ce654357947d119218936b780ca8f911c55a65c6 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Thu, 12 Oct 2023 12:18:01 +1100 Subject: [PATCH 26/38] remove remix mdx frontmatter plugin, update docs --- .changeset/config.json | 1 - docs/future/vite.md | 103 ++++++++++++++--- jest.config.js | 1 - package.json | 1 - .../remark-remix-mdx-frontmatter/index.ts | 105 ------------------ .../jest.config.js | 6 - .../remark-remix-mdx-frontmatter/package.json | 39 ------- .../rollup.config.js | 71 ------------ .../tsconfig.json | 19 ---- scripts/publish.js | 1 - scripts/utils.js | 1 - tsconfig.json | 1 - 12 files changed, 87 insertions(+), 262 deletions(-) delete mode 100644 packages/remark-remix-mdx-frontmatter/index.ts delete mode 100644 packages/remark-remix-mdx-frontmatter/jest.config.js delete mode 100644 packages/remark-remix-mdx-frontmatter/package.json delete mode 100644 packages/remark-remix-mdx-frontmatter/rollup.config.js delete mode 100644 packages/remark-remix-mdx-frontmatter/tsconfig.json diff --git a/.changeset/config.json b/.changeset/config.json index c7573ed3778..12d246d66c0 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -17,7 +17,6 @@ "@remix-run/express", "@remix-run/node", "@remix-run/react", - "@remix-run/remark-remix-mdx-frontmatter", "@remix-run/serve", "@remix-run/server-runtime", "@remix-run/testing" diff --git a/docs/future/vite.md b/docs/future/vite.md index 9de4d7705db..3a9d1774c04 100644 --- a/docs/future/vite.md +++ b/docs/future/vite.md @@ -247,7 +247,7 @@ export default defineConfig({ ### MDX -Since Vite's plugin API is an extension of the Rollup plugin API, you can use the official MDX Rollup plugin in Vite: +Since Vite's plugin API is an extension of the Rollup plugin API, you can use the official [MDX Rollup plugin][mdx-rollup-plugin]: ```shellscript nonumber npm install -D @mdx-js/rollup @@ -265,21 +265,23 @@ export default defineConfig({ }); ``` -The Remix compiler allowed you to define [frontmatter in MDX][mdx-frontmatter] including `headers`, `meta` and `handle` route exports. To reinstate this feature, you can install the following [Remark][remark] plugins: +#### MDX Frontmatter + +The Remix compiler allowed you to define [frontmatter in MDX][mdx-frontmatter]. You can achieve this in Vite using [remark-mdx-frontmatter]. + +First, install the required [Remark][remark] plugins: ```shellscript nonumber -npm install -D remark-frontmatter @remix-run/remix-remark-mdx-frontmatter +npm install -D remark-frontmatter remark-mdx-frontmatter ``` -These plugins are entirely optional. You can use [remark-mdx-frontmatter] if you don't need your frontmatter to contain Remix route exports (these can be defined with regular export statements if you prefer), or you can skip using frontmatter entirely. - Then provide these plugins to the MDX Rollup plugin: ```js filename=vite.config.mjs import mdx from "@mdx-js/rollup"; import { unstable_remixVitePlugin } from "@remix-run/dev"; -import { experimental_remarkRemixMdxFrontmatter } from "@remix-run/remix-remark-mdx-frontmatter"; import remarkFrontmatter from "remark-frontmatter"; +import remarkMdxFrontmatter from "remark-mdx-frontmatter"; import { defineConfig } from "vite"; export default defineConfig({ @@ -288,22 +290,20 @@ export default defineConfig({ mdx({ remarkPlugins: [ remarkFrontmatter, - experimental_remarkRemixMdxFrontmatter, + remarkMdxFrontmatter, ], }), ], }); ``` -By default the `@remix-run/remark-remix-mdx-frontmatter` plugin provides frontmatter via the `frontmatter` export. This differs from the Remix compiler's frontmatter export name of `attributes`. - -To maintain backwards compatibility with the Remix compiler, you can override this via the `name` option and revert it to its original `attributes` export: +In the Remix compiler, the frontmatter export was named `attributes`. This differs from the frontmatter plugin's default export name of `frontmatter`. To maintain backwards compatibility with the Remix compiler, you can override this via the `name` option: ```js filename=vite.config.mjs import mdx from "@mdx-js/rollup"; import { unstable_remixVitePlugin } from "@remix-run/dev"; -import { experimental_remarkRemixMdxFrontmatter } from "@remix-run/remix-remark-mdx-frontmatter"; import remarkFrontmatter from "remark-frontmatter"; +import remarkMdxFrontmatter from "remark-mdx-frontmatter"; import { defineConfig } from "vite"; export default defineConfig({ @@ -312,16 +312,85 @@ export default defineConfig({ mdx({ remarkPlugins: [ remarkFrontmatter, - [ - experimental_remarkRemixMdxFrontmatter, - { name: "attributes" }, - ], + [remarkMdxFrontmatter, { name: "attributes" }], ], }), ], }); ``` +##### MDX Route Frontmatter + +The Remix compiler allowed you to define `headers`, `meta` and `handle` route exports in your frontmatter. This Remix-specific feature is obviously not supported by the `remark-mdx-frontmatter` plugin, but you can manually map frontmatter to route exports yourself: + +```mdx +--- +meta: + - title: My First Post + - name: description + content: Isn't this awesome? +headers: + Cache-Control: no-cache +--- + +export const meta = frontmatter.meta; +export const headers = frontmatter.headers; + +# Hello World +``` + +By writing these MDX route exports yourself, you're free to use whatever frontmatter structure you like. + +```mdx +--- +title: My First Post +description: Isn't this awesome? +--- + +export const meta = () => { + return [ + { title: frontmatter.title }, + { + name: "description", + content: frontmatter.description, + }, + ]; +}; + +# Hello World +``` + +You may even want to take this a step further and create a [remark plugin][remark-plugin] that automatically maps frontmatter to route exports for you. + +##### MDX Filename Export + +The Remix compiler also provided a `filename` export from all MDX files. This was primarily designed to enable linking to collections of MDX routes. In Vite, you should achieve this via [glob imports][glob-imports] which give you a handy data structure that maps file names to modules. This makes it much easier to maintain a list of MDX files since you no longer need to import each one manually. + +For example, to import all MDX files in the `posts` directory: + +```ts +const posts = import.meta.glob("./posts/*.mdx"); +``` + +This is equivalent to writing this by hand: + +```ts +const posts = { + "./posts/a.mdx": () => import("./posts/a.mdx"), + "./posts/b.mdx": () => import("./posts/b.mdx"), + "./posts/c.mdx": () => import("./posts/c.mdx"), + // etc. +}; +``` + +You can also eagerly import all MDX files if you'd prefer: + +```ts +const posts = import.meta.glob("./posts/*.mdx", { + eager: true, +}); +``` + ### `*.server.ts` and `*.client.ts` Extensions The Remix compiler allowed you to define server/client-only files using the `*.server.ts`/`*.client.ts` extensions. The Remix Vite plugin supports this pattern with one notable difference. @@ -439,9 +508,11 @@ We're definitely late to the Vite party, but we're excited to be here now! [tailwind-postcss]: https://tailwindcss.com/docs/installation/using-postcss [vanilla-extract]: https://vanilla-extract.style [vanilla-extract-vite-plugin]: https://vanilla-extract.style/documentation/integrations/vite +[mdx-rollup-plugin]: https://mdxjs.com/packages/rollup [mdx-frontmatter]: https://mdxjs.com/guides/frontmatter [remark-mdx-frontmatter]: /~https://github.com/remcohaszing/remark-mdx-frontmatter -[remark]: https://remark.js.org +[remark-plugin]: /~https://github.com/remarkjs/remark/blob/main/doc/plugins.md +[glob-imports]: https://vitejs.dev/guide/features.html#glob-import [use_loader_data]: ../hooks/use-loader-data [react_refresh]: /~https://github.com/facebook/react/tree/main/packages/react-refresh [vite-team]: https://vitejs.dev/team.html diff --git a/jest.config.js b/jest.config.js index f880f9a4bbc..5ffd3c56699 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,7 +7,6 @@ module.exports = { ], projects: [ "packages/create-remix", - "packages/remark-remix-mdx-frontmatter", "packages/remix", "packages/remix-architect", "packages/remix-cloudflare", diff --git a/package.json b/package.json index a30b1dd27d8..61c44294391 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,6 @@ "workspaces": [ "integration", "packages/create-remix", - "packages/remark-remix-mdx-frontmatter", "packages/remix", "packages/remix-architect", "packages/remix-cloudflare", diff --git a/packages/remark-remix-mdx-frontmatter/index.ts b/packages/remark-remix-mdx-frontmatter/index.ts deleted file mode 100644 index 11f66af12b4..00000000000 --- a/packages/remark-remix-mdx-frontmatter/index.ts +++ /dev/null @@ -1,105 +0,0 @@ -import type { Plugin } from "unified"; -import type { Literal, Root } from "mdast"; -import type { ExportNamedDeclaration } from "estree"; -import { valueToEstree } from "estree-util-value-to-estree"; -import { parse as parseToml } from "toml"; -import { parse as parseYaml } from "yaml"; - -type FrontmatterParsers = Record unknown>; - -export interface RemarkRemixMdxFrontmatterOptions { - name?: string; - parsers?: FrontmatterParsers; -} - -export const experimental_remarkRemixMdxFrontmatter: Plugin< - [RemarkRemixMdxFrontmatterOptions?], - any -> = ({ name: frontmatterExportName = "frontmatter", parsers } = {}) => { - let allParsers: FrontmatterParsers = { - toml: parseToml, - yaml: parseYaml, - ...parsers, - }; - - return (rootNode: Root, { basename = "" }) => { - let frontmatter: unknown; - - let node = rootNode.children.find(({ type }) => - Object.hasOwnProperty.call(allParsers, type) - ); - - if (node) { - let parser = allParsers[node.type]; - frontmatter = parser((node as Literal).value); - } - - let frontmatterHasKey = (key: string): boolean => - typeof frontmatter === "object" && - frontmatter !== null && - key in frontmatter; - - rootNode.children.unshift({ - type: "mdxjsEsm", - value: "", - data: { - estree: { - type: "Program", - sourceType: "module", - body: [ - { - type: "ExportNamedDeclaration", - specifiers: [], - declaration: { - type: "VariableDeclaration", - kind: "const", - declarations: [ - { - type: "VariableDeclarator", - id: { - type: "Identifier", - name: frontmatterExportName, - }, - init: valueToEstree(frontmatter), - }, - ], - }, - }, - ...["headers", "meta", "handle"].filter(frontmatterHasKey).map( - (remixExportName: string): ExportNamedDeclaration => ({ - type: "ExportNamedDeclaration", - specifiers: [], - declaration: { - type: "VariableDeclaration", - kind: "const", - declarations: [ - { - type: "VariableDeclarator", - id: { - type: "Identifier", - name: remixExportName, - }, - init: { - type: "MemberExpression", - optional: false, - computed: false, - object: { - type: "Identifier", - name: frontmatterExportName, - }, - property: { - type: "Identifier", - name: remixExportName, - }, - }, - }, - ], - }, - }) - ), - ], - }, - }, - }); - }; -}; diff --git a/packages/remark-remix-mdx-frontmatter/jest.config.js b/packages/remark-remix-mdx-frontmatter/jest.config.js deleted file mode 100644 index 5ed8a4f1c93..00000000000 --- a/packages/remark-remix-mdx-frontmatter/jest.config.js +++ /dev/null @@ -1,6 +0,0 @@ -/** @type {import('jest').Config} */ -module.exports = { - ...require("../../jest/jest.config.shared"), - displayName: "remark-remix-mdx-frontmatter", - setupFiles: [], -}; diff --git a/packages/remark-remix-mdx-frontmatter/package.json b/packages/remark-remix-mdx-frontmatter/package.json deleted file mode 100644 index bef8ff2cf0e..00000000000 --- a/packages/remark-remix-mdx-frontmatter/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "@remix-run/remark-remix-mdx-frontmatter", - "version": "2.0.1", - "description": "Remark plugin to parse Remix-specific frontmatter in MDX files", - "bugs": { - "url": "/~https://github.com/remix-run/remix/issues" - }, - "repository": { - "type": "git", - "url": "/~https://github.com/remix-run/remix", - "directory": "packages/remark-remix-mdx-frontmatter" - }, - "license": "MIT", - "main": "./dist/index.js", - "module": "./dist/esm/index.js", - "typings": "./dist/index.d.ts", - "files": [ - "dist/", - "CHANGELOG.md", - "LICENSE.md", - "README.md" - ], - "dependencies": { - "@types/estree": "^1.0.2", - "@types/mdast": "^3.0.0", - "estree-util-value-to-estree": "^3.0.1", - "toml": "^3.0.0", - "unified": "^10.0.0", - "yaml": "^2.0.0" - }, - "devDependencies": { - "@mdx-js/mdx": "^2.3.0", - "estree-jsx": "^0.0.1", - "mdast-util-mdx": "^2.0.0" - }, - "engines": { - "node": ">=18.0.0" - } -} diff --git a/packages/remark-remix-mdx-frontmatter/rollup.config.js b/packages/remark-remix-mdx-frontmatter/rollup.config.js deleted file mode 100644 index 370db3e0184..00000000000 --- a/packages/remark-remix-mdx-frontmatter/rollup.config.js +++ /dev/null @@ -1,71 +0,0 @@ -const path = require("node:path"); -const babel = require("@rollup/plugin-babel").default; -const nodeResolve = require("@rollup/plugin-node-resolve").default; -const copy = require("rollup-plugin-copy"); - -const { - copyToPlaygrounds, - createBanner, - getOutputDir, - isBareModuleId, -} = require("../../rollup.utils"); -const { name: packageName, version } = require("./package.json"); - -/** @returns {import("rollup").RollupOptions[]} */ -module.exports = function rollup() { - let sourceDir = "packages/remark-remix-mdx-frontmatter"; - let outputDir = getOutputDir(packageName); - let outputDist = path.join(outputDir, "dist"); - - return [ - { - external(id) { - return isBareModuleId(id); - }, - input: `${sourceDir}/index.ts`, - output: { - banner: createBanner(packageName, version), - dir: outputDist, - format: "cjs", - preserveModules: true, - exports: "named", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts"], - }), - nodeResolve({ extensions: [".ts"] }), - copy({ - targets: [ - { src: `LICENSE.md`, dest: outputDir }, - { src: `${sourceDir}/package.json`, dest: outputDir }, - { src: `${sourceDir}/README.md`, dest: outputDir }, - ], - }), - ], - }, - { - external(id) { - return isBareModuleId(id); - }, - input: `${sourceDir}/index.ts`, - output: { - banner: createBanner(packageName, version), - dir: `${outputDist}/esm`, - format: "esm", - preserveModules: true, - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts"], - }), - nodeResolve({ extensions: [".ts"] }), - copyToPlaygrounds(), - ], - }, - ]; -}; diff --git a/packages/remark-remix-mdx-frontmatter/tsconfig.json b/packages/remark-remix-mdx-frontmatter/tsconfig.json deleted file mode 100644 index 659f2e5245c..00000000000 --- a/packages/remark-remix-mdx-frontmatter/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "include": ["**/*.ts", "package.json"], - "exclude": ["dist", "__tests__", "node_modules"], - "compilerOptions": { - "lib": ["ES2022"], - "target": "ES2022", - "module": "ES2022", - "types": ["mdast-util-mdx"], - "moduleResolution": "Bundler", - "allowSyntheticDefaultImports": true, - "strict": true, - "declaration": true, - "emitDeclarationOnly": true, - "skipLibCheck": true, - "resolveJsonModule": true, - "outDir": "../../build/node_modules/@remix-run/remark-remix-mdx-frontmatter/dist", - "rootDir": "." - } -} diff --git a/scripts/publish.js b/scripts/publish.js index fb250203e36..dfb87429cc4 100644 --- a/scripts/publish.js +++ b/scripts/publish.js @@ -56,7 +56,6 @@ async function run() { "serve", "css-bundle", "testing", - "remark-remix-mdx-frontmatter", ]) { publish(path.join(buildDir, "@remix-run", name), tag); } diff --git a/scripts/utils.js b/scripts/utils.js index 10ff865161f..21b9655b44c 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -17,7 +17,6 @@ let remixPackages = { "eslint-config", "css-bundle", "testing", - "remark-remix-mdx-frontmatter", ], get all() { return [...this.adapters, ...this.runtimes, ...this.core, "serve"]; diff --git a/tsconfig.json b/tsconfig.json index 5fd499a5d16..fe138b06305 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,6 @@ "references": [ { "path": "integration" }, { "path": "packages/create-remix" }, - { "path": "packages/remark-remix-mdx-frontmatter" }, { "path": "packages/remix" }, { "path": "packages/remix-architect" }, { "path": "packages/remix-cloudflare" }, From ab37bf9b9b820108f78cc8cc237729861d7e0814 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Thu, 12 Oct 2023 14:25:10 +1100 Subject: [PATCH 27/38] rename vite plugin export --- docs/future/vite.md | 39 +++++++++++++++----------------- integration/vite-build-test.ts | 4 ++-- integration/vite-dev-test.ts | 4 ++-- packages/remix-dev/index.ts | 2 +- packages/remix-dev/vite/index.ts | 2 +- 5 files changed, 24 insertions(+), 27 deletions(-) diff --git a/docs/future/vite.md b/docs/future/vite.md index 3a9d1774c04..037b3299fac 100644 --- a/docs/future/vite.md +++ b/docs/future/vite.md @@ -30,11 +30,11 @@ npm install -D vite Then add `vite.config.mjs` to the project root, providing the Remix plugin to the `plugins` array: ```js filename=vite.config.mjs -import { unstable_remixVitePlugin } from "@remix-run/dev"; +import { unstable_vitePlugin as remix } from "@remix-run/dev"; import { defineConfig } from "vite"; export default defineConfig({ - plugins: [unstable_remixVitePlugin()], + plugins: [remix()], }); ``` @@ -53,12 +53,12 @@ The Vite plugin accepts the following subset of Remix config options: For example: ```js filename=vite.config.mjs -import { unstable_remixVitePlugin } from "@remix-run/dev"; +import { unstable_vitePlugin as remix } from "@remix-run/dev"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [ - unstable_remixVitePlugin({ + remix({ ignoredRouteFiles: ["**/.*"], }), ], @@ -100,12 +100,12 @@ npm install -D vite-tsconfig-paths Then add it to your Vite config: ```js filename=vite.config.mjs -import { unstable_remixVitePlugin } from "@remix-run/dev"; +import { unstable_vitePlugin as remix } from "@remix-run/dev"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ - plugins: [unstable_remixVitePlugin(), tsconfigPaths()], + plugins: [remix(), tsconfigPaths()], }); ``` @@ -114,7 +114,7 @@ Alternatively, you can define path aliases without referencing `tsconfig.json` b ```js filename=vite.config.mjs import { fileURLToPath, URL } from "node:url"; -import { unstable_remixVitePlugin } from "@remix-run/dev"; +import { unstable_vitePlugin as remix } from "@remix-run/dev"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; @@ -124,7 +124,7 @@ export default defineConfig({ "~": fileURLToPath(new URL("./app", import.meta.url)), }, }, - plugins: [unstable_remixVitePlugin()], + plugins: [remix()], }); ``` @@ -154,12 +154,12 @@ If you're using Vite and the Remix compiler in the same project, you can enable This option is only intended for use during the transition to Vite and will be removed in the future. ```js filename=vite.config.mjs -import { unstable_remixVitePlugin } from "@remix-run/dev"; +import { unstable_vitePlugin as remix } from "@remix-run/dev"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [ - unstable_remixVitePlugin({ + remix({ legacyCssImports: true, }), ], @@ -233,15 +233,12 @@ npm install -D @vanilla-extract/vite-plugin Then add the plugin to your Vite config: ```js filename=vite.config.mjs -import { unstable_remixVitePlugin } from "@remix-run/dev"; +import { unstable_vitePlugin as remix } from "@remix-run/dev"; import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin"; import { defineConfig } from "vite"; export default defineConfig({ - plugins: [ - unstable_remixVitePlugin(), - vanillaExtractPlugin(), - ], + plugins: [remix(), vanillaExtractPlugin()], }); ``` @@ -257,11 +254,11 @@ Then add the Rollup plugin to your Vite config: ```js filename=vite.config.mjs import mdx from "@mdx-js/rollup"; -import { unstable_remixVitePlugin } from "@remix-run/dev"; +import { unstable_vitePlugin as remix } from "@remix-run/dev"; import { defineConfig } from "vite"; export default defineConfig({ - plugins: [unstable_remixVitePlugin(), mdx()], + plugins: [remix(), mdx()], }); ``` @@ -279,14 +276,14 @@ Then provide these plugins to the MDX Rollup plugin: ```js filename=vite.config.mjs import mdx from "@mdx-js/rollup"; -import { unstable_remixVitePlugin } from "@remix-run/dev"; +import { unstable_vitePlugin as remix } from "@remix-run/dev"; import remarkFrontmatter from "remark-frontmatter"; import remarkMdxFrontmatter from "remark-mdx-frontmatter"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [ - unstable_remixVitePlugin(), + remix(), mdx({ remarkPlugins: [ remarkFrontmatter, @@ -301,14 +298,14 @@ In the Remix compiler, the frontmatter export was named `attributes`. This diffe ```js filename=vite.config.mjs import mdx from "@mdx-js/rollup"; -import { unstable_remixVitePlugin } from "@remix-run/dev"; +import { unstable_vitePlugin as remix } from "@remix-run/dev"; import remarkFrontmatter from "remark-frontmatter"; import remarkMdxFrontmatter from "remark-mdx-frontmatter"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [ - unstable_remixVitePlugin(), + remix(), mdx({ remarkPlugins: [ remarkFrontmatter, diff --git a/integration/vite-build-test.ts b/integration/vite-build-test.ts index bdef82a0612..798d436456a 100644 --- a/integration/vite-build-test.ts +++ b/integration/vite-build-test.ts @@ -24,10 +24,10 @@ test.describe("Vite build", () => { `, "vite.config.mjs": js` import { defineConfig } from "vite"; - import { unstable_remixVitePlugin } from "@remix-run/dev"; + import { unstable_vitePlugin } from "@remix-run/dev"; export default defineConfig({ - plugins: [unstable_remixVitePlugin()], + plugins: [unstable_vitePlugin()], }); `, "app/root.tsx": js` diff --git a/integration/vite-dev-test.ts b/integration/vite-dev-test.ts index 8441dba3b77..ffeab516c47 100644 --- a/integration/vite-dev-test.ts +++ b/integration/vite-dev-test.ts @@ -28,7 +28,7 @@ test.describe("Vite dev", () => { `, "vite.config.mjs": js` import { defineConfig } from "vite"; - import { unstable_remixVitePlugin } from "@remix-run/dev"; + import { unstable_vitePlugin } from "@remix-run/dev"; export default defineConfig({ optimizeDeps: { @@ -38,7 +38,7 @@ test.describe("Vite dev", () => { port: ${devPort}, strictPort: true, }, - plugins: [unstable_remixVitePlugin()], + plugins: [unstable_vitePlugin()], }); `, "app/root.tsx": js` diff --git a/packages/remix-dev/index.ts b/packages/remix-dev/index.ts index acc5229f671..4dbdb3faf21 100644 --- a/packages/remix-dev/index.ts +++ b/packages/remix-dev/index.ts @@ -6,4 +6,4 @@ export * as cli from "./cli/index"; export type { Manifest as AssetsManifest } from "./manifest"; export { getDependenciesToBundle } from "./dependencies"; -export { unstable_remixVitePlugin } from "./vite"; +export { unstable_vitePlugin } from "./vite"; diff --git a/packages/remix-dev/vite/index.ts b/packages/remix-dev/vite/index.ts index 111949cedd8..1bcb08bb692 100644 --- a/packages/remix-dev/vite/index.ts +++ b/packages/remix-dev/vite/index.ts @@ -3,7 +3,7 @@ // be imported at the top level. import type { RemixVitePlugin } from "./plugin"; -export const unstable_remixVitePlugin: RemixVitePlugin = (...args) => { +export const unstable_vitePlugin: RemixVitePlugin = (...args) => { // eslint-disable-next-line @typescript-eslint/consistent-type-imports let { remixVitePlugin } = require("./plugin") as typeof import("./plugin"); return remixVitePlugin(...args); From 38674f20a78c0294c5b22680dd959201c256de17 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 13 Oct 2023 10:54:26 +1100 Subject: [PATCH 28/38] stop wrapping Vite CLI, update docs and tests --- docs/future/vite.md | 14 ++--- integration/helpers/create-fixture.ts | 75 ++++++++++++++++----------- integration/vite-build-test.ts | 8 ++- integration/vite-dev-test.ts | 26 ++++------ package.json | 2 + packages/remix-dev/cli/commands.ts | 18 ------- packages/remix-dev/cli/run.ts | 22 ++------ packages/remix-dev/package.json | 2 - packages/remix-dev/vite/build.ts | 19 ------- packages/remix-dev/vite/dev.ts | 42 --------------- packages/remix-dev/vite/plugin.ts | 21 +++++--- yarn.lock | 30 ----------- 12 files changed, 84 insertions(+), 195 deletions(-) delete mode 100644 packages/remix-dev/vite/build.ts delete mode 100644 packages/remix-dev/vite/dev.ts diff --git a/docs/future/vite.md b/docs/future/vite.md index 037b3299fac..73626663dac 100644 --- a/docs/future/vite.md +++ b/docs/future/vite.md @@ -67,16 +67,16 @@ export default defineConfig({ All other bundling-related options are now [configured with Vite][vite-config]. This means you have much greater control over the bundling process. -To start a development server or run a production build using Vite, set the `REMIX_UNSTABLE_VITE` environment variable when running Remix's `dev` and `build` commands: - -You can use [cross-env](https://www.npmjs.com/package/cross-env) to set this environment variable in a cross-platform manner. +To start a development server, just run Vite's `dev` command directly. ```shellscript nonumber -# Start a development server: -cross-env REMIX_UNSTABLE_VITE=1 remix dev +vite dev +``` -# Run a production build: -cross-env REMIX_UNSTABLE_VITE=1 remix build +To run a production build, first run Vite's `build` command for the client, then for the server using the `--ssr` flag. + +```shellscript nonumber +vite build && vite build --ssr ``` ## Differences When Using Vite diff --git a/integration/helpers/create-fixture.ts b/integration/helpers/create-fixture.ts index 95963f5d269..3ba594cf237 100644 --- a/integration/helpers/create-fixture.ts +++ b/integration/helpers/create-fixture.ts @@ -5,6 +5,7 @@ import fse from "fs-extra"; import express from "express"; import getPort from "get-port"; import dedent from "dedent"; +import resolveBin from "resolve-bin"; import stripIndent from "strip-indent"; import serializeJavaScript from "serialize-javascript"; import { sync as spawnSync, spawn } from "cross-spawn"; @@ -21,6 +22,8 @@ import { installGlobals } from "../../build/node_modules/@remix-run/node/dist/in const TMP_DIR = path.join(process.cwd(), ".tmp", "integration"); const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); +const viteBin = resolveBin.sync("vite"); + export interface FixtureInit { buildStdio?: Writable; sourcemap?: boolean; @@ -28,7 +31,7 @@ export interface FixtureInit { template?: "cf-template" | "deno-template" | "node-template"; config?: Partial; useRemixServe?: boolean; - env?: Record; + compiler?: "remix" | "vite"; } export type Fixture = Awaited>; @@ -216,6 +219,7 @@ export async function createFixtureProject( let integrationTemplateDir = path.resolve(__dirname, template); let projectName = `remix-${template}-${Math.random().toString(32).slice(2)}`; let projectDir = path.join(TMP_DIR, projectName); + let compiler = init.compiler ?? "remix"; await fse.ensureDir(projectDir); await fse.copy(integrationTemplateDir, projectDir); @@ -273,7 +277,7 @@ export async function createFixtureProject( ); fse.writeFileSync(path.join(projectDir, "remix.config.js"), contents); - build(projectDir, init.buildStdio, init.sourcemap, mode, init.env); + build(projectDir, init.buildStdio, init.sourcemap, mode, compiler); return projectDir; } @@ -283,7 +287,7 @@ function build( buildStdio?: Writable, sourcemap?: boolean, mode?: ServerMode, - env?: Record + compiler?: "remix" | "vite" ) { // We have a "require" instead of a dynamic import in readConfig gated // behind mode === ServerMode.Test to make jest happy, but that doesn't @@ -291,37 +295,46 @@ function build( // force the mode to be production for ESM configs when runtime mode is // tested. mode = mode === ServerMode.Test ? ServerMode.Production : mode; - let buildArgs = ["node_modules/@remix-run/dev/dist/cli.js", "build"]; - if (sourcemap) { - buildArgs.push("--sourcemap"); - } - let buildSpawn = spawnSync("node", buildArgs, { - cwd: projectDir, - env: { - ...process.env, - ...env, - NODE_ENV: mode || ServerMode.Production, - }, - }); + let remixBin = "node_modules/@remix-run/dev/dist/cli.js"; + + let commands: string[][] = + compiler === "vite" + ? [ + [viteBin, "build"], + [viteBin, "build", "--ssr"], + ] + : [[remixBin, "build", ...(sourcemap ? ["--sourcemap"] : [])]]; + + commands.forEach((buildArgs) => { + let buildSpawn = spawnSync("node", buildArgs, { + cwd: projectDir, + env: { + ...process.env, + NODE_ENV: mode || ServerMode.Production, + }, + }); - // These logs are helpful for debugging. Remove comments if needed. - // console.log("spawning @remix-run/dev/cli.js `build`:\n"); - // console.log(" STDOUT:"); - // console.log(" " + buildSpawn.stdout.toString("utf-8")); - // console.log(" STDERR:"); - // console.log(" " + buildSpawn.stderr.toString("utf-8")); - - if (buildStdio) { - buildStdio.write(buildSpawn.stdout.toString("utf-8")); - buildStdio.write(buildSpawn.stderr.toString("utf-8")); - buildStdio.end(); - } + // These logs are helpful for debugging. Remove comments if needed. + // console.log("spawning node " + buildArgs.join(" ") + ":\n"); + // console.log(" STDOUT:"); + // console.log(" " + buildSpawn.stdout.toString("utf-8")); + // console.log(" STDERR:"); + // console.log(" " + buildSpawn.stderr.toString("utf-8")); + + if (buildStdio) { + buildStdio.write(buildSpawn.stdout.toString("utf-8")); + buildStdio.write(buildSpawn.stderr.toString("utf-8")); + buildStdio.end(); + } - if (buildSpawn.error || buildSpawn.status) { - console.error(buildSpawn.stderr.toString("utf-8")); - throw buildSpawn.error || new Error(`Build failed, check the output above`); - } + if (buildSpawn.error || buildSpawn.status) { + console.error(buildSpawn.stderr.toString("utf-8")); + throw ( + buildSpawn.error || new Error(`Build failed, check the output above`) + ); + } + }); } async function writeTestFiles(init: FixtureInit, dir: string) { diff --git a/integration/vite-build-test.ts b/integration/vite-build-test.ts index 798d436456a..387c37c022f 100644 --- a/integration/vite-build-test.ts +++ b/integration/vite-build-test.ts @@ -14,9 +14,7 @@ test.describe("Vite build", () => { test.beforeAll(async () => { fixture = await createFixture({ - env: { - REMIX_UNSTABLE_VITE: "1", - }, + compiler: "vite", files: { "remix.config.js": js` throw new Error("Remix should not access remix.config.js when using Vite"); @@ -24,10 +22,10 @@ test.describe("Vite build", () => { `, "vite.config.mjs": js` import { defineConfig } from "vite"; - import { unstable_vitePlugin } from "@remix-run/dev"; + import { unstable_vitePlugin as remix } from "@remix-run/dev"; export default defineConfig({ - plugins: [unstable_vitePlugin()], + plugins: [remix()], }); `, "app/root.tsx": js` diff --git a/integration/vite-dev-test.ts b/integration/vite-dev-test.ts index ffeab516c47..1f0eb4a9c8b 100644 --- a/integration/vite-dev-test.ts +++ b/integration/vite-dev-test.ts @@ -3,6 +3,7 @@ import type { Readable } from "node:stream"; import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; +import resolveBin from "resolve-bin"; import execa from "execa"; import pidtree from "pidtree"; import getPort from "get-port"; @@ -18,9 +19,7 @@ test.describe("Vite dev", () => { test.beforeAll(async () => { devPort = await getPort(); projectDir = await createFixtureProject({ - env: { - REMIX_UNSTABLE_VITE: "1", - }, + compiler: "vite", files: { "remix.config.js": js` throw new Error("Remix should not access remix.config.js when using Vite"); @@ -28,7 +27,7 @@ test.describe("Vite dev", () => { `, "vite.config.mjs": js` import { defineConfig } from "vite"; - import { unstable_vitePlugin } from "@remix-run/dev"; + import { unstable_vitePlugin as remix } from "@remix-run/dev"; export default defineConfig({ optimizeDeps: { @@ -38,7 +37,7 @@ test.describe("Vite dev", () => { port: ${devPort}, strictPort: true, }, - plugins: [unstable_vitePlugin()], + plugins: [remix()], }); `, "app/root.tsx": js` @@ -85,16 +84,13 @@ test.describe("Vite dev", () => { }, }); - let nodebin = process.argv[0]; - devProc = spawn( - nodebin, - ["./node_modules/@remix-run/dev/dist/cli.js", "dev"], - { - cwd: projectDir, - env: { ...process.env, REMIX_UNSTABLE_VITE: "1" }, - stdio: "pipe", - } - ); + let nodeBin = process.argv[0]; + let viteBin = resolveBin.sync("vite"); + devProc = spawn(nodeBin, [viteBin, "dev"], { + cwd: projectDir, + env: process.env, + stdio: "pipe", + }); let devStdout = bufferize(devProc.stdout); let devStderr = bufferize(devProc.stderr); diff --git a/package.json b/package.json index 61c44294391..e619a5de393 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "@types/react": "^18.2.20", "@types/react-dom": "^18.2.7", "@types/react-test-renderer": "^18.0.0", + "@types/resolve-bin": "^0.4.1", "@types/retry": "^0.12.0", "@types/semver": "^7.3.4", "@types/serialize-javascript": "^5.0.2", @@ -122,6 +123,7 @@ "simple-git": "^3.16.0", "to-vfile": "7.2.3", "typescript": "^5.1.0", + "resolve-bin": "^1.0.1", "unified": "^10.1.2", "unist-util-remove": "^3.1.0", "unist-util-visit": "^4.1.1", diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index cbbeef75ca1..18b7c19b603 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -11,8 +11,6 @@ import * as compiler from "../compiler"; import * as devServer from "../devServer"; import * as devServer_unstable from "../devServer_unstable"; import type { RemixConfig } from "../config"; -import { type ViteDevOptions } from "../vite/dev"; -import { type ViteBuildOptions } from "../vite/build"; import { readConfig } from "../config"; import { formatRoutes, RoutesFormat, isRoutesFormat } from "../config/format"; import { detectPackageManager } from "./detectPackageManager"; @@ -134,17 +132,6 @@ export async function build( logger.info("built" + pc.gray(` (${prettyMs(Date.now() - start)})`)); } -export async function viteBuild(options: ViteBuildOptions = {}) { - console.warn( - pc.yellow( - " ⚠️ Remix support for Vite is unstable\n and not recommended for production\n" - ) - ); - - let { build } = await import("../vite/build"); - await build(options); -} - export async function watch( remixRootOrConfig: string | RemixConfig, mode?: string @@ -188,11 +175,6 @@ export async function dev( await new Promise(() => {}); } -export async function viteDev(options: ViteDevOptions = {}) { - let { dev } = await import("../vite/dev"); - await dev(options); -} - let clientEntries = ["entry.client.tsx", "entry.client.js", "entry.client.jsx"]; let serverEntries = ["entry.server.tsx", "entry.server.js", "entry.server.jsx"]; let entries = ["entry.client", "entry.server"]; diff --git a/packages/remix-dev/cli/run.ts b/packages/remix-dev/cli/run.ts index 6951e3f7982..0a026909194 100644 --- a/packages/remix-dev/cli/run.ts +++ b/packages/remix-dev/cli/run.ts @@ -115,16 +115,6 @@ export async function run(argv: string[] = process.argv.slice(2)) { "-p": "--port", "--tls-key": String, "--tls-cert": String, - - ...(process.env.REMIX_UNSTABLE_VITE - ? { - "--strictPort": Boolean, - "--config": String, - // TODO: Also support boolean usage? e.g. remix dev --host - // Note that arg doesn't support this: /~https://github.com/vercel/arg/issues/61 - "--host": String, - } - : {}), }, { argv, @@ -179,12 +169,8 @@ export async function run(argv: string[] = process.argv.slice(2)) { await commands.routes(input[1], flags.json ? "json" : "jsx"); break; case "build": - if (process.env.REMIX_UNSTABLE_VITE) { - await commands.viteBuild(flags); - } else { - if (!process.env.NODE_ENV) process.env.NODE_ENV = "production"; - await commands.build(input[1], process.env.NODE_ENV, flags.sourcemap); - } + if (!process.env.NODE_ENV) process.env.NODE_ENV = "production"; + await commands.build(input[1], process.env.NODE_ENV, flags.sourcemap); break; case "watch": if (!process.env.NODE_ENV) process.env.NODE_ENV = "development"; @@ -199,9 +185,7 @@ export async function run(argv: string[] = process.argv.slice(2)) { break; } case "dev": - await (process.env.REMIX_UNSTABLE_VITE - ? commands.viteDev(flags) - : commands.dev(input[1], flags)); + await commands.dev(input[1], flags); break; default: // `remix ./my-project` is shorthand for `remix dev ./my-project` diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 4ffd8be0f38..bbe83068269 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -68,7 +68,6 @@ "react-refresh": "^0.14.0", "remark-frontmatter": "4.0.1", "remark-mdx-frontmatter": "^1.0.1", - "resolve-bin": "^1.0.1", "semver": "^7.3.7", "set-cookie-parser": "^2.6.0", "tar-fs": "^2.1.1", @@ -88,7 +87,6 @@ "@types/npmcli__package-json": "^4.0.0", "@types/picomatch": "^2.3.0", "@types/prettier": "^2.7.3", - "@types/resolve-bin": "^0.4.1", "@types/shelljs": "^0.8.11", "@types/tar-fs": "^2.0.1", "@types/ws": "^7.4.1", diff --git a/packages/remix-dev/vite/build.ts b/packages/remix-dev/vite/build.ts deleted file mode 100644 index c863db92ac1..00000000000 --- a/packages/remix-dev/vite/build.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as vite from "vite"; - -export interface ViteBuildOptions { - config?: string; - force?: boolean; -} - -export async function build({ config: configFile, force }: ViteBuildOptions) { - async function viteBuild({ ssr }: { ssr: boolean }) { - await vite.build({ - configFile, - build: { ssr }, - optimizeDeps: { force }, - }); - } - - await viteBuild({ ssr: false }); - await viteBuild({ ssr: true }); -} diff --git a/packages/remix-dev/vite/dev.ts b/packages/remix-dev/vite/dev.ts deleted file mode 100644 index bec43ce9572..00000000000 --- a/packages/remix-dev/vite/dev.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { spawn } from "cross-spawn"; -import resolveBin from "resolve-bin"; - -export interface ViteDevOptions { - config?: string; - port?: string; - strictPort?: boolean; - host?: true | string; - force?: boolean; -} - -export async function dev({ - config, - port, - strictPort, - host, - force, -}: ViteDevOptions) { - let viteBin = resolveBin.sync("vite"); - - spawn( - "node", - [ - JSON.stringify(viteBin), - "dev", - ...(config ? ["--config", config] : []), - ...(port ? ["--port", port] : []), - ...(strictPort ? ["--strictPort"] : []), - ...(force ? ["--force"] : []), - ...(host ? ["--host", ...(typeof host === "string" ? [host] : [])] : []), - ], - { - shell: true, - stdio: "inherit", - env: { - ...process.env, - }, - } - ); - - await new Promise(() => {}); -} diff --git a/packages/remix-dev/vite/plugin.ts b/packages/remix-dev/vite/plugin.ts index 8d244c37ac6..51b3f2c1d12 100644 --- a/packages/remix-dev/vite/plugin.ts +++ b/packages/remix-dev/vite/plugin.ts @@ -202,6 +202,14 @@ const findEntry = (dir: string, basename: string): string | undefined => { const addTrailingSlash = (path: string): string => path.endsWith("/") ? path : path + "/"; +const showUnstableWarning = () => { + console.warn( + colors.yellow( + "\n ⚠️ Remix support for Vite is unstable\n and not recommended for production\n" + ) + ); +}; + export type RemixVitePlugin = ( options?: RemixVitePluginOptions ) => VitePlugin[]; @@ -591,15 +599,14 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => { cssModulesManifest[id] = code; } }, + buildStart() { + if (viteCommand === "build") { + showUnstableWarning(); + } + }, configureServer(vite) { vite.httpServer?.on("listening", () => { - setTimeout(() => { - vite.config.logger.warn( - colors.yellow( - "\n ⚠️ Remix support for Vite is unstable\n and not recommended for production\n" - ) - ); - }, 50); + setTimeout(showUnstableWarning, 50); }); return () => { vite.middlewares.use(async (req, res, next) => { diff --git a/yarn.lock b/yarn.lock index 448e77ab37a..eca542c858c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2651,11 +2651,6 @@ resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== -"@types/estree@0.0.42": - version "0.0.42" - resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.42.tgz#8d0c1f480339efedb3e46070e22dd63e0430dd11" - integrity sha512-K1DPVvnBCPxzD+G51/cxVIoc2X8uUVl1zpJeE6iKcgHMj4+tbat5Xu4TjV7v2QSDbIeAfLi2hIk+u2+s0MlpUQ== - "@types/estree@^0.0.46": version "0.0.46" resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.46.tgz" @@ -2666,11 +2661,6 @@ resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz" integrity sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew== -"@types/estree@^1.0.2": - version "1.0.2" - resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.2.tgz#ff02bc3dc8317cd668dfec247b750ba1f1d62453" - integrity sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA== - "@types/express-serve-static-core@^4.17.18": version "4.17.24" resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz" @@ -5736,13 +5726,6 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== -estree-jsx@^0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/estree-jsx/-/estree-jsx-0.0.1.tgz#14ac8a16de858a15677764428ab7929c08381988" - integrity sha512-M/bgmzaDP/aPZJoE5znuCsa8gleRMj33eeioZHpnc1SM5lJk7V1ZH7Tp+y4BeBYuWi73XiGPwPmMmzpz5HCS7w== - dependencies: - "@types/estree" "0.0.42" - estree-util-attach-comments@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-2.0.0.tgz" @@ -5785,14 +5768,6 @@ estree-util-value-to-estree@^1.0.0: dependencies: is-plain-obj "^3.0.0" -estree-util-value-to-estree@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.0.1.tgz#0b7b5d6b6a4aaad5c60999ffbc265a985df98ac5" - integrity sha512-b2tdzTurEIbwRh+mKrEcaWfu1wgb8J1hVsgREg7FFiecWwK/PhO8X0kyc+0bIcKNtD4sqxIdNoRy6/p/TvECEA== - dependencies: - "@types/estree" "^1.0.0" - is-plain-obj "^4.0.0" - estree-util-visit@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-1.1.0.tgz" @@ -12712,11 +12687,6 @@ yaml@^1.10.2: resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yaml@^2.0.0: - version "2.3.2" - resolved "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz#f522db4313c671a0ca963a75670f1c12ea909144" - integrity sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg== - yaml@^2.1.1: version "2.2.1" resolved "https://registry.npmjs.org/yaml/-/yaml-2.2.1.tgz" From 79deb5bfb2a3a893f21586cf03deb58402bf7266 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 13 Oct 2023 12:28:27 +1100 Subject: [PATCH 29/38] dedupe config resolution logic --- packages/remix-dev/config.ts | 30 ++++-- packages/remix-dev/vite/plugin.ts | 168 ++++++------------------------ 2 files changed, 52 insertions(+), 146 deletions(-) diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 79b05e36efa..dd328b68942 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -355,12 +355,8 @@ export interface RemixConfig { */ export async function readConfig( remixRoot?: string, - serverMode = ServerMode.Production + serverMode?: ServerMode ): Promise { - if (!isValidServerMode(serverMode)) { - throw new Error(`Invalid server mode "${serverMode}"`); - } - if (!remixRoot) { remixRoot = process.env.REMIX_ROOT || process.cwd(); } @@ -393,6 +389,26 @@ export async function readConfig( } } + return await resolveConfig(appConfig, { + rootDirectory, + serverMode, + }); +} + +export async function resolveConfig( + appConfig: AppConfig, + { + rootDirectory, + serverMode = ServerMode.Production, + }: { + rootDirectory: string; + serverMode?: ServerMode; + } +): Promise { + if (!isValidServerMode(serverMode)) { + throw new Error(`Invalid server mode "${serverMode}"`); + } + let serverBuildPath = path.resolve( rootDirectory, appConfig.serverBuildPath ?? "build/index.js" @@ -436,7 +452,7 @@ export async function readConfig( let entryServerFile: string; let entryClientFile = userEntryClientFile || "entry.client.tsx"; - let pkgJson = await PackageJson.load(remixRoot); + let pkgJson = await PackageJson.load(rootDirectory); let deps = pkgJson.content.dependencies ?? {}; if (userEntryServerFile) { @@ -479,7 +495,7 @@ export async function readConfig( let packageManager = detectPackageManager() ?? "npm"; execSync(`${packageManager} install`, { - cwd: remixRoot, + cwd: rootDirectory, stdio: "inherit", }); } diff --git a/packages/remix-dev/vite/plugin.ts b/packages/remix-dev/vite/plugin.ts index 51b3f2c1d12..8b6d89fe6ce 100644 --- a/packages/remix-dev/vite/plugin.ts +++ b/packages/remix-dev/vite/plugin.ts @@ -1,10 +1,7 @@ import { type BinaryLike, createHash } from "node:crypto"; import * as path from "node:path"; import * as fs from "node:fs/promises"; -import { existsSync as fsExistsSync } from "node:fs"; -import { execSync } from "node:child_process"; import babel from "@babel/core"; -import PackageJson from "@npmcli/package-json"; import { type ServerBuild } from "@remix-run/server-runtime"; import { type Plugin as VitePlugin, @@ -20,15 +17,15 @@ import { parse as esModuleLexer, } from "es-module-lexer"; import jsesc from "jsesc"; +import pick from "lodash/pick"; import colors from "picocolors"; -import { defineRoutes, type RouteManifest } from "../config/routes"; +import { type RouteManifest } from "../config/routes"; import { type AppConfig as RemixUserConfig, type RemixConfig as ResolvedRemixConfig, + resolveConfig, } from "../config"; -import { flatRoutes } from "../config/flat-routes"; -import { detectPackageManager } from "../cli/detectPackageManager"; import { type Manifest } from "../manifest"; import { createRequestHandler } from "./node/adapter"; import { getStylesForUrl, isCssModulesFile } from "./styles"; @@ -37,15 +34,20 @@ import { removeExports } from "./remove-exports"; import { transformLegacyCssImports } from "./legacy-css-imports"; import { replaceImportSpecifier } from "./replace-import-specifier"; +const supportedRemixConfigKeys = [ + "appDirectory", + "assetsBuildDirectory", + "ignoredRouteFiles", + "publicPath", + "routes", + "serverBuildPath", + "serverModuleFormat", +] as const satisfies ReadonlyArray; +type SupportedRemixConfigKey = typeof supportedRemixConfigKeys[number]; + export type RemixVitePluginOptions = Pick< RemixUserConfig, - | "appDirectory" - | "assetsBuildDirectory" - | "ignoredRouteFiles" - | "publicPath" - | "routes" - | "serverBuildPath" - | "serverModuleFormat" + SupportedRemixConfigKey > & { legacyCssImports?: boolean; }; @@ -189,19 +191,6 @@ const getRouteModuleExports = async ( return exportNames; }; -const entryExts = [".js", ".jsx", ".ts", ".tsx"]; -const findEntry = (dir: string, basename: string): string | undefined => { - for (let ext of entryExts) { - let file = path.resolve(dir, basename + ext); - if (fsExistsSync(file)) return path.relative(dir, file); - } - - return undefined; -}; - -const addTrailingSlash = (path: string): string => - path.endsWith("/") ? path : path + "/"; - const showUnstableWarning = () => { console.warn( colors.yellow( @@ -228,121 +217,22 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => { async (): Promise => { let rootDirectory = viteUserConfig.root ?? process.env.REMIX_ROOT ?? process.cwd(); - let appDirectory = path.resolve( - rootDirectory, - options.appDirectory ?? "app" - ); - let serverBuildPath = path.resolve( - rootDirectory, - options.serverBuildPath ?? "build/index.js" - ); - let serverModuleFormat = options.serverModuleFormat ?? "esm"; - let relativeAssetsBuildDirectory = - options.assetsBuildDirectory ?? path.join("public", "build"); - let assetsBuildDirectory = path.resolve( - rootDirectory, - relativeAssetsBuildDirectory - ); - - let defaultsDirectory = path.resolve( - __dirname, - "..", - "config", - "defaults" - ); - - let userEntryClientFile = findEntry(appDirectory, "entry.client"); - let entryClientFile = userEntryClientFile ?? "entry.client.tsx"; - - let userEntryServerFile = findEntry(appDirectory, "entry.server"); - let entryServerFile: string; - - let pkgJson = await PackageJson.load(rootDirectory); - let deps = pkgJson.content.dependencies ?? {}; - - if (userEntryServerFile) { - entryServerFile = userEntryServerFile; - } else { - let serverRuntime = deps["@remix-run/deno"] - ? "deno" - : deps["@remix-run/cloudflare"] - ? "cloudflare" - : deps["@remix-run/node"] - ? "node" - : undefined; - - if (!serverRuntime) { - let serverRuntimes = [ - "@remix-run/deno", - "@remix-run/cloudflare", - "@remix-run/node", - ]; - let disjunctionListFormat = new Intl.ListFormat("en", { - style: "long", - type: "disjunction", - }); - let formattedList = disjunctionListFormat.format(serverRuntimes); - throw new Error( - `Could not determine server runtime. Please install one of the following: ${formattedList}` - ); - } - if (!deps["isbot"]) { - console.log( - "adding `isbot` to your package.json, you should commit this change" - ); + // Avoid leaking any config options that the Vite plugin doesn't support + let config = pick(options, supportedRemixConfigKeys); - pkgJson.update({ - dependencies: { - ...pkgJson.content.dependencies, - isbot: "latest", - }, - }); - - await pkgJson.save(); - - let packageManager = detectPackageManager() ?? "npm"; - - execSync(`${packageManager} install`, { - cwd: rootDirectory, - stdio: "inherit", - }); - } - - entryServerFile = `entry.server.${serverRuntime}.tsx`; - } - - let entryClientFilePath = userEntryClientFile - ? path.resolve(appDirectory, userEntryClientFile) - : path.resolve(defaultsDirectory, entryClientFile); - - let entryServerFilePath = userEntryServerFile - ? path.resolve(appDirectory, userEntryServerFile) - : path.resolve(defaultsDirectory, entryServerFile); - - let publicPath = addTrailingSlash(options.publicPath ?? "/build/"); - - let rootRouteFile = findEntry(appDirectory, "root"); - if (!rootRouteFile) { - throw new Error(`Missing "root" route file in ${appDirectory}`); - } - - let routes: RouteManifest = { - root: { path: "", id: "root", file: rootRouteFile }, - }; - - if (fsExistsSync(path.resolve(appDirectory, "routes"))) { - let fileRoutes = flatRoutes(appDirectory, options.ignoredRouteFiles); - for (let route of Object.values(fileRoutes)) { - routes[route.id] = { ...route, parentId: route.parentId || "root" }; - } - } - if (options.routes) { - let manualRoutes = await options.routes(defineRoutes); - for (let route of Object.values(manualRoutes)) { - routes[route.id] = { ...route, parentId: route.parentId || "root" }; - } - } + // Only select the Remix config options that the Vite plugin uses + let { + appDirectory, + assetsBuildDirectory, + entryClientFilePath, + publicPath, + routes, + entryServerFilePath, + serverBuildPath, + serverModuleFormat, + relativeAssetsBuildDirectory, + } = await resolveConfig(config, { rootDirectory }); return { appDirectory, From c555072cefa77ec6a9fcc7dee441ed9ca9eeea8b Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 13 Oct 2023 13:32:10 +1100 Subject: [PATCH 30/38] remove redundant comment --- packages/remix-dev/cli/run.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/remix-dev/cli/run.ts b/packages/remix-dev/cli/run.ts index 0a026909194..05796f3a0a2 100644 --- a/packages/remix-dev/cli/run.ts +++ b/packages/remix-dev/cli/run.ts @@ -189,7 +189,6 @@ export async function run(argv: string[] = process.argv.slice(2)) { break; default: // `remix ./my-project` is shorthand for `remix dev ./my-project` - // TODO: Support this in Vite mode await commands.dev(input[0], flags); } } From 4571afca8ce6544be8b342e221b21b6434c8cfc5 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 13 Oct 2023 13:33:42 +1100 Subject: [PATCH 31/38] update remix router version --- packages/remix-dev/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index bbe83068269..e5a3f5de03d 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -29,7 +29,7 @@ "@mdx-js/mdx": "^2.3.0", "@npmcli/package-json": "^4.0.1", "@remix-run/node": "2.0.1", - "@remix-run/router": "1.9.0", + "@remix-run/router": "1.10.0-pre.0", "@remix-run/server-runtime": "2.0.1", "@types/mdx": "^2.0.5", "@vanilla-extract/integration": "^6.2.0", From 7823547f2e508a2da9d1c73dd7e3f3f64d0140e8 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 13 Oct 2023 14:30:57 +1100 Subject: [PATCH 32/38] Update docs/future/vite.md Co-authored-by: Pedro Cattori --- docs/future/vite.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/future/vite.md b/docs/future/vite.md index 73626663dac..b0de4525c63 100644 --- a/docs/future/vite.md +++ b/docs/future/vite.md @@ -7,7 +7,7 @@ toc: false Vite support is currently unstable and only intended to gather early feedback. We don't yet recommend using this in production. -[Vite] is a powerful, performant and extensible development environment for JavaScript projects. In order to improve and extend Remix's bundling capabilities, we're currently exploring the use of Vite as an alternative compiler to esbuild. +[Vite][vite] is a powerful, performant and extensible development environment for JavaScript projects. In order to improve and extend Remix's bundling capabilities, we're currently exploring the use of Vite as an alternative compiler to esbuild. **Legend**: ✅ (Tested),❓ (Untested), ⏳ (Not Yet Supported) From 2b0fa95ef6eb7147185a9245b5f5cedddad92d93 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 13 Oct 2023 14:34:26 +1100 Subject: [PATCH 33/38] Update docs/future/vite.md Co-authored-by: Pedro Cattori --- docs/future/vite.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/future/vite.md b/docs/future/vite.md index b0de4525c63..9448f41da7a 100644 --- a/docs/future/vite.md +++ b/docs/future/vite.md @@ -186,7 +186,7 @@ export const links: LinksFunction = () => [ ### Tailwind -To use [Tailwind] in Vite, first install the required dependencies: +To use [Tailwind][tailwind] in Vite, first install the required dependencies: ```shellscript nonumber npm install -D tailwindcss postcss autoprefixer From 9d79355eda3a351c0bcc8c73048ce758045d92c8 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 13 Oct 2023 14:42:11 +1100 Subject: [PATCH 34/38] remove remark plugin note --- docs/future/vite.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/future/vite.md b/docs/future/vite.md index 9448f41da7a..4c673b5c9e7 100644 --- a/docs/future/vite.md +++ b/docs/future/vite.md @@ -357,8 +357,6 @@ export const meta = () => { # Hello World ``` -You may even want to take this a step further and create a [remark plugin][remark-plugin] that automatically maps frontmatter to route exports for you. - ##### MDX Filename Export The Remix compiler also provided a `filename` export from all MDX files. This was primarily designed to enable linking to collections of MDX routes. In Vite, you should achieve this via [glob imports][glob-imports] which give you a handy data structure that maps file names to modules. This makes it much easier to maintain a list of MDX files since you no longer need to import each one manually. @@ -508,7 +506,6 @@ We're definitely late to the Vite party, but we're excited to be here now! [mdx-rollup-plugin]: https://mdxjs.com/packages/rollup [mdx-frontmatter]: https://mdxjs.com/guides/frontmatter [remark-mdx-frontmatter]: /~https://github.com/remcohaszing/remark-mdx-frontmatter -[remark-plugin]: /~https://github.com/remarkjs/remark/blob/main/doc/plugins.md [glob-imports]: https://vitejs.dev/guide/features.html#glob-import [use_loader_data]: ../hooks/use-loader-data [react_refresh]: /~https://github.com/facebook/react/tree/main/packages/react-refresh From 42acbd21563df13a3156a7d407ac2de62c6293fd Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 13 Oct 2023 14:46:11 +1100 Subject: [PATCH 35/38] clean up server/client file docs --- docs/future/vite.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/docs/future/vite.md b/docs/future/vite.md index 4c673b5c9e7..f7adbe37f43 100644 --- a/docs/future/vite.md +++ b/docs/future/vite.md @@ -386,17 +386,6 @@ const posts = import.meta.glob("./posts/*.mdx", { }); ``` -### `*.server.ts` and `*.client.ts` Extensions - -The Remix compiler allowed you to define server/client-only files using the `*.server.ts`/`*.client.ts` extensions. The Remix Vite plugin supports this pattern with one notable difference. - -Since Vite leverages ESM at runtime to load modules, you may need to use `import *` when importing server/client-only files if the import statement hasn't been removed by dead code elimination. - -```diff --import { something } from "./file.server.ts"; -+import * as serverOnly from "./file.server.ts"; -``` - ## HMR & HDR ### React Fast Refresh limitations From 09941cd73a5ac7457e75310540e2f848475985d0 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 13 Oct 2023 14:58:40 +1100 Subject: [PATCH 36/38] tweak hmr docs --- docs/future/vite.md | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/docs/future/vite.md b/docs/future/vite.md index f7adbe37f43..a8b5c22ef00 100644 --- a/docs/future/vite.md +++ b/docs/future/vite.md @@ -388,21 +388,27 @@ const posts = import.meta.glob("./posts/*.mdx", { ## HMR & HDR -### React Fast Refresh limitations +### React Fast Refresh Limitations -[React Fast Refresh][react_refresh] does not preserve state for class components. +[React Fast Refresh][react_refresh] has some limitations that are worth being aware of. + +#### Class Component State + +React Fast Refresh does not preserve state for class components. This includes higher-order components that internally return classes: ```ts export class ComponentA extends Component {} // ❌ -export const ComponentB = HOC(ComponentC); // ❌ won't work if HOC returns a class component +export const ComponentB = HOC(ComponentC); // ❌ Won't work if HOC returns a class component export function ComponentD() {} // ✅ export const ComponentE = () => {}; // ✅ export default function ComponentF() {} // ✅ ``` +#### Named Function Components + Function components must be named, not anonymous, for React Fast Refresh to track changes: ```ts @@ -415,22 +421,26 @@ export default ComponentA; // ✅ export default function ComponentB() {} // ✅ ``` -React Fast Refresh can only handle component exports. While Remix manages special route exports like `meta`, `links`, and `header` for you, any user-defined, will cause full reloads: +#### Supported Exports + +React Fast Refresh can only handle component exports. While Remix manages special route exports like `meta`, `links`, and `header` for you, any user-defined exports will cause full reloads: ```ts -// these exports are specially handled by Remix to be HMR-compatible +// These exports are handled by the Remix Vite plugin +// to be HMR-compatible export const meta = { title: "Home" }; // ✅ export const links = [ { rel: "stylesheet", href: "style.css" }, ]; // ✅ export const headers = { "Cache-Control": "max-age=3600" }; // ✅ -// these exports are treeshaken by Remix, so they never affect HMR +// These exports are removed by the Remix Vite plugin +// so they never affect HMR export const loader = () => {}; // ✅ export const action = () => {}; // ✅ -// This is not a Remix export, nor a component export -// so it will cause a full reloads for this route +// This is not a Remix export, nor a component export, +// so it will cause a full reload for this route export const myValue = "some value"; // ❌ export default function Route() {} // ✅ @@ -443,14 +453,13 @@ If you want to reuse values across routes, stick them in their own non-route mod export const myValue = "some value"; ``` -React Fast Refresh cannot track changes for a component when hooks are being added or removed from it, -causing full reloads just for the next render. After the hooks has been added, changes should result in hot updates again. -For example, if you add [`useLoaderData`][use_loader_data] to your component, you may lose state local to that component for that render. +#### Adding and Removing Hooks + +React Fast Refresh cannot track changes for a component when hooks are being added or removed from it, causing full reloads just for the next render. After the hooks have been updated, changes should result in hot updates again. For example, if you add [`useLoaderData`][use_loader_data] to your component, you may lose state local to that component for that render. -In some cases React cannot distinguish between existing components being changed and new components being added. -[React needs `key`s][react_keys] to disambiguate these cases and track changes when sibling elements are modified. +#### Component Keys -These are all limitations of React and [React Refresh][react_refresh], not Remix. +In some cases, React cannot distinguish between existing components being changed and new components being added. [React needs `key`s][react_keys] to disambiguate these cases and track changes when sibling elements are modified. ## Acknowledgements @@ -468,7 +477,7 @@ Finally, we were inspired by how other frameworks implemented Vite support: - [Astro][astro] - [SolidStart][solidstart] -- [SvelteKit][svletekit] +- [SvelteKit][sveltekit] We're definitely late to the Vite party, but we're excited to be here now! From c5954ba872dd6ccde5733140623915d00dfc2de3 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Fri, 13 Oct 2023 12:08:44 -0400 Subject: [PATCH 37/38] Update docs/future/vite.md --- docs/future/vite.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/future/vite.md b/docs/future/vite.md index a8b5c22ef00..9690279e4b6 100644 --- a/docs/future/vite.md +++ b/docs/future/vite.md @@ -432,10 +432,10 @@ export const meta = { title: "Home" }; // ✅ export const links = [ { rel: "stylesheet", href: "style.css" }, ]; // ✅ -export const headers = { "Cache-Control": "max-age=3600" }; // ✅ // These exports are removed by the Remix Vite plugin // so they never affect HMR +export const headers = { "Cache-Control": "max-age=3600" }; // ✅ export const loader = () => {}; // ✅ export const action = () => {}; // ✅ From b605be3a6cb27302e24611c2cff314d2e4fb5919 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Fri, 13 Oct 2023 12:12:37 -0400 Subject: [PATCH 38/38] add changeset --- .changeset/perfect-rockets-approve.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .changeset/perfect-rockets-approve.md diff --git a/.changeset/perfect-rockets-approve.md b/.changeset/perfect-rockets-approve.md new file mode 100644 index 00000000000..e7d51ca2cba --- /dev/null +++ b/.changeset/perfect-rockets-approve.md @@ -0,0 +1,18 @@ +--- +"integration-tests": minor +"create-remix": minor +"@remix-run/dev": minor +"@remix-run/react": minor +"@remix-run/server-runtime": minor +"@remix-run/testing": minor +--- + +Unstable Vite support for Node-based Remix apps + +- `remix build` 👉 `vite build && vite build --ssr` +- `remix dev` 👉 `vite dev` + +Other runtimes (e.g. Deno, Cloudflare) not yet supported. +Custom server (e.g. Express) not yet supported. + +See "Future > Vite" in the Remix Docs for details.