diff --git a/integration/bug-report-test.ts b/integration/bug-report-test.ts index 54f3e094d4b..fd44db2b9ca 100644 --- a/integration/bug-report-test.ts +++ b/integration/bug-report-test.ts @@ -107,7 +107,7 @@ test("[description of what you expect it to do]", async ({ page }) => { // If you need to test interactivity use the `app` await app.goto("/"); await app.clickLink("/burgers"); - expect(await app.getHtml()).toMatch("cheeseburger"); + await page.waitForSelector("text=cheeseburger"); // If you're not sure what's going on, you can "poke" the app, it'll // automatically open up in your browser for 20 seconds, so be quick! diff --git a/integration/helpers/create-fixture.ts b/integration/helpers/create-fixture.ts index 92854e92038..743dd98b152 100644 --- a/integration/helpers/create-fixture.ts +++ b/integration/helpers/create-fixture.ts @@ -7,7 +7,7 @@ import getPort from "get-port"; import dedent from "dedent"; import stripIndent from "strip-indent"; import serializeJavaScript from "serialize-javascript"; -import { sync as spawnSync } from "cross-spawn"; +import { sync as spawnSync, spawn } from "cross-spawn"; import type { JsonObject } from "type-fest"; import type { AppConfig } from "@remix-run/dev"; @@ -27,6 +27,7 @@ export interface FixtureInit { files?: { [filename: string]: string }; template?: "cf-template" | "deno-template" | "node-template"; config?: Partial; + useRemixServe?: boolean; } export type Fixture = Awaited>; @@ -97,6 +98,7 @@ export async function createFixture(init: FixtureInit, mode?: ServerMode) { requestData, postDocument, getBrowserAsset, + useRemixServe: init.useRemixServe, }; } @@ -105,6 +107,63 @@ export async function createAppFixture(fixture: Fixture, mode?: ServerMode) { port: number; stop: VoidFunction; }> => { + if (fixture.useRemixServe) { + return new Promise(async (accept, reject) => { + let port = await getPort(); + + let nodebin = process.argv[0]; + let serveProcess = spawn( + nodebin, + ["node_modules/@remix-run/serve/dist/cli.js", "build/index.js"], + { + env: { + NODE_ENV: mode || "production", + PORT: port.toFixed(0), + }, + cwd: fixture.projectDir, + stdio: "pipe", + } + ); + // Wait for `started at http://localhost:${port}` to be printed + // and extract the port from it. + let started = false; + let stdout = ""; + let rejectTimeout = setTimeout(() => { + reject(new Error("Timed out waiting for remix-serve to start")); + }, 20000); + serveProcess.stderr.pipe(process.stderr); + serveProcess.stdout.on("data", (chunk) => { + if (started) return; + let newChunk = chunk.toString(); + stdout += newChunk; + let match: RegExpMatchArray | null = stdout.match( + /started at http:\/\/localhost:(\d+)\s/ + ); + if (match) { + clearTimeout(rejectTimeout); + started = true; + let parsedPort = parseInt(match[1], 10); + + if (port !== parsedPort) { + reject( + new Error( + `Expected remix-serve to start on port ${port}, but it started on port ${parsedPort}` + ) + ); + return; + } + + accept({ + stop: () => { + serveProcess.kill(); + }, + port, + }); + } + }); + }); + } + return new Promise(async (accept) => { let port = await getPort(); let app = express(); diff --git a/integration/remix-serve-test.ts b/integration/remix-serve-test.ts new file mode 100644 index 00000000000..7703569731c --- /dev/null +++ b/integration/remix-serve-test.ts @@ -0,0 +1,70 @@ +import { test, expect } from "@playwright/test"; + +import { PlaywrightFixture } from "./helpers/playwright-fixture.js"; +import type { Fixture, AppFixture } from "./helpers/create-fixture.js"; +import { + createAppFixture, + createFixture, + js, +} from "./helpers/create-fixture.js"; + +let fixture: Fixture; +let appFixture: AppFixture; + +test.beforeEach(async ({ context }) => { + await context.route(/_data/, async (route) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + route.continue(); + }); +}); + +test.beforeAll(async () => { + fixture = await createFixture({ + useRemixServe: true, + files: { + "app/routes/_index.tsx": js` + import { json } from "@remix-run/node"; + import { useLoaderData, Link } from "@remix-run/react"; + + export function loader() { + return json("pizza"); + } + + export default function Index() { + let data = useLoaderData(); + return ( +
+ {data} + Other Route +
+ ) + } + `, + + "app/routes/burgers.tsx": js` + export default function Index() { + return
cheeseburger
; + } + `, + }, + }); + + // This creates an interactive app using playwright. + appFixture = await createAppFixture(fixture); +}); + +test.afterAll(() => { + appFixture.close(); +}); + +test("should start and perform client side navigation", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + // You can test any request your app might get using `fixture`. + let response = await fixture.requestDocument("/"); + expect(await response.text()).toMatch("pizza"); + + // If you need to test interactivity use the `app` + await app.goto("/"); + await app.clickLink("/burgers"); + await page.waitForSelector("text=cheeseburger"); +}); diff --git a/packages/remix-serve/cli.ts b/packages/remix-serve/cli.ts index e704d0e5f48..c1b655cdeee 100644 --- a/packages/remix-serve/cli.ts +++ b/packages/remix-serve/cli.ts @@ -1,6 +1,7 @@ import "./env"; import path from "node:path"; import os from "node:os"; +import url from "node:url"; import { broadcastDevReady, installGlobals } from "@remix-run/node"; import sourceMapSupport from "source-map-support"; @@ -9,49 +10,57 @@ import { createApp } from "./index"; sourceMapSupport.install(); installGlobals(); -let port = process.env.PORT ? Number(process.env.PORT) : 3000; -if (Number.isNaN(port)) port = 3000; +run(); -let buildPathArg = process.argv[2]; +async function run() { + let port = process.env.PORT ? Number(process.env.PORT) : 3000; + if (Number.isNaN(port)) port = 3000; -if (!buildPathArg) { - console.error(` - Usage: remix-serve `); - process.exit(1); -} + let buildPathArg = process.argv[2]; -let buildPath = path.resolve(process.cwd(), buildPathArg); -let build = require(buildPath); - -let onListen = () => { - let address = - process.env.HOST || - Object.values(os.networkInterfaces()) - .flat() - .find((ip) => String(ip?.family).includes("4") && !ip?.internal)?.address; - - if (!address) { - console.log(`Remix App Server started at http://localhost:${port}`); - } else { - console.log( - `Remix App Server started at http://localhost:${port} (http://${address}:${port})` - ); - } - if (process.env.NODE_ENV === "development") { - void broadcastDevReady(build); + if (!buildPathArg) { + console.error(` + Usage: remix-serve `); + process.exit(1); } -}; - -let app = createApp( - buildPath, - process.env.NODE_ENV, - build.publicPath, - build.assetsBuildDirectory -); -let server = process.env.HOST - ? app.listen(port, process.env.HOST, onListen) - : app.listen(port, onListen); - -["SIGTERM", "SIGINT"].forEach((signal) => { - process.once(signal, () => server?.close(console.error)); -}); + + let buildPath = url.pathToFileURL( + path.resolve(process.cwd(), buildPathArg) + ).href; + + let build = await import(buildPath); + + let onListen = () => { + let address = + process.env.HOST || + Object.values(os.networkInterfaces()) + .flat() + .find((ip) => String(ip?.family).includes("4") && !ip?.internal) + ?.address; + + if (!address) { + console.log(`Remix App Server started at http://localhost:${port}`); + } else { + console.log( + `Remix App Server started at http://localhost:${port} (http://${address}:${port})` + ); + } + if (process.env.NODE_ENV === "development") { + void broadcastDevReady(build); + } + }; + + let app = createApp( + buildPath, + process.env.NODE_ENV, + build.publicPath, + build.assetsBuildDirectory + ); + let server = process.env.HOST + ? app.listen(port, process.env.HOST, onListen) + : app.listen(port, onListen); + + ["SIGTERM", "SIGINT"].forEach((signal) => { + process.once(signal, () => server?.close(console.error)); + }); +} diff --git a/rollup.utils.js b/rollup.utils.js index 55e5d04bdc0..4d46afe4bde 100644 --- a/rollup.utils.js +++ b/rollup.utils.js @@ -189,6 +189,15 @@ function getCliConfig({ packageName, version }) { extensions: [".ts"], }), nodeResolve({ extensions: [".ts"] }), + { + name: "dynamic-import-polyfill", + renderDynamicImport() { + return { + left: "import(", + right: ")", + }; + }, + }, copyPublishFiles(packageName), copyToPlaygrounds(), ],