Skip to content

Commit

Permalink
chore: add deno/cloudflare flavor of streaming
Browse files Browse the repository at this point in the history
Signed-off-by: Logan McAnsh <logan@mcan.sh>
  • Loading branch information
mcansh committed Jan 10, 2023
1 parent 15395f1 commit 6918568
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 58 deletions.
117 changes: 65 additions & 52 deletions packages/remix-dev/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as fse from "fs-extra";
import ora from "ora";
import prettyMs from "pretty-ms";
import * as esbuild from "esbuild";
import type { PackageJson } from "@npmcli/package-json";
import NPMCliPackageJson from "@npmcli/package-json";

import * as colors from "../colors";
Expand Down Expand Up @@ -238,21 +239,19 @@ export async function codemod(
}
}

export async function generateEntry(remixRoot: string, entry: string) {
// 1. validate requested entry file
let clientEntries = new Set([
"entry.client.tsx",
"entry.client.js",
"entry.client.jsx",
]);
let serverEntries = new Set([
"entry.server.tsx",
"entry.server.js",
"entry.server.jsx",
]);

let entries = new Set([...clientEntries, ...serverEntries]);
let clientEntries = new Set([
"entry.client.tsx",
"entry.client.js",
"entry.client.jsx",
]);
let serverEntries = new Set([
"entry.server.tsx",
"entry.server.js",
"entry.server.jsx",
]);
let entries = new Set([...clientEntries, ...serverEntries]);

export async function generateEntry(remixRoot: string, entry: string) {
if (!entries.has(entry)) {
// @ts-expect-error available in node 12+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat#browser_compatibility
Expand All @@ -270,63 +269,50 @@ export async function generateEntry(remixRoot: string, entry: string) {
return process.exit(1);
}

let defaultsDirectory = path.resolve(__dirname, "..", "config", "defaults");
let defaultEntryClient = path.resolve(defaultsDirectory, "entry.client.tsx");
let defaultEntryServer = path.resolve(defaultsDirectory, "entry.server.tsx");
let pkgJson = await NPMCliPackageJson.load(remixRoot);
let deps = pkgJson.content.dependencies ?? {};

// 2. check if any of the requested entry file exists
let entryType = entry.startsWith("entry.client.") ? "client" : "server";
let inputFile =
entryType === "client" ? defaultEntryClient : defaultEntryServer;
let outputFile = path.resolve(remixRoot, "app", entry);
let runtime = deps["@remix-run/deno"]
? "deno"
: deps["@remix-run/cloudflare"]
? "cloudflare"
: deps["@remix-run/node"]
? "node"
: undefined;

let entriesToCheck = entryType === "client" ? clientEntries : serverEntries;
for (let entryToCheck of entriesToCheck) {
let entryExists = await fse.pathExists(
path.resolve(remixRoot, "app", entryToCheck)
if (!runtime) {
throw new Error(
`Could not determine runtime. Please install one of the following: @remix-run/deno, @remix-run/cloudflare, @remix-run/node`
);
if (entryExists) {
console.log(
colors.gray(
`Entry file ${path.relative(remixRoot, entryToCheck)} already exists.`
)
);
return process.exit(1);
}
}

let contents: string | undefined;
let defaultsDirectory = path.resolve(__dirname, "..", "config", "defaults");
let defaultEntryClient = path.resolve(defaultsDirectory, "entry.client.tsx");
let defaultEntryServer = path.resolve(
defaultsDirectory,
`entry.server.${runtime}.tsx`
);

// 3. if server entry, update runtime import
if (entryType === "server") {
contents = await fse.readFile(inputFile, "utf-8");
let isServerEntry = entry.startsWith("entry.server.");

let pkgJson = await NPMCliPackageJson.load(remixRoot);
let deps = pkgJson.content.dependencies ?? {};
let contents = isServerEntry
? await createServerEntry(remixRoot, defaultEntryServer)
: await createClientEntry(remixRoot, defaultEntryClient);

if (deps["@remix-run/node"]) {
// we good
} else if (deps["@remix-run/cloudflare"]) {
contents = contents.replace(/@remix-run\/node/g, "@remix-run/cloudflare");
} else if (deps["@remix-run/deno"]) {
contents = contents.replace(/@remix-run\/node/g, "@remix-run/deno");
}
}
let inputFile = isServerEntry ? defaultEntryServer : defaultEntryClient;
let outputFile = path.resolve(remixRoot, "app", entry);

// 3. if entry is js/jsx, convert to js
// otherwise, copy the entry file from the defaults
if (/\.jsx?$/.test(entry)) {
contents ||= await fse.readFile(inputFile, "utf-8");
let javascript = convertTSFileToJS({
filename: inputFile,
projectDir: remixRoot,
source: contents,
});
await fse.writeFile(outputFile, javascript, "utf-8");
} else if (contents) {
await fse.writeFile(outputFile, contents, "utf-8");
} else {
await fse.copyFile(inputFile, outputFile);
await fse.writeFile(outputFile, contents, "utf-8");
}

console.log(
Expand All @@ -337,3 +323,30 @@ export async function generateEntry(remixRoot: string, entry: string) {

return process.exit(0);
}

async function checkForEntry(remixRoot: string, entries: Set<string>) {
for (let entryToCheck of entries) {
let entryPath = path.resolve(remixRoot, "app", entryToCheck);
let entryExists = await fse.pathExists(entryPath);
if (entryExists) {
console.log(
colors.red(
`Entry file ${path.relative(remixRoot, entryToCheck)} already exists.`
)
);
return process.exit(1);
}
}
}

async function createServerEntry(remixRoot: string, inputFile: string) {
await checkForEntry(remixRoot, serverEntries);
let contents = await fse.readFile(inputFile, "utf-8");
return contents;
}

async function createClientEntry(remixRoot: string, inputFile: string) {
await checkForEntry(remixRoot, clientEntries);
let contents = await fse.readFile(inputFile, "utf-8");
return contents;
}
33 changes: 27 additions & 6 deletions packages/remix-dev/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as path from "path";
import { pathToFileURL } from "url";
import * as fse from "fs-extra";
import getPort from "get-port";
import NPMCliPackageJson from "@npmcli/package-json";

import type { RouteManifest, DefineRoutesFunction } from "./config/routes";
import { defineRoutes } from "./config/routes";
Expand Down Expand Up @@ -365,26 +366,46 @@ export async function readConfig(

let defaultsDirectory = path.resolve(__dirname, "config", "defaults");
let defaultEntryClient = path.resolve(defaultsDirectory, "entry.client.tsx");
let defaultEntryServer = path.resolve(defaultsDirectory, "entry.server.tsx");

let userEntryClientFile = findEntry(appDirectory, "entry.client");
let userEntryServerFile = findEntry(appDirectory, "entry.server");

let entryServerFile: string;

if (!userEntryServerFile) {
let pkgJson = await NPMCliPackageJson.load(remixRoot);
let deps = pkgJson.content.dependencies ?? {};

let runtime = deps["@remix-run/deno"]
? "deno"
: deps["@remix-run/cloudflare"]
? "cloudflare"
: deps["@remix-run/node"]
? "node"
: undefined;

if (!runtime) {
throw new Error(
`Could not determine runtime. Please install one of the following: @remix-run/deno, @remix-run/cloudflare, @remix-run/node`
);
}

entryServerFile = `entry.server.${runtime}.tsx`;
} else {
entryServerFile = userEntryServerFile;
}

let entryClientFile = userEntryClientFile
? userEntryClientFile
: "entry.client.tsx";

let entryServerFile = userEntryServerFile
? userEntryServerFile
: "entry.server.tsx";

let entryClientFilePath = userEntryClientFile
? path.resolve(appDirectory, userEntryClientFile)
: defaultEntryClient;

let entryServerFilePath = userEntryServerFile
? path.resolve(appDirectory, userEntryServerFile)
: defaultEntryServer;
: path.resolve(defaultsDirectory, entryServerFile);

let serverBuildPath = "build/index.js";
switch (serverBuildTarget) {
Expand Down
78 changes: 78 additions & 0 deletions packages/remix-dev/config/defaults/entry.server.cloudflare.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { EntryContext } from "@remix-run/cloudflare";
import { RemixServer } from "@remix-run/react";
import { renderToReadableStream } from "react-dom/server";
import isbot from "isbot";

export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return isbot(request.headers.get("user-agent"))
? handleBotRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
)
: handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
);
}

async function handleBotRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
let didError = false;

let stream = await renderToReadableStream(
<RemixServer context={remixContext} url={request.url} />,
{
signal: request.signal,
onError(error: unknown) {
didError = true;
console.error(error);
},
}
);

responseHeaders.set("Content-Type", "text/html");

return new Response(stream, {
headers: responseHeaders,
status: didError ? 500 : responseStatusCode,
});
}

async function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
let didError = false;

let stream = await renderToReadableStream(
<RemixServer context={remixContext} url={request.url} />,
{
signal: request.signal,
onError(error: unknown) {
didError = true;
console.error(error);
},
}
);

responseHeaders.set("Content-Type", "text/html");
return new Response(stream, {
headers: responseHeaders,
status: didError ? 500 : responseStatusCode,
});
}
78 changes: 78 additions & 0 deletions packages/remix-dev/config/defaults/entry.server.deno.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { EntryContext } from "@remix-run/deno";
import { RemixServer } from "@remix-run/react";
import { renderToReadableStream } from "react-dom/server";
import isbot from "isbot";

export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return isbot(request.headers.get("user-agent"))
? handleBotRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
)
: handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
);
}

async function handleBotRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
let didError = false;

let stream = await renderToReadableStream(
<RemixServer context={remixContext} url={request.url} />,
{
signal: request.signal,
onError(error: unknown) {
didError = true;
console.error(error);
},
}
);

responseHeaders.set("Content-Type", "text/html");

return new Response(stream, {
headers: responseHeaders,
status: didError ? 500 : responseStatusCode,
});
}

async function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
let didError = false;

let stream = await renderToReadableStream(
<RemixServer context={remixContext} url={request.url} />,
{
signal: request.signal,
onError(error: unknown) {
didError = true;
console.error(error);
},
}
);

responseHeaders.set("Content-Type", "text/html");
return new Response(stream, {
headers: responseHeaders,
status: didError ? 500 : responseStatusCode,
});
}

0 comments on commit 6918568

Please sign in to comment.