From d2129ed1be2c02ec6af4d3776872085bae201ae5 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Fri, 26 May 2023 10:41:09 -0400 Subject: [PATCH 1/4] refactor(dev): logger as compiler context and replace `warnOnce` calls in compiler with `logger.warn` --- packages/remix-dev/cli/commands.ts | 4 +- packages/remix-dev/compiler/js/compiler.ts | 3 +- packages/remix-dev/compiler/options.ts | 4 +- .../plugins/deprecatedRemixPackage.ts | 2 +- .../compiler/server/plugins/bareImports.ts | 29 +++----- packages/remix-dev/devServer/liveReload.ts | 4 +- .../remix-dev/devServer_unstable/index.ts | 4 +- packages/remix-dev/package.json | 1 + packages/remix-dev/tux/logger.ts | 68 +++++++++++++++++++ 9 files changed, 91 insertions(+), 28 deletions(-) create mode 100644 packages/remix-dev/tux/logger.ts diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index 0a9074923bc..31133065063 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -23,8 +23,8 @@ import runCodemod from "../codemod"; import { CodemodError } from "../codemod/utils/error"; import { TaskError } from "../codemod/utils/task"; import { transpile as convertFileToJS } from "./useJavascript"; -import { warnOnce } from "../warnOnce"; import type { Options } from "../compiler/options"; +import { logger } from "../tux/logger"; export async function create({ appTemplate, @@ -173,7 +173,7 @@ export async function build( let options: Options = { mode, sourcemap, - onWarning: warnOnce, + logger, }; if (mode === "development" && config.future.unstable_dev) { let origin = await resolveDevOrigin(config); diff --git a/packages/remix-dev/compiler/js/compiler.ts b/packages/remix-dev/compiler/js/compiler.ts index 7cc89714ff8..9a937b8bda2 100644 --- a/packages/remix-dev/compiler/js/compiler.ts +++ b/packages/remix-dev/compiler/js/compiler.ts @@ -147,7 +147,6 @@ const createEsbuildConfig = ( let packageName = getNpmPackageName(args.path); let pkgManager = detectPackageManager() ?? "npm"; if ( - ctx.options.onWarning && !isNodeBuiltIn(packageName) && !/\bnode_modules\b/.test(args.importer) && // Silence spurious warnings when using Yarn PnP. Yarn PnP doesn’t use @@ -159,7 +158,7 @@ const createEsbuildConfig = ( try { require.resolve(args.path); } catch (error: unknown) { - ctx.options.onWarning( + ctx.options.logger.warn( `The path "${args.path}" is imported in ` + `${path.relative(process.cwd(), args.importer)} but ` + `"${args.path}" was not found in your node_modules. ` + diff --git a/packages/remix-dev/compiler/options.ts b/packages/remix-dev/compiler/options.ts index d128116d7de..ccf9960288c 100644 --- a/packages/remix-dev/compiler/options.ts +++ b/packages/remix-dev/compiler/options.ts @@ -1,9 +1,11 @@ +import type { Logger } from "../tux/logger"; + type Mode = "development" | "production" | "test"; export type Options = { mode: Mode; sourcemap: boolean; - onWarning?: (message: string, key: string) => void; + logger: Logger; // TODO: required in v2 devOrigin?: { diff --git a/packages/remix-dev/compiler/plugins/deprecatedRemixPackage.ts b/packages/remix-dev/compiler/plugins/deprecatedRemixPackage.ts index 43ef5aa4014..511d20348b6 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.onWarning?.(warningMessage, importer); + ctx.options.logger.warn(warningMessage, importer); } return undefined; }); diff --git a/packages/remix-dev/compiler/server/plugins/bareImports.ts b/packages/remix-dev/compiler/server/plugins/bareImports.ts index 075361fa6bf..bb08a7935d8 100644 --- a/packages/remix-dev/compiler/server/plugins/bareImports.ts +++ b/packages/remix-dev/compiler/server/plugins/bareImports.ts @@ -18,10 +18,10 @@ import type { Context } from "../../context"; * This includes externalizing for node based platforms, and bundling for single file * environments such as cloudflare. */ -export function serverBareModulesPlugin({ config, options }: Context): Plugin { +export function serverBareModulesPlugin(ctx: Context): Plugin { // Resolve paths according to tsconfig paths property - let matchPath = config.tsconfigPath - ? createMatchPath(config.tsconfigPath) + let matchPath = ctx.config.tsconfigPath + ? createMatchPath(ctx.config.tsconfigPath) : undefined; function resolvePath(id: string) { if (!matchPath) { @@ -76,7 +76,6 @@ export function serverBareModulesPlugin({ config, options }: Context): Plugin { // Warn if we can't find an import for a package. if ( - options.onWarning && !isNodeBuiltIn(packageName) && !/\bnode_modules\b/.test(importer) && // Silence spurious warnings when using Yarn PnP. Yarn PnP doesn’t use @@ -88,7 +87,7 @@ export function serverBareModulesPlugin({ config, options }: Context): Plugin { try { require.resolve(path, { paths: [importer] }); } catch (error: unknown) { - options.onWarning( + ctx.options.logger.warn( `The path "${path}" is imported in ` + `${relative(process.cwd(), importer)} but ` + `"${path}" was not found in your node_modules. ` + @@ -98,11 +97,11 @@ export function serverBareModulesPlugin({ config, options }: Context): Plugin { } } - if (config.serverDependenciesToBundle === "all") { + if (ctx.config.serverDependenciesToBundle === "all") { return undefined; } - for (let pattern of config.serverDependenciesToBundle) { + for (let pattern of ctx.config.serverDependenciesToBundle) { // bundle it if the path matches the pattern if ( typeof pattern === "string" ? path === pattern : pattern.test(path) @@ -112,17 +111,11 @@ export function serverBareModulesPlugin({ config, options }: Context): Plugin { } if ( - options.onWarning && !isNodeBuiltIn(packageName) && kind !== "dynamic-import" && - config.serverPlatform === "node" + ctx.config.serverPlatform === "node" ) { - warnOnceIfEsmOnlyPackage( - packageName, - path, - importer, - options.onWarning - ); + warnOnceIfEsmOnlyPackage(ctx, packageName, path, importer); } // Externalize everything else if we've gotten here. @@ -151,10 +144,10 @@ function isBareModuleId(id: string): boolean { } function warnOnceIfEsmOnlyPackage( + ctx: Context, packageName: string, fullImportPath: string, - importer: string, - onWarning: (msg: string, key: string) => void + importer: string ) { try { let packageDir = resolveModuleBasePath( @@ -187,7 +180,7 @@ function warnOnceIfEsmOnlyPackage( } if (isEsmOnly) { - onWarning( + ctx.options.logger.warn( `${packageName} is possibly an ESM only package and should be bundled with ` + `"serverDependenciesToBundle" in remix.config.js.`, packageName + ":esm-only" diff --git a/packages/remix-dev/devServer/liveReload.ts b/packages/remix-dev/devServer/liveReload.ts index c09f48887a4..10890c1c3fd 100644 --- a/packages/remix-dev/devServer/liveReload.ts +++ b/packages/remix-dev/devServer/liveReload.ts @@ -6,7 +6,7 @@ import WebSocket from "ws"; import { watch } from "../compiler"; import type { RemixConfig } from "../config"; -import { warnOnce } from "../warnOnce"; +import { logger } from "../tux/logger"; const relativePath = (file: string) => path.relative(process.cwd(), file); @@ -44,7 +44,7 @@ export async function liveReload(config: RemixConfig) { options: { mode: "development", sourcemap: true, - onWarning: warnOnce, + logger, }, }, { diff --git a/packages/remix-dev/devServer_unstable/index.ts b/packages/remix-dev/devServer_unstable/index.ts index 1bbf1de6aee..ca245c676c4 100644 --- a/packages/remix-dev/devServer_unstable/index.ts +++ b/packages/remix-dev/devServer_unstable/index.ts @@ -14,12 +14,12 @@ import { type RemixConfig } from "../config"; import { loadEnv } from "./env"; import * as Socket from "./socket"; import * as HMR from "./hmr"; -import { warnOnce } from "../warnOnce"; import { detectPackageManager } from "../cli/detectPackageManager"; import * as HDR from "./hdr"; import type { Result } from "../result"; import { err, ok } from "../result"; import invariant from "../invariant"; +import { logger } from "../tux/logger"; type Origin = { scheme: string; @@ -176,7 +176,7 @@ export let serve = async ( options: { mode: "development", sourcemap: true, - onWarning: warnOnce, + logger, devOrigin: origin, }, }, diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index d790225b8a8..00cfffa8e0b 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -53,6 +53,7 @@ "minimatch": "^9.0.0", "node-fetch": "^2.6.9", "ora": "^5.4.1", + "picocolors": "^1.0.0", "postcss": "^8.4.19", "postcss-discard-duplicates": "^5.1.0", "postcss-load-config": "^4.0.1", diff --git a/packages/remix-dev/tux/logger.ts b/packages/remix-dev/tux/logger.ts new file mode 100644 index 00000000000..e6b5e3d8c9a --- /dev/null +++ b/packages/remix-dev/tux/logger.ts @@ -0,0 +1,68 @@ +import pc from "picocolors"; + +type Level = "debug" | "info" | "warn" | "error"; +type Log = (message: string, key?: string) => void; + +export type Logger = { + debug: Log; + info: Log; + warn: Log; + error: Log; +}; + +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 logline = (level: Level, message: string) => { + let line = ""; + + // timestamp + let now = formatDate(new Date()); + line += pc.dim(now) + " "; + + // level + let color = { + debug: pc.green, + info: pc.blue, + warn: pc.yellow, + error: pc.red, + }[level]; + line += + (pc.isColorSupported ? pc.inverse(color(` ${level} `)) : `[${level}]`) + + " "; + + // message + line += message + "\n"; + + return line; +}; + +let once = (log: (msg: string) => void) => { + let logged = new Set(); + return (msg: string, key?: string) => { + if (key === undefined) return log(msg); + if (logged.has(key)) return; + logged.add(key); + log(msg); + }; +}; + +let debug = once(log("debug")); +let info = once(log("info")); +let warn = once(log("warn")); +let error = once(log("error")); + +export let logger: Logger = { + debug, + info, + warn, + error, +}; From b6d3bd0d351e9120da05bc0ea9e75d270b294896 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Fri, 26 May 2023 11:03:00 -0400 Subject: [PATCH 2/4] refactor(dev): replace console with logger in compiler --- packages/remix-dev/compiler/server/plugins/bareImports.ts | 2 +- packages/remix-dev/compiler/watch.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/remix-dev/compiler/server/plugins/bareImports.ts b/packages/remix-dev/compiler/server/plugins/bareImports.ts index bb08a7935d8..ad7f0647b66 100644 --- a/packages/remix-dev/compiler/server/plugins/bareImports.ts +++ b/packages/remix-dev/compiler/server/plugins/bareImports.ts @@ -158,7 +158,7 @@ function warnOnceIfEsmOnlyPackage( let packageJsonFile = path.join(packageDir, "package.json"); if (!fs.existsSync(packageJsonFile)) { - console.log(packageJsonFile, `does not exist`); + ctx.options.logger.warn(packageJsonFile, `does not exist`); return; } let pkg = JSON.parse(fs.readFileSync(packageJsonFile, "utf-8")); diff --git a/packages/remix-dev/compiler/watch.ts b/packages/remix-dev/compiler/watch.ts index 0cede310af3..da0e1006335 100644 --- a/packages/remix-dev/compiler/watch.ts +++ b/packages/remix-dev/compiler/watch.ts @@ -99,7 +99,7 @@ export async function watch( pollInterval: 100, }, }) - .on("error", (error) => console.error(error)) + .on("error", (error) => ctx.options.logger.error(String(error))) .on("change", async (file) => { onFileChanged?.(file); await rebuild(); From a88da474f5b985a4a2999555b5e52c7ef012ce9d Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Fri, 26 May 2023 16:17:47 -0400 Subject: [PATCH 3/4] wip --- packages/remix-dev/cli/commands.ts | 11 ++- packages/remix-dev/devServer/serve.ts | 3 +- packages/remix-dev/devServer_unstable/env.ts | 9 ++- .../remix-dev/devServer_unstable/index.ts | 78 ++++++++++++------- .../remix-dev/devServer_unstable/socket.ts | 3 +- 5 files changed, 65 insertions(+), 39 deletions(-) diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index 31133065063..9323a754f11 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -8,6 +8,7 @@ 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"; @@ -221,11 +222,13 @@ export async function dev( tlsCert?: string; } = {} ) { + // clear screen + process.stdout.write("\x1Bc"); + if (process.env.NODE_ENV && process.env.NODE_ENV !== "development") { - console.warn( - `Forcing NODE_ENV to be 'development'. Was: ${JSON.stringify( - process.env.NODE_ENV - )}` + logger.warn( + "forcing `NODE_ENV=development`" + + pc.gray(` (was ${JSON.stringify(process.env.NODE_ENV)})`) ); } process.env.NODE_ENV = "development"; diff --git a/packages/remix-dev/devServer/serve.ts b/packages/remix-dev/devServer/serve.ts index cd85d27b9bf..7ea0f1f2cd3 100644 --- a/packages/remix-dev/devServer/serve.ts +++ b/packages/remix-dev/devServer/serve.ts @@ -5,6 +5,7 @@ import os from "os"; import { loadEnv } from "../devServer_unstable/env"; import { liveReload } from "./liveReload"; import type { RemixConfig } from "../config"; +import { logger } from "../tux/logger"; function purgeAppRequireCache(buildPath: string) { for (let key in require.cache) { @@ -36,7 +37,7 @@ export async function serve(config: RemixConfig, portPreference?: number) { // eslint-disable-next-line @typescript-eslint/consistent-type-imports let express = tryImport("express") as typeof import("express"); - await loadEnv(config.rootDirectory); + await loadEnv(config.rootDirectory, { logger }); let port = await getPort({ port: portPreference diff --git a/packages/remix-dev/devServer_unstable/env.ts b/packages/remix-dev/devServer_unstable/env.ts index 9f9df9c4ff0..7e4b1ca9725 100644 --- a/packages/remix-dev/devServer_unstable/env.ts +++ b/packages/remix-dev/devServer_unstable/env.ts @@ -1,12 +1,17 @@ import * as fse from "fs-extra"; import * as path from "path"; +import type { Logger } from "../tux/logger"; + // Import environment variables from: .env, failing gracefully if it doesn't exist -export async function loadEnv(rootDirectory: string): Promise { +export async function loadEnv( + rootDirectory: string, + options: { logger: Logger } +): Promise { let envPath = path.join(rootDirectory, ".env"); if (!fse.existsSync(envPath)) return; - console.log(`Loading environment variables from .env`); + options.logger.debug(`Loading environment variables from .env`); let result = require("dotenv").config({ path: envPath }); if (result.error) throw result.error; } diff --git a/packages/remix-dev/devServer_unstable/index.ts b/packages/remix-dev/devServer_unstable/index.ts index ca245c676c4..b45fcc884f2 100644 --- a/packages/remix-dev/devServer_unstable/index.ts +++ b/packages/remix-dev/devServer_unstable/index.ts @@ -6,6 +6,7 @@ import fs from "fs-extra"; import prettyMs from "pretty-ms"; import execa from "execa"; import express from "express"; +import pc from "picocolors"; import * as Channel from "../channel"; import { type Manifest } from "../manifest"; @@ -52,7 +53,7 @@ export let serve = async ( tlsCert?: string; } ) => { - await loadEnv(initialConfig.rootDirectory); + await loadEnv(initialConfig.rootDirectory, { logger }); let state: { appServer?: execa.ExecaChildProcess; manifest?: Manifest; @@ -68,7 +69,7 @@ export let serve = async ( .post("/ping", (req, res) => { let { buildHash } = req.body; if (typeof buildHash !== "string") { - console.warn(`Unrecognized payload: ${req.body}`); + logger.warn(`unrecognized payload: ${req.body}`); res.sendStatus(400); } if (buildHash === state.manifest?.version) { @@ -103,7 +104,6 @@ export let serve = async ( process.cwd(), initialConfig.serverBuildPath )}`; - console.log(`> ${cmd}`); let newAppServer = execa .command(cmd, { stdio: "pipe", @@ -124,18 +124,20 @@ export let serve = async ( invariant("path" in e && typeof e.path === "string", "path missing"); if (command === undefined) { - console.error( - [ - "", - `┏ [error] command not found: ${e.path}`, - `┃ \`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'`", - "", - ].join("\n") + 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") ); process.exit(1); } + logger.error("app failed to start" + pc.gray(` (${command})`)); throw e; }); @@ -186,7 +188,10 @@ export let serve = async ( state.appReady?.err(); clean(ctx.config); - websocket.log(state.prevManifest ? "Rebuilding..." : "Building..."); + + let msg = state.prevManifest ? "rebuilding..." : "building..."; + websocket.log(msg); + logger.info(msg); state.loaderChanges = HDR.detectLoaderChanges(ctx).then(ok, err); }, @@ -196,15 +201,19 @@ export let serve = async ( }, onBuildFinish: async (ctx, durationMs, succeeded) => { if (!succeeded) return; - websocket.log( - (state.prevManifest ? "Rebuilt" : "Built") + - ` in ${prettyMs(durationMs)}` - ); + + let msg = + (state.prevManifest ? "rebuilt" : "built") + + pc.gray(` (${prettyMs(durationMs)})`); + websocket.log(msg); + logger.info(msg); // accumulate new state, but only update state after updates are processed let newState: typeof state = { prevManifest: state.manifest }; try { - console.log(`Waiting for app server (${state.manifest?.version})`); + logger.info( + "waiting for app server" + pc.gray(` (${state.manifest?.version})`) + ); let start = Date.now(); if (state.appServer === undefined || options.restart) { await kill(state.appServer); @@ -212,7 +221,9 @@ export let serve = async ( } let appReady = await state.appReady!.result; if (!appReady.ok) return; - console.log(`App server took ${prettyMs(Date.now() - start)}`); + logger.info( + `app server ready` + pc.gray(` (${prettyMs(Date.now() - start)})`) + ); // HMR + HDR let loaderChanges = await state.loaderChanges!; @@ -230,32 +241,39 @@ export let serve = async ( websocket.hmr(state.manifest, updates); let hdr = updates.some((u) => u.revalidate); - console.log("> HMR" + (hdr ? " + HDR" : "")); + logger.info("hmr" + (hdr ? " + hdr" : "")); return; } // Live Reload if (state.prevManifest !== undefined) { websocket.reload(); - console.log("> Live reload"); + logger.info("live reload"); } } finally { // commit accumulated state Object.assign(state, newState); + + // newline formatting + console.log(); } }, - onFileCreated: (file) => - websocket.log(`File created: ${relativePath(file)}`), - onFileChanged: (file) => - websocket.log(`File changed: ${relativePath(file)}`), - onFileDeleted: (file) => - websocket.log(`File deleted: ${relativePath(file)}`), + onFileCreated: (file) => { + logger.info(`file created` + pc.gray(` (${relativePath(file)})`)); + websocket.log(`file created: ${relativePath(file)}`); + }, + onFileChanged: (file) => { + logger.info(`file changed` + pc.gray(` (${relativePath(file)})`)); + websocket.log(`file changed: ${relativePath(file)}`); + }, + onFileDeleted: (file) => { + logger.info(`file deleted` + pc.gray(` (${relativePath(file)})`)); + websocket.log(`file deleted: ${relativePath(file)}`); + }, } ); - server.listen(origin.port, () => { - console.log("Remix dev server ready"); - }); + server.listen(origin.port); return new Promise(() => {}).finally(async () => { await kill(state.appServer); diff --git a/packages/remix-dev/devServer_unstable/socket.ts b/packages/remix-dev/devServer_unstable/socket.ts index a66ca95a0db..27c07f3df1f 100644 --- a/packages/remix-dev/devServer_unstable/socket.ts +++ b/packages/remix-dev/devServer_unstable/socket.ts @@ -27,8 +27,7 @@ export let serve = (server: HTTPServer) => { }; let log = (messageText: string) => { - let _message = `💿 ${messageText}`; - console.log(_message); + let _message = `[remix] ${messageText}`; broadcast({ type: "LOG", message: _message }); }; From fc30e7cf1e28fcce5607c4615343e86a2a9b9e56 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Thu, 8 Jun 2023 15:19:43 +0200 Subject: [PATCH 4/4] wip --- packages/remix-dev/cli/commands.ts | 10 +- packages/remix-dev/compiler/js/compiler.ts | 2 +- .../plugins/deprecatedRemixPackage.ts | 2 +- .../compiler/server/plugins/bareImports.ts | 6 +- packages/remix-dev/config.ts | 102 +++++++++++------- .../remix-dev/devServer_unstable/index.ts | 20 ++-- packages/remix-dev/tux/logger.ts | 56 ++++++---- 7 files changed, 118 insertions(+), 80 deletions(-) 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"));