diff --git a/docs/file-conventions/remix-config.md b/docs/file-conventions/remix-config.md index a37bc5b1bbe..ee634410583 100644 --- a/docs/file-conventions/remix-config.md +++ b/docs/file-conventions/remix-config.md @@ -19,7 +19,6 @@ module.exports = { }); }, serverBuildPath: "build/index.js", - serverBuildTarget: "node-cjs", }; ``` @@ -103,7 +102,8 @@ either a `.js` or `.ts` file extension. ## serverBuildDirectory This option is deprecated and will likely be removed in a future -stable release. Use [`serverBuildPath`][server-build-path] instead. +stable release. Use [`serverBuildPath`][server-build-path] +instead. The path to the server build, relative to `remix.config.js`. Defaults to "build". This needs to be deployed to your server. @@ -111,13 +111,19 @@ The path to the server build, relative to `remix.config.js`. Defaults to ## serverBuildPath The path to the server build file, relative to `remix.config.js`. This file -should end in a `.js` extension and should be deployed to your server. - -If omitted, the default build path will be based on your -[`serverBuildTarget`][server-build-target]. +should end in a `.js` extension and should be deployed to your server. Defaults +to `"build/index.js"`. ## serverBuildTarget +This option is deprecated and will likely be removed in a future +stable release. Use a combination of [`publicPath`][public-path], +[`serverBuildPath`][server-build-path], [`serverConditions`][server-conditions], +[`serverDependenciesToBundle`][server-dependencies-to-bundle] +[`serverMainFields`][server-main-fields], [`serverMinify`][server-minify], +[`serverModuleFormat`][server-module-format] and/or +[`serverPlatform`][server-platform] instead. + The target of the server build. Defaults to `"node-cjs"`. The `serverBuildTarget` can be one of the following: @@ -130,11 +136,22 @@ The `serverBuildTarget` can be one of the following: - [`"node-cjs"`][node-cjs] - [`"vercel"`][vercel] +## serverConditions + +The order of conditions to use when resolving server dependencies' `exports` +field in `package.json`. + ## serverDependenciesToBundle -A list of regex patterns that determines if a module is transpiled and included in the server bundle. This can be useful when consuming ESM only packages in a CJS build, or when consuming packages with [CSS side-effect imports][css-side-effect-imports]. +A list of regex patterns that determines if a module is transpiled and included +in the server bundle. This can be useful when consuming ESM only packages in a +CJS build, or when consuming packages with [CSS side effect +imports][css-side-effect-imports]. -For example, the `unified` ecosystem is all ESM-only. Let's also say we're using a `@sindresorhus/slugify` which is ESM-only as well. Here's how you would be able to consume those packages in a CJS app without having to use dynamic imports: +For example, the `unified` ecosystem is all ESM-only. Let's also say we're using +a `@sindresorhus/slugify` which is ESM-only as well. Here's how you would be +able to consume those packages in a CJS app without having to use dynamic +imports: ```ts filename=remix.config.js lines=[8-13] /** @type {import('@remix-run/dev').AppConfig} */ @@ -153,6 +170,29 @@ module.exports = { }; ``` +If you want to bundle all server dependencies, you can +`serverDependenciesToBundle` to `"all"`. + +## serverMainFields + +The order of main fields to use when resolving server dependencies. Defaults to +`["main", "module"]` when `serverModuleFormat` is set to `"cjs"`. Defaults to +`["module", "main"]` when `serverModuleFormat` is set to `"esm"`. + +## serverMinify + +Whether to minify the server build in production or not. Defaults to `false`. + +## serverModuleFormat + +The output format of the server build, which can either be `"cjs"` or `"esm"`. +Defaults to `"cjs"`. + +## serverPlatform + +The platform the server build is targeting, which can either be `"neutral"` or +`"node"`. Defaults to `"node"`. + ## watchPaths An array, string, or async function that defines custom directories, relative to the project root, to watch while running [remix dev][remix-dev]. These directories are in addition to [`appDirectory`][app-directory]. @@ -173,8 +213,14 @@ There are a few conventions that Remix uses you should be aware of. [Dilum Sanjaya][dilum-sanjaya] made [an awesome visualization][an-awesome-visualization] of how routes in the file system map to the URL in your app that might help you understand these conventions. [minimatch]: https://www.npmjs.com/package/minimatch +[public-path]: #publicpath [server-build-path]: #serverbuildpath -[server-build-target]: #serverbuildtarget +[server-conditions]: #serverconditions +[server-dependencies-to-bundle]: #serverdependenciestobundle +[server-main-fields]: #servermainfields +[server-minify]: #serverminify +[server-module-format]: #servermoduleformat +[server-platform]: #serverplatform [arc]: https://arc.codes [cloudflare-pages]: https://pages.cloudflare.com [cloudflare-workers]: https://workers.cloudflare.com diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index 98f71a24c59..eb5de05fa57 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -63,8 +63,14 @@ describe("readConfig", () => { "serverBuildPath": Any, "serverBuildTarget": undefined, "serverBuildTargetEntryModule": "export * from \\"@remix-run/dev/server-build\\";", + "serverConditions": undefined, "serverDependenciesToBundle": Array [], "serverEntryPoint": undefined, + "serverMainFields": Array [ + "main", + "module", + ], + "serverMinify": false, "serverMode": "production", "serverModuleFormat": "cjs", "serverPlatform": "node", diff --git a/packages/remix-dev/compiler/compilerServer.ts b/packages/remix-dev/compiler/compilerServer.ts index ec9d3cc5ee1..99caee12f24 100644 --- a/packages/remix-dev/compiler/compilerServer.ts +++ b/packages/remix-dev/compiler/compilerServer.ts @@ -45,11 +45,6 @@ const createEsbuildConfig = ( }; } - let isCloudflareRuntime = ["cloudflare-pages", "cloudflare-workers"].includes( - config.serverBuildTarget ?? "" - ); - let isDenoRuntime = config.serverBuildTarget === "deno"; - let { mode } = options; let { rootDirectory } = config; let outputCss = false; @@ -84,11 +79,7 @@ const createEsbuildConfig = ( stdin, entryPoints, outfile: config.serverBuildPath, - conditions: isCloudflareRuntime - ? ["worker"] - : isDenoRuntime - ? ["deno", "worker"] - : undefined, + conditions: config.serverConditions, platform: config.serverPlatform, format: config.serverModuleFormat, treeShaking: true, @@ -100,12 +91,8 @@ const createEsbuildConfig = ( // PR makes dev mode behave closer to production in terms of dead // code elimination / tree shaking is concerned. minifySyntax: true, - minify: options.mode === "production" && isCloudflareRuntime, - mainFields: isCloudflareRuntime - ? ["browser", "module", "main"] - : config.serverModuleFormat === "esm" - ? ["module", "main"] - : ["main", "module"], + minify: options.mode === "production" && config.serverMinify, + mainFields: config.serverMainFields, target: options.target, loader: loaders, bundle: true, diff --git a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts index e2c840cd817..1c15aa00e45 100644 --- a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts @@ -103,12 +103,8 @@ export function serverBareModulesPlugin( } } - switch (remixConfig.serverBuildTarget) { - // Always bundle everything for cloudflare. - case "cloudflare-pages": - case "cloudflare-workers": - case "deno": - return undefined; + if (remixConfig.serverDependenciesToBundle === "all") { + return undefined; } for (let pattern of remixConfig.serverDependenciesToBundle) { diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index c41db0e614e..913d3b264eb 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -74,23 +74,6 @@ export interface AppConfig { defineRoutes: DefineRoutesFunction ) => Promise>; - /** - * The path to the server build, relative to `remix.config.js`. Defaults to - * "build". - * - * @deprecated Use {@link ServerConfig.serverBuildPath} instead. - */ - serverBuildDirectory?: string; - - /** - * The path to the server build file, relative to `remix.config.js`. This file - * should end in a `.js` extension and should be deployed to your server. - * - * If omitted, the default build path will be based on your - * {@link ServerConfig.serverBuildTarget}. - */ - serverBuildPath?: string; - /** * The path to the browser build, relative to `remix.config.js`. Defaults to * "public/build". @@ -101,7 +84,7 @@ export interface AppConfig { * The path to the browser build, relative to remix.config.js. Defaults to * "public/build". * - * @deprecated Use `{@link ServerConfig.assetsBuildDirectory}` instead + * @deprecated Use `{@link AppConfig.assetsBuildDirectory}` instead */ browserBuildDirectory?: string; @@ -128,45 +111,75 @@ export interface AppConfig { mdx?: RemixMdxConfig | RemixMdxConfigFunction; /** - * The output format of the server build. Defaults to "cjs". - * - * @deprecated Use {@link ServerConfig.serverBuildTarget} instead. + * A server entrypoint, relative to the root directory that becomes your + * server's main module. If specified, Remix will compile this file along with + * your application into a single file to be deployed to your server. This + * file can use either a `.js` or `.ts` file extension. */ - serverModuleFormat?: ServerModuleFormat; + server?: string; /** - * The platform the server build is targeting. Defaults to "node". + * The path to the server build, relative to `remix.config.js`. Defaults to + * "build". * - * @deprecated Use {@link ServerConfig.serverBuildTarget} instead. + * @deprecated Use {@link AppConfig.serverBuildPath} instead. */ - serverPlatform?: ServerPlatform; + serverBuildDirectory?: string; /** - * The target of the server build. Defaults to "node-cjs". + * The path to the server build file, relative to `remix.config.js`. This file + * should end in a `.js` extension and should be deployed to your server. */ - serverBuildTarget?: ServerBuildTarget; + serverBuildPath?: string; /** - * A server entrypoint, relative to the root directory that becomes your - * server's main module. If specified, Remix will compile this file along with - * your application into a single file to be deployed to your server. This - * file can use either a `.js` or `.ts` file extension. + * The target of the server build. Defaults to "node-cjs". + * + * @deprecated Use a combination of `{@link AppConfig.publicPath}`, `{@link AppConfig.serverBuildPath}`, `{@link AppConfig.serverConditions}`, `{@link AppConfig.serverDependenciesToBundle}`, `{@link AppConfig.serverMainFields}`, `{@link AppConfig.serverMinify}`, `{@link AppConfig.serverModuleFormat}` and/or `{@link AppConfig.serverPlatform}` instead. */ - server?: string; + serverBuildTarget?: ServerBuildTarget; /** - * A list of filenames or a glob patterns to match files in the `app/routes` - * directory that Remix will ignore. Matching files will not be recognized as - * routes. + * The order of conditions to use when resolving server dependencies' + * `exports` field in `package.json`. */ - ignoredRouteFiles?: string[]; + serverConditions?: string[]; /** * A list of patterns that determined if a module is transpiled and included * in the server bundle. This can be useful when consuming ESM only packages * in a CJS build. */ - serverDependenciesToBundle?: Array; + serverDependenciesToBundle?: "all" | Array; + + /** + * The order of main fields to use when resolving server dependencies. + * Defaults to `["main", "module"]`. + */ + serverMainFields?: string[]; + + /** + * Whether to minify the server build in production or not. + * Defaults to `false`. + */ + serverMinify?: boolean; + + /** + * The output format of the server build. Defaults to "cjs". + */ + serverModuleFormat?: ServerModuleFormat; + + /** + * The platform the server build is targeting. Defaults to "node". + */ + serverPlatform?: ServerPlatform; + + /** + * A list of filenames or a glob patterns to match files in the `app/routes` + * directory that Remix will ignore. Matching files will not be recognized as + * routes. + */ + ignoredRouteFiles?: string[]; /** * A function for defining custom directories to watch while running `remix dev`, in addition to `appDirectory`. @@ -213,12 +226,6 @@ export interface RemixConfig { */ routes: RouteManifest; - /** - * The path to the server build file. This file should end in a `.js`. Defaults - * are based on {@link ServerConfig.serverBuildTarget}. - */ - serverBuildPath: string; - /** * The absolute path to the assets build directory. */ @@ -234,11 +241,6 @@ export interface RemixConfig { */ publicPath: string; - /** - * The mode to use to run the server. - */ - serverMode: ServerMode; - /** * The port number to use for the dev (asset) server. */ @@ -255,36 +257,67 @@ export interface RemixConfig { mdx?: RemixMdxConfig | RemixMdxConfigFunction; /** - * The output format of the server build. Defaults to "cjs". - */ - serverModuleFormat: ServerModuleFormat; - - /** - * The platform the server build is targeting. Defaults to "node". + * The path to the server build file. This file should end in a `.js`. */ - serverPlatform: ServerPlatform; + serverBuildPath: string; /** - * The target of the server build. - */ + * The target of the server build. Defaults to "node-cjs". + * + * @deprecated Use a combination of `{@link AppConfig.publicPath}`, `{@link AppConfig.serverBuildPath}`, `{@link AppConfig.serverConditions}`, `{@link AppConfig.serverDependenciesToBundle}`, `{@link AppConfig.serverMainFields}`, `{@link AppConfig.serverMinify}`, `{@link AppConfig.serverModuleFormat}` and/or `{@link AppConfig.serverPlatform}` instead. */ serverBuildTarget?: ServerBuildTarget; /** - * The default entry module for the server build if a {@see RemixConfig.customServer} is not provided. + * The default entry module for the server build if a {@see AppConfig.server} + * is not provided. */ serverBuildTargetEntryModule: string; /** - * A server entrypoint relative to the root directory that becomes your server's main module. + * The order of conditions to use when resolving server dependencies' + * `exports` field in `package.json`. */ - serverEntryPoint?: string; + serverConditions?: string[]; /** * A list of patterns that determined if a module is transpiled and included * in the server bundle. This can be useful when consuming ESM only packages * in a CJS build. */ - serverDependenciesToBundle: Array; + serverDependenciesToBundle: "all" | Array; + + /** + * A server entrypoint relative to the root directory that becomes your + * server's main module. + */ + serverEntryPoint?: string; + + /** + * The order of main fields to use when resolving server dependencies. + * Defaults to `["main", "module"]`. + */ + serverMainFields: string[]; + + /** + * Whether to minify the server build in production or not. + * Defaults to `false`. + */ + serverMinify: boolean; + + /** + * The mode to use to run the server. + */ + serverMode: ServerMode; + + /** + * The output format of the server build. Defaults to "cjs". + */ + serverModuleFormat: ServerModuleFormat; + + /** + * The platform the server build is targeting. Defaults to "node". + */ + serverPlatform: ServerPlatform; /** * A list of directories to watch. @@ -340,19 +373,41 @@ export async function readConfig( } } - let customServerEntryPoint = appConfig.server; - let serverBuildTarget: ServerBuildTarget | undefined = - appConfig.serverBuildTarget; + let isCloudflareRuntime = ["cloudflare-pages", "cloudflare-workers"].includes( + appConfig.serverBuildTarget ?? "" + ); + let isDenoRuntime = appConfig.serverBuildTarget === "deno"; + + let serverBuildPath = resolveServerBuildPath(rootDirectory, appConfig); + let serverBuildTarget = appConfig.serverBuildTarget; + let serverBuildTargetEntryModule = `export * from ${JSON.stringify( + serverBuildVirtualModule.id + )};`; + let serverConditions = appConfig.serverConditions; + let serverDependenciesToBundle = appConfig.serverDependenciesToBundle || []; + let serverEntryPoint = appConfig.server; + let serverMainFields = appConfig.serverMainFields || ["main", "module"]; + let serverMinify = appConfig.serverMinify || false; let serverModuleFormat: ServerModuleFormat = appConfig.serverModuleFormat || "cjs"; let serverPlatform: ServerPlatform = appConfig.serverPlatform || "node"; - switch (appConfig.serverBuildTarget) { - case "cloudflare-pages": - case "cloudflare-workers": - case "deno": - serverModuleFormat = "esm"; - serverPlatform = "neutral"; - break; + if (isCloudflareRuntime) { + serverConditions = ["worker"]; + serverDependenciesToBundle = "all"; + serverMainFields = ["browser", "module", "main"]; + serverMinify = true; + serverModuleFormat = "esm"; + serverPlatform = "neutral"; + } + if (isDenoRuntime) { + serverConditions = ["deno", "worker"]; + serverDependenciesToBundle = "all"; + serverMainFields = ["module", "main"]; + serverModuleFormat = "esm"; + serverPlatform = "neutral"; + } + if (serverModuleFormat === "esm") { + serverMainFields = ["module", "main"]; } let mdx = appConfig.mdx; @@ -377,35 +432,6 @@ export async function readConfig( throw new Error(`Missing "entry.server" file in ${appDirectory}`); } - let serverBuildPath = "build/index.js"; - switch (serverBuildTarget) { - case "arc": - serverBuildPath = "server/index.js"; - break; - case "cloudflare-pages": - serverBuildPath = "functions/[[path]].js"; - break; - case "netlify": - serverBuildPath = ".netlify/functions-internal/server.js"; - break; - case "vercel": - serverBuildPath = "api/index.js"; - break; - } - serverBuildPath = path.resolve(rootDirectory, serverBuildPath); - - // retain deprecated behavior for now - if (appConfig.serverBuildDirectory) { - serverBuildPath = path.resolve( - rootDirectory, - path.join(appConfig.serverBuildDirectory, "index.js") - ); - } - - if (appConfig.serverBuildPath) { - serverBuildPath = path.resolve(rootDirectory, appConfig.serverBuildPath); - } - let assetsBuildDirectory = appConfig.assetsBuildDirectory || appConfig.browserBuildDirectory || @@ -423,12 +449,8 @@ export async function readConfig( process.env.REMIX_DEV_SERVER_WS_PORT = `${devServerPort}`; let devServerBroadcastDelay = appConfig.devServerBroadcastDelay || 0; - let defaultPublicPath = "/build/"; - switch (serverBuildTarget) { - case "arc": - defaultPublicPath = "/_static/build/"; - break; - } + let defaultPublicPath = + appConfig.serverBuildTarget === "arc" ? "/_static/build/" : "/build/"; let publicPath = addTrailingSlash(appConfig.publicPath || defaultPublicPath); @@ -477,12 +499,6 @@ export async function readConfig( ); } - let serverBuildTargetEntryModule = `export * from ${JSON.stringify( - serverBuildVirtualModule.id - )};`; - - let serverDependenciesToBundle = appConfig.serverDependenciesToBundle || []; - // When tsconfigPath is undefined, the default "tsconfig.json" is not // found in the root directory. let tsconfigPath: string | undefined; @@ -523,13 +539,16 @@ export async function readConfig( rootDirectory, routes, serverBuildPath, - serverMode, - serverModuleFormat, - serverPlatform, serverBuildTarget, serverBuildTargetEntryModule, - serverEntryPoint: customServerEntryPoint, + serverConditions, serverDependenciesToBundle, + serverEntryPoint, + serverMainFields, + serverMinify, + serverMode, + serverModuleFormat, + serverPlatform, mdx, watchPaths, tsconfigPath, @@ -562,3 +581,36 @@ function findConfig(dir: string, basename: string): string | undefined { return undefined; } + +const resolveServerBuildPath = ( + rootDirectory: string, + appConfig: AppConfig +) => { + let serverBuildPath = "build/index.js"; + + switch (appConfig.serverBuildTarget) { + case "arc": + serverBuildPath = "server/index.js"; + break; + case "cloudflare-pages": + serverBuildPath = "functions/[[path]].js"; + break; + case "netlify": + serverBuildPath = ".netlify/functions-internal/server.js"; + break; + case "vercel": + serverBuildPath = "api/index.js"; + break; + } + + // retain deprecated behavior for now + if (appConfig.serverBuildDirectory) { + serverBuildPath = path.join(appConfig.serverBuildDirectory, "index.js"); + } + + if (appConfig.serverBuildPath) { + serverBuildPath = appConfig.serverBuildPath; + } + + return path.resolve(rootDirectory, serverBuildPath); +};