diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index 9323a754f11..d0c9c08fcc6 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -8,7 +8,6 @@ import prettyMs from "pretty-ms"; import * as esbuild from "esbuild"; import NPMCliPackageJson from "@npmcli/package-json"; import { coerce } from "semver"; -import pc from "picocolors"; import * as colors from "../colors"; import * as compiler from "../compiler"; @@ -225,11 +224,12 @@ export async function dev( // clear screen process.stdout.write("\x1Bc"); + // TODO: statically get version + let version = "1.17.0"; + console.log(`\n 💿 remix dev v${version}\n`); + if (process.env.NODE_ENV && process.env.NODE_ENV !== "development") { - logger.warn( - "forcing `NODE_ENV=development`" + - pc.gray(` (was ${JSON.stringify(process.env.NODE_ENV)})`) - ); + logger.warn(`overriding NODE_ENV=${process.env.NODE_ENV} to development`); } process.env.NODE_ENV = "development"; if (flags.debug) inspector.open(); diff --git a/packages/remix-dev/compiler/js/compiler.ts b/packages/remix-dev/compiler/js/compiler.ts index 9a937b8bda2..e2d0329cc47 100644 --- a/packages/remix-dev/compiler/js/compiler.ts +++ b/packages/remix-dev/compiler/js/compiler.ts @@ -163,7 +163,7 @@ const createEsbuildConfig = ( `${path.relative(process.cwd(), args.importer)} but ` + `"${args.path}" was not found in your node_modules. ` + `Did you forget to install it?`, - args.path + { key: args.path } ); } } diff --git a/packages/remix-dev/compiler/plugins/deprecatedRemixPackage.ts b/packages/remix-dev/compiler/plugins/deprecatedRemixPackage.ts index 511d20348b6..b0c0872ab9e 100644 --- a/packages/remix-dev/compiler/plugins/deprecatedRemixPackage.ts +++ b/packages/remix-dev/compiler/plugins/deprecatedRemixPackage.ts @@ -20,7 +20,7 @@ export function deprecatedRemixPackagePlugin(ctx: Context): Plugin { `underlying \`@remix-run/*\` package. ` + `Run \`npx @remix-run/dev@latest codemod replace-remix-magic-imports\` ` + `to automatically migrate your code.`; - ctx.options.logger.warn(warningMessage, importer); + ctx.options.logger.warn(warningMessage, { key: importer }); } return undefined; }); diff --git a/packages/remix-dev/compiler/server/plugins/bareImports.ts b/packages/remix-dev/compiler/server/plugins/bareImports.ts index ad7f0647b66..fd1557d2c19 100644 --- a/packages/remix-dev/compiler/server/plugins/bareImports.ts +++ b/packages/remix-dev/compiler/server/plugins/bareImports.ts @@ -92,7 +92,7 @@ export function serverBareModulesPlugin(ctx: Context): Plugin { `${relative(process.cwd(), importer)} but ` + `"${path}" was not found in your node_modules. ` + `Did you forget to install it?`, - path + { key: path } ); } } @@ -158,7 +158,7 @@ function warnOnceIfEsmOnlyPackage( let packageJsonFile = path.join(packageDir, "package.json"); if (!fs.existsSync(packageJsonFile)) { - ctx.options.logger.warn(packageJsonFile, `does not exist`); + ctx.options.logger.warn(`${packageJsonFile} does not exist`); return; } let pkg = JSON.parse(fs.readFileSync(packageJsonFile, "utf-8")); @@ -183,7 +183,7 @@ function warnOnceIfEsmOnlyPackage( ctx.options.logger.warn( `${packageName} is possibly an ESM only package and should be bundled with ` + `"serverDependenciesToBundle" in remix.config.js.`, - packageName + ":esm-only" + { key: packageName + ":esm-only" } ); } } diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 68e91d48641..c9fb9e84e76 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -5,6 +5,7 @@ import fse from "fs-extra"; import getPort from "get-port"; import NPMCliPackageJson from "@npmcli/package-json"; import { coerce } from "semver"; +import pc from "picocolors"; import type { RouteManifest, DefineRoutesFunction } from "./config/routes"; import { defineRoutes } from "./config/routes"; @@ -14,6 +15,7 @@ import { serverBuildVirtualModule } from "./compiler/server/virtualModules"; import { flatRoutes } from "./config/flat-routes"; import { detectPackageManager } from "./cli/detectPackageManager"; import { warnOnce } from "./warnOnce"; +import { logger } from "./tux/logger"; export interface RemixMdxConfig { rehypePlugins?: any[]; @@ -430,19 +432,19 @@ export async function readConfig( } if (!appConfig.future?.v2_errorBoundary) { - warnOnce(errorBoundaryWarning, "v2_errorBoundary"); + logger.warn(...errorBoundaryWarning); } if (!appConfig.future?.v2_normalizeFormMethod) { - warnOnce(formMethodWarning, "v2_normalizeFormMethod"); + logger.warn(...formMethodWarning); } if (!appConfig.future?.v2_meta) { - warnOnce(metaWarning, "v2_meta"); + logger.warn(...metaWarning); } if (!appConfig.future?.v2_headers) { - warnOnce(headersWarning, "v2_headers"); + logger.warn(...headersWarning); } let isCloudflareRuntime = ["cloudflare-pages", "cloudflare-workers"].includes( @@ -462,7 +464,7 @@ export async function readConfig( let serverMinify = appConfig.serverMinify; if (!appConfig.serverModuleFormat) { - warnOnce(serverModuleFormatWarning, "serverModuleFormatWarning"); + logger.warn(...serverModuleFormatWarning); } let serverModuleFormat = appConfig.serverModuleFormat || "cjs"; @@ -690,7 +692,7 @@ export async function readConfig( if (appConfig.future?.v2_routeConvention) { routesConvention = flatRoutes; } else { - warnOnce(flatRoutesWarning, "v2_routeConvention"); + logger.warn(...flatRoutesWarning); routesConvention = defineConventionalRoutes; } @@ -884,6 +886,23 @@ let disjunctionListFormat = new Intl.ListFormat("en", { type: "disjunction", }); +let futureWarn = ( + message: string, + options: { + flag: string; + link: string; + } +): Parameters => [ + pc.yellow("future") + " " + message, + { + details: [ + `You can use the \`${options.flag}\` future flag to opt-in early`, + `-> ${options.link}`, + ], + key: options.flag, + }, +]; + export let browserBuildDirectoryWarning = "⚠️ REMIX FUTURE CHANGE: The `browserBuildDirectory` config option will be removed in v2. " + "Use `assetsBuildDirectory` instead. " + @@ -902,39 +921,48 @@ export let serverBuildTargetWarning = "For instructions on making this change see " + "https://remix.run/docs/en/v1.15.0/pages/v2#serverbuildtarget"; -export const serverModuleFormatWarning = - "⚠️ REMIX FUTURE CHANGE: The `serverModuleFormat` config default option will be changing in v2 " + - "from `cjs` to `esm`. You can prepare for this change by explicitly specifying `serverModuleFormat: 'cjs'`. " + - "For instructions on making this change see " + - "https://remix.run/docs/en/v1.16.0/pages/v2#servermoduleformat"; +const serverModuleFormatWarning = futureWarn( + "The `serverModuleFormat` config default option will be changing in v2", + { + flag: "TODO", + // "from `cjs` to `esm`. You can prepare for this change by explicitly specifying `serverModuleFormat: 'cjs'`. "; + link: "https://remix.run/docs/en/v1.16.0/pages/v2#servermoduleformat", + } +); -export let flatRoutesWarning = - "⚠️ REMIX FUTURE CHANGE: The route file convention is changing in v2. " + - "You can prepare for this change at your convenience with the `v2_routeConvention` future flag. " + - "For instructions on making this change see " + - "https://remix.run/docs/en/v1.15.0/pages/v2#file-system-route-convention"; +const flatRoutesWarning = futureWarn( + "The route file convention is changing in v2", + { + flag: "v2_routeConvention", + link: "https://remix.run/docs/en/v1.15.0/pages/v2#file-system-route-convention", + } +); -export const errorBoundaryWarning = - "⚠️ REMIX FUTURE CHANGE: The behaviors of `CatchBoundary` and `ErrorBoundary` are changing in v2. " + - "You can prepare for this change at your convenience with the `v2_errorBoundary` future flag. " + - "For instructions on making this change see " + - "https://remix.run/docs/en/v1.15.0/pages/v2#catchboundary-and-errorboundary"; +const errorBoundaryWarning = futureWarn( + "The behaviors of `CatchBoundary` and `ErrorBoundary` are changing in v2", + { + flag: "v2_errorBoundary", + link: "https://remix.run/docs/en/v1.15.0/pages/v2#catchboundary-and-errorboundary", + } +); -export const formMethodWarning = - "⚠️ REMIX FUTURE CHANGE: APIs that provide `formMethod` will be changing in v2. " + - "All values will be uppercase (GET, POST, etc.) instead of lowercase (get, post, etc.) " + - "You can prepare for this change at your convenience with the `v2_normalizeFormMethod` future flag. " + - "For instructions on making this change see " + - "https://remix.run/docs/en/v1.15.0/pages/v2#formMethod"; +const formMethodWarning = futureWarn( + "APIs that provide `formMethod` will be changing in v2", + { + flag: "v2_normalizeFormMethod", + link: "https://remix.run/docs/en/v1.15.0/pages/v2#formMethod", + } +); -export const metaWarning = - "⚠️ REMIX FUTURE CHANGE: The route `meta` export signature is changing in v2. " + - "You can prepare for this change at your convenience with the `v2_meta` future flag. " + - "For instructions on making this change see " + - "https://remix.run/docs/en/v1.15.0/pages/v2#meta"; +const metaWarning = futureWarn( + "route `meta` export signature is changing in v2", + { + flag: "v2_meta", + link: "https://remix.run/docs/en/v1.15.0/pages/v2#meta", + } +); -export const headersWarning = - "⚠️ REMIX FUTURE CHANGE: The route `headers` export behavior is changing in v2. " + - "You can prepare for this change at your convenience with the `v2_headers` future flag. " + - "For instructions on making this change see " + - "https://remix.run/docs/en/v1.17.0/pages/v2#route-headers"; +const headersWarning = futureWarn("`headers` export is changing in v2", { + flag: "v2_headers", + link: "https://remix.run/docs/en/v1.17.0/pages/v2#route-headers", +}); diff --git a/packages/remix-dev/devServer_unstable/index.ts b/packages/remix-dev/devServer_unstable/index.ts index b45fcc884f2..45c3f76f131 100644 --- a/packages/remix-dev/devServer_unstable/index.ts +++ b/packages/remix-dev/devServer_unstable/index.ts @@ -106,7 +106,7 @@ export let serve = async ( )}`; let newAppServer = execa .command(cmd, { - stdio: "pipe", + // stdio: "pipe", env: { NODE_ENV: "development", PATH: @@ -124,17 +124,13 @@ export let serve = async ( invariant("path" in e && typeof e.path === "string", "path missing"); if (command === undefined) { - logger.error( - `command not found: ${e.path}\n` + - [ - ` ┃ \`remix dev\` did not receive \`--command\` nor \`-c\`, defaulting to \`${cmd}\`.`, - " ┃ You probably meant to use `-c` for your app server command.", - " ┗ For example: `remix dev -c 'node ./server.js'`", - "", - ] - .map(pc.gray) - .join("\n") - ); + logger.error(`command not found: ${e.path}`, { + details: [ + `\`remix dev\` did not receive \`--command\` nor \`-c\`, defaulting to \`${cmd}\`.`, + "You probably meant to use `-c` for your app server command.", + "For example: `remix dev -c 'node ./server.js'`", + ], + }); process.exit(1); } logger.error("app failed to start" + pc.gray(` (${command})`)); diff --git a/packages/remix-dev/tux/logger.ts b/packages/remix-dev/tux/logger.ts index e6b5e3d8c9a..e73ebc45385 100644 --- a/packages/remix-dev/tux/logger.ts +++ b/packages/remix-dev/tux/logger.ts @@ -1,32 +1,38 @@ import pc from "picocolors"; type Level = "debug" | "info" | "warn" | "error"; -type Log = (message: string, key?: string) => void; +type Log = (message: string, details?: string[]) => void; +type LogOnce = ( + message: string, + options?: { details?: string[]; key?: string } +) => void; export type Logger = { - debug: Log; - info: Log; - warn: Log; - error: Log; + debug: LogOnce; + info: LogOnce; + warn: LogOnce; + error: LogOnce; }; -let { format: formatDate } = new Intl.DateTimeFormat([], { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", -}); +// let { format: formatDate } = new Intl.DateTimeFormat([], { +// hour: "2-digit", +// minute: "2-digit", +// second: "2-digit", +// }); -let log = (level: Level) => (message: string) => { - let dest = level === "error" ? process.stderr : process.stdout; - dest.write(logline(level, message)); -}; +let log = + (level: Level): Log => + (message, details) => { + let dest = level === "error" ? process.stderr : process.stdout; + dest.write(logline(level, message, details)); + }; -let logline = (level: Level, message: string) => { +let logline = (level: Level, message: string, details: string[] = []) => { let line = ""; // timestamp - let now = formatDate(new Date()); - line += pc.dim(now) + " "; + // let now = formatDate(new Date()); + // line += pc.dim(now) + " "; // level let color = { @@ -42,17 +48,25 @@ let logline = (level: Level, message: string) => { // message line += message + "\n"; + details.forEach((detail, i) => { + // let symbol = i === details?.length - 1 ? "┗" : "┃"; + // line += color(symbol) + " " + pc.gray(detail) + "\n"; + line += color("┃") + " " + pc.gray(detail) + "\n"; + }); + if (details.length > 0) line += color("┗") + "\n"; + return line; }; -let once = (log: (msg: string) => void) => { +let once = (log: Log) => { let logged = new Set(); - return (msg: string, key?: string) => { - if (key === undefined) return log(msg); + let logOnce: LogOnce = (msg, { details, key } = {}) => { + if (key === undefined) return log(msg, details); if (logged.has(key)) return; logged.add(key); - log(msg); + log(msg, details); }; + return logOnce; }; let debug = once(log("debug"));